Skip to content

[Bug] Exiting Visual mode at end of line leaves the Normal-mode cursor past the final character #277

Description

@nettlesh

Related: #38 reports entering an invalid end-of-line cursor position by clicking with the mouse. This report reproduces a similar symptom entirely through Vim keys and identifies a separate path in exitVisualMode.

Versions and environment

  • @replit/codemirror-vim: 6.4.0
  • @replit/codemirror-vim-core: 0.1.0
  • @codemirror/state: 6.7.5
  • @codemirror/view: 6.43.12
  • Reproduced in native macOS WebKit, both with a bare CodeMirror editor and in an application integration.

Reproduction

  1. Start with a single line containing alpha, with the cursor on a in Normal mode.
  2. Press v, then press l five times to reach the position after the final character.
  3. Press Escape.
  4. Press l once more.

After step 3, the Normal-mode cursor remains at zero-based column 5, after the final a. Step 4 moves it backward to column 4 despite being a rightward motion.

Expected: Escape should leave the Normal-mode cursor at column 4 immediately, and the following l should not move it. Unconfigured Vim behaves this way.

Minimal setup and programmatic reproduction:

import { EditorView, drawSelection } from '@codemirror/view';
import { vim, Vim, getCM } from '@replit/codemirror-vim';

const view = new EditorView({
  doc: 'alpha',
  extensions: [vim(), drawSelection()],
  parent: document.body,
});

const cm = getCM(view);
for (const key of ['v', 'l', 'l', 'l', 'l', 'l', '<Esc>']) {
  Vim.handleKey(cm, key, 'user');
}
console.log(cm.getCursor()); // Actual: { line: 0, ch: 5 }; expected ch: 4
Vim.handleKey(cm, 'l', 'user');
console.log(cm.getCursor()); // { line: 0, ch: 4 }

Apparent cause and candidate fix

In packages/codemirror-vim-core/vim.js, exitVisualMode calls clipCursorToContent before clearing vim.visualMode. That helper permits the line-break position while Visual mode is active, so it uses Visual-mode bounds for the resulting Normal-mode cursor.

Moving the cursor-clipping block after clearing the Visual-mode flags corrects this reproduction. updateLastSelection must still run before clearing the flags so gv retains the previous selection:

function exitVisualMode(cm, moveHead) {
  var vim = cm.state.vim;
  updateLastSelection(cm, vim);
  vim.visualMode = false;
  vim.visualLine = false;
  vim.visualBlock = false;
  if (moveHead !== false) {
    cm.setCursor(clipCursorToContent(cm, vim.sel.head));
  }
  if (!vim.insertMode) CM.signal(cm, "vim-mode-change", {mode: "normal"});
}

Local checks with that change covered nonempty and empty lines, a supplementary-plane emoji, gv, visual yank, linewise put, and undo.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions