diff --git a/src/providers/codexCellEditorProvider/codexCellEditorMessagehandling.ts b/src/providers/codexCellEditorProvider/codexCellEditorMessagehandling.ts index b0e8cf6da..443d20368 100644 --- a/src/providers/codexCellEditorProvider/codexCellEditorMessagehandling.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorMessagehandling.ts @@ -1651,6 +1651,116 @@ const messageHandlers: Record Promise { + const typedEvent = event as Extract; + const { enabled, currentMilestoneIndex, currentSubsectionIndex } = typedEvent.content; + + const baseFileName = path.basename(document.uri.fsPath).replace(/\.(source|codex)$/, ""); + + if (enabled) { + provider.chapterSyncEnabled.add(baseFileName); + + // Find the paired panel's current milestone and sync THIS panel to it + const panels = provider.getWebviewPanels(); + for (const [panelUri] of panels) { + if (panelUri === document.uri.toString()) continue; + const panelBaseName = path.basename(panelUri).replace(/\.(source|codex)$/, ""); + if (panelBaseName !== baseFileName) continue; + + const pairedMilestone = provider.currentMilestoneSubsectionMap.get(panelUri); + if (!pairedMilestone) continue; + + const { milestoneIndex: targetMilestoneIndex, subsectionIndex: targetSubsectionIndex } = pairedMilestone; + + const config = vscode.workspace.getConfiguration("codex-editor-extension"); + const cellsPerPage = config.get("cellsPerPage", 50); + const isThisSource = document.uri.toString().includes(".source"); + + const cells = document.getCellsForMilestone(targetMilestoneIndex, targetSubsectionIndex, cellsPerPage); + const allCellsInMilestone = document.getAllCellsForMilestone(targetMilestoneIndex); + const processedCells = provider.mergeRangesAndProcess(cells, provider.isCorrectionEditorMode, isThisSource); + const processedAllCellsInMilestone = provider.mergeRangesAndProcess(allCellsInMilestone, provider.isCorrectionEditorMode, isThisSource); + + const sourceCellMap: { [k: string]: { content: string; versions: string[]; }; } = {}; + for (const cell of cells) { + const cellId = cell.cellMarkers?.[0]; + if (cellId && document._sourceCellMap[cellId]) { + sourceCellMap[cellId] = document._sourceCellMap[cellId]; + } + } + + provider.currentMilestoneSubsectionMap.set(document.uri.toString(), { + milestoneIndex: targetMilestoneIndex, + subsectionIndex: targetSubsectionIndex, + }); + + safePostMessageToPanel(webviewPanel, { + type: "providerSendsCellPage", + rev: provider.getDocumentRevision(document.uri.toString()), + milestoneIndex: targetMilestoneIndex, + subsectionIndex: targetSubsectionIndex, + cells: processedCells, + allCellsInMilestone: processedAllCellsInMilestone, + sourceCellMap, + }); + break; + } + } else { + provider.chapterSyncEnabled.delete(baseFileName); + } + }, + + syncChapterToOther: async ({ event, document, webviewPanel, provider }) => { + const typedEvent = event as Extract; + const { milestoneIndex, subsectionIndex } = typedEvent.content; + + const baseFileName = path.basename(document.uri.fsPath).replace(/\.(source|codex)$/, ""); + if (!provider.chapterSyncEnabled.has(baseFileName)) return; + + const panels = provider.getWebviewPanels(); + for (const [panelUri, targetPanel] of panels) { + if (panelUri === document.uri.toString()) continue; + const panelBaseName = path.basename(panelUri).replace(/\.(source|codex)$/, ""); + if (panelBaseName !== baseFileName) continue; + + const targetDocument = provider.getDocumentByUri(panelUri); + if (!targetDocument) continue; + + const config = vscode.workspace.getConfiguration("codex-editor-extension"); + const cellsPerPage = config.get("cellsPerPage", 50); + const isTargetSource = panelUri.includes(".source"); + + const cells = targetDocument.getCellsForMilestone(milestoneIndex, subsectionIndex, cellsPerPage); + const allCellsInMilestone = targetDocument.getAllCellsForMilestone(milestoneIndex); + const processedCells = provider.mergeRangesAndProcess(cells, provider.isCorrectionEditorMode, isTargetSource); + const processedAllCellsInMilestone = provider.mergeRangesAndProcess(allCellsInMilestone, provider.isCorrectionEditorMode, isTargetSource); + + const sourceCellMap: { [k: string]: { content: string; versions: string[]; }; } = {}; + for (const cell of cells) { + const cellId = cell.cellMarkers?.[0]; + if (cellId && targetDocument._sourceCellMap[cellId]) { + sourceCellMap[cellId] = targetDocument._sourceCellMap[cellId]; + } + } + + provider.currentMilestoneSubsectionMap.set(panelUri, { + milestoneIndex, + subsectionIndex, + }); + + safePostMessageToPanel(targetPanel, { + type: "providerSendsCellPage", + rev: provider.getDocumentRevision(panelUri), + milestoneIndex, + subsectionIndex, + cells: processedCells, + allCellsInMilestone: processedAllCellsInMilestone, + sourceCellMap, + }); + break; + } + }, + closeCurrentDocument: async ({ event }) => { const typedEvent = event as Extract; debug("Close document request received:", typedEvent.content); diff --git a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts index 74b3bdd61..3ae930068 100755 --- a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts @@ -218,6 +218,9 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider = new Map(); + // Track which file pairs have chapter sync enabled (keyed by base filename e.g. "GEN") + public chapterSyncEnabled: Set = new Set(); + public static getInstance(): CodexCellEditorProvider | undefined { return CodexCellEditorProvider.instance; } @@ -503,6 +506,10 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider { debug("Webview panel state changed, active:", e.webviewPanel.active); @@ -2723,7 +2730,9 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider { debug("Loading bible book map"); diff --git a/types/index.d.ts b/types/index.d.ts index 8c8695e81..4eadd0832 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -818,6 +818,21 @@ export type EditorPostMessages = | { command: "refreshWebviewAfterMilestoneEdits"; content?: Record; // Empty content + } + | { + command: "toggleChapterSync"; + content: { + enabled: boolean; + currentMilestoneIndex: number; + currentSubsectionIndex: number; + }; + } + | { + command: "syncChapterToOther"; + content: { + milestoneIndex: number; + subsectionIndex: number; + }; }; // (revalidateMissingForCell added above in EditorPostMessages union) diff --git a/webviews/codex-webviews/package.json b/webviews/codex-webviews/package.json index b2d1764ff..e395741f6 100644 --- a/webviews/codex-webviews/package.json +++ b/webviews/codex-webviews/package.json @@ -57,7 +57,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", - "@tanstack/react-virtual": "^3.13.21", + "@tanstack/react-virtual": "^3.13.22", "@types/animejs": "^3.1.13", "@uidotdev/usehooks": "^2.4.1", "@vscode/webview-ui-toolkit": "^1.4.0", diff --git a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx index 963a49676..77bdf3b22 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx @@ -453,9 +453,20 @@ ChapterNavigationHeaderProps) { // requestCellsForMilestone handles state updates internally // (both for cached pages and when loading new pages) requestCellsForMilestone(newMilestoneIdx, newSubsectionIdx); + + // If scroll sync is enabled, notify the paired panel to sync + if (scrollSyncEnabled) { + vscode.postMessage({ + command: "syncChapterToOther", + content: { + milestoneIndex: newMilestoneIdx, + subsectionIndex: newSubsectionIdx, + }, + }); + } } }, - [unsavedChanges, currentMilestoneIndex, currentSubsectionIndex, requestCellsForMilestone, milestoneIndex?.milestones.length] + [unsavedChanges, currentMilestoneIndex, currentSubsectionIndex, requestCellsForMilestone, milestoneIndex?.milestones.length, scrollSyncEnabled, vscode] ); // Use dynamic responsive state variables based on content overflow @@ -507,6 +518,7 @@ ChapterNavigationHeaderProps) { shouldUseMinimalLayout ? "p-1" : "p-2" } max-w-full items-center transition-all duration-200 ease-in-out`} ref={headerContainerRef} + onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); }} > {/* Hamburger menu positioned on the left when space is insufficient */} {shouldShowHamburger && ( diff --git a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx index 4d3d11a8f..bff78bd61 100755 --- a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx @@ -3242,7 +3242,18 @@ const CodexCellEditor: React.FC = () => { onSaveMetadata={handleSaveMetadata} onPickFile={handlePickFile} onUpdateVideoUrl={handleUpdateVideoUrl} - toggleScrollSync={() => setScrollSyncEnabled(!scrollSyncEnabled)} + toggleScrollSync={() => { + const newState = !scrollSyncEnabled; + setScrollSyncEnabled(newState); + vscode.postMessage({ + command: "toggleChapterSync", + content: { + enabled: newState, + currentMilestoneIndex: currentMilestoneIndex, + currentSubsectionIndex: currentSubsectionIndex, + }, + } as EditorPostMessages); + }} scrollSyncEnabled={scrollSyncEnabled} translationUnitsForSection={translationUnitsWithCurrentEditorContent} isTranslatingCell={translationQueue.length > 0 || isProcessingCell}