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