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  }