Compare commits

..

6 Commits

8 changed files with 101 additions and 257 deletions
+2 -50
View File
@@ -1,5 +1,5 @@
import { closeBrackets } from '@codemirror/autocomplete';
import { indentWithTab, toggleLineComment } from '@codemirror/commands';
import { indentWithTab } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
@@ -12,7 +12,7 @@ import {
lineNumbers,
} from '@codemirror/view';
import { persistentAtom } from '@nanostores/persistent';
import { logger, registerControl, repl } from '@strudel/core';
import { registerControl, repl } from '@strudel/core';
import { cleanupDraw, Drawer } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
@@ -257,39 +257,6 @@ export class StrudelMirror {
}
};
document.addEventListener('start-repl', this.onStartRepl);
// Handle global evaluation requests (e.g., from Vim :w)
this.onEvaluateRequest = (e) => {
try {
if (e.detail.view !== this.editor) {
return; // ignore events from other editors
}
// 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)
this.onToggleComment = (e) => {
try {
if (e.detail.view !== this.editor) {
return; // ignore events from other editors
}
// 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) {
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
@@ -317,18 +284,6 @@ export class StrudelMirror {
this.repl.scheduler.stop();
}
// Listen for global stop requests (e.g., from Vim :q)
onStopRequest = (e) => {
try {
if (e.detail.view !== this.editor) {
return; // ignore events from other editors
}
this.stop();
e?.cancelable && e.preventDefault?.();
} catch (err) {
console.error('Error handling repl-stop event', err);
}
};
async toggle() {
if (this.repl.scheduler.started) {
this.repl.stop();
@@ -415,9 +370,6 @@ export class StrudelMirror {
}
clear() {
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() {
return this.editor.state.selection.main.head;
+1 -106
View File
@@ -3,10 +3,9 @@ import { Prec, EditorState } from '@codemirror/state';
import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs';
import { vim, Vim } from '@replit/codemirror-vim';
import { vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { logger } from '@strudel/core';
const vscodePlugin = ViewPlugin.fromClass(
class {
@@ -20,110 +19,6 @@ const vscodePlugin = ViewPlugin.fromClass(
);
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 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.cm6;
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' });
} catch (e) {
console.error('Vim gc mapping failed', e);
}
// :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);
}
});
// :w to evaluate
Vim.defineEx('write', 'w', (cm) => {
const view = cm.cm6;
try {
view.focus?.();
// Let the app know this came from Vim :w
try {
logger('[vim] :w — evaluating code');
} 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);
}
} 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 = {
vim,
emacs,
+23
View File
@@ -1143,3 +1143,26 @@ export const perx = new Pattern(function (state) {
const n = Fraction(1).div(state.span.duration);
return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)];
});
/**
* A signal that goes from `0` to `1` over one cycle, and then stays at `1` forever.
* Mainly useful in combination with other functions that reset the cycle. See also
* `close`.
* @tags temporal
* @example
* sound("bd sd [- bd] sd".fast(2))
* .lpf(open.slow(4).range(300,3000)) // close envelope over 4 cycles
* .restart("x/4") // restart every 4 cycles
*/
export const open = signal((t) => t.min(1));
/**
* The same as `open`, but goes from `1` to `0` over one cycle, and then
* stays on `0` forever.
* @tags temporal
* @example
* sound("bd sd [- bd] sd".fast(2))
* .lpf(close.slow(4).range(300,3000)) // close envelope over 4 cycles
* .restart("x/4") // restart every 4 cycles
*/
export const close = signal((t) => Fraction(1).sub(t).max(0));
+74
View File
@@ -2440,6 +2440,43 @@ exports[`runs examples > example "clip" example index 0 1`] = `
]
`;
exports[`runs examples > example "close" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd cutoff:3000 ]",
"[ 1/8 → 1/4 | s:sd cutoff:2915.625 ]",
"[ 5/16 → 3/8 | s:bd cutoff:2789.0625 ]",
"[ 3/8 → 1/2 | s:sd cutoff:2746.875 ]",
"[ 1/2 → 5/8 | s:bd cutoff:2662.5 ]",
"[ 5/8 → 3/4 | s:sd cutoff:2578.125 ]",
"[ 13/16 → 7/8 | s:bd cutoff:2451.5625 ]",
"[ 7/8 → 1/1 | s:sd cutoff:2409.375 ]",
"[ 1/1 → 9/8 | s:bd cutoff:2325 ]",
"[ 9/8 → 5/4 | s:sd cutoff:2240.625 ]",
"[ 21/16 → 11/8 | s:bd cutoff:2114.0625 ]",
"[ 11/8 → 3/2 | s:sd cutoff:2071.875 ]",
"[ 3/2 → 13/8 | s:bd cutoff:1987.5 ]",
"[ 13/8 → 7/4 | s:sd cutoff:1903.125 ]",
"[ 29/16 → 15/8 | s:bd cutoff:1776.5625 ]",
"[ 15/8 → 2/1 | s:sd cutoff:1734.375 ]",
"[ 2/1 → 17/8 | s:bd cutoff:1650 ]",
"[ 17/8 → 9/4 | s:sd cutoff:1565.625 ]",
"[ 37/16 → 19/8 | s:bd cutoff:1439.0625 ]",
"[ 19/8 → 5/2 | s:sd cutoff:1396.875 ]",
"[ 5/2 → 21/8 | s:bd cutoff:1312.5 ]",
"[ 21/8 → 11/4 | s:sd cutoff:1228.125 ]",
"[ 45/16 → 23/8 | s:bd cutoff:1101.5625 ]",
"[ 23/8 → 3/1 | s:sd cutoff:1059.375 ]",
"[ 3/1 → 25/8 | s:bd cutoff:975 ]",
"[ 25/8 → 13/4 | s:sd cutoff:890.625 ]",
"[ 53/16 → 27/8 | s:bd cutoff:764.0625 ]",
"[ 27/8 → 7/2 | s:sd cutoff:721.875 ]",
"[ 7/2 → 29/8 | s:bd cutoff:637.5 ]",
"[ 29/8 → 15/4 | s:sd cutoff:553.125 ]",
"[ 61/16 → 31/8 | s:bd cutoff:426.5625 ]",
"[ 31/8 → 4/1 | s:sd cutoff:384.375 ]",
]
`;
exports[`runs examples > example "coarse" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh coarse:1 ]",
@@ -8764,6 +8801,43 @@ exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
]
`;
exports[`runs examples > example "open" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd cutoff:300 ]",
"[ 1/8 → 1/4 | s:sd cutoff:384.375 ]",
"[ 5/16 → 3/8 | s:bd cutoff:510.9375 ]",
"[ 3/8 → 1/2 | s:sd cutoff:553.125 ]",
"[ 1/2 → 5/8 | s:bd cutoff:637.5 ]",
"[ 5/8 → 3/4 | s:sd cutoff:721.875 ]",
"[ 13/16 → 7/8 | s:bd cutoff:848.4375 ]",
"[ 7/8 → 1/1 | s:sd cutoff:890.625 ]",
"[ 1/1 → 9/8 | s:bd cutoff:975 ]",
"[ 9/8 → 5/4 | s:sd cutoff:1059.375 ]",
"[ 21/16 → 11/8 | s:bd cutoff:1185.9375 ]",
"[ 11/8 → 3/2 | s:sd cutoff:1228.125 ]",
"[ 3/2 → 13/8 | s:bd cutoff:1312.5 ]",
"[ 13/8 → 7/4 | s:sd cutoff:1396.875 ]",
"[ 29/16 → 15/8 | s:bd cutoff:1523.4375 ]",
"[ 15/8 → 2/1 | s:sd cutoff:1565.625 ]",
"[ 2/1 → 17/8 | s:bd cutoff:1650 ]",
"[ 17/8 → 9/4 | s:sd cutoff:1734.375 ]",
"[ 37/16 → 19/8 | s:bd cutoff:1860.9375 ]",
"[ 19/8 → 5/2 | s:sd cutoff:1903.125 ]",
"[ 5/2 → 21/8 | s:bd cutoff:1987.5 ]",
"[ 21/8 → 11/4 | s:sd cutoff:2071.875 ]",
"[ 45/16 → 23/8 | s:bd cutoff:2198.4375 ]",
"[ 23/8 → 3/1 | s:sd cutoff:2240.625 ]",
"[ 3/1 → 25/8 | s:bd cutoff:2325 ]",
"[ 25/8 → 13/4 | s:sd cutoff:2409.375 ]",
"[ 53/16 → 27/8 | s:bd cutoff:2535.9375 ]",
"[ 27/8 → 7/2 | s:sd cutoff:2578.125 ]",
"[ 7/2 → 29/8 | s:bd cutoff:2662.5 ]",
"[ 29/8 → 15/4 | s:sd cutoff:2746.875 ]",
"[ 61/16 → 31/8 | s:bd cutoff:2873.4375 ]",
"[ 31/8 → 4/1 | s:sd cutoff:2915.625 ]",
]
`;
exports[`runs examples > example "orbit" example index 0 1`] = `
[
"[ 0/1 → 1/6 | s:hh delay:0.5 delaytime:0.25 orbit:1 ]",
@@ -117,10 +117,6 @@ Here's an example AST for `c3 [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
After an instance of `Pattern` is obtained from the user code,
@@ -1,58 +0,0 @@
---
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
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, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again.
## Adding custom keybindings
To add custom keybindings the `Vim` object can be used (either from within a pattern or in a prebake script)
Example:
```javascript
// Map 'jk' to Escape in normal mode
Vim.map('jk', '<Esc>', 'insert');
// Map 'U' to :w (Evaluate the current code)
Vim.map('U', ':w<CR>', 'normal');
// Map 'Q' to :q — Stop/pause playback
Vim.map('Q', ':q<CR>', 'normal');
// Map 'J' to find next '$' (jump to next label)
Vim.map('J', '/\\$<CR>', 'normal');
// Map 'K' to find previous '$' (jump to previous label)
Vim.map('K', '?\\$<CR>', 'normal');
```
For more information on how to use the `Vim` object see [CodeMirror Vim](https://github.com/replit/codemirror-vim)
@@ -9,7 +9,7 @@ import { confirmDialog } from '../../util.mjs';
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
import { ActionButton } from '../button/action-button.jsx';
import { exportScript, ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
import { useEffect, useRef } from 'react';
import { useRef } from 'react';
import cx from '@src/cx.mjs';
const inputClass =
@@ -350,11 +350,6 @@ function MainSettingsContent({ started }) {
function PrebakeSettingsContent() {
const { fontFamily, includePrebakeScriptInShare, prebakeScript } = useSettings();
const editorRef = useRef();
useEffect(() => {
return () => {
editorRef.current?.cleanup();
};
});
return (
<div className="flex flex-col h-full text-foreground w-full overflow-auto" style={{ fontFamily }}>
-33
View File
@@ -1,4 +1,3 @@
import { toggleLineComment } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
@@ -63,28 +62,6 @@ export class PrebakeCodeMirror {
state,
parent: container,
});
const handleSaveEvent = async (e) => {
if (e.detail.view !== this.view) {
return; // ignore events from other editors
}
await this.savePrebake();
e?.cancelable && e.preventDefault?.();
};
const handleToggleComment = (e) => {
if (e.detail.view !== this.view) {
return; // ignore events from other editors
}
this.toggleComment();
e?.cancelable && e.preventDefault?.();
};
document.addEventListener('repl-evaluate', handleSaveEvent);
document.addEventListener('repl-toggle-comment', handleToggleComment);
this.cleanup = () => {
document.removeEventListener('prebake-evaluate', handleSaveEvent);
document.removeEventListener('prebake-toggle-comment', handleToggleComment);
};
}
async savePrebake() {
@@ -94,16 +71,6 @@ export class PrebakeCodeMirror {
logger('[prebake] prebake saved');
}
toggleComment() {
try {
// Honor selections; toggleLineComment handles both selections and
// single line
toggleLineComment(this.view);
} catch (err) {
console.error('Error handling repl-toggle-comment event', err);
}
}
setCode(code) {
const changes = {
from: 0,