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'; 21 22export interface EditorAttrs { 23 // Initial state for the editor. 24 initialText?: string; 25 26 // Changing generation is used to force resetting of the editor state 27 // to the current value of initialText. 28 generation?: number; 29 30 // Callback for the Ctrl/Cmd + Enter key binding. 31 onExecute?: (text: string) => void; 32 33 // Callback for every change to the text. 34 onUpdate?: (text: string) => void; 35} 36 37export class Editor implements m.ClassComponent<EditorAttrs> { 38 private editorView?: EditorView; 39 private generation?: number; 40 41 oncreate({dom, attrs}: m.CVnodeDOM<EditorAttrs>) { 42 const keymaps = [indentWithTab]; 43 const onExecute = attrs.onExecute; 44 const onUpdate = attrs.onUpdate; 45 46 if (onExecute) { 47 keymaps.push({ 48 key: 'Mod-Enter', 49 run: (view: EditorView) => { 50 const state = view.state; 51 const selection = state.selection; 52 let text = state.doc.toString(); 53 if (!selection.main.empty) { 54 let selectedText = ''; 55 56 for (const r of selection.ranges) { 57 selectedText += text.slice(r.from, r.to); 58 } 59 60 text = selectedText; 61 } 62 onExecute(text); 63 return true; 64 }, 65 }); 66 } 67 68 let dispatch; 69 if (onUpdate) { 70 dispatch = (tr: Transaction, view: EditorView) => { 71 view.update([tr]); 72 const text = view.state.doc.toString(); 73 onUpdate(text); 74 }; 75 } 76 77 this.generation = attrs.generation; 78 79 this.editorView = new EditorView({ 80 doc: attrs.initialText ?? '', 81 extensions: [keymap.of(keymaps), oneDarkTheme, basicSetup], 82 parent: dom, 83 dispatch, 84 }); 85 } 86 87 onupdate({attrs}: m.CVnodeDOM<EditorAttrs>): void { 88 const {initialText, generation} = attrs; 89 const editorView = this.editorView; 90 if (editorView && this.generation !== generation) { 91 const state = editorView.state; 92 editorView.dispatch( 93 state.update({ 94 changes: {from: 0, to: state.doc.length, insert: initialText}, 95 }), 96 ); 97 this.generation = generation; 98 } 99 } 100 101 onremove(): void { 102 if (this.editorView) { 103 this.editorView.destroy(); 104 this.editorView = undefined; 105 } 106 } 107 108 view({}: m.Vnode<EditorAttrs, this>): void | m.Children { 109 return m('.pf-editor'); 110 } 111} 112