xref: /aosp_15_r20/external/perfetto/ui/src/widgets/editor.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2023 The Android Open Source Project
2*6dbdd20aSAndroid Build Coastguard Worker//
3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License");
4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License.
5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at
6*6dbdd20aSAndroid Build Coastguard Worker//
7*6dbdd20aSAndroid Build Coastguard Worker//      http://www.apache.org/licenses/LICENSE-2.0
8*6dbdd20aSAndroid Build Coastguard Worker//
9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS,
11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and
13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License.
14*6dbdd20aSAndroid Build Coastguard Worker
15*6dbdd20aSAndroid Build Coastguard Workerimport {indentWithTab} from '@codemirror/commands';
16*6dbdd20aSAndroid Build Coastguard Workerimport {Transaction} from '@codemirror/state';
17*6dbdd20aSAndroid Build Coastguard Workerimport {oneDarkTheme} from '@codemirror/theme-one-dark';
18*6dbdd20aSAndroid Build Coastguard Workerimport {keymap} from '@codemirror/view';
19*6dbdd20aSAndroid Build Coastguard Workerimport {basicSetup, EditorView} from 'codemirror';
20*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril';
21*6dbdd20aSAndroid Build Coastguard Workerimport {assertExists} from '../base/logging';
22*6dbdd20aSAndroid Build Coastguard Workerimport {DragGestureHandler} from '../base/drag_gesture_handler';
23*6dbdd20aSAndroid Build Coastguard Workerimport {DisposableStack} from '../base/disposable_stack';
24*6dbdd20aSAndroid Build Coastguard Workerimport {scheduleFullRedraw} from './raf';
25*6dbdd20aSAndroid Build Coastguard Worker
26*6dbdd20aSAndroid Build Coastguard Workerexport interface EditorAttrs {
27*6dbdd20aSAndroid Build Coastguard Worker  // Initial state for the editor.
28*6dbdd20aSAndroid Build Coastguard Worker  initialText?: string;
29*6dbdd20aSAndroid Build Coastguard Worker
30*6dbdd20aSAndroid Build Coastguard Worker  // Changing generation is used to force resetting of the editor state
31*6dbdd20aSAndroid Build Coastguard Worker  // to the current value of initialText.
32*6dbdd20aSAndroid Build Coastguard Worker  generation?: number;
33*6dbdd20aSAndroid Build Coastguard Worker
34*6dbdd20aSAndroid Build Coastguard Worker  // Callback for the Ctrl/Cmd + Enter key binding.
35*6dbdd20aSAndroid Build Coastguard Worker  onExecute?: (text: string) => void;
36*6dbdd20aSAndroid Build Coastguard Worker
37*6dbdd20aSAndroid Build Coastguard Worker  // Callback for every change to the text.
38*6dbdd20aSAndroid Build Coastguard Worker  onUpdate?: (text: string) => void;
39*6dbdd20aSAndroid Build Coastguard Worker}
40*6dbdd20aSAndroid Build Coastguard Worker
41*6dbdd20aSAndroid Build Coastguard Workerexport class Editor implements m.ClassComponent<EditorAttrs> {
42*6dbdd20aSAndroid Build Coastguard Worker  private editorView?: EditorView;
43*6dbdd20aSAndroid Build Coastguard Worker  private generation?: number;
44*6dbdd20aSAndroid Build Coastguard Worker  private trash = new DisposableStack();
45*6dbdd20aSAndroid Build Coastguard Worker
46*6dbdd20aSAndroid Build Coastguard Worker  oncreate({dom, attrs}: m.CVnodeDOM<EditorAttrs>) {
47*6dbdd20aSAndroid Build Coastguard Worker    const keymaps = [indentWithTab];
48*6dbdd20aSAndroid Build Coastguard Worker    const onExecute = attrs.onExecute;
49*6dbdd20aSAndroid Build Coastguard Worker    const onUpdate = attrs.onUpdate;
50*6dbdd20aSAndroid Build Coastguard Worker
51*6dbdd20aSAndroid Build Coastguard Worker    if (onExecute) {
52*6dbdd20aSAndroid Build Coastguard Worker      keymaps.push({
53*6dbdd20aSAndroid Build Coastguard Worker        key: 'Mod-Enter',
54*6dbdd20aSAndroid Build Coastguard Worker        run: (view: EditorView) => {
55*6dbdd20aSAndroid Build Coastguard Worker          const state = view.state;
56*6dbdd20aSAndroid Build Coastguard Worker          const selection = state.selection;
57*6dbdd20aSAndroid Build Coastguard Worker          let text = state.doc.toString();
58*6dbdd20aSAndroid Build Coastguard Worker          if (!selection.main.empty) {
59*6dbdd20aSAndroid Build Coastguard Worker            let selectedText = '';
60*6dbdd20aSAndroid Build Coastguard Worker
61*6dbdd20aSAndroid Build Coastguard Worker            for (const r of selection.ranges) {
62*6dbdd20aSAndroid Build Coastguard Worker              selectedText += text.slice(r.from, r.to);
63*6dbdd20aSAndroid Build Coastguard Worker            }
64*6dbdd20aSAndroid Build Coastguard Worker
65*6dbdd20aSAndroid Build Coastguard Worker            text = selectedText;
66*6dbdd20aSAndroid Build Coastguard Worker          }
67*6dbdd20aSAndroid Build Coastguard Worker          onExecute(text);
68*6dbdd20aSAndroid Build Coastguard Worker          scheduleFullRedraw('force');
69*6dbdd20aSAndroid Build Coastguard Worker          return true;
70*6dbdd20aSAndroid Build Coastguard Worker        },
71*6dbdd20aSAndroid Build Coastguard Worker      });
72*6dbdd20aSAndroid Build Coastguard Worker    }
73*6dbdd20aSAndroid Build Coastguard Worker
74*6dbdd20aSAndroid Build Coastguard Worker    let dispatch;
75*6dbdd20aSAndroid Build Coastguard Worker    if (onUpdate) {
76*6dbdd20aSAndroid Build Coastguard Worker      dispatch = (tr: Transaction, view: EditorView) => {
77*6dbdd20aSAndroid Build Coastguard Worker        view.update([tr]);
78*6dbdd20aSAndroid Build Coastguard Worker        const text = view.state.doc.toString();
79*6dbdd20aSAndroid Build Coastguard Worker        onUpdate(text);
80*6dbdd20aSAndroid Build Coastguard Worker        scheduleFullRedraw('force');
81*6dbdd20aSAndroid Build Coastguard Worker      };
82*6dbdd20aSAndroid Build Coastguard Worker    }
83*6dbdd20aSAndroid Build Coastguard Worker
84*6dbdd20aSAndroid Build Coastguard Worker    this.generation = attrs.generation;
85*6dbdd20aSAndroid Build Coastguard Worker
86*6dbdd20aSAndroid Build Coastguard Worker    this.editorView = new EditorView({
87*6dbdd20aSAndroid Build Coastguard Worker      doc: attrs.initialText ?? '',
88*6dbdd20aSAndroid Build Coastguard Worker      extensions: [keymap.of(keymaps), oneDarkTheme, basicSetup],
89*6dbdd20aSAndroid Build Coastguard Worker      parent: dom,
90*6dbdd20aSAndroid Build Coastguard Worker      dispatch,
91*6dbdd20aSAndroid Build Coastguard Worker    });
92*6dbdd20aSAndroid Build Coastguard Worker
93*6dbdd20aSAndroid Build Coastguard Worker    // Install the drag handler for the resize bar.
94*6dbdd20aSAndroid Build Coastguard Worker    let initialH = 0;
95*6dbdd20aSAndroid Build Coastguard Worker    this.trash.use(
96*6dbdd20aSAndroid Build Coastguard Worker      new DragGestureHandler(
97*6dbdd20aSAndroid Build Coastguard Worker        assertExists(dom.querySelector('.resize-handler')) as HTMLElement,
98*6dbdd20aSAndroid Build Coastguard Worker        /* onDrag */
99*6dbdd20aSAndroid Build Coastguard Worker        (_x, y) => ((dom as HTMLElement).style.height = `${initialH + y}px`),
100*6dbdd20aSAndroid Build Coastguard Worker        /* onDragStarted */
101*6dbdd20aSAndroid Build Coastguard Worker        () => (initialH = dom.clientHeight),
102*6dbdd20aSAndroid Build Coastguard Worker        /* onDragFinished */
103*6dbdd20aSAndroid Build Coastguard Worker        () => {},
104*6dbdd20aSAndroid Build Coastguard Worker      ),
105*6dbdd20aSAndroid Build Coastguard Worker    );
106*6dbdd20aSAndroid Build Coastguard Worker  }
107*6dbdd20aSAndroid Build Coastguard Worker
108*6dbdd20aSAndroid Build Coastguard Worker  onupdate({attrs}: m.CVnodeDOM<EditorAttrs>): void {
109*6dbdd20aSAndroid Build Coastguard Worker    const {initialText, generation} = attrs;
110*6dbdd20aSAndroid Build Coastguard Worker    const editorView = this.editorView;
111*6dbdd20aSAndroid Build Coastguard Worker    if (editorView && this.generation !== generation) {
112*6dbdd20aSAndroid Build Coastguard Worker      const state = editorView.state;
113*6dbdd20aSAndroid Build Coastguard Worker      editorView.dispatch(
114*6dbdd20aSAndroid Build Coastguard Worker        state.update({
115*6dbdd20aSAndroid Build Coastguard Worker          changes: {from: 0, to: state.doc.length, insert: initialText},
116*6dbdd20aSAndroid Build Coastguard Worker        }),
117*6dbdd20aSAndroid Build Coastguard Worker      );
118*6dbdd20aSAndroid Build Coastguard Worker      this.generation = generation;
119*6dbdd20aSAndroid Build Coastguard Worker    }
120*6dbdd20aSAndroid Build Coastguard Worker  }
121*6dbdd20aSAndroid Build Coastguard Worker
122*6dbdd20aSAndroid Build Coastguard Worker  onremove(): void {
123*6dbdd20aSAndroid Build Coastguard Worker    if (this.editorView) {
124*6dbdd20aSAndroid Build Coastguard Worker      this.editorView.destroy();
125*6dbdd20aSAndroid Build Coastguard Worker      this.editorView = undefined;
126*6dbdd20aSAndroid Build Coastguard Worker    }
127*6dbdd20aSAndroid Build Coastguard Worker    this.trash.dispose();
128*6dbdd20aSAndroid Build Coastguard Worker  }
129*6dbdd20aSAndroid Build Coastguard Worker
130*6dbdd20aSAndroid Build Coastguard Worker  view({}: m.Vnode<EditorAttrs, this>): void | m.Children {
131*6dbdd20aSAndroid Build Coastguard Worker    return m('.pf-editor', m('.resize-handler'));
132*6dbdd20aSAndroid Build Coastguard Worker  }
133*6dbdd20aSAndroid Build Coastguard Worker}
134