• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2023 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {indentWithTab} from '@codemirror/commands';
16import {Transaction} from '@codemirror/state';
17import {oneDarkTheme} from '@codemirror/theme-one-dark';
18import {keymap} from '@codemirror/view';
19import {basicSetup, EditorView} from 'codemirror';
20import m from 'mithril';
21
22export interface EditorAttrs {
23  // Initial state for the editor.
24  initialText?: string;
25
26  // Changing generation is used to force resetting of the editor state
27  // to the current value of initialText.
28  generation?: number;
29
30  // Callback for the Ctrl/Cmd + Enter key binding.
31  onExecute?: (text: string) => void;
32
33  // Callback for every change to the text.
34  onUpdate?: (text: string) => void;
35}
36
37export class Editor implements m.ClassComponent<EditorAttrs> {
38  private editorView?: EditorView;
39  private generation?: number;
40
41  oncreate({dom, attrs}: m.CVnodeDOM<EditorAttrs>) {
42    const keymaps = [indentWithTab];
43    const onExecute = attrs.onExecute;
44    const onUpdate = attrs.onUpdate;
45
46    if (onExecute) {
47      keymaps.push({
48        key: 'Mod-Enter',
49        run: (view: EditorView) => {
50          const state = view.state;
51          const selection = state.selection;
52          let text = state.doc.toString();
53          if (!selection.main.empty) {
54            let selectedText = '';
55
56            for (const r of selection.ranges) {
57              selectedText += text.slice(r.from, r.to);
58            }
59
60            text = selectedText;
61          }
62          onExecute(text);
63          return true;
64        },
65      });
66    }
67
68    let dispatch;
69    if (onUpdate) {
70      dispatch = (tr: Transaction, view: EditorView) => {
71        view.update([tr]);
72        const text = view.state.doc.toString();
73        onUpdate(text);
74      };
75    }
76
77    this.generation = attrs.generation;
78
79    this.editorView = new EditorView({
80      doc: attrs.initialText ?? '',
81      extensions: [keymap.of(keymaps), oneDarkTheme, basicSetup],
82      parent: dom,
83      dispatch,
84    });
85  }
86
87  onupdate({attrs}: m.CVnodeDOM<EditorAttrs>): void {
88    const {initialText, generation} = attrs;
89    const editorView = this.editorView;
90    if (editorView && this.generation !== generation) {
91      const state = editorView.state;
92      editorView.dispatch(
93        state.update({
94          changes: {from: 0, to: state.doc.length, insert: initialText},
95        }),
96      );
97      this.generation = generation;
98    }
99  }
100
101  onremove(): void {
102    if (this.editorView) {
103      this.editorView.destroy();
104      this.editorView = undefined;
105    }
106  }
107
108  view({}: m.Vnode<EditorAttrs, this>): void | m.Children {
109    return m('.pf-editor');
110  }
111}
112