Skip to content

Commit 93081c8

Browse files
committed
frontend/latex/output: simplify auto sync state management
1 parent 3a34c1a commit 93081c8

File tree

5 files changed

+106
-89
lines changed

5 files changed

+106
-89
lines changed

src/packages/frontend/frame-editors/latex-editor/actions.ts

Lines changed: 74 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
33
* License: MS-RSL – see LICENSE.md for details
44
*/
@@ -7,7 +7,6 @@
77
LaTeX Editor Actions.
88
*/
99

10-
1110
// cSpell:ignore rtex cmdl ramdisk maketitle documentclass outdirflag latexer rescan
1211

1312
const MINIMAL = `\\documentclass{article}
@@ -106,7 +105,7 @@ interface LatexEditorState extends CodeEditorState {
106105
switch_output_to_pdf_tab?: boolean; // used for SyncTeX to switch output panel to PDF tab
107106
output_panel_id_for_sync?: string; // stores the output panel ID for SyncTeX operations
108107
// job_infos: JobInfos;
109-
sync_in_progress?: boolean; // flag to prevent sync loops - true when any sync operation is in progress
108+
autoSyncInProgress?: boolean; // unified flag to prevent sync loops - true when any auto sync operation is in progress
110109
}
111110

112111
export class Actions extends BaseActions<LatexEditorState> {
@@ -136,8 +135,6 @@ export class Actions extends BaseActions<LatexEditorState> {
136135
private canonical_paths: { [path: string]: string } = {};
137136
private parsed_output_log?: IProcessedLatexLog;
138137

139-
// Flag to prevent infinite sync loops - use local variable for immediate sync control
140-
private _sync_in_progress = false;
141138
private _last_sync_time = 0;
142139

143140
// Auto-sync function for cursor position changes (forward sync: source → PDF)
@@ -146,19 +143,26 @@ export class Actions extends BaseActions<LatexEditorState> {
146143
column: number,
147144
filename: string,
148145
): Promise<void> {
149-
if (this._sync_in_progress) {
146+
if (this.is_auto_sync_in_progress()) {
150147
return; // Prevent sync loops
151148
}
152149

153-
this._sync_in_progress = true;
154-
this.set_sync_in_progress(true); // Also update state for UI
150+
this.set_auto_sync_in_progress(true);
155151
try {
156152
await this.synctex_tex_to_pdf(line, column, filename);
153+
154+
// Fallback: Clear flag after timeout if viewport change doesn't happen
155+
setTimeout(() => {
156+
if (this.is_auto_sync_in_progress()) {
157+
this.set_auto_sync_in_progress(false);
158+
}
159+
}, 2000);
160+
161+
// Note: The autoSyncInProgress flag will be cleared when PDF viewport actually changes
157162
} catch (error) {
158163
console.warn("Auto-sync forward search failed:", error);
159-
} finally {
160-
this._sync_in_progress = false;
161-
this.set_sync_in_progress(false); // Also update state for UI
164+
// Clear flag on error since viewport won't change
165+
this.set_auto_sync_in_progress(false);
162166
}
163167
}
164168

@@ -1332,8 +1336,20 @@ export class Actions extends BaseActions<LatexEditorState> {
13321336
this.update_gutters();
13331337
}
13341338

1335-
async synctex_pdf_to_tex(page: number, x: number, y: number): Promise<void> {
1336-
// Note: Flag management is handled by the caller
1339+
async synctex_pdf_to_tex(
1340+
page: number,
1341+
x: number,
1342+
y: number,
1343+
manual: boolean = false,
1344+
): Promise<void> {
1345+
// Only check auto sync flag for automatic sync, not manual double-clicks
1346+
if (!manual && this.is_auto_sync_in_progress()) {
1347+
return; // Prevent sync loops
1348+
}
1349+
1350+
if (!manual) {
1351+
this.set_auto_sync_in_progress(true);
1352+
}
13371353
this.set_status("Running SyncTex...");
13381354
try {
13391355
const info = await synctex.pdf_to_tex({
@@ -1365,10 +1381,18 @@ export class Actions extends BaseActions<LatexEditorState> {
13651381
this.set_error(
13661382
'Synctex failed to run. Try "Force Rebuild" your project (use the Build frame) or retry once the build is complete.',
13671383
);
1384+
// Clear flag since sync failed (only for automatic sync)
1385+
if (!manual) {
1386+
this.set_auto_sync_in_progress(false);
1387+
}
13681388
return;
13691389
}
13701390
console.warn("ERROR ", err);
13711391
this.set_error(err);
1392+
// Clear flag since sync failed (only for automatic sync)
1393+
if (!manual) {
1394+
this.set_auto_sync_in_progress(false);
1395+
}
13721396
} finally {
13731397
this.set_status("");
13741398
}
@@ -1381,6 +1405,13 @@ export class Actions extends BaseActions<LatexEditorState> {
13811405
if (this.knitr) {
13821406
// #v0 will not support multi-file knitr.
13831407
this.programmatically_goto_line(line, true, true);
1408+
// Clear auto sync flag after cursor has moved (backward sync completion)
1409+
// Only for automatic sync - manual sync doesn't set the flag
1410+
if (this.is_auto_sync_in_progress()) {
1411+
setTimeout(() => {
1412+
this.set_auto_sync_in_progress(false);
1413+
}, 200); // Give time for cursor to actually move
1414+
}
13841415
return;
13851416
}
13861417
// Focus a cm frame so that we split a code editor below.
@@ -1393,6 +1424,14 @@ export class Actions extends BaseActions<LatexEditorState> {
13931424
throw Error(`actions for "${path}" must be defined`);
13941425
}
13951426
(actions as BaseActions).programmatically_goto_line(line, true, true, id);
1427+
1428+
// Clear auto sync flag after cursor has moved (backward sync completion)
1429+
// Only for automatic sync - manual sync doesn't set the flag
1430+
if (this.is_auto_sync_in_progress()) {
1431+
setTimeout(() => {
1432+
this.set_auto_sync_in_progress(false);
1433+
}, 200); // Give time for cursor to actually move
1434+
}
13961435
}
13971436

13981437
// Check if auto-sync is enabled for any output panel
@@ -1404,9 +1443,10 @@ export class Actions extends BaseActions<LatexEditorState> {
14041443
for (const [key, value] of local_view_state.entrySeq()) {
14051444
// Only check output panels
14061445
if (this._is_output_panel(key) && value) {
1407-
const autoSyncEnabled = typeof value.get === 'function'
1408-
? value.get("autoSyncEnabled")
1409-
: value.autoSyncEnabled;
1446+
const autoSyncEnabled =
1447+
typeof value.get === "function"
1448+
? value.get("autoSyncEnabled")
1449+
: value.autoSyncEnabled;
14101450
if (autoSyncEnabled) {
14111451
return true;
14121452
}
@@ -1415,25 +1455,22 @@ export class Actions extends BaseActions<LatexEditorState> {
14151455
return false;
14161456
}
14171457

1418-
// Set sync in progress flag in state (for UI components)
1419-
private set_sync_in_progress(inProgress: boolean): void {
1420-
// Defer state update to avoid React rendering conflicts
1421-
setTimeout(() => {
1422-
this.setState({ sync_in_progress: inProgress });
1423-
}, 0);
1458+
// Set auto sync in progress flag in state
1459+
private set_auto_sync_in_progress(inProgress: boolean): void {
1460+
this.setState({ autoSyncInProgress: inProgress });
14241461
}
14251462

1426-
// Check if sync is currently in progress
1427-
private is_sync_in_progress(): boolean {
1428-
return this._sync_in_progress;
1463+
// Check if auto sync is currently in progress
1464+
private is_auto_sync_in_progress(): boolean {
1465+
return this.store.get("autoSyncInProgress") ?? false;
14291466
}
14301467

14311468
// Handle cursor movement - called by BaseActions.set_cursor_locs
14321469
public handle_cursor_move(locs: any[]): void {
14331470
if (!this.is_auto_sync_enabled() || locs.length === 0) return;
14341471

14351472
// Prevent duplicate sync operations
1436-
if (this.is_sync_in_progress()) return;
1473+
if (this.is_auto_sync_in_progress()) return;
14371474

14381475
// Throttle sync operations to prevent excessive calls (max once every 500ms)
14391476
const now = Date.now();
@@ -1457,7 +1494,7 @@ export class Actions extends BaseActions<LatexEditorState> {
14571494
if (!this.is_auto_sync_enabled()) return;
14581495

14591496
// Prevent duplicate sync operations
1460-
if (this.is_sync_in_progress()) return;
1497+
if (this.is_auto_sync_in_progress()) return;
14611498

14621499
// Use current file if filename not provided
14631500
const file = filename || this.path;
@@ -1474,7 +1511,10 @@ export class Actions extends BaseActions<LatexEditorState> {
14741511

14751512
_get_most_recent_output_panel(): string | undefined {
14761513
let result = this._get_most_recent_active_frame_id_of_type("output");
1477-
console.log("LaTeX: _get_most_recent_output_panel() via active history returning", result);
1514+
console.log(
1515+
"LaTeX: _get_most_recent_output_panel() via active history returning",
1516+
result,
1517+
);
14781518

14791519
// If no recently active output panel found, look for any output panel
14801520
if (!result) {
@@ -1546,20 +1586,22 @@ export class Actions extends BaseActions<LatexEditorState> {
15461586
this._get_most_recent_output_panel();
15471587
let pdfjs_id: string | undefined;
15481588

1549-
console.log("LaTeX forward sync: output_panel_id =", output_panel_id);
1589+
// console.log("LaTeX forward sync: output_panel_id =", output_panel_id);
15501590

15511591
if (output_panel_id) {
15521592
// There's an output panel - switch it to PDF tab and use it
1553-
console.log("LaTeX forward sync: Using output panel", output_panel_id);
1593+
// console.log("LaTeX forward sync: Using output panel", output_panel_id);
15541594
this._switch_output_panel_to_pdf(output_panel_id);
15551595
pdfjs_id = output_panel_id;
15561596
} else {
15571597
// No output panel, look for standalone PDF viewer
1558-
console.log("LaTeX forward sync: No output panel found, looking for standalone PDFJS");
1598+
// console.log(
1599+
// "LaTeX forward sync: No output panel found, looking for standalone PDFJS",
1600+
// );
15591601
pdfjs_id = this._get_most_recent_pdfjs();
15601602
if (!pdfjs_id) {
15611603
// no pdfjs preview, so make one
1562-
console.log("LaTeX forward sync: Creating new PDFJS panel");
1604+
// console.log("LaTeX forward sync: Creating new PDFJS panel");
15631605
this.split_frame("col", this._get_active_id(), "pdfjs_canvas");
15641606
pdfjs_id = this._get_most_recent_pdfjs();
15651607
if (!pdfjs_id) {

src/packages/frontend/frame-editors/latex-editor/output.tsx

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,7 @@ import {
3434
} from "@cocalc/frontend/components";
3535
import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown";
3636
import { EditorState } from "@cocalc/frontend/frame-editors/frame-tree/types";
37-
import {
38-
exec,
39-
project_api,
40-
} from "@cocalc/frontend/frame-editors/generic/client";
37+
import { project_api } from "@cocalc/frontend/frame-editors/generic/client";
4138
import { filenameIcon } from "@cocalc/frontend/file-associations";
4239
import { editor, labels } from "@cocalc/frontend/i18n";
4340
import { path_split, plural } from "@cocalc/util/misc";
@@ -119,15 +116,6 @@ export function Output(props: OutputProps) {
119116
setViewportInfo(null);
120117
}, []);
121118

122-
// Flag to temporarily disable viewport tracking during auto-sync
123-
const [disableViewportTracking, setDisableViewportTracking] = useState(false);
124-
125-
// Watch for sync in progress to disable viewport tracking
126-
const syncInProgress = useRedux([name, "sync_in_progress"]) ?? false;
127-
React.useEffect(() => {
128-
setDisableViewportTracking(syncInProgress);
129-
}, [syncInProgress]);
130-
131119
// Table of contents data
132120
const contents: TableOfContentsEntryList | undefined = useRedux([
133121
name,
@@ -170,16 +158,6 @@ export function Output(props: OutputProps) {
170158
setTimeout(() => actions.updateTableOfContents(true));
171159
}, []);
172160

173-
// Also disable viewport tracking during PDF scrolling operations
174-
const scrollIntoView = useRedux([name, "scroll_pdf_into_view"]);
175-
React.useEffect(() => {
176-
if (scrollIntoView) {
177-
setDisableViewportTracking(true);
178-
// Re-enable after scroll completes
179-
setTimeout(() => setDisableViewportTracking(false), 1000);
180-
}
181-
}, [scrollIntoView]);
182-
183161
// Sync state with stored values when they change
184162
React.useEffect(() => {
185163
setActiveTab(storedTab);
@@ -325,8 +303,17 @@ export function Output(props: OutputProps) {
325303
(actions as any)._save_local_view_state?.();
326304
}}
327305
onViewportInfo={(page, x, y) => {
328-
if (!disableViewportTracking) {
329-
setViewportInfo({ page, x, y });
306+
setViewportInfo({ page, x, y });
307+
308+
// Clear auto sync flag when PDF viewport changes (forward sync completion)
309+
const autoSyncInProgress =
310+
actions.store.get("autoSyncInProgress");
311+
if (autoSyncInProgress) {
312+
// Debounce the flag clearing to avoid clearing too early during scrolling
313+
clearTimeout((window as any).__autoSyncClearTimeout);
314+
(window as any).__autoSyncClearTimeout = setTimeout(() => {
315+
actions.setState({ autoSyncInProgress: false });
316+
}, 500); // Wait longer to ensure scrolling has stabilized
330317
}
331318
}}
332319
/>

src/packages/frontend/frame-editors/latex-editor/pdf-controls.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -95,21 +95,17 @@ export function PDFControls({
9595
storedAutoSyncEnabled,
9696
);
9797

98-
// Check if sync is in progress
99-
const syncInProgress = useRedux([actions.name, "sync_in_progress"]) ?? false;
98+
// Check if auto sync is in progress
99+
const autoSyncInProgress =
100+
useRedux([actions.name, "autoSyncInProgress"]) ?? false;
100101

101102
// Handle inverse sync (PDF → source)
102103
const handleViewportSync = useCallback(
103104
async (page: number, x: number, y: number) => {
104-
if (syncInProgress) {
105+
if (autoSyncInProgress) {
105106
return; // Prevent sync loops
106107
}
107108

108-
// Set sync in progress flag (deferred to avoid React rendering conflicts)
109-
setTimeout(() => {
110-
actions.setState({ sync_in_progress: true });
111-
}, 0);
112-
113109
try {
114110
await actions.synctex_pdf_to_tex(page, x, y);
115111

@@ -121,14 +117,9 @@ export function PDFControls({
121117
}
122118
} catch (error) {
123119
console.warn("Auto-sync reverse search failed:", error);
124-
} finally {
125-
// Clear sync in progress flag (deferred to avoid React rendering conflicts)
126-
setTimeout(() => {
127-
actions.setState({ sync_in_progress: false });
128-
}, 0);
129120
}
130121
},
131-
[actions, syncInProgress],
122+
[actions, autoSyncInProgress, onClearViewportInfo],
132123
);
133124

134125
// Sync state with stored values when they change
@@ -138,10 +129,15 @@ export function PDFControls({
138129

139130
// Auto-sync effect when viewport changes and auto-sync is enabled
140131
useEffect(() => {
141-
if (localAutoSyncEnabled && viewportInfo && !syncInProgress) {
132+
if (localAutoSyncEnabled && viewportInfo && !autoSyncInProgress) {
142133
handleViewportSync(viewportInfo.page, viewportInfo.x, viewportInfo.y);
143134
}
144-
}, [localAutoSyncEnabled, viewportInfo, syncInProgress, handleViewportSync]);
135+
}, [
136+
localAutoSyncEnabled,
137+
viewportInfo,
138+
autoSyncInProgress,
139+
handleViewportSync,
140+
]);
145141

146142
// Note: Page initialization is handled by the output component through actions.setPage
147143
// and page updates from PDF scrolling are handled through onPageInfo callback

src/packages/frontend/frame-editors/latex-editor/pdfjs-page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export default function Page({
101101
const offsetY = event.nativeEvent.clientY - divRect.top;
102102
const x: number = offsetX / scale;
103103
const y: number = offsetY / scale;
104-
actions.synctex_pdf_to_tex(n, x, y);
104+
actions.synctex_pdf_to_tex(n, x, y, true); // true = manual sync
105105
}}
106106
>
107107
<CanvasPage

0 commit comments

Comments
 (0)