diff --git a/packages/find-and-replace/lib/find-options.js b/packages/find-and-replace/lib/find-options.js index 0ac5efb73f..c75ca5cf9e 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 = `\\b${expression}\\b`; } + 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); } diff --git a/packages/find-and-replace/spec/find-view-spec.js b/packages/find-and-replace/spec/find-view-spec.js index d41ca02e3b..cc220560a5 100644 --- a/packages/find-and-replace/spec/find-view-spec.js +++ b/packages/find-and-replace/spec/find-view-spec.js @@ -11,6 +11,10 @@ describe("FindView", () => { return workspaceElement.querySelector(".find-and-replace").parentNode; } + // usage: + // 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 = []; const decorations = editor.decorationsStateForScreenRowRange(0, editor.getLineCount()) @@ -458,6 +462,40 @@ 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", () => { + // 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); + + findView.findEditor.setText("-word"); + 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]]); + }); + + 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", () => { 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); }); });