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