From 5fef23650d4b42d7d029e3a597925b033fd3116b Mon Sep 17 00:00:00 2001 From: icecream17 Date: Sun, 21 Apr 2024 13:45:14 -0500 Subject: [PATCH 1/8] allow non word characters at the start or end of ctrl+f whole word selection --- packages/find-and-replace/lib/find-options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/find-and-replace/lib/find-options.js b/packages/find-and-replace/lib/find-options.js index 0ac5efb73f..7c0e7305b0 100644 --- a/packages/find-and-replace/lib/find-options.js +++ b/packages/find-and-replace/lib/find-options.js @@ -90,7 +90,7 @@ module.exports = class FindOptions { expression = escapeRegExp(this.findPattern); } - if (this.wholeWord) { expression = `\\b${expression}\\b`; } + if (this.wholeWord) { expression = `(?:^|\\W)${expression}(?:$|\\W)`; } return new RegExp(expression, flags); } From 7ee681ad50d0a8ef561db790057dfcaa13102900 Mon Sep 17 00:00:00 2001 From: icecream17 Date: Sun, 21 Apr 2024 14:26:14 -0500 Subject: [PATCH 2/8] add more tests [skip ci] --- .../find-and-replace/spec/find-view-spec.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/find-and-replace/spec/find-view-spec.js b/packages/find-and-replace/spec/find-view-spec.js index d41ca02e3b..554d729ace 100644 --- a/packages/find-and-replace/spec/find-view-spec.js +++ b/packages/find-and-replace/spec/find-view-spec.js @@ -11,6 +11,9 @@ describe("FindView", () => { return workspaceElement.querySelector(".find-and-replace").parentNode; } + // usage: + // getResultDecorations(editor, "current-result") --> length = number of currently selected results + // getResultDecorations(editor, "find-result") --> length = number of results that are not currently selected function getResultDecorations(editor, clazz) { const result = []; const decorations = editor.decorationsStateForScreenRowRange(0, editor.getLineCount()) @@ -458,6 +461,30 @@ describe("FindView", () => { expect(getResultDecorations(editor, "find-result")).toHaveLength(1); expect(getResultDecorations(editor, "current-result")).toHaveLength(1); }); + + it("finds the whole words even when the word starts or ends with a non-word character", () => { + findView.findEditor.setText("-word"); + atom.commands.dispatch(findView.findEditor.element, "core:confirm"); + expect(editor.getSelectedBufferRange()).toEqual([[2, 5], [2, 10]]); + + findView.findEditor.setText("whole-"); + atom.commands.dispatch(findView.findEditor.element, "core:confirm"); + expect(editor.getSelectedBufferRange()).toEqual([[2, 0], [2, 6]]); + }); + + it("doesn't highlight the search inside words (non-word character at start)", () => { + findView.findEditor.setText("-word"); + atom.commands.dispatch(findView.findEditor.element, "core:confirm"); + expect(getResultDecorations(editor, "find-result")).toHaveLength(0); + expect(getResultDecorations(editor, "current-result")).toHaveLength(1); + }); + + it("doesn't highlight the search inside words (non-word character at end)", () => { + findView.findEditor.setText("whole-"); + atom.commands.dispatch(findView.findEditor.element, "core:confirm"); + expect(getResultDecorations(editor, "find-result")).toHaveLength(1); + expect(getResultDecorations(editor, "current-result")).toHaveLength(1); + }); }); it("doesn't change the selection, beeps if there are no matches and keeps focus on the find view", () => { From 74d4dee27ec738f392ff0104b5d9fb8c3a3ebbb5 Mon Sep 17 00:00:00 2001 From: icecream17 Date: Sun, 21 Apr 2024 14:30:07 -0500 Subject: [PATCH 3/8] use lookbehind/lookahead the non-capturing group still selects, apparently --- packages/find-and-replace/lib/find-options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/find-and-replace/lib/find-options.js b/packages/find-and-replace/lib/find-options.js index 7c0e7305b0..4152138533 100644 --- a/packages/find-and-replace/lib/find-options.js +++ b/packages/find-and-replace/lib/find-options.js @@ -90,7 +90,7 @@ module.exports = class FindOptions { expression = escapeRegExp(this.findPattern); } - if (this.wholeWord) { expression = `(?:^|\\W)${expression}(?:$|\\W)`; } + if (this.wholeWord) { expression = `(?<=^|\\W)${expression}(?=$|\\W)`; } return new RegExp(expression, flags); } From 63968d0dfd99651481ec9b2f9ab92ca7cb626f79 Mon Sep 17 00:00:00 2001 From: icecream17 Date: Sat, 22 Jun 2024 21:14:35 -0500 Subject: [PATCH 4/8] allow non-word | non-word [skip ci] --- packages/find-and-replace/lib/find-options.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/find-and-replace/lib/find-options.js b/packages/find-and-replace/lib/find-options.js index 4152138533..9b6fdb4811 100644 --- a/packages/find-and-replace/lib/find-options.js +++ b/packages/find-and-replace/lib/find-options.js @@ -90,7 +90,17 @@ module.exports = class FindOptions { expression = escapeRegExp(this.findPattern); } - if (this.wholeWord) { expression = `(?<=^|\\W)${expression}(?=$|\\W)`; } + if (this.wholeWord) { + // https://github.com/pulsar-edit/pulsar/pull/987 + // wholeWord is usually defined by `\\b${expression}\\b`, + // where \\b is a word boundary: a position between a word and non-word character. + // + // However, if the selection (the "word") starts or ends with a non-word character, + // then, say, `$word` in ` $word ` isnt selected, as both the space and the dollar + // are non-word characters. To allow this, we can either have a word boundary or + // a lookahead/lookbehind for the start/end non-word character: + expression = `(?<=\\b|(?=\\W))${expression}(?=\\b|(?<=\\W))`; + } return new RegExp(expression, flags); } From 0275e540187136c3951075b9228830c36639069b Mon Sep 17 00:00:00 2001 From: icecream17 Date: Sat, 22 Jun 2024 21:17:46 -0500 Subject: [PATCH 5/8] tests now cover both {word, non-word} x {word} [skip ci] --- packages/find-and-replace/spec/find-view-spec.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/find-and-replace/spec/find-view-spec.js b/packages/find-and-replace/spec/find-view-spec.js index 554d729ace..170d15da2e 100644 --- a/packages/find-and-replace/spec/find-view-spec.js +++ b/packages/find-and-replace/spec/find-view-spec.js @@ -12,8 +12,8 @@ describe("FindView", () => { } // usage: - // getResultDecorations(editor, "current-result") --> length = number of currently selected results - // getResultDecorations(editor, "find-result") --> length = number of results that are not currently selected + // getResultDecorations(editor, "current-result") --> length = number of currently selected results (usually 1) + // getResultDecorations(editor, "find-result") --> length = number of results that are not currently selected (usually = # results - 1) function getResultDecorations(editor, clazz) { const result = []; const decorations = editor.decorationsStateForScreenRowRange(0, editor.getLineCount()) @@ -463,6 +463,10 @@ describe("FindView", () => { }); it("finds the whole words even when the word starts or ends with a non-word character", () => { + findView.findEditor.setText("-"); + expect(getResultDecorations(editor, "find-result")).toHaveLength(7); + expect(getResultDecorations(editor, "current-result")).toHaveLength(1); + findView.findEditor.setText("-word"); atom.commands.dispatch(findView.findEditor.element, "core:confirm"); expect(editor.getSelectedBufferRange()).toEqual([[2, 5], [2, 10]]); From ff939dd3a8bcebba35246f13489cf2073e48b659 Mon Sep 17 00:00:00 2001 From: icecream17 Date: Sat, 22 Jun 2024 21:21:16 -0500 Subject: [PATCH 6/8] simplify [skip ci] --- packages/find-and-replace/lib/find-options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/find-and-replace/lib/find-options.js b/packages/find-and-replace/lib/find-options.js index 9b6fdb4811..c75ca5cf9e 100644 --- a/packages/find-and-replace/lib/find-options.js +++ b/packages/find-and-replace/lib/find-options.js @@ -99,7 +99,7 @@ module.exports = class FindOptions { // then, say, `$word` in ` $word ` isnt selected, as both the space and the dollar // are non-word characters. To allow this, we can either have a word boundary or // a lookahead/lookbehind for the start/end non-word character: - expression = `(?<=\\b|(?=\\W))${expression}(?=\\b|(?<=\\W))`; + expression = `(?:\\b|(?=\\W))${expression}(?:\\b|(?<=\\W))`; } return new RegExp(expression, flags); From 024fae4beedeeabfc119189da9e5eb162ac45629 Mon Sep 17 00:00:00 2001 From: icecream17 Date: Sat, 22 Jun 2024 21:22:36 -0500 Subject: [PATCH 7/8] Update test regex (project-find-view-spec.js) --- packages/find-and-replace/spec/project-find-view-spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/find-and-replace/spec/project-find-view-spec.js b/packages/find-and-replace/spec/project-find-view-spec.js index 4d019f41ca..d620f744ab 100644 --- a/packages/find-and-replace/spec/project-find-view-spec.js +++ b/packages/find-and-replace/spec/project-find-view-spec.js @@ -689,7 +689,7 @@ describe(`ProjectFindView (ripgrep=${ripgrep})`, () => { await resultsPromise(); expect(projectFindView.refs.wholeWordOptionButton).toHaveClass('selected'); - expect(atom.workspace.scan.mostRecentCall.args[0]).toEqual(/\bwholeword\b/gim); + expect(atom.workspace.scan.mostRecentCall.args[0]).toEqual(/(?:\b|(?=\W))wholeword(?:\b|(?<=\W))/gim); }); it("toggles whole word option via a button and finds files matching the pattern", async () => { @@ -699,7 +699,7 @@ describe(`ProjectFindView (ripgrep=${ripgrep})`, () => { await resultsPromise(); expect(projectFindView.refs.wholeWordOptionButton).toHaveClass('selected'); - expect(atom.workspace.scan.mostRecentCall.args[0]).toEqual(/\bwholeword\b/gim); + expect(atom.workspace.scan.mostRecentCall.args[0]).toEqual(/(?:\b|(?=\W))wholeword(?:\b|(?<=\W))/gim); }); }); From fd6f880f1fd89b6c8a2bdd79a8dc2cdff401b548 Mon Sep 17 00:00:00 2001 From: icecream17 Date: Sun, 23 Jun 2024 09:52:07 -0500 Subject: [PATCH 8/8] Fix test --- packages/find-and-replace/spec/find-view-spec.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/find-and-replace/spec/find-view-spec.js b/packages/find-and-replace/spec/find-view-spec.js index 170d15da2e..cc220560a5 100644 --- a/packages/find-and-replace/spec/find-view-spec.js +++ b/packages/find-and-replace/spec/find-view-spec.js @@ -12,7 +12,8 @@ describe("FindView", () => { } // usage: - // getResultDecorations(editor, "current-result") --> length = number of currently selected results (usually 1) + // getResultDecorations(editor, "current-result") --> length = number of currently selected results (usually 1, after a + // atom.commands.dispatch(findView.findEditor.element, "core:confirm");) // getResultDecorations(editor, "find-result") --> length = number of results that are not currently selected (usually = # results - 1) function getResultDecorations(editor, clazz) { const result = []; @@ -463,7 +464,10 @@ describe("FindView", () => { }); it("finds the whole words even when the word starts or ends with a non-word character", () => { + // Arguably, a non-symbol doesn't form a whole word when surrounded by the same non-symbols, + // but the current code doesn't check for that. findView.findEditor.setText("-"); + atom.commands.dispatch(findView.findEditor.element, "core:confirm"); expect(getResultDecorations(editor, "find-result")).toHaveLength(7); expect(getResultDecorations(editor, "current-result")).toHaveLength(1); @@ -471,8 +475,11 @@ describe("FindView", () => { atom.commands.dispatch(findView.findEditor.element, "core:confirm"); expect(editor.getSelectedBufferRange()).toEqual([[2, 5], [2, 10]]); + // The cursor is on line 2 because of ^^ so the first find is on line 4 findView.findEditor.setText("whole-"); atom.commands.dispatch(findView.findEditor.element, "core:confirm"); + expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [4, 6]]); + atom.commands.dispatch(findView.findEditor.element, "core:confirm"); expect(editor.getSelectedBufferRange()).toEqual([[2, 0], [2, 6]]); });