All files / src/components/docpane CodePreview.tsx

30.43% Statements 7/23
0% Branches 0/7
25% Functions 1/4
30.43% Lines 7/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79                                                            2x 2x     2x 2x     2x                                                     2x                     2x    
import { useEffect, useRef, type JSX } from "react";
import { EditorState, type Extension } from "@codemirror/state";
import { EditorView, lineNumbers } from "@codemirror/view";
import { syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language";
import { oneDark } from "@codemirror/theme-one-dark";
import { languageExtensionForUri } from "./languages.js";
import styles from "./DocPane.module.scss";
 
/** Props for {@link CodePreview}. */
export interface CodePreviewProps {
  /** The file's text content. */
  content: string;
  /** The file `file://` URI (drives language detection). */
  uri: string;
  /** Whether the app is in a dark theme (selects the CodeMirror highlight). */
  dark: boolean;
}
 
/**
 * Read-only CodeMirror 6 preview of a text/code file (#1396).
 *
 * A thin manual `EditorView` wrapper (no React binding lib, so no extra React
 * peer surface). The editor is created once per `(uri, dark)`; external content
 * changes (the agent rewrote the file → bridge re-read) are pushed in via
 * `view.dispatch` so the view refreshes without losing the instance. This is
 * also the editable seam for v1 (#1397).
 *
 * Lazy-loaded by {@link DocPane} so CodeMirror stays in its own async chunk.
 */
export function CodePreview({ content, uri, dark }: CodePreviewProps): JSX.Element {
  const hostRef = useRef<HTMLDivElement | null>(null);
  const viewRef = useRef<EditorView | null>(null);
  // Latest content, read at editor-creation time without forcing a recreate on
  // every content change (refs are stable, so they don't belong in effect deps).
  const contentRef = useRef<string>(content);
  contentRef.current = content;
 
  // Create (and recreate on language/theme change) the editor.
  useEffect(() => {
    const host = hostRef.current;
    Iif (host === null) {
      return;
    }
    const language = languageExtensionForUri(uri);
    const extensions: Extension[] = [
      lineNumbers(),
      EditorView.editable.of(false),
      EditorState.readOnly.of(true),
      EditorView.lineWrapping,
      syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
      ...(dark ? [oneDark] : []),
      ...(language ? [language] : []),
    ];
    const view = new EditorView({
      state: EditorState.create({ doc: contentRef.current, extensions }),
      parent: host,
    });
    viewRef.current = view;
    return () => {
      view.destroy();
      viewRef.current = null;
    };
  }, [uri, dark]);
 
  // Reflect external content changes (live refresh) into the existing editor.
  useEffect(() => {
    const view = viewRef.current;
    Iif (view === null) {
      return;
    }
    const current = view.state.doc.toString();
    Iif (current !== content) {
      view.dispatch({ changes: { from: 0, to: current.length, insert: content } });
    }
  }, [content]);
 
  return <div ref={hostRef} className={styles.codeHost} data-testid="doc-code" />;
}