Find and replace always replaces the first match when track changes is enabled
Suggested merge commit message (convention)
Fix (find-and-replace): Do not replace only the first found word when using it with Track Changes plugin.
MINOR BREAKING CHANGE (find-and-replace): The FindCommand#execute now accepts a findCallback that can return an array of results or an object containing an array of results together with the searchText used to invalidate search results in commands like find previous, find next, and replace search.
Additional information
Closes: https://github.com/cksource/ckeditor5-commercial/issues/6193 Commercial PR: https://github.com/cksource/ckeditor5-commercial/pull/6275
Debug notes
- When search is performed,
FindCommand#executeis called. - It can be called with
callbackorstringas a search parameter. - This bug is reproducible if
callbackis passed as search parameter. -
Track Changesinterceptsfind and replacefindcommandexecuteand forces usingcallbackparameter (it was originally called withstringsearch parameter). -
FindCommand#executeclearsFindAndReplaceState#searchText. - If
stringsearch parameter is passed,FindAndReplaceState#searchTextis being reverted after search (inFindCommand#execute). Ifcallbacksearch parameter is passed, nothing happens andFindAndReplaceState#searchTextstays empty. - It means that passing
callbacksearch parameter resets highlight index here and here, and it's causing an issue.state.searchTextis empty (it was cleared in previous steps while using callback search), on the other hand,data.searchTextis filled with current input value.
Possible solutions
- Return
searchTextalong withresultsinFindCallback. 1.1. It's more flexible if the command is executed with callback which defines the search phrase inside itself [1]. 1.2. It's safer according to tests (none failed). 1.3. It introduces a breaking change in integrations that depends on arguments passed toFindCommand#execute. 1.4. Search UI seems not to be bound tosearchTextcommand value. It needs to be refactored, probably. 1.5. It's not super clear for the users whysearchTexthas to be forwarded (despite adding comment). - Get rid of
state.searchText2.1. Refactoring whole solution, it's kinda complex and many of the tests will be broken. 2.2. RenamesearchTexttoqueryIDor something like that. It'll be more obvious for the users. Less dangerous, but under the hood it'll work exactly like1solution. - Get rid of support
callbackfind. It's an old feature, there are probably a lot of integrations that might depend on this behavior.
[1]
findCommand.execute( () => {
const callback = typeof callbackOrText === 'string' ? plugin.findByTextCallback( 'Bunny' ) : 'Bunny';
...
return {
results: ...,
searchText: 'Bunny'
};
} );
Reproduction video
https://github.com/ckeditor/ckeditor5/assets/3949262/944f3f63-a1a9-4ecf-adb8-336f93f44b2c
@DawidKossowski / @oleq / @scofalik Can you take a look?
I started looking into this PR and the logic in general (well, wanted to understand what's going on before looking into changes itself). Couple of general remarks (not related to the changes itself):
- The important piece of the puzzle here is what @Mati365 mentioned in:
It means that passing callback search parameter resets highlight index here and here, and it's causing an issue. state.searchText is empty (it was cleared in previous steps while using callback search), on the other hand, data.searchText is filled with current input value.
Since state.searchText it's not there, pressing Replace dialog button, the logic there determines that search term is different than before and reruns find logic (line 79):
https://github.com/ckeditor/ckeditor5/blob/a477c477d5158d0b24059c6e66b504d0b379641b/packages/ckeditor5-find-and-replace/src/findandreplace.ts#L77-L82
This won't happen when using dialog (because Replace button gets disabled when search term changes and user has to perform Find first), but can happen when using API directly.
-
TBH I find the logic of overwriting
executeCommanda bit convoluted. In short, the code overwrites originalfindwith own implementation, which calls originalfind, passing custom find callback, which is then executed multiple times in originalfind(for each root children). And every time original/custom find is called, we checktypeof callbackOrText === 'string' ? ...there. And it's only to filter out deleted parts of the content from find results. I can describe this in more detail if there is time - I know it's not for now but maybe there will be a chance to look into this one day. -
This is broken for every integration which passes
findcallback function so not only revision history. I see we don't have other usages like that, and probably haven't received any external requests about this. So maybe it's safe to assume that we can fix/extend how it works to allow future integrations to work correctly (what @Mati365 did here). Not sure if fixing this without touching integrations code would be reasonable/doable.
@f1ames
I went through the code and I think the direction you @Mati365 proposed is valid 👍
Fixed CR remarks. Can you check again?
1.4. Search UI seems not to be bound to searchText command value. It needs to be refactored, probably.
From what I can see, here: https://github.com/ckeditor/ckeditor5/blob/ck/6193/packages/ckeditor5-find-and-replace/src/ui/findandreplaceformview.ts#L389-L401 we are using plain DOM attributes to accept search text which is later forwarded on submission of form to the state of the search. I'd refactor it to bind the search text value to the state directly and, depending on change events of such text value, clear the history of search results accordingly.
Found another issue while testing. Not related to the changes from this PR though, reproducible on master too - https://github.com/ckeditor/ckeditor5/issues/16648.
@f1ames fixed CR remarks
Fix (find-and-replace): Do not replace only the first found word when using it with
Track Changesplugin.MINOR BREAKING CHANGE (find-and-replace): The
FindCommand#executenow accepts afindCallbackthat can return an array of results or an object containing an array of results together with thesearchTextused to invalidate search results in commands likefind previous,find next, andreplace search.
Just a remark here, I don't see it as a breaking change - here, findCallback type was extended, nothing removed, so all previous implementations will work the same way (with the issue as before). So there is a full backwards compatibility in those changes.