jevstrudel.git / website / src / repl / prebakeCodeMirror.mjs
1import { toggleLineComment } from '@codemirror/commands';
2import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
3import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
4import { Compartment, EditorState, Prec } from '@codemirror/state';
5import { drawSelection, EditorView, keymap } from '@codemirror/view';
6import { logger } from '@strudel/core';
7import { basicSetup, flash, initTheme, extensions, parseBooleans, codemirrorSettings } from '@strudel/codemirror';
8import { evaluate } from '@strudel/transpiler';
9
10export class PrebakeCodeMirror {
11  constructor(initialCode, storePrebake, container) {
12    const settings = codemirrorSettings.get();
13    this.storePrebake = storePrebake;
14    const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
15    const initialSettings = Object.keys(compartments).map((key) =>
16      compartments[key].of(extensions[key](parseBooleans(settings[key]))),
17    );
18    initTheme(settings.theme);
19    let state = EditorState.create({
20      doc: initialCode,
21      extensions: [
22        ...initialSettings,
23        basicSetup,
24        javascript(),
25        javascriptLanguage.data.of({
26          closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
27          bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
28        }),
29        syntaxHighlighting(defaultHighlightStyle),
30        EditorView.updateListener.of((v) => {
31          if (v.docChanged) {
32            this.code = v.state.doc.toString();
33          }
34        }),
35        drawSelection({ cursorBlinkRate: 0 }),
36        Prec.highest(
37          keymap.of([
38            {
39              mac: 'Meta-Enter',
40              run: () => {
41                this.savePrebake();
42              },
43            },
44            {
45              key: 'Ctrl-Enter',
46              run: () => {
47                this.savePrebake();
48              },
49            },
50            {
51              key: 'Alt-Enter',
52              run: () => {
53                this.savePrebake();
54              },
55            },
56          ]),
57        ),
58      ],
59    });
60
61    this.code = initialCode;
62    this.view = new EditorView({
63      state,
64      parent: container,
65    });
66
67    const handleSaveEvent = async (e) => {
68      if (e.detail.view !== this.view) {
69        return; // ignore events from other editors
70      }
71      await this.savePrebake();
72      e?.cancelable && e.preventDefault?.();
73    };
74    const handleToggleComment = (e) => {
75      if (e.detail.view !== this.view) {
76        return; // ignore events from other editors
77      }
78      this.toggleComment();
79      e?.cancelable && e.preventDefault?.();
80    };
81
82    document.addEventListener('repl-evaluate', handleSaveEvent);
83    document.addEventListener('repl-toggle-comment', handleToggleComment);
84    this.cleanup = () => {
85      document.removeEventListener('prebake-evaluate', handleSaveEvent);
86      document.removeEventListener('prebake-toggle-comment', handleToggleComment);
87    };
88  }
89
90  async savePrebake() {
91    flash(this.view);
92    this.storePrebake(this.code);
93    evaluate(this.code, { addReturn: false }); // run prebake
94    logger('[prebake] prebake saved');
95  }
96
97  toggleComment() {
98    try {
99      // Honor selections; toggleLineComment handles both selections and
100      // single line
101      toggleLineComment(this.view);
102    } catch (err) {
103      console.error('Error handling repl-toggle-comment event', err);
104    }
105  }
106
107  setCode(code) {
108    const changes = {
109      from: 0,
110      to: this.view.state.doc.length,
111      insert: code,
112    };
113    this.view.dispatch({ changes });
114  }
115}