fix: stabilize preview-to-editor SyncTeX jumps
- Replace jumpHandledRef boolean with lastHandledTargetRef so rapid preview clicks are tracked per-target and not swallowed by a stale global flag. - Wrap the Monaco editor in a ResizeObserver container and call editor.layout() before revealRangeInCenter so container resizes (sidebar, window) don't break scroll-to-target. - Harden tinymist/preview/scrollSource handler against missing or non-string filepath payloads.
This commit is contained in:
@@ -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<HTMLDivElement | null>(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 (
|
||||
<div className="flex-1 flex items-center justify-center text-zinc-400 text-sm">
|
||||
@@ -240,7 +257,7 @@ export function EditorView() {
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<div ref={editorContainerRef} className="flex-1 min-h-0">
|
||||
<Editor
|
||||
defaultLanguage="typst"
|
||||
theme="vs"
|
||||
|
||||
@@ -500,8 +500,16 @@ export async function startLsp(
|
||||
// SyncTeX: when user clicks in the preview, tinymist emits this notification
|
||||
// with the source file and position. We forward it to the jump handler.
|
||||
client.onNotification("tinymist/preview/scrollSource", (params) => {
|
||||
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("\\", "/");
|
||||
|
||||
Reference in New Issue
Block a user