From 055f009357f0b8b7438e9b7b06945c04c65167cd Mon Sep 17 00:00:00 2001 From: space-shell Date: Tue, 13 Jan 2026 19:47:12 +0100 Subject: [PATCH] feat: Refactor Vim/Helix keybindings with shared helper functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Factor out the repl-eval and repl-stop event dispatching code into replEval() and replStop() helper functions that are shared between Vim and Helix keybindings. This refactoring: - Reduces code duplication between Vim :w/:q and Helix :w/:q commands - Makes the keybinding handlers more maintainable - Properly implements Helix commands using the commands.of() API - Ensures consistent behavior across both keybinding modes The Helix keybindings now properly integrate with the REPL using the same event dispatch mechanism as Vim, dispatching custom repl-evaluate and repl-stop events with fallbacks to keyboard events. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/codemirror/keybindings.mjs | 163 ++++++++++++++++++---------- 1 file changed, 106 insertions(+), 57 deletions(-) diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index bc13f3234..3e6606306 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -6,7 +6,7 @@ import { emacs } from '@replit/codemirror-emacs'; import { vim, Vim } from '@replit/codemirror-vim'; // import { vim } from './vim_test.mjs'; import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; -import { helix } from 'codemirror-helix'; +import { helix, commands } from 'codemirror-helix'; import { logger } from '@strudel/core'; const vscodePlugin = ViewPlugin.fromClass( @@ -21,6 +21,70 @@ const vscodePlugin = ViewPlugin.fromClass( ); const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []); +function replEval(view) { + try { + // Dispatch a dedicated evaluate event first + let handled = false; + try { + const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true }); + handled = document.dispatchEvent(ev) === false; // false means preventDefault was called + } catch (e) { + console.error('Error dispatching repl-evaluate event', e); + } + if (handled) { + return; + } + // Try Ctrl+Enter first if not handled by custom event + const ctrlEnter = new KeyboardEvent('keydown', { + key: 'Enter', + code: 'Enter', + ctrlKey: true, + bubbles: true, + cancelable: true, + }); + view?.dom?.dispatchEvent?.(ctrlEnter); + // If not handled (no handler called preventDefault), try Alt+Enter as + // fallback + if (!ctrlEnter.defaultPrevented) { + const altEnter = new KeyboardEvent('keydown', { + key: 'Enter', + code: 'Enter', + altKey: true, + bubbles: true, + cancelable: true, + }); + view?.dom?.dispatchEvent?.(altEnter); + } + } catch (e) { + console.error('Error dispatching repl evaluation event', e); + } +} + +function replStop(view) { + try { + // First try dispatching our custom stop event, then fallback to Alt+. + let handled = false; + try { + const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true }); + handled = document.dispatchEvent(ev) === false; + } catch (e) { + console.error('Error dispatching repl-stop event', e); + } + if (!handled) { + const altDot = new KeyboardEvent('keydown', { + key: '.', + code: 'Period', + altKey: true, + bubbles: true, + cancelable: true, + }); + view?.dom?.dispatchEvent?.(altDot); + } + } catch (e) { + console.error('Error dispatching repl stop event', e); + } +} + // Map Vim :w to trigger the same action as evaluation. We dispatch a custom // event 'repl-evaluate' that the editor listens for, and also simulate // Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it @@ -48,29 +112,8 @@ try { // :q to pause/stop Vim.defineEx('quit', 'q', (cm) => { - try { - const view = cm?.view || cm; - // First try dispatching our custom stop event, then fallback to Alt+. - let handled = false; - try { - const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true }); - handled = document.dispatchEvent(ev) === false; - } catch (e) { - console.error('Error dispatching repl-stop event', e); - } - if (!handled) { - const altDot = new KeyboardEvent('keydown', { - key: '.', - code: 'Period', - altKey: true, - bubbles: true, - cancelable: true, - }); - view?.dom?.dispatchEvent?.(altDot); - } - } catch (e) { - console.error('Error dispatching :q stop event', e); - } + const view = cm?.view || cm; + replStop(view); }); // :w to evaluate @@ -84,38 +127,7 @@ try { } catch (e) { console.error('Error logging Vim :w evaluation', e); } - // Dispatch a dedicated evaluate event first - let handled = false; - try { - const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true }); - handled = document.dispatchEvent(ev) === false; // false means preventDefault was called - } catch (e) { - console.error('Error dispatching repl-evaluate event', e); - } - if (handled) { - return; - } - // Try Ctrl+Enter first if not handled by custom event - const ctrlEnter = new KeyboardEvent('keydown', { - key: 'Enter', - code: 'Enter', - ctrlKey: true, - bubbles: true, - cancelable: true, - }); - view?.dom?.dispatchEvent?.(ctrlEnter); - // If not handled (no handler called preventDefault), try Alt+Enter as - // fallback - if (!ctrlEnter.defaultPrevented) { - const altEnter = new KeyboardEvent('keydown', { - key: 'Enter', - code: 'Enter', - altKey: true, - bubbles: true, - cancelable: true, - }); - view?.dom?.dispatchEvent?.(altEnter); - } + replEval(view); } catch (e) { console.error('Error dispatching :w evaluation event', e); } @@ -125,12 +137,49 @@ try { console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e); } +// Map Helix :w to trigger the same action as evaluation. We dispatch a custom +// event 'repl-evaluate' that the editor listens for, and also simulate +// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it +// appears in the Console panel. +const helixCommands = commands.of([ + { + // :w to evaluate + name: 'write', + aliases: ['w'], + help: 'Repl-eval', + handler(view, args) { + try { + view?.focus?.(); // Let the app know this came from Helix :w + logger('[helix] :w — evaluating code'); + replEval(view); + } catch (e) { + console.error('Error dispatching helix :w evaluation event', e); + } + }, + }, + { + // :q to pause/stop + name: 'quit', + aliases: ['q'], + help: 'Repl-stop', + handler(view, args) { + try { + view?.focus?.(); // Let the app know this came from Helix :q + logger('[helix] :q — stopping repl'); + replStop(view); + } catch (e) { + console.error('Error dispatching helix :q stop event', e); + } + }, + }, +]); + const keymaps = { vim, emacs, - helix, codemirror: () => keymap.of(defaultKeymap), vscode: vscodeExtension, + helix: () => [helix(), helixCommands], }; export function keybindings(name) {