aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/editor/EditorStore.ts
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/editor/EditorStore.ts')
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts289
1 files changed, 289 insertions, 0 deletions
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
new file mode 100644
index 00000000..5760de28
--- /dev/null
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -0,0 +1,289 @@
1import { autocompletion, completionKeymap } from '@codemirror/autocomplete';
2import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets';
3import { defaultKeymap, indentWithTab } from '@codemirror/commands';
4import { commentKeymap } from '@codemirror/comment';
5import { foldGutter, foldKeymap } from '@codemirror/fold';
6import { highlightActiveLineGutter, lineNumbers } from '@codemirror/gutter';
7import { classHighlightStyle } from '@codemirror/highlight';
8import {
9 history,
10 historyKeymap,
11 redo,
12 redoDepth,
13 undo,
14 undoDepth,
15} from '@codemirror/history';
16import { indentOnInput } from '@codemirror/language';
17import {
18 Diagnostic,
19 lintKeymap,
20 setDiagnostics,
21} from '@codemirror/lint';
22import { bracketMatching } from '@codemirror/matchbrackets';
23import { rectangularSelection } from '@codemirror/rectangular-selection';
24import { searchConfig, searchKeymap } from '@codemirror/search';
25import {
26 EditorState,
27 StateCommand,
28 StateEffect,
29 Transaction,
30 TransactionSpec,
31} from '@codemirror/state';
32import {
33 drawSelection,
34 EditorView,
35 highlightActiveLine,
36 highlightSpecialChars,
37 keymap,
38} from '@codemirror/view';
39import {
40 makeAutoObservable,
41 observable,
42 reaction,
43} from 'mobx';
44
45import { findOccurrences, IOccurrence, setOccurrences } from './findOccurrences';
46import { problemLanguageSupport } from '../language/problemLanguageSupport';
47import {
48 IHighlightRange,
49 semanticHighlighting,
50 setSemanticHighlighting,
51} from './semanticHighlighting';
52import type { ThemeStore } from '../theme/ThemeStore';
53import { getLogger } from '../utils/logger';
54import { XtextClient } from '../xtext/XtextClient';
55
56const log = getLogger('editor.EditorStore');
57
58export class EditorStore {
59 private readonly themeStore;
60
61 state: EditorState;
62
63 private readonly client: XtextClient;
64
65 showLineNumbers = false;
66
67 showSearchPanel = false;
68
69 showLintPanel = false;
70
71 errorCount = 0;
72
73 warningCount = 0;
74
75 infoCount = 0;
76
77 private readonly defaultDispatcher = (tr: Transaction): void => {
78 this.onTransaction(tr);
79 };
80
81 private dispatcher = this.defaultDispatcher;
82
83 constructor(initialValue: string, themeStore: ThemeStore) {
84 this.themeStore = themeStore;
85 this.state = EditorState.create({
86 doc: initialValue,
87 extensions: [
88 autocompletion({
89 activateOnTyping: true,
90 override: [
91 (context) => this.client.contentAssist(context),
92 ],
93 }),
94 classHighlightStyle.extension,
95 closeBrackets(),
96 bracketMatching(),
97 drawSelection(),
98 EditorState.allowMultipleSelections.of(true),
99 EditorView.theme({}, {
100 dark: this.themeStore.darkMode,
101 }),
102 findOccurrences,
103 highlightActiveLine(),
104 highlightActiveLineGutter(),
105 highlightSpecialChars(),
106 history(),
107 indentOnInput(),
108 rectangularSelection(),
109 searchConfig({
110 top: true,
111 matchCase: true,
112 }),
113 semanticHighlighting,
114 // We add the gutters to `extensions` in the order we want them to appear.
115 lineNumbers(),
116 foldGutter(),
117 keymap.of([
118 { key: 'Mod-Shift-f', run: () => this.formatText() },
119 ...closeBracketsKeymap,
120 ...commentKeymap,
121 ...completionKeymap,
122 ...foldKeymap,
123 ...historyKeymap,
124 indentWithTab,
125 // Override keys in `lintKeymap` to go through the `EditorStore`.
126 { key: 'Mod-Shift-m', run: () => this.setLintPanelOpen(true) },
127 ...lintKeymap,
128 // Override keys in `searchKeymap` to go through the `EditorStore`.
129 { key: 'Mod-f', run: () => this.setSearchPanelOpen(true), scope: 'editor search-panel' },
130 { key: 'Escape', run: () => this.setSearchPanelOpen(false), scope: 'editor search-panel' },
131 ...searchKeymap,
132 ...defaultKeymap,
133 ]),
134 problemLanguageSupport(),
135 ],
136 });
137 this.client = new XtextClient(this);
138 reaction(
139 () => this.themeStore.darkMode,
140 (darkMode) => {
141 log.debug('Update editor dark mode', darkMode);
142 this.dispatch({
143 effects: [
144 StateEffect.appendConfig.of(EditorView.theme({}, {
145 dark: darkMode,
146 })),
147 ],
148 });
149 },
150 );
151 makeAutoObservable(this, {
152 state: observable.ref,
153 });
154 }
155
156 updateDispatcher(newDispatcher: ((tr: Transaction) => void) | null): void {
157 this.dispatcher = newDispatcher || this.defaultDispatcher;
158 }
159
160 onTransaction(tr: Transaction): void {
161 log.trace('Editor transaction', tr);
162 this.state = tr.state;
163 this.client.onTransaction(tr);
164 }
165
166 dispatch(...specs: readonly TransactionSpec[]): void {
167 this.dispatcher(this.state.update(...specs));
168 }
169
170 doStateCommand(command: StateCommand): boolean {
171 return command({
172 state: this.state,
173 dispatch: this.dispatcher,
174 });
175 }
176
177 updateDiagnostics(diagnostics: Diagnostic[]): void {
178 this.dispatch(setDiagnostics(this.state, diagnostics));
179 this.errorCount = 0;
180 this.warningCount = 0;
181 this.infoCount = 0;
182 diagnostics.forEach(({ severity }) => {
183 switch (severity) {
184 case 'error':
185 this.errorCount += 1;
186 break;
187 case 'warning':
188 this.warningCount += 1;
189 break;
190 case 'info':
191 this.infoCount += 1;
192 break;
193 }
194 });
195 }
196
197 get highestDiagnosticLevel(): Diagnostic['severity'] | null {
198 if (this.errorCount > 0) {
199 return 'error';
200 }
201 if (this.warningCount > 0) {
202 return 'warning';
203 }
204 if (this.infoCount > 0) {
205 return 'info';
206 }
207 return null;
208 }
209
210 updateSemanticHighlighting(ranges: IHighlightRange[]): void {
211 this.dispatch(setSemanticHighlighting(ranges));
212 }
213
214 updateOccurrences(write: IOccurrence[], read: IOccurrence[]): void {
215 this.dispatch(setOccurrences(write, read));
216 }
217
218 /**
219 * @returns `true` if there is history to undo
220 */
221 get canUndo(): boolean {
222 return undoDepth(this.state) > 0;
223 }
224
225 // eslint-disable-next-line class-methods-use-this
226 undo(): void {
227 log.debug('Undo', this.doStateCommand(undo));
228 }
229
230 /**
231 * @returns `true` if there is history to redo
232 */
233 get canRedo(): boolean {
234 return redoDepth(this.state) > 0;
235 }
236
237 // eslint-disable-next-line class-methods-use-this
238 redo(): void {
239 log.debug('Redo', this.doStateCommand(redo));
240 }
241
242 toggleLineNumbers(): void {
243 this.showLineNumbers = !this.showLineNumbers;
244 log.debug('Show line numbers', this.showLineNumbers);
245 }
246
247 /**
248 * Sets whether the CodeMirror search panel should be open.
249 *
250 * This method can be used as a CodeMirror command,
251 * because it returns `false` if it didn't execute,
252 * allowing other commands for the same keybind to run instead.
253 * This matches the behavior of the `openSearchPanel` and `closeSearchPanel`
254 * commands from `'@codemirror/search'`.
255 *
256 * @param newShosSearchPanel whether we should show the search panel
257 * @returns `true` if the state was changed, `false` otherwise
258 */
259 setSearchPanelOpen(newShowSearchPanel: boolean): boolean {
260 if (this.showSearchPanel === newShowSearchPanel) {
261 return false;
262 }
263 this.showSearchPanel = newShowSearchPanel;
264 log.debug('Show search panel', this.showSearchPanel);
265 return true;
266 }
267
268 toggleSearchPanel(): void {
269 this.setSearchPanelOpen(!this.showSearchPanel);
270 }
271
272 setLintPanelOpen(newShowLintPanel: boolean): boolean {
273 if (this.showLintPanel === newShowLintPanel) {
274 return false;
275 }
276 this.showLintPanel = newShowLintPanel;
277 log.debug('Show lint panel', this.showLintPanel);
278 return true;
279 }
280
281 toggleLintPanel(): void {
282 this.setLintPanelOpen(!this.showLintPanel);
283 }
284
285 formatText(): boolean {
286 this.client.formatText();
287 return true;
288 }
289}