diff --git a/typst-leaf-frontend/src/components/Editor.tsx b/typst-leaf-frontend/src/components/Editor.tsx index 4bec81c..77459c3 100644 --- a/typst-leaf-frontend/src/components/Editor.tsx +++ b/typst-leaf-frontend/src/components/Editor.tsx @@ -142,10 +142,32 @@ export function EditorView() { ); }; - // Jump target effect — reactive (no polling) - const jumpHandledRef = useRef(false); + // Resize observer keeps Monaco laid out when the flex container changes + // size (sidebar toggles, window resize, etc.). Without this, revealRange + // and cursor positioning can be off after a layout change. + const editorContainerRef = useRef(null); + useEffect(() => { + if (!editorReady || !editorContainerRef.current || !editorRef.current) { + return; + } + const editor = editorRef.current; + const ro = new ResizeObserver(() => editor.layout()); + ro.observe(editorContainerRef.current); + return () => ro.disconnect(); + }, [editorReady]); + + // Jump target effect — reactive (no polling). Track the last handled + // target by object identity so rapid clicks don't get swallowed by a stale + // boolean flag, and call editor.layout() before reveal so container + // resizes don't break the scroll. + const lastHandledTargetRef = useRef<{ + file: string; + start?: [number, number]; + end?: [number, number]; + } | null>(null); useEffect(() => { if (!state.jumpTarget || !editorRef.current || !monaco || !model) return; + if (state.jumpTarget === lastHandledTargetRef.current) return; const target = state.jumpTarget; const modelUri = model.uri.toString(); @@ -155,8 +177,7 @@ export function EditorView() { : null; if (!expectedModelUri || modelUri !== expectedModelUri) return; - if (jumpHandledRef.current) return; - jumpHandledRef.current = true; + lastHandledTargetRef.current = target; let selection: Monaco["Selection"] | null = null; if (target.start && target.end) { @@ -176,6 +197,7 @@ export function EditorView() { } if (selection) { + editorRef.current.layout(); editorRef.current.setSelection(selection); editorRef.current.revealRangeInCenter(selection); editorRef.current.focus(); @@ -184,11 +206,6 @@ export function EditorView() { dispatch({ type: "clearJumpTarget" }); }, [state.jumpTarget, model, monaco, uri]); - // Reset jumpHandledRef when a new jump target arrives - if (state.jumpTarget === null) { - jumpHandledRef.current = false; - } - if (!project) { return (
@@ -240,7 +257,7 @@ export function EditorView() {
-
+
{ - const p = params as { filepath: string; start?: [number, number]; end?: [number, number] }; + const p = params as { + filepath?: string; + start?: [number, number]; + end?: [number, number]; + }; if (!onJumpToSource) return; + if (typeof p.filepath !== "string") { + console.warn("[lsp] tinymist/preview/scrollSource missing filepath", params); + return; + } const filepath = p.filepath.includes("://") ? decodeURIComponent(new URL(p.filepath).pathname) : p.filepath.replaceAll("\\", "/");