diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-12-12 17:48:47 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-12-12 17:48:47 +0100 |
commit | fc7e9312d00e60171ed77c477ed91231d3dbfff9 (patch) | |
tree | cc185dd088b5fa6e9357aab3c9062a70626d1953 /subprojects/language-web/src/main/js/editor/EditorStore.ts | |
parent | build: refactor java-application conventions (diff) | |
download | refinery-fc7e9312d00e60171ed77c477ed91231d3dbfff9.tar.gz refinery-fc7e9312d00e60171ed77c477ed91231d3dbfff9.tar.zst refinery-fc7e9312d00e60171ed77c477ed91231d3dbfff9.zip |
build: move modules into subproject directory
Diffstat (limited to 'subprojects/language-web/src/main/js/editor/EditorStore.ts')
-rw-r--r-- | subprojects/language-web/src/main/js/editor/EditorStore.ts | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/subprojects/language-web/src/main/js/editor/EditorStore.ts b/subprojects/language-web/src/main/js/editor/EditorStore.ts new file mode 100644 index 00000000..5760de28 --- /dev/null +++ b/subprojects/language-web/src/main/js/editor/EditorStore.ts | |||
@@ -0,0 +1,289 @@ | |||
1 | import { autocompletion, completionKeymap } from '@codemirror/autocomplete'; | ||
2 | import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets'; | ||
3 | import { defaultKeymap, indentWithTab } from '@codemirror/commands'; | ||
4 | import { commentKeymap } from '@codemirror/comment'; | ||
5 | import { foldGutter, foldKeymap } from '@codemirror/fold'; | ||
6 | import { highlightActiveLineGutter, lineNumbers } from '@codemirror/gutter'; | ||
7 | import { classHighlightStyle } from '@codemirror/highlight'; | ||
8 | import { | ||
9 | history, | ||
10 | historyKeymap, | ||
11 | redo, | ||
12 | redoDepth, | ||
13 | undo, | ||
14 | undoDepth, | ||
15 | } from '@codemirror/history'; | ||
16 | import { indentOnInput } from '@codemirror/language'; | ||
17 | import { | ||
18 | Diagnostic, | ||
19 | lintKeymap, | ||
20 | setDiagnostics, | ||
21 | } from '@codemirror/lint'; | ||
22 | import { bracketMatching } from '@codemirror/matchbrackets'; | ||
23 | import { rectangularSelection } from '@codemirror/rectangular-selection'; | ||
24 | import { searchConfig, searchKeymap } from '@codemirror/search'; | ||
25 | import { | ||
26 | EditorState, | ||
27 | StateCommand, | ||
28 | StateEffect, | ||
29 | Transaction, | ||
30 | TransactionSpec, | ||
31 | } from '@codemirror/state'; | ||
32 | import { | ||
33 | drawSelection, | ||
34 | EditorView, | ||
35 | highlightActiveLine, | ||
36 | highlightSpecialChars, | ||
37 | keymap, | ||
38 | } from '@codemirror/view'; | ||
39 | import { | ||
40 | makeAutoObservable, | ||
41 | observable, | ||
42 | reaction, | ||
43 | } from 'mobx'; | ||
44 | |||
45 | import { findOccurrences, IOccurrence, setOccurrences } from './findOccurrences'; | ||
46 | import { problemLanguageSupport } from '../language/problemLanguageSupport'; | ||
47 | import { | ||
48 | IHighlightRange, | ||
49 | semanticHighlighting, | ||
50 | setSemanticHighlighting, | ||
51 | } from './semanticHighlighting'; | ||
52 | import type { ThemeStore } from '../theme/ThemeStore'; | ||
53 | import { getLogger } from '../utils/logger'; | ||
54 | import { XtextClient } from '../xtext/XtextClient'; | ||
55 | |||
56 | const log = getLogger('editor.EditorStore'); | ||
57 | |||
58 | export 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 | } | ||