diff --git a/surfsense_web/components/editor/source-code-editor.tsx b/surfsense_web/components/editor/source-code-editor.tsx index 9102dffe9..0277cde85 100644 --- a/surfsense_web/components/editor/source-code-editor.tsx +++ b/surfsense_web/components/editor/source-code-editor.tsx @@ -2,7 +2,7 @@ import dynamic from "next/dynamic"; import { useTheme } from "next-themes"; -import { useEffect, useRef } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { Spinner } from "@/components/ui/spinner"; const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { @@ -17,6 +17,8 @@ interface SourceCodeEditorProps { readOnly?: boolean; fontSize?: number; onSave?: () => Promise | void; + /** 1-based inclusive line range to reveal and highlight (e.g. a citation). */ + highlightLines?: { start: number; end: number } | null; } export function SourceCodeEditor({ @@ -27,10 +29,40 @@ export function SourceCodeEditor({ readOnly = false, fontSize = 12, onSave, + highlightLines = null, }: SourceCodeEditorProps) { const { resolvedTheme } = useTheme(); const onSaveRef = useRef(onSave); const monacoRef = useRef(null); + const editorRef = useRef(null); + const decorationsRef = useRef(null); + const highlightLinesRef = useRef(highlightLines); + highlightLinesRef.current = highlightLines; + + const applyHighlight = useCallback(() => { + const editor = editorRef.current; + const monaco = monacoRef.current; + if (!editor || !monaco) return; + if (decorationsRef.current) { + decorationsRef.current.clear(); + decorationsRef.current = null; + } + const range = highlightLinesRef.current; + if (!range) return; + const start = Math.max(1, Math.floor(range.start)); + const end = Math.max(start, Math.floor(range.end)); + decorationsRef.current = editor.createDecorationsCollection([ + { + range: new monaco.Range(start, 1, end, 1), + options: { isWholeLine: true, className: "citation-line-highlight" }, + }, + ]); + editor.revealLinesInCenter(start, end); + }, []); + + useEffect(() => { + applyHighlight(); + }, [applyHighlight, highlightLines?.start, highlightLines?.end]); const normalizedModelPath = (() => { const raw = (path || "local-file.txt").trim(); const withLeadingSlash = raw.startsWith("/") ? raw : `/${raw}`; @@ -104,7 +136,10 @@ export function SourceCodeEditor({ }} onMount={(editor, monaco) => { monacoRef.current = monaco; + editorRef.current = editor; applySidebarTheme(monaco); + // Defer one frame so the model is laid out before revealing. + requestAnimationFrame(() => applyHighlight()); if (!isManualSaveEnabled) return; editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { void onSaveRef.current?.();