// CodeMirror 6 editor for one .m file. The parent gives each instance a key // of the file name, so switching files remounts rather than syncing docs. import { useEffect, useRef } from 'react'; import { EditorState } from '@codemirror/state'; import { EditorView, keymap, lineNumbers, highlightActiveLine } from '@codemirror/view'; import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'; import { StreamLanguage, syntaxHighlighting, HighlightStyle } from '@codemirror/language'; import type { StreamParser } from '@codemirror/language'; import { octave } from '@codemirror/legacy-modes/mode/octave'; import { tags as t } from '@lezer/highlight'; // The octave mode's keyword list is missing these MATLAB keywords, so promote // them after it has tokenized the word. const EXTRA_KEYWORDS = new Set(['function', 'break', 'parfor', 'spmd']); const matlabMode: StreamParser = { ...octave, token(stream, state) { const start = stream.pos; const tok = octave.token(stream, state); if (EXTRA_KEYWORDS.has(stream.string.slice(start, stream.pos))) return 'keyword'; return tok; }, }; const matlabHighlight = HighlightStyle.define([ { tag: t.keyword, color: '#e8a33d' }, { tag: t.comment, color: '#6b7a86', fontStyle: 'italic' }, { tag: t.string, color: '#a5d6a7' }, { tag: t.number, color: '#4dd0e1' }, { tag: t.operator, color: '#c9a0dc' }, { tag: t.variableName, color: '#d6dae2' }, { tag: t.standard(t.variableName), color: '#82b1ff' }, // builtins: zeros, sqrt, ... { tag: t.function(t.variableName), color: '#82b1ff' }, { tag: t.atom, color: '#4dd0e1' }, { tag: t.meta, color: '#c9a0dc' }, { tag: t.invalid, color: '#e57373' }, ]); const editorTheme = EditorView.theme( { '&': { color: '#d6dae2', backgroundColor: '#181b20', height: '100%' }, '.cm-content': { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: '12.5px' }, '.cm-gutters': { backgroundColor: '#181b20', color: '#4d5560', border: 'none' }, '.cm-activeLine': { backgroundColor: '#20242b' }, '.cm-cursor': { borderLeftColor: '#e8a33d' }, '&.cm-focused': { outline: 'none' }, '.cm-scroller': { overflow: 'auto' }, '.cm-selectionBackground, ::selection': { backgroundColor: '#31465a' }, '&.cm-focused .cm-selectionBackground': { backgroundColor: '#31465a' }, }, { dark: true }, ); interface Props { source: string; onChange: (source: string) => void; } export default function CodeEditor({ source, onChange }: Props) { const hostRef = useRef(null); const onChangeRef = useRef(onChange); onChangeRef.current = onChange; useEffect(() => { const host = hostRef.current!; const view = new EditorView({ parent: host, state: EditorState.create({ doc: source, extensions: [ lineNumbers(), highlightActiveLine(), history(), keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]), StreamLanguage.define(matlabMode), syntaxHighlighting(matlabHighlight), editorTheme, EditorView.updateListener.of((u) => { if (u.docChanged) onChangeRef.current(u.state.doc.toString()); }), ], }), }); return () => view.destroy(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return
; }