SPB Git

Repository Public

monaco.tsx 3.5 KB 91 lines tsx

1'use client';2/** Monaco wrappers. This module is only ever loaded through next/dynamic (it pulls the whole editor). */3import { useEffect, useRef } from 'react';4import Editor, { DiffEditor, type OnMount } from '@monaco-editor/react';5import { setupMonaco } from './monaco-setup';6import { useTheme } from '@/stores/theme';78const monaco = setupMonaco();9const common = {10  fontFamily: '"JetBrains Mono", ui-monospace, Menlo, monospace',11  fontLigatures: false,12  minimap: { enabled: false },13  scrollBeyondLastLine: false,14  smoothScrolling: false,15  renderLineHighlight: 'line' as const,16  padding: { top: 8, bottom: 8 },17  automaticLayout: true,18  tabSize: 2,19  stickyScroll: { enabled: false },20  overviewRulerBorder: false,21  unicodeHighlight: { ambiguousCharacters: false },22};23const Loading = <div className="flex h-full items-center justify-center text-xs text-muted">Loading editor…</div>;2425export interface CodeEditorProps {26  /** Model identity (keeps undo history per file). */27  modelPath: string;28  value: string;29  onChange?: (value: string) => void;30  onSave?: () => void;31  readOnly?: boolean;32  language?: string;33  fontSize?: number;34  mobile?: boolean;35}36export function CodeEditor({ modelPath, value, onChange, onSave, readOnly, language, fontSize = 13, mobile }: CodeEditorProps) {37  const theme = useTheme((t) => t.theme);38  const saveRef = useRef(onSave);39  saveRef.current = onSave;40  const onMount: OnMount = (editor) => {41    editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => saveRef.current?.());42    if (process.env.NEXT_PUBLIC_E2E === '1') (window as unknown as { __crEditor: unknown }).__crEditor = editor;43  };44  return (45    <Editor46      height="100%"47      theme={theme === 'dark' ? 'cr-dark' : 'cr-light'}48      path={`inmemory://cr/${modelPath}`}49      language={language}50      value={value}51      onChange={(v) => onChange?.(v ?? '')}52      onMount={onMount}53      loading={Loading}54      options={{ ...common, fontSize: mobile ? Math.max(fontSize, 14) : fontSize, readOnly, lineNumbers: mobile ? 'off' : 'on', folding: !mobile, wordWrap: mobile ? 'on' : 'off', glyphMargin: false, lineDecorationsWidth: mobile ? 4 : 10 }}55    />56  );57}5859const diffRefs = new Map<string, number>();6061export function DiffView({ original, modified, modelPath, mobile }: { original: string; modified: string; modelPath: string; mobile?: boolean }) {62  const theme = useTheme((t) => t.theme);63  // The DiffEditor keeps its models (disposing them while the widget is alive throws); they are64  // released shortly after the last view of this diff unmounts.65  useEffect(() => {66    diffRefs.set(modelPath, (diffRefs.get(modelPath) ?? 0) + 1);67    return () => {68      diffRefs.set(modelPath, (diffRefs.get(modelPath) ?? 1) - 1);69      setTimeout(() => {70        if ((diffRefs.get(modelPath) ?? 0) > 0) return;71        diffRefs.delete(modelPath);72        for (const side of ['a', 'b']) monaco.editor.getModel(monaco.Uri.parse(`inmemory://cr-diff/${side}/${modelPath}`))?.dispose();73      }, 250);74    };75  }, [modelPath]);76  return (77    <DiffEditor78      height="100%"79      theme={theme === 'dark' ? 'cr-dark' : 'cr-light'}80      original={original}81      modified={modified}82      originalModelPath={`inmemory://cr-diff/a/${modelPath}`}83      modifiedModelPath={`inmemory://cr-diff/b/${modelPath}`}84      keepCurrentOriginalModel85      keepCurrentModifiedModel86      loading={Loading}87      options={{ ...common, readOnly: true, renderSideBySide: !mobile, originalEditable: false, renderOverviewRuler: false }}88    />89  );90}91
Files main
Menu

Theme