diff --git a/CHANGELOG.md b/CHANGELOG.md index 913d80e..0dc048b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version ## [Unreleased] +### Fixed + +- `e` in visual mode now moves to end of word instead of behaving like `w` ([#52](https://github.com/oribarilan/vimcode/issues/52)). + ## [0.15.1] — 2026-06-23 ### Fixed diff --git a/src/index.ts b/src/index.ts index 6238b91..9e69b26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -352,7 +352,7 @@ const plugin: TuiPluginModule = { state.mode === "insert" ? handleInsertKey(state, key, ctx.event, prompt) : state.mode === "visual" - ? handleVisualKey(state, key, ctx.event) + ? handleVisualKey(state, key, ctx.event, prompt) : handleNormalKey(state, key, ctx.event, prompt); if (handlerMode === "normal") finishOneShotIfComplete(state, result); diff --git a/src/vim.ts b/src/vim.ts index be2df0e..aeced32 100644 --- a/src/vim.ts +++ b/src/vim.ts @@ -28,6 +28,7 @@ export type VimState = { yankRegister: string; oneShotNormal: boolean; disabled: boolean; + visualAnchor?: number; }; export type KeyEvent = { @@ -67,7 +68,6 @@ export const SELECT_MOTIONS: Record = { k: "input.select.up", w: "input.select.word.forward", b: "input.select.word.backward", - e: "input.select.word.forward", "0": "input.select.line.home", "^": "input.select.line.home", $: "input.select.line.end", @@ -458,6 +458,7 @@ export function handleNormalKey(state: VimState, key: string, ev: KeyEvent, prom if (key === "V") { const range = currentLineRange(prompt.getPlainText(), prompt.getCursorOffset()); state.mode = "visual"; + state.visualAnchor = prompt.getCursorOffset(); state.oneShotNormal = false; resetPending(state); return { @@ -471,6 +472,7 @@ export function handleNormalKey(state: VimState, key: string, ev: KeyEvent, prom if (key === "v") { state.mode = "visual"; + state.visualAnchor = prompt.getCursorOffset(); state.oneShotNormal = false; resetPending(state); return { consume: true, actions: [{ type: "mode", mode: "visual" }] }; @@ -513,7 +515,7 @@ export function handleNormalKey(state: VimState, key: string, ev: KeyEvent, prom return { consume: true, actions }; } -export function handleVisualKey(state: VimState, key: string, ev: KeyEvent): HandlerResult { +export function handleVisualKey(state: VimState, key: string, ev: KeyEvent, prompt: PromptAccess): HandlerResult { if (ev.meta || ev.super) return PASS; if (ev.ctrl) return PASS; @@ -561,6 +563,14 @@ export function handleVisualKey(state: VimState, key: string, ev: KeyEvent): Han return { consume: true, actions }; } + // e — extend selection to end of word (custom, not a host command) + if (key === "e") { + const n = consumeCount(state); + const target = endOfWord(prompt.getPlainText(), prompt.getCursorOffset(), n); + actions.push({ type: "selectRange", start: state.visualAnchor ?? 0, end: target }); + return { consume: true, actions }; + } + // Motions extend selection if (key in SELECT_MOTIONS) { pushN(actions, SELECT_MOTIONS[key], consumeCount(state)); diff --git a/test/vim.test.ts b/test/vim.test.ts index 4c4c67a..143bb94 100644 --- a/test/vim.test.ts +++ b/test/vim.test.ts @@ -936,10 +936,25 @@ describe("handleVisualKey — motions", () => { }); it("G dispatches input.select.buffer.end", () => { - const r = handleVisualKey(state, "G", ev("g", { shift: true })); + const r = handleVisualKey(state, "G", ev("g", { shift: true }), mockPrompt); expect(cmds(r.actions)).toEqual(["input.select.buffer.end"]); }); + it("e selects from visual anchor to end of word", () => { + // "hello world" with cursor at 0, anchor at 0 → end of "hello" is offset 4 + state.visualAnchor = 0; + const r = handleVisualKey(state, "e", ev("e"), mockPrompt); + expect(selectRanges(r.actions)).toEqual([{ start: 0, end: 4 }]); + }); + + it("2e selects from visual anchor to end of 2nd word", () => { + // "hello world" with cursor at 0, anchor at 0 → end of "world" is offset 10 + state.visualAnchor = 0; + handleVisualKey(state, "2", ev("2"), mockPrompt); + const r = handleVisualKey(state, "e", ev("e"), mockPrompt); + expect(selectRanges(r.actions)).toEqual([{ start: 0, end: 10 }]); + }); + it("g sets pendingChar, no actions", () => { const r = handleVisualKey(state, "g", ev("g")); expect(r.consume).toBe(true);