feat(codemirror): add Vim :w evaluate, :q stop, and gc/gcc toggle comment via custom events; docs: add Vim shortcuts page and link from REPL manual
This commit is contained in:
parent
00872bd1de
commit
be9368e8a2
4 changed files with 203 additions and 18 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import { closeBrackets } from '@codemirror/autocomplete';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
|
import { toggleLineComment } from '@codemirror/commands';
|
||||||
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
||||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||||
import { indentWithTab } from '@codemirror/commands';
|
import { indentWithTab } from '@codemirror/commands';
|
||||||
|
|
@ -13,7 +14,7 @@ import {
|
||||||
lineNumbers,
|
lineNumbers,
|
||||||
drawSelection,
|
drawSelection,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { repl, registerControl } from '@strudel/core';
|
import { repl, registerControl, logger } from '@strudel/core';
|
||||||
import { Drawer, cleanupDraw } from '@strudel/draw';
|
import { Drawer, cleanupDraw } from '@strudel/draw';
|
||||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||||
import { isTooltipEnabled } from './tooltip.mjs';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
|
|
@ -245,6 +246,32 @@ export class StrudelMirror {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('start-repl', this.onStartRepl);
|
document.addEventListener('start-repl', this.onStartRepl);
|
||||||
|
|
||||||
|
// Handle global evaluation requests (e.g., from Vim :w)
|
||||||
|
this.onEvaluateRequest = (e) => {
|
||||||
|
try {
|
||||||
|
// Evaluate current editor on repl-evaluate
|
||||||
|
logger('[repl] evaluate via event');
|
||||||
|
this.evaluate();
|
||||||
|
e?.cancelable && e.preventDefault?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling repl-evaluate event', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('repl-evaluate', this.onEvaluateRequest);
|
||||||
|
document.addEventListener('repl-stop', this.onStopRequest);
|
||||||
|
|
||||||
|
// Toggle comments requested from Vim (gc/gcc)
|
||||||
|
this.onToggleComment = (e) => {
|
||||||
|
try {
|
||||||
|
// Honor selections; toggleLineComment handles both selections and single line
|
||||||
|
toggleLineComment(this.editor);
|
||||||
|
e?.cancelable && e.preventDefault?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling repl-toggle-comment event', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('repl-toggle-comment', this.onToggleComment);
|
||||||
}
|
}
|
||||||
draw(haps, time, painters) {
|
draw(haps, time, painters) {
|
||||||
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
|
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
|
||||||
|
|
@ -271,6 +298,16 @@ export class StrudelMirror {
|
||||||
async stop() {
|
async stop() {
|
||||||
this.repl.scheduler.stop();
|
this.repl.scheduler.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for global stop requests (e.g., from Vim :q)
|
||||||
|
onStopRequest = (e) => {
|
||||||
|
try {
|
||||||
|
this.stop();
|
||||||
|
e?.cancelable && e.preventDefault?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling repl-stop event', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
async toggle() {
|
async toggle() {
|
||||||
if (this.repl.scheduler.started) {
|
if (this.repl.scheduler.started) {
|
||||||
this.repl.stop();
|
this.repl.stop();
|
||||||
|
|
@ -351,6 +388,9 @@ export class StrudelMirror {
|
||||||
}
|
}
|
||||||
clear() {
|
clear() {
|
||||||
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
||||||
|
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
|
||||||
|
this.onStopRequest && document.removeEventListener('repl-stop', this.onStopRequest);
|
||||||
|
this.onToggleComment && document.removeEventListener('repl-toggle-comment', this.onToggleComment);
|
||||||
}
|
}
|
||||||
getCursorLocation() {
|
getCursorLocation() {
|
||||||
return this.editor.state.selection.main.head;
|
return this.editor.state.selection.main.head;
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,137 @@
|
||||||
import { Prec } from '@codemirror/state';
|
import {defaultKeymap} from '@codemirror/commands';
|
||||||
import { keymap, ViewPlugin } from '@codemirror/view';
|
import {Prec} from '@codemirror/state';
|
||||||
|
import {keymap, ViewPlugin} from '@codemirror/view';
|
||||||
// import { searchKeymap } from '@codemirror/search';
|
// import { searchKeymap } from '@codemirror/search';
|
||||||
import { emacs } from '@replit/codemirror-emacs';
|
import {emacs} from '@replit/codemirror-emacs';
|
||||||
import { vim } from '@replit/codemirror-vim';
|
import {vim, Vim} from '@replit/codemirror-vim';
|
||||||
|
import { logger } from '@strudel/core';
|
||||||
// import { vim } from './vim_test.mjs';
|
// import { vim } from './vim_test.mjs';
|
||||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
import {vscodeKeymap} from '@replit/codemirror-vscode-keymap';
|
||||||
import { defaultKeymap } from '@codemirror/commands';
|
|
||||||
|
|
||||||
const vscodePlugin = ViewPlugin.fromClass(
|
const vscodePlugin = ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: () => {
|
provide : () => { return Prec.highest(keymap.of([...vscodeKeymap ])); },
|
||||||
return Prec.highest(keymap.of([...vscodeKeymap]));
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
);
|
);
|
||||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// appears in the Console panel.
|
||||||
|
try {
|
||||||
|
if (Vim && typeof Vim.defineEx === 'function') {
|
||||||
|
// Map gc/gcc to toggle line comments by dispatching a custom event that our
|
||||||
|
// CodeMirror integration listens to. This avoids depending on Vim's internal
|
||||||
|
// actions and works with current selections/visual mode.
|
||||||
|
try {
|
||||||
|
Vim.defineAction('strudelToggleComment', (cm) => {
|
||||||
|
const view = cm?.view || cm;
|
||||||
|
try {
|
||||||
|
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
||||||
|
document.dispatchEvent(ev);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('strudelToggleComment dispatch failed', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'normal' });
|
||||||
|
Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'visual' });
|
||||||
|
Vim.mapCommand('gcc', 'action', 'strudelToggleComment', {}, { context: 'normal' });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Vim gc/gcc mapping failed', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// :q to pause/stop
|
||||||
|
// :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 {}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// :w to evaluate
|
||||||
|
// :w to evaluate
|
||||||
|
Vim.defineEx('write', 'w', (cm) => {
|
||||||
|
const view =
|
||||||
|
cm?.view ||
|
||||||
|
cm; // CM6 Vim passes either an object with view or the view itself
|
||||||
|
try {
|
||||||
|
view?.focus?.();
|
||||||
|
// Let the app know this came from Vim :w
|
||||||
|
try {
|
||||||
|
logger('[vim] :w — evaluating code');
|
||||||
|
} catch {}
|
||||||
|
// 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 {}
|
||||||
|
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 :w evaluation event', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
|
||||||
|
}
|
||||||
|
|
||||||
const keymaps = {
|
const keymaps = {
|
||||||
vim,
|
vim,
|
||||||
|
// Add extra Vim keymap for gc to toggle line comment
|
||||||
|
// We will include a Vim-specific keymap that calls the CM command
|
||||||
|
// respecting the current selection.
|
||||||
emacs,
|
emacs,
|
||||||
codemirror: () => keymap.of(defaultKeymap),
|
codemirror : () => keymap.of(defaultKeymap),
|
||||||
vscode: vscodeExtension,
|
vscode : vscodeExtension,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function keybindings(name) {
|
export function keybindings(name) {
|
||||||
const active = keymaps[name];
|
const active = keymaps[name];
|
||||||
return [active ? Prec.high(active()) : []];
|
return [ active ? Prec.high(active()) : [] ];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,10 @@ Here's an example AST for `c3 [e3 g3]`
|
||||||
|
|
||||||
which translates to `seq(c3, seq(e3, g3))`
|
which translates to `seq(c3, seq(e3, g3))`
|
||||||
|
|
||||||
|
## Vim Keybindings
|
||||||
|
|
||||||
|
See the separate page on Vim shortcuts for a quick reference: [/technical-manual/vim](/technical-manual/vim)
|
||||||
|
|
||||||
## Scheduling Events
|
## Scheduling Events
|
||||||
|
|
||||||
After an instance of `Pattern` is obtained from the user code,
|
After an instance of `Pattern` is obtained from the user code,
|
||||||
|
|
|
||||||
36
website/src/pages/technical-manual/vim.mdx
Normal file
36
website/src/pages/technical-manual/vim.mdx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
title: Vim Shortcuts
|
||||||
|
layout: ../../layouts/MainLayout.astro
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vim Shortcuts in the REPL
|
||||||
|
|
||||||
|
When the REPL editor (CodeMirror) is configured to use Vim keybindings, the following commands are available:
|
||||||
|
|
||||||
|
- :w — Evaluate the current code
|
||||||
|
- Triggers the same evaluation as Ctrl+Enter / Alt+Enter
|
||||||
|
- You’ll see messages in the Console panel such as:
|
||||||
|
- [vim] :w — evaluating code
|
||||||
|
- [repl] evaluate via event
|
||||||
|
- [eval] code updated
|
||||||
|
|
||||||
|
- :q — Stop/pause playback
|
||||||
|
- Triggers the same stop action as Alt+.
|
||||||
|
- Useful to quickly stop scheduling without leaving Vim mode
|
||||||
|
|
||||||
|
- gc — Toggle line comments for the current selection(s)
|
||||||
|
- Works in normal and visual mode
|
||||||
|
- If there’s a selection, all selected lines are toggled
|
||||||
|
|
||||||
|
- gcc — Toggle comment for the current line (normal mode)
|
||||||
|
|
||||||
|
Notes
|
||||||
|
|
||||||
|
- Behavior respects the current language mode in the editor for comment syntax.
|
||||||
|
- If multiple REPL editors are open, commands target the active editor. The implementation dispatches custom events handled by the editor.
|
||||||
|
- If you don’t see the Console panel, open the right panel in the REPL UI.
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
|
||||||
|
- If :w logs but evaluation doesn’t apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback.
|
||||||
|
- For :q / gc / gcc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue