diff --git a/plugins/find-and-replace.js b/plugins/find-and-replace.js index e312ed9..564ac90 100644 --- a/plugins/find-and-replace.js +++ b/plugins/find-and-replace.js @@ -186,7 +186,9 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin { } /** - * Show a find-and-replace dialog. + * Show a find-and-replace dialog. The first match focused is the first one at or + * after the caret's position when this is called, wrapping back round to the start + * of the code if there is no such match. * @param {codeInput.CodeInput} codeInputElement the `` element. * @param {boolean} replacePartExpanded whether the replace part of the find-and-replace dialog should be expanded */ @@ -412,6 +414,12 @@ codeInput.plugins.FindAndReplace = class extends codeInput.Plugin { dialog.selectionStart = codeInputElement.textareaElement.selectionStart; dialog.selectionEnd = codeInputElement.textareaElement.selectionEnd; + // Start searching from the caret's position at the moment the dialog was + // opened, like most IDEs do. The findMatchState is only constructed when the + // dialog is first created, so without this every reopening of the dialog would + // carry on from wherever the previous search left off instead. + dialog.findMatchState.focusedMatchStartIndex = dialog.selectionStart; + if(dialog.selectionStart < dialog.selectionEnd) { // Copy selected text to Find input let textToFind = codeInputElement.textareaElement.value.substring(dialog.selectionStart, dialog.selectionEnd); diff --git a/tests/tester.js b/tests/tester.js index f619b12..a1cc92b 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -761,6 +761,38 @@ console.log("I've got another line!", 2 < 3, "should be true."); codeInputElement.querySelector(".code-input_find-and-replace_dialog").dispatchEvent(new KeyboardEvent("keydown", { "key": "Escape" })); codeInputElement.querySelector(".code-input_find-and-replace_dialog").dispatchEvent(new KeyboardEvent("keyup", { "key": "Escape" })); + // Opening the dialog searches from the caret's position at that moment, rather than + // carrying on from wherever the previous search left off. In the code above, "hi" is + // at indexes 3, 16 and 29. + // Open the dialog with the caret at the given index, then close it, which selects the + // focused match; returns the index that match starts at. + async function findFromCaret(caret) { + findInput.value = "hi"; + textarea.focus(); + textarea.selectionStart = textarea.selectionEnd = caret; + if(navigator.platform.startsWith("Mac") || navigator.platform === "iPhone") { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "f", "metaKey": true })); + } else { + textarea.dispatchEvent(new KeyboardEvent("keydown", { "cancelable": true, "key": "f", "ctrlKey": true })); + } + await waitAsync(250); // Wait for highlighting so matches update + + const dialog = codeInputElement.querySelector(".code-input_find-and-replace_dialog"); + dialog.dispatchEvent(new KeyboardEvent("keydown", { "key": "Escape" })); + dialog.dispatchEvent(new KeyboardEvent("keyup", { "key": "Escape" })); + await waitAsync(150); // Wait for the dialog to close and select the focused match + + return textarea.selectionStart; + } + + // These two are ordered so the match the previous one left focused differs from the + // correct answer, so they fail if the caret's position is ignored. + assertEqual("FindAndReplace", "Finds First Match After Caret when Reopened", await findFromCaret(17), 29); + assertEqual("FindAndReplace", "Finds Match Starting Exactly at Caret", await findFromCaret(16), 16); + // These two lock in the surrounding behaviour, and pass either way. + assertEqual("FindAndReplace", "Wraps to First Match when Caret is After Last Match", await findFromCaret(30), 3); + assertEqual("FindAndReplace", "Keeps Focused Match when Reopened without Moving Caret", await findFromCaret(3), 3); + // GoToLine // Replace all code textarea.selectionStart = 0;