diff options
Diffstat (limited to 'language-web/src/main/js')
-rw-r--r-- | language-web/src/main/js/RootStore.tsx | 4 | ||||
-rw-r--r-- | language-web/src/main/js/editor/EditorArea.tsx | 142 | ||||
-rw-r--r-- | language-web/src/main/js/editor/EditorButtons.tsx | 35 | ||||
-rw-r--r-- | language-web/src/main/js/editor/EditorParent.ts | 61 | ||||
-rw-r--r-- | language-web/src/main/js/editor/EditorStore.ts | 294 | ||||
-rw-r--r-- | language-web/src/main/js/editor/editor.ts | 18 | ||||
-rw-r--r-- | language-web/src/main/js/index.tsx | 3 | ||||
-rw-r--r-- | language-web/src/main/js/theme/ThemeStore.ts | 4 |
8 files changed, 363 insertions, 198 deletions
diff --git a/language-web/src/main/js/RootStore.tsx b/language-web/src/main/js/RootStore.tsx index 88b8a445..96e1b26a 100644 --- a/language-web/src/main/js/RootStore.tsx +++ b/language-web/src/main/js/RootStore.tsx | |||
@@ -8,9 +8,9 @@ export class RootStore { | |||
8 | 8 | ||
9 | themeStore; | 9 | themeStore; |
10 | 10 | ||
11 | constructor() { | 11 | constructor(initialValue: string) { |
12 | this.themeStore = new ThemeStore(); | 12 | this.themeStore = new ThemeStore(); |
13 | this.editorStore = new EditorStore(this.themeStore); | 13 | this.editorStore = new EditorStore(initialValue, this.themeStore); |
14 | } | 14 | } |
15 | } | 15 | } |
16 | 16 | ||
diff --git a/language-web/src/main/js/editor/EditorArea.tsx b/language-web/src/main/js/editor/EditorArea.tsx index 531a57c9..58d65184 100644 --- a/language-web/src/main/js/editor/EditorArea.tsx +++ b/language-web/src/main/js/editor/EditorArea.tsx | |||
@@ -1,41 +1,123 @@ | |||
1 | import { Command, EditorView } from '@codemirror/view'; | ||
2 | import { closeSearchPanel, openSearchPanel } from '@codemirror/search'; | ||
3 | import { closeLintPanel, openLintPanel } from '@codemirror/lint'; | ||
1 | import { observer } from 'mobx-react-lite'; | 4 | import { observer } from 'mobx-react-lite'; |
2 | import React, { useRef } from 'react'; | 5 | import React, { useEffect, useRef, useState } from 'react'; |
3 | 6 | ||
7 | import { EditorParent } from './EditorParent'; | ||
8 | import { getLogger } from '../logging'; | ||
4 | import { useRootStore } from '../RootStore'; | 9 | import { useRootStore } from '../RootStore'; |
5 | 10 | ||
11 | const log = getLogger('EditorArea'); | ||
12 | |||
13 | function usePanel( | ||
14 | label: string, | ||
15 | stateToSet: boolean, | ||
16 | editorView: EditorView | null, | ||
17 | openCommand: Command, | ||
18 | closeCommand: Command, | ||
19 | ) { | ||
20 | const [cachedViewState, setCachedViewState] = useState<boolean>(false); | ||
21 | useEffect(() => { | ||
22 | if (editorView === null || cachedViewState === stateToSet) { | ||
23 | return; | ||
24 | } | ||
25 | const success = stateToSet ? openCommand(editorView) : closeCommand(editorView); | ||
26 | if (!success) { | ||
27 | log.error( | ||
28 | 'Failed to synchronize', | ||
29 | label, | ||
30 | 'panel state - store state:', | ||
31 | cachedViewState, | ||
32 | 'view state:', | ||
33 | stateToSet, | ||
34 | ); | ||
35 | } | ||
36 | setCachedViewState(stateToSet); | ||
37 | }, [ | ||
38 | stateToSet, | ||
39 | editorView, | ||
40 | cachedViewState, | ||
41 | label, | ||
42 | openCommand, | ||
43 | closeCommand, | ||
44 | ]); | ||
45 | return setCachedViewState; | ||
46 | } | ||
47 | |||
6 | export const EditorArea = observer(() => { | 48 | export const EditorArea = observer(() => { |
7 | const { editorStore } = useRootStore(); | 49 | const { editorStore } = useRootStore(); |
8 | const { CodeMirror } = editorStore.chunk || {}; | 50 | const editorParentRef = useRef<HTMLDivElement | null>(null); |
9 | const fallbackTextarea = useRef<HTMLTextAreaElement>(null); | 51 | const [editorViewState, setEditorViewState] = useState<EditorView | null>(null); |
10 | 52 | ||
11 | if (!CodeMirror) { | 53 | const setSearchPanelOpen = usePanel( |
12 | return ( | 54 | 'search', |
13 | <textarea | 55 | editorStore.showSearchPanel, |
14 | value={editorStore.value} | 56 | editorViewState, |
15 | onChange={(e) => editorStore.updateValue(e.target.value)} | 57 | openSearchPanel, |
16 | ref={fallbackTextarea} | 58 | closeSearchPanel, |
17 | className={`problem-fallback-editor cm-s-${editorStore.codeMirrorTheme}`} | 59 | ); |
18 | /> | 60 | |
19 | ); | 61 | const setLintPanelOpen = usePanel( |
20 | } | 62 | 'lint', |
21 | 63 | editorStore.showLintPanel, | |
22 | const textarea = fallbackTextarea.current; | 64 | editorViewState, |
23 | if (textarea) { | 65 | openLintPanel, |
24 | editorStore.setInitialSelection( | 66 | closeLintPanel, |
25 | textarea.selectionStart, | 67 | ); |
26 | textarea.selectionEnd, | 68 | |
27 | document.activeElement === textarea, | 69 | useEffect(() => { |
28 | ); | 70 | if (editorParentRef.current === null) { |
29 | } | 71 | // Nothing to clean up. |
72 | return () => {}; | ||
73 | } | ||
74 | |||
75 | const editorView = new EditorView({ | ||
76 | state: editorStore.state, | ||
77 | parent: editorParentRef.current, | ||
78 | dispatch: (transaction) => { | ||
79 | editorStore.onTransaction(transaction); | ||
80 | editorView.update([transaction]); | ||
81 | if (editorView.state !== editorStore.state) { | ||
82 | log.error( | ||
83 | 'Failed to synchronize editor state - store state:', | ||
84 | editorStore.state, | ||
85 | 'view state:', | ||
86 | editorView.state, | ||
87 | ); | ||
88 | } | ||
89 | }, | ||
90 | }); | ||
91 | setEditorViewState(editorView); | ||
92 | setSearchPanelOpen(false); | ||
93 | setLintPanelOpen(false); | ||
94 | // `dispatch` is bound to the view instance, | ||
95 | // so it does not have to be called as a method. | ||
96 | // eslint-disable-next-line @typescript-eslint/unbound-method | ||
97 | editorStore.updateDispatcher(editorView.dispatch); | ||
98 | log.info('Editor created'); | ||
99 | |||
100 | return () => { | ||
101 | editorStore.updateDispatcher(null); | ||
102 | editorView.destroy(); | ||
103 | log.info('Editor destroyed'); | ||
104 | }; | ||
105 | }, [ | ||
106 | editorParentRef, | ||
107 | editorStore, | ||
108 | setSearchPanelOpen, | ||
109 | setLintPanelOpen, | ||
110 | ]); | ||
30 | 111 | ||
31 | return ( | 112 | return ( |
32 | <CodeMirror | 113 | <EditorParent |
33 | value={editorStore.value} | 114 | className="dark" |
34 | options={editorStore.codeMirrorOptions} | 115 | sx={{ |
35 | editorDidMount={(editor) => editorStore.editorDidMount(editor)} | 116 | '.cm-lineNumbers': editorStore.showLineNumbers ? {} : { |
36 | editorWillUnmount={() => editorStore.editorWillUnmount()} | 117 | display: 'none !important', |
37 | onBeforeChange={(_editor, _data, value) => editorStore.updateValue(value)} | 118 | }, |
38 | onChange={() => editorStore.reportChanged()} | 119 | }} |
120 | ref={editorParentRef} | ||
39 | /> | 121 | /> |
40 | ); | 122 | ); |
41 | }); | 123 | }); |
diff --git a/language-web/src/main/js/editor/EditorButtons.tsx b/language-web/src/main/js/editor/EditorButtons.tsx index 56577e82..9622475c 100644 --- a/language-web/src/main/js/editor/EditorButtons.tsx +++ b/language-web/src/main/js/editor/EditorButtons.tsx | |||
@@ -2,8 +2,10 @@ import { observer } from 'mobx-react-lite'; | |||
2 | import Stack from '@mui/material/Stack'; | 2 | import Stack from '@mui/material/Stack'; |
3 | import ToggleButton from '@mui/material/ToggleButton'; | 3 | import ToggleButton from '@mui/material/ToggleButton'; |
4 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; | 4 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; |
5 | import CheckIcon from '@mui/icons-material/Check'; | ||
5 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; | 6 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; |
6 | import RedoIcon from '@mui/icons-material/Redo'; | 7 | import RedoIcon from '@mui/icons-material/Redo'; |
8 | import SearchIcon from '@mui/icons-material/Search'; | ||
7 | import UndoIcon from '@mui/icons-material/Undo'; | 9 | import UndoIcon from '@mui/icons-material/Undo'; |
8 | import React from 'react'; | 10 | import React from 'react'; |
9 | 11 | ||
@@ -37,15 +39,34 @@ export const EditorButtons = observer(() => { | |||
37 | <RedoIcon fontSize="small" /> | 39 | <RedoIcon fontSize="small" /> |
38 | </ToggleButton> | 40 | </ToggleButton> |
39 | </ToggleButtonGroup> | 41 | </ToggleButtonGroup> |
40 | <ToggleButton | 42 | <ToggleButtonGroup |
41 | selected={editorStore.showLineNumbers} | ||
42 | onChange={() => editorStore.toggleLineNumbers()} | ||
43 | size="small" | 43 | size="small" |
44 | aria-label="Show line numbers" | ||
45 | value="show-line-numbers" | ||
46 | > | 44 | > |
47 | <FormatListNumberedIcon fontSize="small" /> | 45 | <ToggleButton |
48 | </ToggleButton> | 46 | selected={editorStore.showLineNumbers} |
47 | onClick={() => editorStore.toggleLineNumbers()} | ||
48 | aria-label="Show line numbers" | ||
49 | value="show-line-numbers" | ||
50 | > | ||
51 | <FormatListNumberedIcon fontSize="small" /> | ||
52 | </ToggleButton> | ||
53 | <ToggleButton | ||
54 | selected={editorStore.showSearchPanel} | ||
55 | onClick={() => editorStore.toggleSearchPanel()} | ||
56 | aria-label="Show find/replace" | ||
57 | value="show-search-panel" | ||
58 | > | ||
59 | <SearchIcon fontSize="small" /> | ||
60 | </ToggleButton> | ||
61 | <ToggleButton | ||
62 | selected={editorStore.showLintPanel} | ||
63 | onClick={() => editorStore.toggleLintPanel()} | ||
64 | aria-label="Show errors and warnings" | ||
65 | value="show-lint-panel" | ||
66 | > | ||
67 | <CheckIcon fontSize="small" /> | ||
68 | </ToggleButton> | ||
69 | </ToggleButtonGroup> | ||
49 | </Stack> | 70 | </Stack> |
50 | ); | 71 | ); |
51 | }); | 72 | }); |
diff --git a/language-web/src/main/js/editor/EditorParent.ts b/language-web/src/main/js/editor/EditorParent.ts new file mode 100644 index 00000000..bf67522b --- /dev/null +++ b/language-web/src/main/js/editor/EditorParent.ts | |||
@@ -0,0 +1,61 @@ | |||
1 | import { styled } from '@mui/material/styles'; | ||
2 | |||
3 | export const EditorParent = styled('div')(({ theme }) => ({ | ||
4 | background: theme.palette.background.default, | ||
5 | '&, .cm-editor': { | ||
6 | height: '100%', | ||
7 | }, | ||
8 | '.cm-scroller': { | ||
9 | fontSize: 16, | ||
10 | fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', | ||
11 | fontFeatureSettings: '"liga", "calt"', | ||
12 | fontWeight: 400, | ||
13 | letterSpacing: 0, | ||
14 | textRendering: 'optimizeLegibility', | ||
15 | color: theme.palette.text.secondary, | ||
16 | }, | ||
17 | '.cm-gutters': { | ||
18 | background: theme.palette.background.default, | ||
19 | color: theme.palette.text.disabled, | ||
20 | border: 'none', | ||
21 | }, | ||
22 | '.cm-specialChar': { | ||
23 | color: theme.palette.secondary.main, | ||
24 | }, | ||
25 | '.cm-activeLine': { | ||
26 | background: 'rgba(0, 0, 0, 0.3)', | ||
27 | }, | ||
28 | '.cm-activeLineGutter': { | ||
29 | background: 'rgba(0, 0, 0, 0.3)', | ||
30 | color: theme.palette.text.primary, | ||
31 | }, | ||
32 | '.cm-cursor, .cm-cursor-primary': { | ||
33 | borderColor: theme.palette.primary.main, | ||
34 | background: theme.palette.common.black, | ||
35 | }, | ||
36 | '.cm-selectionBackground': { | ||
37 | background: '#3e4453', | ||
38 | }, | ||
39 | '.cm-focused': { | ||
40 | outline: 'none', | ||
41 | '.cm-selectionBackground': { | ||
42 | background: '#3e4453', | ||
43 | }, | ||
44 | }, | ||
45 | '.cm-panels-top': { | ||
46 | color: theme.palette.text.secondary, | ||
47 | }, | ||
48 | '.cm-panel': { | ||
49 | background: theme.palette.background.paper, | ||
50 | borderTop: `1px solid ${theme.palette.divider}`, | ||
51 | 'button[name="close"]': { | ||
52 | // HACK We can't hook the panel close button to go through `EditorStore`, | ||
53 | // so we hide it altogether. | ||
54 | display: 'none', | ||
55 | }, | ||
56 | }, | ||
57 | '.cmt-comment': { | ||
58 | fontVariant: 'italic', | ||
59 | color: theme.palette.text.disabled, | ||
60 | }, | ||
61 | })); | ||
diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts index 705020b9..326c02a1 100644 --- a/language-web/src/main/js/editor/EditorStore.ts +++ b/language-web/src/main/js/editor/EditorStore.ts | |||
@@ -1,201 +1,217 @@ | |||
1 | import type { Editor, EditorConfiguration } from 'codemirror'; | 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 { lintKeymap } from '@codemirror/lint'; | ||
18 | import { bracketMatching } from '@codemirror/matchbrackets'; | ||
19 | import { rectangularSelection } from '@codemirror/rectangular-selection'; | ||
20 | import { searchConfig, searchKeymap } from '@codemirror/search'; | ||
21 | import { | ||
22 | EditorState, | ||
23 | StateCommand, | ||
24 | StateEffect, | ||
25 | Transaction, | ||
26 | TransactionSpec, | ||
27 | } from '@codemirror/state'; | ||
28 | import { | ||
29 | drawSelection, | ||
30 | EditorView, | ||
31 | highlightActiveLine, | ||
32 | highlightSpecialChars, | ||
33 | keymap, | ||
34 | } from '@codemirror/view'; | ||
2 | import { | 35 | import { |
3 | createAtom, | ||
4 | makeAutoObservable, | 36 | makeAutoObservable, |
5 | observable, | 37 | observable, |
6 | runInAction, | 38 | reaction, |
7 | } from 'mobx'; | 39 | } from 'mobx'; |
8 | import type { IXtextOptions, IXtextServices } from 'xtext/xtext-codemirror'; | ||
9 | 40 | ||
10 | import type { IEditorChunk } from './editor'; | ||
11 | import { getLogger } from '../logging'; | 41 | import { getLogger } from '../logging'; |
12 | import type { ThemeStore } from '../theme/ThemeStore'; | 42 | import type { ThemeStore } from '../theme/ThemeStore'; |
13 | 43 | ||
14 | const log = getLogger('EditorStore'); | 44 | const log = getLogger('EditorStore'); |
15 | 45 | ||
16 | const xtextLang = 'problem'; | ||
17 | |||
18 | const xtextOptions: IXtextOptions = { | ||
19 | xtextLang, | ||
20 | enableFormattingAction: true, | ||
21 | }; | ||
22 | |||
23 | const codeMirrorGlobalOptions: EditorConfiguration = { | ||
24 | mode: `xtext/${xtextLang}`, | ||
25 | indentUnit: 2, | ||
26 | styleActiveLine: true, | ||
27 | screenReaderLabel: 'Model source code', | ||
28 | inputStyle: 'contenteditable', | ||
29 | }; | ||
30 | |||
31 | export class EditorStore { | 46 | export class EditorStore { |
32 | themeStore; | 47 | themeStore; |
33 | 48 | ||
34 | atom; | 49 | state: EditorState; |
35 | 50 | ||
36 | chunk?: IEditorChunk; | 51 | emptyHistory: unknown; |
37 | 52 | ||
38 | editor?: Editor; | 53 | showLineNumbers = false; |
39 | 54 | ||
40 | xtextServices?: IXtextServices; | 55 | showSearchPanel = false; |
41 | 56 | ||
42 | value = ''; | 57 | showLintPanel = false; |
43 | 58 | ||
44 | showLineNumbers = false; | 59 | readonly defaultDispatcher = (tr: Transaction): void => { |
60 | this.onTransaction(tr); | ||
61 | }; | ||
45 | 62 | ||
46 | initialSelection!: { start: number, end: number, focused: boolean }; | 63 | dispatcher = this.defaultDispatcher; |
47 | 64 | ||
48 | constructor(themeStore: ThemeStore) { | 65 | constructor(initialValue: string, themeStore: ThemeStore) { |
49 | this.themeStore = themeStore; | 66 | this.themeStore = themeStore; |
50 | this.atom = createAtom('EditorStore'); | 67 | this.state = EditorState.create({ |
51 | this.resetInitialSelection(); | 68 | doc: initialValue, |
69 | extensions: [ | ||
70 | autocompletion(), | ||
71 | classHighlightStyle.extension, | ||
72 | closeBrackets(), | ||
73 | bracketMatching(), | ||
74 | drawSelection(), | ||
75 | EditorState.allowMultipleSelections.of(true), | ||
76 | EditorView.theme({}, { | ||
77 | dark: this.themeStore.darkMode, | ||
78 | }), | ||
79 | highlightActiveLine(), | ||
80 | highlightActiveLineGutter(), | ||
81 | highlightSpecialChars(), | ||
82 | history(), | ||
83 | indentOnInput(), | ||
84 | rectangularSelection(), | ||
85 | searchConfig({ | ||
86 | top: true, | ||
87 | matchCase: true, | ||
88 | }), | ||
89 | // We add the gutters to `extensions` in the order we want them to appear. | ||
90 | foldGutter(), | ||
91 | lineNumbers(), | ||
92 | keymap.of([ | ||
93 | ...closeBracketsKeymap, | ||
94 | ...commentKeymap, | ||
95 | ...completionKeymap, | ||
96 | ...foldKeymap, | ||
97 | ...historyKeymap, | ||
98 | indentWithTab, | ||
99 | // Override keys in `lintKeymap` to go through the `EditorStore`. | ||
100 | { key: 'Mod-Shift-m', run: () => this.setLintPanelOpen(true) }, | ||
101 | ...lintKeymap, | ||
102 | // Override keys in `searchKeymap` to go through the `EditorStore`. | ||
103 | { key: 'Mod-f', run: () => this.setSearchPanelOpen(true), scope: 'editor search-panel' }, | ||
104 | { key: 'Escape', run: () => this.setSearchPanelOpen(false), scope: 'editor search-panel' }, | ||
105 | ...searchKeymap, | ||
106 | ...defaultKeymap, | ||
107 | ]), | ||
108 | ], | ||
109 | }); | ||
110 | reaction( | ||
111 | () => this.themeStore.darkMode, | ||
112 | (darkMode) => { | ||
113 | log.debug('Update editor dark mode', darkMode); | ||
114 | this.dispatch({ | ||
115 | effects: [ | ||
116 | StateEffect.appendConfig.of(EditorView.theme({}, { | ||
117 | dark: darkMode, | ||
118 | })), | ||
119 | ], | ||
120 | }); | ||
121 | }, | ||
122 | ); | ||
52 | makeAutoObservable(this, { | 123 | makeAutoObservable(this, { |
53 | themeStore: false, | 124 | themeStore: false, |
54 | atom: false, | 125 | state: observable.ref, |
55 | chunk: observable.ref, | 126 | defaultDispatcher: false, |
56 | editor: observable.ref, | 127 | dispatcher: false, |
57 | xtextServices: observable.ref, | ||
58 | initialSelection: false, | ||
59 | }); | 128 | }); |
60 | this.loadChunk(); | ||
61 | } | 129 | } |
62 | 130 | ||
63 | private loadChunk(): void { | 131 | updateDispatcher(newDispatcher: ((tr: Transaction) => void) | null): void { |
64 | const loadingStartMillis = Date.now(); | 132 | this.dispatcher = newDispatcher || this.defaultDispatcher; |
65 | log.info('Requesting editor chunk'); | ||
66 | import('./editor').then(({ editorChunk }) => { | ||
67 | runInAction(() => { | ||
68 | this.chunk = editorChunk; | ||
69 | }); | ||
70 | const loadingDurationMillis = Date.now() - loadingStartMillis; | ||
71 | log.info('Loaded editor chunk in', loadingDurationMillis, 'ms'); | ||
72 | }).catch((error) => { | ||
73 | log.error('Error while loading editor', error); | ||
74 | }); | ||
75 | } | 133 | } |
76 | 134 | ||
77 | setInitialSelection(start: number, end: number, focused: boolean): void { | 135 | onTransaction(tr: Transaction): void { |
78 | this.initialSelection = { start, end, focused }; | 136 | log.trace('Editor transaction', tr); |
79 | this.applyInitialSelectionToEditor(); | 137 | this.state = tr.state; |
80 | } | 138 | } |
81 | 139 | ||
82 | private resetInitialSelection(): void { | 140 | dispatch(...specs: readonly TransactionSpec[]): void { |
83 | this.initialSelection = { | 141 | this.dispatcher(this.state.update(...specs)); |
84 | start: 0, | ||
85 | end: 0, | ||
86 | focused: false, | ||
87 | }; | ||
88 | } | 142 | } |
89 | 143 | ||
90 | private applyInitialSelectionToEditor(): void { | 144 | doStateCommand(command: StateCommand): boolean { |
91 | if (this.editor) { | 145 | return command({ |
92 | const { start, end, focused } = this.initialSelection; | 146 | state: this.state, |
93 | const doc = this.editor.getDoc(); | 147 | dispatch: this.dispatcher, |
94 | const startPos = doc.posFromIndex(start); | 148 | }); |
95 | const endPos = doc.posFromIndex(end); | ||
96 | doc.setSelection(startPos, endPos, { | ||
97 | scroll: true, | ||
98 | }); | ||
99 | if (focused) { | ||
100 | this.editor.focus(); | ||
101 | } | ||
102 | this.resetInitialSelection(); | ||
103 | } | ||
104 | } | 149 | } |
105 | 150 | ||
106 | /** | 151 | /** |
107 | * Attaches a new CodeMirror instance and creates Xtext services. | 152 | * @returns `true` if there is history to undo |
108 | * | ||
109 | * The store will not subscribe to any CodeMirror events. Instead, | ||
110 | * the editor component should subscribe to them and relay them to the store. | ||
111 | * | ||
112 | * @param newEditor The new CodeMirror instance | ||
113 | */ | 153 | */ |
114 | editorDidMount(newEditor: Editor): void { | 154 | get canUndo(): boolean { |
115 | if (!this.chunk) { | 155 | return undoDepth(this.state) > 0; |
116 | throw new Error('Editor not loaded yet'); | ||
117 | } | ||
118 | if (this.editor) { | ||
119 | throw new Error('CoreMirror editor mounted before unmounting'); | ||
120 | } | ||
121 | this.editor = newEditor; | ||
122 | this.xtextServices = this.chunk.createServices(newEditor, xtextOptions); | ||
123 | this.applyInitialSelectionToEditor(); | ||
124 | } | 156 | } |
125 | 157 | ||
126 | editorWillUnmount(): void { | 158 | // eslint-disable-next-line class-methods-use-this |
127 | if (!this.chunk) { | 159 | undo(): void { |
128 | throw new Error('Editor not loaded yet'); | 160 | log.debug('Undo', this.doStateCommand(undo)); |
129 | } | ||
130 | if (this.editor) { | ||
131 | this.chunk.removeServices(this.editor); | ||
132 | } | ||
133 | delete this.editor; | ||
134 | delete this.xtextServices; | ||
135 | } | 161 | } |
136 | 162 | ||
137 | /** | 163 | /** |
138 | * Updates the contents of the editor. | 164 | * @returns `true` if there is history to redo |
139 | * | ||
140 | * @param newValue The new contents of the editor | ||
141 | */ | 165 | */ |
142 | updateValue(newValue: string): void { | 166 | get canRedo(): boolean { |
143 | this.value = newValue; | 167 | return redoDepth(this.state) > 0; |
144 | } | ||
145 | |||
146 | reportChanged(): void { | ||
147 | this.atom.reportChanged(); | ||
148 | } | ||
149 | |||
150 | protected observeEditorChanges(): void { | ||
151 | this.atom.reportObserved(); | ||
152 | } | 168 | } |
153 | 169 | ||
154 | get codeMirrorTheme(): string { | 170 | // eslint-disable-next-line class-methods-use-this |
155 | return `problem-${this.themeStore.className}`; | 171 | redo(): void { |
172 | log.debug('Redo', this.doStateCommand(redo)); | ||
156 | } | 173 | } |
157 | 174 | ||
158 | get codeMirrorOptions(): EditorConfiguration { | 175 | toggleLineNumbers(): void { |
159 | return { | 176 | this.showLineNumbers = !this.showLineNumbers; |
160 | ...codeMirrorGlobalOptions, | 177 | log.debug('Show line numbers', this.showLineNumbers); |
161 | theme: this.codeMirrorTheme, | ||
162 | lineNumbers: this.showLineNumbers, | ||
163 | }; | ||
164 | } | 178 | } |
165 | 179 | ||
166 | /** | 180 | /** |
167 | * @returns `true` if there is history to undo | 181 | * Sets whether the CodeMirror search panel should be open. |
182 | * | ||
183 | * This method can be used as a CodeMirror command, | ||
184 | * because it returns `false` if it didn't execute, | ||
185 | * allowing other commands for the same keybind to run instead. | ||
186 | * This matches the behavior of the `openSearchPanel` and `closeSearchPanel` | ||
187 | * commands from `'@codemirror/search'`. | ||
188 | * | ||
189 | * @param newShosSearchPanel whether we should show the search panel | ||
190 | * @returns `true` if the state was changed, `false` otherwise | ||
168 | */ | 191 | */ |
169 | get canUndo(): boolean { | 192 | setSearchPanelOpen(newShowSearchPanel: boolean): boolean { |
170 | this.observeEditorChanges(); | 193 | if (this.showSearchPanel === newShowSearchPanel) { |
171 | if (!this.editor) { | ||
172 | return false; | 194 | return false; |
173 | } | 195 | } |
174 | const { undo: undoSize } = this.editor.historySize(); | 196 | this.showSearchPanel = newShowSearchPanel; |
175 | return undoSize > 0; | 197 | log.debug('Show search panel', this.showSearchPanel); |
198 | return true; | ||
176 | } | 199 | } |
177 | 200 | ||
178 | undo(): void { | 201 | toggleSearchPanel(): void { |
179 | this.editor?.undo(); | 202 | this.setSearchPanelOpen(!this.showSearchPanel); |
180 | } | 203 | } |
181 | 204 | ||
182 | /** | 205 | setLintPanelOpen(newShowLintPanel: boolean): boolean { |
183 | * @returns `true` if there is history to redo | 206 | if (this.showLintPanel === newShowLintPanel) { |
184 | */ | ||
185 | get canRedo(): boolean { | ||
186 | this.observeEditorChanges(); | ||
187 | if (!this.editor) { | ||
188 | return false; | 207 | return false; |
189 | } | 208 | } |
190 | const { redo: redoSize } = this.editor.historySize(); | 209 | this.showLintPanel = newShowLintPanel; |
191 | return redoSize > 0; | 210 | log.debug('Show lint panel', this.showLintPanel); |
211 | return true; | ||
192 | } | 212 | } |
193 | 213 | ||
194 | redo(): void { | 214 | toggleLintPanel(): void { |
195 | this.editor?.redo(); | 215 | this.setLintPanelOpen(!this.showLintPanel); |
196 | } | ||
197 | |||
198 | toggleLineNumbers(): void { | ||
199 | this.showLineNumbers = !this.showLineNumbers; | ||
200 | } | 216 | } |
201 | } | 217 | } |
diff --git a/language-web/src/main/js/editor/editor.ts b/language-web/src/main/js/editor/editor.ts deleted file mode 100644 index fbf8796b..00000000 --- a/language-web/src/main/js/editor/editor.ts +++ /dev/null | |||
@@ -1,18 +0,0 @@ | |||
1 | import 'codemirror/addon/selection/active-line'; | ||
2 | import 'mode-problem'; | ||
3 | import { Controlled } from 'react-codemirror2'; | ||
4 | import { createServices, removeServices } from 'xtext/xtext-codemirror'; | ||
5 | |||
6 | export interface IEditorChunk { | ||
7 | CodeMirror: typeof Controlled; | ||
8 | |||
9 | createServices: typeof createServices; | ||
10 | |||
11 | removeServices: typeof removeServices; | ||
12 | } | ||
13 | |||
14 | export const editorChunk: IEditorChunk = { | ||
15 | CodeMirror: Controlled, | ||
16 | createServices, | ||
17 | removeServices, | ||
18 | }; | ||
diff --git a/language-web/src/main/js/index.tsx b/language-web/src/main/js/index.tsx index 80c70f23..66ad1f28 100644 --- a/language-web/src/main/js/index.tsx +++ b/language-web/src/main/js/index.tsx | |||
@@ -44,8 +44,7 @@ age(bob, bobAge). | |||
44 | scope Family = 1, Person += 5..10. | 44 | scope Family = 1, Person += 5..10. |
45 | `; | 45 | `; |
46 | 46 | ||
47 | const rootStore = new RootStore(); | 47 | const rootStore = new RootStore(initialValue); |
48 | rootStore.editorStore.updateValue(initialValue); | ||
49 | 48 | ||
50 | const app = ( | 49 | const app = ( |
51 | <RootStoreProvider rootStore={rootStore}> | 50 | <RootStoreProvider rootStore={rootStore}> |
diff --git a/language-web/src/main/js/theme/ThemeStore.ts b/language-web/src/main/js/theme/ThemeStore.ts index 3bbea3a1..db94d9f7 100644 --- a/language-web/src/main/js/theme/ThemeStore.ts +++ b/language-web/src/main/js/theme/ThemeStore.ts | |||
@@ -51,6 +51,10 @@ export class ThemeStore { | |||
51 | return responsiveFontSizes(materialUiTheme); | 51 | return responsiveFontSizes(materialUiTheme); |
52 | } | 52 | } |
53 | 53 | ||
54 | get darkMode(): boolean { | ||
55 | return this.currentThemeData.paletteMode === 'dark'; | ||
56 | } | ||
57 | |||
54 | get className(): string { | 58 | get className(): string { |
55 | return this.currentThemeData.className; | 59 | return this.currentThemeData.className; |
56 | } | 60 | } |