• 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 {oneDark} from '@codemirror/theme-one-dark';
18import {keymap} from '@codemirror/view';
19import {basicSetup, EditorView} from 'codemirror';
20import m from 'mithril';
21import {assertExists, assertUnreachable} from '../base/logging';
22import {DragGestureHandler} from '../base/drag_gesture_handler';
23import {DisposableStack} from '../base/disposable_stack';
24import {perfettoSql} from '../base/perfetto_sql_lang/language';
25import {removeFalsyValues} from '../base/array_utils';
26
27export interface EditorAttrs {
28  // Initial state for the editor.
29  initialText?: string;
30
31  // Changing generation is used to force resetting of the editor state
32  // to the current value of initialText.
33  generation?: number;
34
35  // Which language use for syntax highlighting et al. Defaults to none.
36  readonly language?: 'perfetto-sql';
37
38  // Callback for the Ctrl/Cmd + Enter key binding.
39  onExecute?: (text: string) => void;
40
41  // Callback for every change to the text.
42  onUpdate?: (text: string) => void;
43}
44
45export class Editor implements m.ClassComponent<EditorAttrs> {
46  private editorView?: EditorView;
47  private generation?: number;
48  private trash = new DisposableStack();
49
50  oncreate({dom, attrs}: m.CVnodeDOM<EditorAttrs>) {
51    const keymaps = [indentWithTab];
52    const onExecute = attrs.onExecute;
53    const onUpdate = attrs.onUpdate;
54
55    if (onExecute) {
56      keymaps.push({
57        key: 'Mod-Enter',
58        run: (view: EditorView) => {
59          const state = view.state;
60          const selection = state.selection;
61          let text = state.doc.toString();
62          if (!selection.main.empty) {
63            let selectedText = '';
64
65            for (const r of selection.ranges) {
66              selectedText += text.slice(r.from, r.to);
67            }
68
69            text = selectedText;
70          }
71          onExecute(text);
72          m.redraw();
73          return true;
74        },
75      });
76    }
77
78    let dispatch;
79    if (onUpdate) {
80      dispatch = (tr: Transaction, view: EditorView) => {
81        view.update([tr]);
82        const text = view.state.doc.toString();
83        onUpdate(text);
84        m.redraw();
85      };
86    }
87
88    this.generation = attrs.generation;
89
90    const lang = (() => {
91      switch (attrs.language) {
92        case undefined:
93          return undefined;
94        case 'perfetto-sql':
95          return perfettoSql();
96        default:
97          assertUnreachable(attrs.language);
98      }
99    })();
100
101    this.editorView = new EditorView({
102      doc: attrs.initialText ?? '',
103      extensions: removeFalsyValues([
104        keymap.of(keymaps),
105        oneDark,
106        basicSetup,
107        lang,
108      ]),
109      parent: dom,
110      dispatch,
111    });
112
113    // Install the drag handler for the resize bar.
114    let initialH = 0;
115    this.trash.use(
116      new DragGestureHandler(
117        assertExists(dom.querySelector('.resize-handler')) as HTMLElement,
118        /* onDrag */
119        (_x, y) => ((dom as HTMLElement).style.height = `${initialH + y}px`),
120        /* onDragStarted */
121        () => (initialH = dom.clientHeight),
122        /* onDragFinished */
123        () => {},
124      ),
125    );
126  }
127
128  onupdate({attrs}: m.CVnodeDOM<EditorAttrs>): void {
129    const {initialText, generation} = attrs;
130    const editorView = this.editorView;
131    if (editorView && this.generation !== generation) {
132      const state = editorView.state;
133      editorView.dispatch(
134        state.update({
135          changes: {from: 0, to: state.doc.length, insert: initialText},
136        }),
137      );
138      this.generation = generation;
139    }
140  }
141
142  onremove(): void {
143    if (this.editorView) {
144      this.editorView.destroy();
145      this.editorView = undefined;
146    }
147    this.trash.dispose();
148  }
149
150  view({}: m.Vnode<EditorAttrs, this>): void | m.Children {
151    return m('.pf-editor', m('.resize-handler'));
152  }
153}
154