From fd3db9478aa3906553144a2911f8f8f7389af11f Mon Sep 17 00:00:00 2001 From: David Ollerhead Date: Sat, 17 Oct 2026 20:31:21 +0100 Subject: [PATCH] Emit vim-mode-change event on command mode enter and exit (#251) --- packages/codemirror-vim-core/test/vim_test.js | 7 +++- packages/codemirror-vim-core/types.ts | 7 ++++ packages/codemirror-vim/src/index.ts | 32 +++++++++++++++++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/codemirror-vim-core/test/vim_test.js b/packages/codemirror-vim-core/test/vim_test.js index 789fc06..801bb65 100644 --- a/packages/codemirror-vim-core/test/vim_test.js +++ b/packages/codemirror-vim-core/test/vim_test.js @@ -1333,9 +1333,14 @@ testVim('on_mode_change', async function(cm, vim, helpers) { test('a', 'insert'); test('', 'normal'); test('v', 'visual'); - test(':', ''); // Event for Command-line mode not implemented. + test(':', isOldCodeMirror ? '' : 'command'); test('y\n', 'normal'); test(":startinsert\n", "insert"); + test('', 'normal'); + if (!isOldCodeMirror) { + test(':', 'command'); + test('', 'normal'); + } }); // Swapcase commands edit in place and do not modify registers. diff --git a/packages/codemirror-vim-core/types.ts b/packages/codemirror-vim-core/types.ts index fbf98a6..d3320fd 100644 --- a/packages/codemirror-vim-core/types.ts +++ b/packages/codemirror-vim-core/types.ts @@ -403,6 +403,13 @@ export type ExParams = { callback?: () => void, } +export type VimMode = 'normal' | 'insert' | 'visual' | 'replace' | 'command'; +export type VimSubMode = 'linewise' | 'blockwise' | ''; + +export type VimModeChangeEvent = + | { mode: 'visual'; subMode?: VimSubMode } + | { mode: 'normal' | 'insert' | 'replace' | 'command'; subMode?: undefined }; + export type PromptOptions = { onClose?: Function; prefix: string|HTMLElement; diff --git a/packages/codemirror-vim/src/index.ts b/packages/codemirror-vim/src/index.ts index 2cc853d..f7f052e 100644 --- a/packages/codemirror-vim/src/index.ts +++ b/packages/codemirror-vim/src/index.ts @@ -47,11 +47,15 @@ const vimStyle = EditorView.baseTheme({ }); type EditorViewExtended = EditorView & { cm: CodeMirror }; +import type { VimMode, VimSubMode, VimModeChangeEvent } from "@replit/codemirror-vim-core"; +export type { VimMode, VimSubMode, VimModeChangeEvent }; + const vimPlugin = ViewPlugin.fromClass( class implements PluginValue { private dom: HTMLElement; private statusButton: HTMLElement; private spacer: HTMLElement; + private inCommandMode = false; public view: EditorViewExtended; public cm: CodeMirror; public status = ""; @@ -72,11 +76,16 @@ const vimPlugin = ViewPlugin.fromClass( this.blockCursor.scheduleRedraw(); this.updateStatus(); }); - this.cm.on("vim-mode-change", (e: any) => { + this.cm.on("vim-mode-change", (e: VimModeChangeEvent) => { if (!cm.state.vim) return; + if (e.mode !== "command") { + this.inCommandMode = false; + } cm.state.vim.mode = e.mode; - if (e.subMode) { - cm.state.vim.mode += e.subMode === "linewise" ? " line" : " block"; + if (e.subMode === "linewise") { + cm.state.vim.mode += " line"; + } else if (e.subMode === "blockwise") { + cm.state.vim.mode += " block"; } cm.state.vim.status = ""; this.blockCursor.scheduleRedraw(); @@ -92,6 +101,23 @@ const vimPlugin = ViewPlugin.fromClass( effects: showVimPanel.of(!!this.cm.state.dialog), }); } + // Emit mode change for interactive command/search prompt enter/exit + const isPrompt = !!this.cm.state.dialog?.querySelector("input"); + if (isPrompt) { + if (!this.inCommandMode) { + this.inCommandMode = true; + CodeMirror.signal(cm, "vim-mode-change", { mode: "command" }); + } + } else if (this.inCommandMode) { + this.inCommandMode = false; + const vim = cm.state.vim; + if (vim) { + CodeMirror.signal(cm, "vim-mode-change", { + mode: vim.insertMode ? "insert" : vim.visualMode ? "visual" : "normal", + subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : undefined, + }); + } + } }); this.dom = document.createElement("span");