Merge pull request 'feat: Add Helix keybindings' (#1821) from space-shell/strudel:space-shell/helix-keybindings into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1821
This commit is contained in:
commit
90ad12300a
5 changed files with 213 additions and 56 deletions
|
|
@ -6,6 +6,7 @@ import { emacs } from '@replit/codemirror-emacs';
|
||||||
import { vim, Vim } from '@replit/codemirror-vim';
|
import { vim, Vim } from '@replit/codemirror-vim';
|
||||||
// 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 { helix, commands } from 'codemirror-helix';
|
||||||
import { logger } from '@strudel/core';
|
import { logger } from '@strudel/core';
|
||||||
|
|
||||||
const vscodePlugin = ViewPlugin.fromClass(
|
const vscodePlugin = ViewPlugin.fromClass(
|
||||||
|
|
@ -20,6 +21,70 @@ const vscodePlugin = ViewPlugin.fromClass(
|
||||||
);
|
);
|
||||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
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
|
// 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
|
// 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
|
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
|
||||||
|
|
@ -47,29 +112,8 @@ try {
|
||||||
|
|
||||||
// :q to pause/stop
|
// :q to pause/stop
|
||||||
Vim.defineEx('quit', 'q', (cm) => {
|
Vim.defineEx('quit', 'q', (cm) => {
|
||||||
try {
|
const view = cm?.view || cm;
|
||||||
const view = cm?.view || cm;
|
replStop(view);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// :w to evaluate
|
// :w to evaluate
|
||||||
|
|
@ -83,38 +127,7 @@ try {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error logging Vim :w evaluation', e);
|
console.error('Error logging Vim :w evaluation', e);
|
||||||
}
|
}
|
||||||
// Dispatch a dedicated evaluate event first
|
replEval(view);
|
||||||
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) {
|
} catch (e) {
|
||||||
console.error('Error dispatching :w evaluation event', e);
|
console.error('Error dispatching :w evaluation event', e);
|
||||||
}
|
}
|
||||||
|
|
@ -124,11 +137,49 @@ try {
|
||||||
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
|
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 = {
|
const keymaps = {
|
||||||
vim,
|
vim,
|
||||||
emacs,
|
emacs,
|
||||||
codemirror: () => keymap.of(defaultKeymap),
|
codemirror: () => keymap.of(defaultKeymap),
|
||||||
vscode: vscodeExtension,
|
vscode: vscodeExtension,
|
||||||
|
helix: () => [helix(), helixCommands],
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Vim } from '@replit/codemirror-vim';
|
export { Vim } from '@replit/codemirror-vim';
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
"@replit/codemirror-emacs": "^6.1.0",
|
"@replit/codemirror-emacs": "^6.1.0",
|
||||||
"@replit/codemirror-vim": "^6.3.0",
|
"@replit/codemirror-vim": "^6.3.0",
|
||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
|
"codemirror-helix": "^0.5.0",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
|
|
|
||||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
|
|
@ -221,6 +221,9 @@ importers:
|
||||||
'@tonaljs/tonal':
|
'@tonaljs/tonal':
|
||||||
specifier: ^4.10.0
|
specifier: ^4.10.0
|
||||||
version: 4.10.0
|
version: 4.10.0
|
||||||
|
codemirror-helix:
|
||||||
|
specifier: ^0.5.0
|
||||||
|
version: 0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||||
nanostores:
|
nanostores:
|
||||||
specifier: ^0.11.3
|
specifier: ^0.11.3
|
||||||
version: 0.11.3
|
version: 0.11.3
|
||||||
|
|
@ -3551,6 +3554,15 @@ packages:
|
||||||
resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==}
|
resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
|
codemirror-helix@0.5.0:
|
||||||
|
resolution: {integrity: sha512-hI56hf9VGz53H1YvL6H1GC7HtP6te8vX+MsIHaE9J7Q3PQ6KFapKtIRg6lqSH898ikHWpMCPu42r6HJN0IfVLA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@codemirror/commands': ^6.0.0
|
||||||
|
'@codemirror/language': ^6.0.0
|
||||||
|
'@codemirror/search': ^6.0.0
|
||||||
|
'@codemirror/state': ^6.0.0
|
||||||
|
'@codemirror/view': ^6.0.0
|
||||||
|
|
||||||
collapse-white-space@2.1.0:
|
collapse-white-space@2.1.0:
|
||||||
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
||||||
|
|
||||||
|
|
@ -11280,6 +11292,14 @@ snapshots:
|
||||||
|
|
||||||
cmd-shim@6.0.3: {}
|
cmd-shim@6.0.3: {}
|
||||||
|
|
||||||
|
codemirror-helix@0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2):
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/commands': 6.8.0
|
||||||
|
'@codemirror/language': 6.10.8
|
||||||
|
'@codemirror/search': 6.5.8
|
||||||
|
'@codemirror/state': 6.5.1
|
||||||
|
'@codemirror/view': 6.36.2
|
||||||
|
|
||||||
collapse-white-space@2.1.0: {}
|
collapse-white-space@2.1.0: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
|
|
|
||||||
85
website/src/pages/technical-manual/helix.mdx
Normal file
85
website/src/pages/technical-manual/helix.mdx
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
---
|
||||||
|
title: Helix Keybindings
|
||||||
|
layout: ../../layouts/MainLayout.astro
|
||||||
|
---
|
||||||
|
|
||||||
|
# Helix Keybindings in the REPL
|
||||||
|
|
||||||
|
The Strudel REPL supports [Helix editor](https://helix-editor.com/) keybindings through the `codemirror-helix` extension. Helix is a post-modern modal text editor with a focus on selection-first editing and multiple cursors.
|
||||||
|
|
||||||
|
## Enabling Helix Mode
|
||||||
|
|
||||||
|
1. Open the Settings panel in the REPL (click the settings icon or use the keyboard shortcut)
|
||||||
|
2. Find the "Keybindings" section
|
||||||
|
3. Select "Helix" from the available options
|
||||||
|
|
||||||
|
## Key Differences from Vim
|
||||||
|
|
||||||
|
Helix uses a selection-first approach, which means:
|
||||||
|
|
||||||
|
- **Selection → Action** (Helix) vs **Action → Motion** (Vim)
|
||||||
|
- For example, to delete a word in Helix: `w` (select word) → `d` (delete)
|
||||||
|
- In Vim, it would be: `dw` (delete word)
|
||||||
|
|
||||||
|
## Common Helix Commands
|
||||||
|
|
||||||
|
### Normal Mode
|
||||||
|
|
||||||
|
- `i` — Enter insert mode before selection
|
||||||
|
- `a` — Enter insert mode after selection
|
||||||
|
- `v` — Enter visual/select mode
|
||||||
|
- `w` — Select next word
|
||||||
|
- `e` — Select to end of word
|
||||||
|
- `b` — Select previous word
|
||||||
|
- `x` — Select/extend line
|
||||||
|
- `d` — Delete selection
|
||||||
|
- `c` — Change selection (delete and enter insert mode)
|
||||||
|
- `y` — Yank (copy) selection
|
||||||
|
- `p` — Paste after selection
|
||||||
|
- `u` — Undo
|
||||||
|
- `U` — Redo
|
||||||
|
- `/` — Search
|
||||||
|
- `n` — Select next search match
|
||||||
|
- `N` — Select previous search match
|
||||||
|
|
||||||
|
### Movement
|
||||||
|
|
||||||
|
- `h, j, k, l` — Move left, down, up, right
|
||||||
|
- `gg` — Go to start of document
|
||||||
|
- `ge` — Go to end of document
|
||||||
|
- `{` / `}` — Move to previous/next paragraph
|
||||||
|
- `%` — Match bracket
|
||||||
|
|
||||||
|
### Multi-cursor
|
||||||
|
|
||||||
|
- `C` — Duplicate cursor to line below
|
||||||
|
- `Alt-C` — Duplicate cursor to line above
|
||||||
|
- `,` — Remove primary cursor
|
||||||
|
- `Alt-,` — Remove all secondary cursors
|
||||||
|
|
||||||
|
## Strudel-Specific Features
|
||||||
|
|
||||||
|
Currently, Helix mode in Strudel provides standard Helix keybindings. Unlike Vim mode, there are no custom Strudel-specific commands (like `:w` to evaluate) yet.
|
||||||
|
|
||||||
|
To evaluate code while in Helix mode, use the standard shortcuts:
|
||||||
|
|
||||||
|
- `Ctrl+Enter` or `Alt+Enter` — Evaluate code
|
||||||
|
- `Alt+.` — Stop playback
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Helix Documentation](https://docs.helix-editor.com/)
|
||||||
|
- [Helix Keymap](https://docs.helix-editor.com/keymap.html)
|
||||||
|
- [codemirror-helix on npm](https://www.npmjs.com/package/codemirror-helix)
|
||||||
|
- [codemirror-helix on GitLab](https://gitlab.com/_rvidal/codemirror-helix)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `codemirror-helix` extension is described as an "initial version" and may not implement all Helix features
|
||||||
|
- Behavior may differ slightly from the native Helix editor
|
||||||
|
- If you encounter issues, you can switch back to other keybinding modes in Settings
|
||||||
|
- The keybinding preference is saved in your browser's local storage
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you'd like to add Strudel-specific Helix commands (similar to Vim's `:w` for evaluate), contributions are welcome! See the implementation in `/packages/codemirror/keybindings.mjs` for reference.
|
||||||
|
|
@ -246,7 +246,7 @@ export function SettingsTab({ started }) {
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
value={keybindings}
|
value={keybindings}
|
||||||
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
|
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
|
||||||
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs', vscode: 'VSCode' }}
|
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs', helix: 'Helix', vscode: 'VSCode' }}
|
||||||
></ButtonGroup>
|
></ButtonGroup>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="Panel Position">
|
<FormItem label="Panel Position">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue