go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/generic_libs/components/code_mirror_editor.tsx (about) 1 // Copyright 2023 The LUCI Authors. 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 15 import 'codemirror/addon/fold/foldgutter.css'; 16 import 'codemirror/lib/codemirror.css'; 17 import 'codemirror/addon/edit/matchbrackets'; 18 import 'codemirror/addon/fold/brace-fold'; 19 import 'codemirror/addon/fold/foldcode'; 20 import 'codemirror/addon/fold/foldgutter'; 21 import 'codemirror/mode/javascript/javascript'; 22 import { Theme } from '@emotion/react'; 23 import styled, { Interpolation } from '@emotion/styled'; 24 import * as CodeMirror from 'codemirror'; 25 import { useEffect, useRef } from 'react'; 26 27 const Container = styled.div` 28 display: block; 29 border-radius: 4px; 30 border: 1px solid var(--divider-color); 31 overflow: auto; 32 33 & .CodeMirror { 34 height: auto; 35 font-size: 12px; 36 } 37 & .cm-property.cm-string { 38 color: #318495; 39 } 40 & .cm-string:not(.cm-property) { 41 color: #036a06; 42 } 43 `; 44 45 export interface CodeMirrorEditorProps { 46 readonly value: string; 47 /** 48 * The editor configuration. The value is only used when initializing the 49 * editor. Updates are not applied. 50 */ 51 readonly initOptions?: CodeMirror.EditorConfiguration; 52 readonly onInit?: (editor: CodeMirror.Editor) => void; 53 readonly css?: Interpolation<Theme>; 54 readonly className?: string; 55 } 56 57 // We cannot use @uiw/react-codemirror@4 because it uses codemirror v6, which 58 // no longer supports viewportMargin: Infinity, which is required to support 59 // searching content hidden behind a scrollbar. 60 // We cannot use @uiw/react-codemirror@3 because it yields various react-dom 61 // validation errors with the current version of React. 62 // And neither version offers a good way to attach fold/unfold event listeners 63 // BEFORE any content is rendered. 64 export function CodeMirrorEditor({ 65 value, 66 initOptions, 67 onInit, 68 css, 69 className, 70 }: CodeMirrorEditorProps) { 71 const textAreaRef = useRef<HTMLTextAreaElement | null>(null); 72 const editorRef = useRef<CodeMirror.EditorFromTextArea | null>(null); 73 74 // Wrap them in refs so eslint doesn't complain about missing dependencies in 75 // `useEffect` hooks. 76 const firstInitOptions = useRef(initOptions); 77 const firstOnInit = useRef(onInit); 78 79 useEffect(() => { 80 // This will never happen, but useful for type narrowing. 81 if (!textAreaRef.current) { 82 return; 83 } 84 85 if (editorRef.current) { 86 return; 87 } 88 89 const editor = CodeMirror.fromTextArea( 90 textAreaRef.current, 91 firstInitOptions.current, 92 ); 93 editorRef.current = editor; 94 firstOnInit.current?.(editorRef.current); 95 }, []); 96 97 useEffect(() => { 98 // This will never happen, but useful for type narrowing. 99 if (!editorRef.current) { 100 return; 101 } 102 103 editorRef.current.setValue(value); 104 105 // Somehow codemirror does not display the content when the editor is 106 // initialized very early (it's likely that codemirror has some internal 107 // asynchronous initialization steps). 108 // Force refresh AFTER all functions in the present queue are executed to 109 // to ensure the content are rendered properly. 110 setTimeout(() => editorRef.current?.refresh()); 111 }, [value]); 112 113 return ( 114 <Container className={className} css={css}> 115 <textarea ref={textAreaRef} /> 116 </Container> 117 ); 118 }