diff options
Diffstat (limited to 'language-web/src/main/js/editor/EditorArea.tsx')
-rw-r--r-- | language-web/src/main/js/editor/EditorArea.tsx | 142 |
1 files changed, 112 insertions, 30 deletions
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 | }); |