intermediate /vanilla repl

+ react is now only used for ui around editor!
+ ui still not fully functional
This commit is contained in:
Felix Roos
2023-11-02 16:31:45 +01:00
parent 07d6bb3c44
commit 7c00aa1a36
10 changed files with 267 additions and 118 deletions
+18 -5
View File
@@ -1,19 +1,21 @@
import { defaultKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { EditorState, Compartment } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
import { Drawer, repl } from '@strudel.cycles/core';
import { flashField, flash } from './flash.mjs';
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
import { highlightExtension, highlightMiniLocations, updateMiniLocations } from './highlight.mjs';
import { oneDark } from './themes/one-dark';
const themeComparment = new Compartment();
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
let state = EditorState.create({
doc: initialCode,
extensions: [
theme,
themeComparment.of(theme),
javascript(),
lineNumbers(),
highlightExtension,
@@ -43,7 +45,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
export class StrudelMirror {
constructor(options) {
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, theme, ...replOptions } = options;
this.code = initialCode;
this.drawer = new Drawer((haps, time) => {
@@ -81,12 +83,14 @@ export class StrudelMirror {
await prebaked;
},
afterEval: (options) => {
updateMiniLocations(this.editor, options.meta?.miniLocations);
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
theme,
initialCode,
onChange: (v) => {
this.code = v.state.doc.toString();
@@ -108,6 +112,15 @@ export class StrudelMirror {
flash(this.editor, ms);
}
highlight(haps, time) {
highlightMiniLocations(this.editor.view, time, haps);
highlightMiniLocations(this.editor, time, haps);
}
setTheme(theme) {
this.editor.dispatch({
effects: themeComparment.reconfigure(theme),
});
}
setCode(code) {
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
this.editor.dispatch({ changes });
}
}
+2 -1
View File
@@ -31,7 +31,8 @@
},
"homepage": "https://strudel.cc",
"dependencies": {
"fraction.js": "^4.2.0"
"fraction.js": "^4.2.0",
"nanostores": "^0.8.1"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
+28 -1
View File
@@ -4,6 +4,19 @@ import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register } from './pattern.mjs';
import { atom } from 'nanostores';
export const $replstate = atom({
schedulerError: undefined,
evalError: undefined,
code: '// LOADING',
activeCode: '// LOADING',
pattern: undefined,
miniLocations: [],
widgets: [],
pending: true,
});
export const setReplState = (key, value) => $replstate.set({ ...$replstate.get(), [key]: value });
export function repl({
interval,
@@ -22,7 +35,10 @@ export function repl({
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle,
onToggle: (started) => {
setReplState('started', started);
onToggle?.(started);
},
});
let playPatterns = [];
const setPattern = (pattern, autostart = true) => {
@@ -35,6 +51,8 @@ export function repl({
throw new Error('no code to evaluate');
}
try {
setReplState('code', code);
setReplState('pending', true);
await beforeEval?.({ code });
playPatterns = [];
let { pattern, meta } = await _evaluate(code, transpiler);
@@ -43,11 +61,20 @@ export function repl({
}
logger(`[eval] code updated`);
setPattern(pattern, autostart);
setReplState('miniLocations', meta?.miniLocations || []);
setReplState('widgets', meta?.widgets || []);
setReplState('activeCode', code);
setReplState('pattern', pattern);
setReplState('evalError', undefined);
setReplState('schedulerError', undefined);
setReplState('pending', false);
afterEval?.({ code, pattern, meta });
return pattern;
} catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
logger(`[eval] error: ${err.message}`, 'error');
setReplState('evalError', err);
setReplState('pending', false);
onEvalError?.(err);
}
};
+3
View File
@@ -102,6 +102,9 @@ importers:
fraction.js:
specifier: ^4.2.0
version: 4.2.0
nanostores:
specifier: ^0.8.1
version: 0.8.1
devDependencies:
vite:
specifier: ^4.3.3
+1 -1
View File
@@ -28,7 +28,7 @@ export function MiniRepl({
useEffect(() => {
// we have to load this package on the client
// because codemirror throws an error on the server
Promise.all([import('@strudel.cycles/react')])
Promise.all([import('@strudel.cycles/react'), init])
.then(([res]) => setRepl(() => res.MiniRepl))
.catch((err) => console.error(err));
}, []);
+14
View File
@@ -0,0 +1,14 @@
---
import HeadCommon from '../components/HeadCommon.astro';
import VanillaRepl from '../repl/VanillaRepl.astro';
---
<html lang="en" class="dark">
<head>
<HeadCommon />
<title>Strudel Vanilla REPL</title>
</head>
<body class="h-app-height bg-background">
<VanillaRepl />
</body>
</html>
+30 -109
View File
@@ -5,32 +5,20 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import { getDrawContext, logger } from '@strudel.cycles/core';
import { CodeMirror, cx } from '@strudel.cycles/react';
import { getAudioContext, resetLoadedSounds } from '@strudel.cycles/webaudio';
import { createClient } from '@supabase/supabase-js';
import { getDrawContext, logger, $replstate } from '@strudel.cycles/core';
import { cx } from '@strudel.cycles/react';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { writeText } from '@tauri-apps/api/clipboard';
import { nanoid } from 'nanoid';
import { createContext, useEffect, useMemo, useState } from 'react';
import { settingsMap, useSettings } from '../settings.mjs';
import { createContext, useState } from 'react';
import { useSettings } from '../settings.mjs';
import { isTauri } from '../tauri.mjs';
import { Footer } from './Footer';
import { Header } from './Header';
import Loader from './Loader';
import './Repl.css';
import { hash2code, code2hash } from './helpers.mjs';
import * as tunes from './tunes.mjs';
import { useRepl } from './useRepl';
import { setLatestCode } from '../settings.mjs';
import { resetSounds } from './prebake.mjs';
const { latestCode } = settingsMap.get();
// Create a single supabase client for interacting with your database
const supabase = createClient(
'https://pidxdsxphlhzjnzmifth.supabase.co',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
);
import { useStore } from '@nanostores/react';
let clearCanvas;
if (typeof window !== 'undefined') {
@@ -38,106 +26,30 @@ if (typeof window !== 'undefined') {
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
}
async function initCode() {
// load code from url hash (either short hash from database or decode long hash)
try {
const initialUrl = window.location.href;
const hash = initialUrl.split('?')[1]?.split('#')?.[0];
const codeParam = window.location.href.split('#')[1] || '';
// looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
if (codeParam) {
// looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length)
return hash2code(codeParam);
} else if (hash) {
return supabase
.from('code')
.select('code')
.eq('hash', hash)
.then(({ data, error }) => {
if (error) {
console.warn('failed to load hash', err);
}
if (data.length) {
//console.log('load hash from database', hash);
return data[0].code;
}
});
}
} catch (err) {
console.warn('failed to decode', err);
}
}
function getRandomTune() {
const allTunes = Object.entries(tunes);
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
const [name, code] = randomItem(allTunes);
return { name, code };
}
const { code: randomTune, name } = getRandomTune();
export const ReplContext = createContext(null);
export function Repl({ embedded = false }) {
const isEmbedded = embedded || window.location !== window.parent.location;
//const isEmbedded = embedded || window.location !== window.parent.location;
const isEmbedded = false;
const [lastShared, setLastShared] = useState();
const { panelPosition, isZen } = useSettings();
const {
codemirror,
code,
setCode,
scheduler,
evaluate,
activateCode,
isDirty,
activeCode,
pattern,
started,
stop,
error,
pending,
setPending,
} = useRepl({
afterEval: ({ code }) => {
setLatestCode(code);
window.location.hash = '#' + code2hash(code);
},
});
// init code
useEffect(() => {
initCode().then((decoded) => {
let msg;
if (decoded) {
setCode(decoded);
msg = `I have loaded the code from the URL.`;
} else if (latestCode) {
setCode(latestCode);
msg = `Your last session has been loaded!`;
} /* if(randomTune) */ else {
setCode(randomTune);
msg = `A random code snippet named "${name}" has been loaded!`;
}
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
setPending(false);
});
}, []);
const replState = useStore($replstate);
//
// UI Actions
//
const handleTogglePlay = async () => {
await getAudioContext().resume(); // fixes no sound in ios webkit
console.log('toggle.');
window.postMessage('strudel-toggle-play');
/* await getAudioContext().resume(); // fixes no sound in ios webkit
if (!started) {
logger('[repl] started. tip: you can also start by pressing ctrl+enter', 'highlight');
activateCode();
} else {
logger('[repl] stopped. tip: you can also stop by pressing ctrl+dot', 'highlight');
stop();
}
} */
};
const handleUpdate = () => {
isDirty && activateCode();
@@ -149,7 +61,7 @@ export function Repl({ embedded = false }) {
logger(`[repl] ✨ loading random tune "${name}"`);
clearCanvas();
await resetSounds();
scheduler.setCps(1);
// scheduler.setCps(1);
await evaluate(code, false);
};
@@ -182,15 +94,18 @@ export function Repl({ embedded = false }) {
logger(message);
}
};
const pending = false;
const error = undefined;
const { started, activeCode } = replState;
const context = {
scheduler,
// scheduler,
embedded,
started,
pending,
isDirty,
isDirty: false,
lastShared,
activeCode,
handleChangeCode: codemirror.handleChangeCode,
// handleChangeCode: codemirror.handleChangeCode,
handleTogglePlay,
handleUpdate,
handleShuffle,
@@ -209,7 +124,7 @@ export function Repl({ embedded = false }) {
>
<Loader active={pending} />
<Header context={context} />
{isEmbedded && !started && (
{/* isEmbedded && !started && (
<button
onClick={() => handleTogglePlay()}
className="text-white text-2xl fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-[1000] m-auto p-4 bg-black rounded-md flex items-center space-x-2"
@@ -217,10 +132,16 @@ export function Repl({ embedded = false }) {
<PlayCircleIcon className="w-6 h-6" />
<span>play</span>
</button>
)}
) */}
<div className="grow flex relative overflow-hidden">
<section className={'text-gray-100 cursor-text pb-0 overflow-auto grow' + (isZen ? ' px-10' : '')} id="code">
<CodeMirror {...codemirror} />
<section
className={'text-gray-100 cursor-text pb-0 overflow-auto grow' + (isZen ? ' px-10' : '')}
id="code"
ref={() => {
window.postMessage('strudel-container');
}}
>
{/* <CodeMirror {...codemirror} /> */}
</section>
{panelPosition === 'right' && !isEmbedded && <Footer context={context} />}
</div>
+15
View File
@@ -0,0 +1,15 @@
---
import HeadCommon from '../components/HeadCommon.astro';
import { Repl } from '../repl/Repl.jsx';
---
<html lang="en" class="dark">
<head>
<HeadCommon />
<title>Strudel REPL</title>
</head>
<body class="h-app-height bg-background">
<Repl client:only="react" />
<script src="./vanillarepl.mjs"></script>
</body>
</html>
+1 -1
View File
@@ -150,7 +150,7 @@ export async function prebake() {
...modules,
);
// register sounds and samples
return Promise.all([initAudio, modulesLoading, registerStockSounds()]);
return Promise.all([/* initAudio, */ modulesLoading, registerStockSounds()]);
// await samples('github:tidalcycles/Dirt-Samples/master');
}
+155
View File
@@ -0,0 +1,155 @@
import { StrudelMirror } from '@strudel/codemirror';
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import { prebake } from './prebake.mjs';
import { settingsMap } from '@src/settings.mjs';
import { themes } from '@src/repl/themes.mjs';
import { setLatestCode } from '../settings.mjs';
import { hash2code, code2hash } from './helpers.mjs';
import { createClient } from '@supabase/supabase-js';
import * as tunes from './tunes.mjs';
const onEvent = (key, callback) => {
const listener = (e) => {
if (e.data === key) {
callback();
}
};
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener);
};
function run() {
const { latestCode } = settingsMap.get();
/* let clearCanvas;
if (typeof window !== 'undefined') {
const drawContext = getDrawContext();
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
} */
// Create a single supabase client for interacting with your database
const supabase = createClient(
'https://pidxdsxphlhzjnzmifth.supabase.co',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
);
async function initCode() {
// load code from url hash (either short hash from database or decode long hash)
try {
const initialUrl = window.location.href;
const hash = initialUrl.split('?')[1]?.split('#')?.[0];
const codeParam = window.location.href.split('#')[1] || '';
// looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
if (codeParam) {
// looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length)
return hash2code(codeParam);
} else if (hash) {
return supabase
.from('code')
.select('code')
.eq('hash', hash)
.then(({ data, error }) => {
if (error) {
console.warn('failed to load hash', error);
}
if (data.length) {
//console.log('load hash from database', hash);
return data[0].code;
}
});
}
} catch (err) {
console.warn('failed to decode', err);
}
}
const container = document.getElementById('code');
const setFontSize = (size) => (container.style.fontSize = size + 'px');
const setFontFamily = (family) => (container.style.fontFamily = family);
/* const drawContext = getDrawContext()
const drawTime = [-2, 2]; */
console.log('container', container);
const editor = new StrudelMirror({
theme: themes.strudelTheme,
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
root: container,
initialCode: '// LOADING',
/* drawTime,
onDraw: (haps, time) =>
drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 1 }), */
prebake: () => prebake(),
afterEval: ({ code }) => {
setLatestCode(code);
window.location.hash = '#' + code2hash(code);
},
});
function getRandomTune() {
const allTunes = Object.entries(tunes);
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
const [name, code] = randomItem(allTunes);
return { name, code };
}
const { code: randomTune, name } = getRandomTune();
function init() {
if (!container) {
console.warn('could not init: no container found');
return;
}
const settings = settingsMap.get();
setFontSize(settings.fontSize);
setFontFamily(settings.fontFamily);
initCode().then((decoded) => {
let msg;
if (decoded) {
editor.setCode(decoded);
msg = `I have loaded the code from the URL.`;
} else if (latestCode) {
editor.setCode(latestCode);
msg = `Your last session has been loaded!`;
} /* if(randomTune) */ else {
editor.setCode(randomTune);
msg = `A random code snippet named "${name}" has been loaded!`;
}
console.log('msg', msg);
/* logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
setPending(false); */
});
editor.setTheme(themes[settings.theme || 'strudelTheme']);
}
init();
settingsMap.listen((settings, key) => {
const value = settings[key];
if (key === 'theme') {
editor.setTheme(themes[value]);
} else if (key === 'fontFamily') {
// console.log('change fontFamily', value);
setFontFamily(value);
} else if (key === 'fontSize') {
// console.log('change fontSize', value);
setFontSize(value);
}
});
onEvent('strudel-toggle-play', () => {
console.log('toggle-play');
editor.evaluate();
});
// const isEmbedded = embedded || window.location !== window.parent.location;
}
let inited = false;
onEvent('strudel-container', () => {
if (!inited) {
inited = true;
run();
}
});