jevstrudel.git / website / src / components / Dough / dough-mirror.mjs
1import {
2  initEditor,
3  codemirrorSettings,
4  flash,
5  compartments,
6  extensions,
7  parseBooleans,
8  activateTheme,
9  updateMiniLocations,
10  highlightMiniLocations,
11} from '@strudel/codemirror';
12import { evalScope, hash2code, code2hash } from '@strudel/core';
13import { Framer } from '@strudel/draw';
14import { persistentAtom } from '@nanostores/persistent';
15import { DoughRepl } from './dough-repl.mjs';
16
17let initialCode = '$: note("c a f e")';
18
19export const code = persistentAtom('vanilla-repl-code', initialCode, {
20  encode: JSON.stringify,
21  decode: JSON.parse,
22});
23
24if (typeof window !== 'undefined') {
25  try {
26    const codeParam = window.location.href.split('#')[1] || '';
27    if (codeParam) {
28      const codeFromHash = hash2code(codeParam);
29      code.set(codeFromHash);
30    }
31  } catch (err) {
32    console.error('could not init code from url');
33  }
34}
35
36export class DoughMirror {
37  constructor(options) {
38    const { root, initialCode = code.get(), bgFill = true } = options;
39    this.root = root;
40    this.code = initialCode;
41    this.repl = new DoughRepl();
42    this.prebaked = this.prebake();
43
44    // init codemirror
45    this.editor = initEditor({
46      root,
47      initialCode: this.code,
48      onChange: (v) => {
49        if (v.docChanged) {
50          this.code = v.state.doc.toString();
51          code.set(this.code);
52        }
53      },
54      onEvaluate: this.evaluate.bind(this),
55      onStop: () => this.stop(),
56      mondo: false,
57    });
58    const settings = codemirrorSettings.get();
59    this.setFontSize(settings.fontSize);
60    this.setFontFamily(settings.fontFamily);
61
62    // init event highlighting
63    this.framer = new Framer(
64      (time) => {
65        const frameHaps = this.repl.processHaps();
66        highlightMiniLocations(this.editor, time, frameHaps);
67      },
68      (err) => console.log('Framer error', err),
69    );
70  }
71  prebake() {
72    const modulesLoading = evalScope(import('@strudel/core'), import('@strudel/tonal'), import('@strudel/mini'));
73    return Promise.all([modulesLoading, this.repl.prebake()]);
74  }
75  async evaluate() {
76    this.framer.start();
77    this.flash();
78    await this.prebaked;
79    const { miniLocations } = await this.repl.evaluate(this.code);
80    window.location.hash = '#' + code2hash(this.code);
81    updateMiniLocations(this.editor, miniLocations);
82  }
83  stop() {
84    this.repl.stop();
85    this.framer.stop();
86    highlightMiniLocations(this.editor, 0, []);
87  }
88  // added synonym (compared to StrudelMirror)
89  settings(settings = {}) {
90    this.updateSettings(settings);
91  }
92
93  // the rest is copy pasted from StrudelMirror:
94
95  updateSettings(settings = {}) {
96    settings.fontSize && this.setFontSize(settings.fontSize);
97    settings.fontFamily && this.setFontFamily(settings.fontFamily);
98    for (let key in extensions) {
99      if (key in settings) {
100        this.reconfigureExtension(key, settings[key]);
101      }
102    }
103    const updated = { ...codemirrorSettings.get(), ...settings };
104    // console.log(updated);
105    codemirrorSettings.set(updated);
106  }
107  reconfigureExtension(key, value) {
108    if (!extensions[key]) {
109      console.warn(`extension ${key} is not known`);
110      return;
111    }
112    value = parseBooleans(value);
113    const newValue = extensions[key](value, this);
114    this.editor.dispatch({
115      effects: compartments[key].reconfigure(newValue),
116    });
117    if (key === 'theme') {
118      activateTheme(value);
119    }
120  }
121  flash(ms) {
122    flash(this.editor, ms);
123  }
124  setFontSize(size) {
125    this.root.style.fontSize = size + 'px';
126  }
127  setFontFamily(family) {
128    this.root.style.fontFamily = family;
129    const scroller = this.root.querySelector('.cm-scroller');
130    if (scroller) {
131      scroller.style.fontFamily = family;
132    }
133  }
134  setCode(code, offset = 0) {
135    const changes = {
136      from: 0,
137      to: this.editor.state.doc.length + offset,
138      insert: code,
139    };
140    this.editor.dispatch({ changes });
141  }
142  getCursorLocation() {
143    return this.editor.state.selection.main.head;
144  }
145  setCursorLocation(col) {
146    return this.editor.dispatch({ selection: { anchor: col } });
147  }
148  appendCode(code) {
149    const cursor = this.getCursorLocation();
150    this.setCode(this.code + code);
151    this.setCursorLocation(cursor);
152  }
153}