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}