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