96aec9cTriangle strike simulator: MATLAB model and interactive web appJeremy Magland 1// CodeMirror 6 editor for one .m file. The parent gives each instance a key
2// of the file name, so switching files remounts rather than syncing docs.
4import { useEffect, useRef } from 'react';
5import { EditorState } from '@codemirror/state';
6import { EditorView, keymap, lineNumbers, highlightActiveLine } from '@codemirror/view';
7import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
8import { StreamLanguage, syntaxHighlighting, HighlightStyle } from '@codemirror/language';
9import type { StreamParser } from '@codemirror/language';
10import { octave } from '@codemirror/legacy-modes/mode/octave';
11import { tags as t } from '@lezer/highlight';
13// The octave mode's keyword list is missing these MATLAB keywords, so promote
14// them after it has tokenized the word.
15const EXTRA_KEYWORDS = new Set(['function', 'break', 'parfor', 'spmd']);
17const matlabMode: StreamParser<unknown> = {
18 ...octave,
19 token(stream, state) {
20 const start = stream.pos;
21 const tok = octave.token(stream, state);
22 if (EXTRA_KEYWORDS.has(stream.string.slice(start, stream.pos))) return 'keyword';
23 return tok;
24 },
25};
27const matlabHighlight = HighlightStyle.define([
28 { tag: t.keyword, color: '#e8a33d' },
29 { tag: t.comment, color: '#6b7a86', fontStyle: 'italic' },
30 { tag: t.string, color: '#a5d6a7' },
31 { tag: t.number, color: '#4dd0e1' },
32 { tag: t.operator, color: '#c9a0dc' },
33 { tag: t.variableName, color: '#d6dae2' },
34 { tag: t.standard(t.variableName), color: '#82b1ff' }, // builtins: zeros, sqrt, ...
35 { tag: t.function(t.variableName), color: '#82b1ff' },
36 { tag: t.atom, color: '#4dd0e1' },
37 { tag: t.meta, color: '#c9a0dc' },
38 { tag: t.invalid, color: '#e57373' },
39]);
41const editorTheme = EditorView.theme(
42 {
43 '&': { color: '#d6dae2', backgroundColor: '#181b20', height: '100%' },
44 '.cm-content': { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: '12.5px' },
45 '.cm-gutters': { backgroundColor: '#181b20', color: '#4d5560', border: 'none' },
46 '.cm-activeLine': { backgroundColor: '#20242b' },
47 '.cm-cursor': { borderLeftColor: '#e8a33d' },
48 '&.cm-focused': { outline: 'none' },
49 '.cm-scroller': { overflow: 'auto' },
50 '.cm-selectionBackground, ::selection': { backgroundColor: '#31465a' },
51 '&.cm-focused .cm-selectionBackground': { backgroundColor: '#31465a' },
52 },
53 { dark: true },
54);
56interface Props {
57 source: string;
58 onChange: (source: string) => void;
59}
61export default function CodeEditor({ source, onChange }: Props) {
62 const hostRef = useRef<HTMLDivElement | null>(null);
63 const onChangeRef = useRef(onChange);
64 onChangeRef.current = onChange;
66 useEffect(() => {
67 const host = hostRef.current!;
68 const view = new EditorView({
69 parent: host,
70 state: EditorState.create({
71 doc: source,
72 extensions: [
73 lineNumbers(),
74 highlightActiveLine(),
75 history(),
76 keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
77 StreamLanguage.define(matlabMode),
78 syntaxHighlighting(matlabHighlight),
79 editorTheme,
80 EditorView.updateListener.of((u) => {
81 if (u.docChanged) onChangeRef.current(u.state.doc.toString());
82 }),
83 ],
84 }),
85 });
86 return () => view.destroy();
87 // eslint-disable-next-line react-hooks/exhaustive-deps
88 }, []);
90 return <div className="code-editor" ref={hostRef} />;
91}