Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(find-and-replace) (whole word selection) Allow non-word to non-word boundaries #987

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
12 changes: 11 additions & 1 deletion packages/find-and-replace/lib/find-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
38 changes: 38 additions & 0 deletions packages/find-and-replace/spec/find-view-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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", () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/find-and-replace/spec/project-find-view-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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);
});
});

Expand Down
Loading