aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/editor/EditorArea.tsx
blob: 58d6518414714cf766022ba972dede7a9a2bdbef (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { Command, EditorView } from '@codemirror/view';
import { closeSearchPanel, openSearchPanel } from '@codemirror/search';
import { closeLintPanel, openLintPanel } from '@codemirror/lint';
import { observer } from 'mobx-react-lite';
import React, { useEffect, useRef, useState } from 'react';

import { EditorParent } from './EditorParent';
import { getLogger } from '../logging';
import { useRootStore } from '../RootStore';

const log = getLogger('EditorArea');

function usePanel(
  label: string,
  stateToSet: boolean,
  editorView: EditorView | null,
  openCommand: Command,
  closeCommand: Command,
) {
  const [cachedViewState, setCachedViewState] = useState<boolean>(false);
  useEffect(() => {
    if (editorView === null || cachedViewState === stateToSet) {
      return;
    }
    const success = stateToSet ? openCommand(editorView) : closeCommand(editorView);
    if (!success) {
      log.error(
        'Failed to synchronize',
        label,
        'panel state - store state:',
        cachedViewState,
        'view state:',
        stateToSet,
      );
    }
    setCachedViewState(stateToSet);
  }, [
    stateToSet,
    editorView,
    cachedViewState,
    label,
    openCommand,
    closeCommand,
  ]);
  return setCachedViewState;
}

export const EditorArea = observer(() => {
  const { editorStore } = useRootStore();
  const editorParentRef = useRef<HTMLDivElement | null>(null);
  const [editorViewState, setEditorViewState] = useState<EditorView | null>(null);

  const setSearchPanelOpen = usePanel(
    'search',
    editorStore.showSearchPanel,
    editorViewState,
    openSearchPanel,
    closeSearchPanel,
  );

  const setLintPanelOpen = usePanel(
    'lint',
    editorStore.showLintPanel,
    editorViewState,
    openLintPanel,
    closeLintPanel,
  );

  useEffect(() => {
    if (editorParentRef.current === null) {
      // Nothing to clean up.
      return () => {};
    }

    const editorView = new EditorView({
      state: editorStore.state,
      parent: editorParentRef.current,
      dispatch: (transaction) => {
        editorStore.onTransaction(transaction);
        editorView.update([transaction]);
        if (editorView.state !== editorStore.state) {
          log.error(
            'Failed to synchronize editor state - store state:',
            editorStore.state,
            'view state:',
            editorView.state,
          );
        }
      },
    });
    setEditorViewState(editorView);
    setSearchPanelOpen(false);
    setLintPanelOpen(false);
    // `dispatch` is bound to the view instance,
    // so it does not have to be called as a method.
    // eslint-disable-next-line @typescript-eslint/unbound-method
    editorStore.updateDispatcher(editorView.dispatch);
    log.info('Editor created');

    return () => {
      editorStore.updateDispatcher(null);
      editorView.destroy();
      log.info('Editor destroyed');
    };
  }, [
    editorParentRef,
    editorStore,
    setSearchPanelOpen,
    setLintPanelOpen,
  ]);

  return (
    <EditorParent
      className="dark"
      sx={{
        '.cm-lineNumbers': editorStore.showLineNumbers ? {} : {
          display: 'none !important',
        },
      }}
      ref={editorParentRef}
    />
  );
});