Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions src/glide/browser/base/content/motions.mts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface Editor {
/**
* An exhaustive list of all currently supported motion operations.
*/
export const MOTIONS = ["iw", "h", "j", "k", "l", "d"] as const;
export const MOTIONS = ["iw", "h", "j", "k", "l", "d", "w"] as const;
type GlideMotion = (typeof MOTIONS)[number];

export function select_motion(
Expand Down Expand Up @@ -180,11 +180,28 @@ export function select_motion(
},
};
}
case "w": {
if (is_bof(editor)) {
editor.selectionController.characterMove(true, true);
} else {
editor.selectionController.characterMove(false, false);
editor.selectionController.characterMove(true, true);
}

const starting_cls = text_obj.cls(current_char(editor));
if (is_eof(editor)) {
break;
}
forward_word(editor, false, "visual");
if (selection_has_cls_white_space(editor) || text_obj.cls(current_char(editor)) !== starting_cls) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for what its worth, I'm surprised the text_obj.cls(current_char(editor)) !== starting_cls check didn't fix the hello\nworld case you mention in the description?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the EOL case in particular, did you try the is_eol helper?

export function is_eol(editor: Editor): boolean {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your comment. Sorry for the delay, but I have also been able to handle cases including line breaks, so could you please check it?
295d46f

editor.selectionController.characterMove(false, true);
}
break;
}
default:
throw assert_never(motion, `Unknown motion: ${motion}`);
}
}

/**
* Returns the offset of the caret in the current line.
*
Expand Down Expand Up @@ -538,6 +555,28 @@ function selection_direction(
return "backwards";
}

/**
* Returns the currently selected text (single-node selections).
* Falls back to empty string if collapsed or no text node.
*/
export function selection_text(editor: Editor): string {
if (editor.selection.isCollapsed) {
return "";
}
const content = editor.selection.focusNode?.textContent ?? "";
const start = Math.min(editor.selection.anchorOffset, editor.selection.focusOffset);
const end = Math.max(editor.selection.anchorOffset, editor.selection.focusOffset);
return content.slice(start, end);
}

/**
* Whether the current selection contains a space character.
*/
export function selection_has_cls_white_space(editor: Editor): boolean {
const text = selection_text(editor);
return text.includes(" ") || text.includes("\t") || text.includes("\n") || text.includes("\r");
}

export function preceding_char(editor: Editor): string | null {
const content = editor.selection.focusNode?.textContent;
if (content == null) {
Expand Down
40 changes: 40 additions & 0 deletions src/glide/browser/base/content/test/motions/browser_words.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,46 @@ add_task(async function test_normal_diw() {
});
});

add_task(async function test_normal_dw() {
await BrowserTestUtils.withNewTab(INPUT_TEST_FILE, async browser => {
const { set_text, test_edit, set_selection } = GlideTestUtils.make_input_test_helpers(browser, {
text_start: 1,
});

await set_text("Hello world", "dw at start of word deletes word + following space");
await set_selection(0);
await test_edit("dw", "world", 0, "w");

await set_text("Hello world", "dw from inside a word deletes to next word boundary (keeps preceding chars)");
await set_selection(2);
await test_edit("dw", "Heworld", 2, "w");

await set_text("Hello world", "dw at start deletes word + all following spaces");
await set_selection(0);
await test_edit("dw", "world", 0, "w");

await set_text("Hello world", "dw from inside a word deletes to next word, skipping extra spaces");
await set_selection(2);
await test_edit("dw", "Heworld", 2, "w");

await set_text("Hello, world", "dw stops at punctuation (keeps punctuation and following space)");
await set_selection(0);
await test_edit("dw", ", world", 0, ",");

await set_text("hello\nworld", "dw treats newline as whitespace and deletes it with the word");
await set_selection(0);
await test_edit("dw", "world", 0, "w");

await set_text("hello?.world", "dw on punctuation run deletes punctuation up to next word");
await set_selection(5);
await test_edit("dw", "helloworld", 5, "w");

await set_text("h h h", "dw deletes a single-letter word + following space");
await set_selection(2);
await test_edit("dw", "h h", 2, "h");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can also set the expected char to make the test easier to read, and I also like to avoid duplicate chars for the same reason

Suggested change
await set_selection(5);
await test_edit("dw", "helloworld", 5, "w");
await set_text("h h h", "dw deletes a single-letter word + following space");
await set_selection(2);
await test_edit("dw", "h h", 2, "h");
await set_selection(5, "?");
await test_edit("dw", "helloworld", 5, "w");
await set_text("h j k", "dw deletes a single-letter word + following space");
await set_selection(2, "j");
await test_edit("dw", "h k", 2, "k");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your comment.
I have adjusted the test code, so please check it again.
d9d145c

});
});

add_task(async function test_normal_w() {
await BrowserTestUtils.withNewTab(INPUT_TEST_FILE, async browser => {
const { set_text, test_motion, set_selection } = GlideTestUtils.make_input_test_helpers(browser, { text_start: 1 });
Expand Down