jevstrudel.git / packages / codemirror / keybindings.mjs
1import { defaultKeymap } from '@codemirror/commands';
2import { Prec, EditorState } from '@codemirror/state';
3import { keymap, ViewPlugin } from '@codemirror/view';
4// import { searchKeymap } from '@codemirror/search';
5import { emacs } from '@replit/codemirror-emacs';
6import { vim, Vim } from '@replit/codemirror-vim';
7// import { vim } from './vim_test.mjs';
8import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
9import { helix, commands } from 'codemirror-helix';
10import { logger } from '@strudel/core';
11
12const vscodePlugin = ViewPlugin.fromClass(
13  class {
14    constructor() {}
15  },
16  {
17    provide: () => {
18      return Prec.highest(keymap.of([...vscodeKeymap]));
19    },
20  },
21);
22const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
23
24function replEval(view) {
25  try {
26    // Dispatch a dedicated evaluate event first
27    let handled = false;
28    try {
29      const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
30      handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
31    } catch (e) {
32      console.error('Error dispatching repl-evaluate event', e);
33    }
34    if (handled) {
35      return;
36    }
37    // Try Ctrl+Enter first if not handled by custom event
38    const ctrlEnter = new KeyboardEvent('keydown', {
39      key: 'Enter',
40      code: 'Enter',
41      ctrlKey: true,
42      bubbles: true,
43      cancelable: true,
44    });
45    view?.dom?.dispatchEvent?.(ctrlEnter);
46    // If not handled (no handler called preventDefault), try Alt+Enter as
47    // fallback
48    if (!ctrlEnter.defaultPrevented) {
49      const altEnter = new KeyboardEvent('keydown', {
50        key: 'Enter',
51        code: 'Enter',
52        altKey: true,
53        bubbles: true,
54        cancelable: true,
55      });
56      view?.dom?.dispatchEvent?.(altEnter);
57    }
58  } catch (e) {
59    console.error('Error dispatching repl evaluation event', e);
60  }
61}
62
63function replStop(view) {
64  try {
65    // First try dispatching our custom stop event, then fallback to Alt+.
66    let handled = false;
67    try {
68      const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
69      handled = document.dispatchEvent(ev) === false;
70    } catch (e) {
71      console.error('Error dispatching repl-stop event', e);
72    }
73    if (!handled) {
74      const altDot = new KeyboardEvent('keydown', {
75        key: '.',
76        code: 'Period',
77        altKey: true,
78        bubbles: true,
79        cancelable: true,
80      });
81      view?.dom?.dispatchEvent?.(altDot);
82    }
83  } catch (e) {
84    console.error('Error dispatching repl stop event', e);
85  }
86}
87
88// Map Vim :w to trigger the same action as evaluation. We dispatch a custom
89// event 'repl-evaluate' that the editor listens for, and also simulate
90// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
91// appears in the Console panel.
92try {
93  if (Vim && typeof Vim.defineEx === 'function') {
94    // Map gc to toggle line comments by dispatching a custom event that our
95    // CodeMirror integration listens to. This avoids depending on Vim's
96    // internal actions and works with current selections/visual mode.
97    try {
98      Vim.defineAction('strudelToggleComment', (cm) => {
99        const view = cm.cm6;
100        try {
101          const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
102          document.dispatchEvent(ev);
103        } catch (e) {
104          console.error('strudelToggleComment dispatch failed', e);
105        }
106      });
107      Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'normal' });
108      Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'visual' });
109    } catch (e) {
110      console.error('Vim gc mapping failed', e);
111    }
112
113    // :q to pause/stop
114    Vim.defineEx('quit', 'q', (cm) => {
115      const view = cm.cm6;
116      logger('[vim] :q — stopping repl');
117      replStop(view);
118    });
119
120    // :w to evaluate
121    Vim.defineEx('write', 'w', (cm) => {
122      const view = cm.cm6;
123      try {
124        view.focus?.();
125        // Let the app know this came from Vim :w
126        try {
127          logger('[vim] :w — evaluating code');
128        } catch (e) {
129          console.error('Error logging Vim :w evaluation', e);
130        }
131        replEval(view);
132      } catch (e) {
133        console.error('Error dispatching :w evaluation event', e);
134      }
135    });
136  }
137} catch (e) {
138  console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
139}
140
141// Map Helix :w to trigger the same action as evaluation. We dispatch a custom
142// event 'repl-evaluate' that the editor listens for, and also simulate
143// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
144// appears in the Console panel.
145const helixCommands = commands.of([
146  {
147    // :w to evaluate
148    name: 'write',
149    aliases: ['w'],
150    help: 'Repl-eval',
151    handler(view, args) {
152      try {
153        view?.focus?.(); // Let the app know this came from Helix :w
154        logger('[helix] :w — evaluating code');
155        replEval(view);
156      } catch (e) {
157        console.error('Error dispatching helix :w evaluation event', e);
158      }
159    },
160  },
161  {
162    // :q to pause/stop
163    name: 'quit',
164    aliases: ['q'],
165    help: 'Repl-stop',
166    handler(view, args) {
167      try {
168        view?.focus?.(); // Let the app know this came from Helix :q
169        logger('[helix] :q — stopping repl');
170        replStop(view);
171      } catch (e) {
172        console.error('Error dispatching helix :q stop event', e);
173      }
174    },
175  },
176]);
177
178const keymaps = {
179  vim,
180  emacs,
181  codemirror: () => keymap.of(defaultKeymap),
182  vscode: vscodeExtension,
183  helix: () => [helix(), helixCommands],
184};
185
186export { Vim } from '@replit/codemirror-vim';
187
188export function keybindings(name) {
189  const active = keymaps[name];
190  const extensions = active ? [Prec.high(active())] : [];
191  if (name === 'vim') {
192    extensions.push(EditorState.allowMultipleSelections.of(true));
193  }
194  return extensions;
195}