summaryrefslogtreecommitdiff
path: root/src/renderer
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer')
-rw-r--r--src/renderer/src/App.tsx26
-rw-r--r--src/renderer/src/components/Editor.tsx9
-rw-r--r--src/renderer/src/ot/overleafSync.ts35
3 files changed, 63 insertions, 7 deletions
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 63be974..cb14123 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -103,7 +103,7 @@ export default function App() {
// Listen for external edits from file sync bridge (disk changes)
const unsubExternalEdit = window.api.onSyncExternalEdit((data) => {
const sync = activeDocSyncs.get(data.docId)
- if (sync) sync.replaceContent(data.content)
+ if (sync) sync.replaceContent(data.content, data.baseContent)
})
// Listen for new docs created locally (e.g. by Claude Code)
@@ -195,6 +195,25 @@ export default function App() {
return unsub
}, [])
+ // MCP compile events (Claude Code triggered compile)
+ useEffect(() => {
+ const unsubStart = window.api.onMcpCompileStarted(() => {
+ const state = useAppStore.getState()
+ state.setCompiling(true)
+ state.clearCompileLog()
+ setStatusMessage('Compiling (triggered by Claude Code)...')
+ })
+ const unsubFinish = window.api.onMcpCompileFinished((data) => {
+ const state = useAppStore.getState()
+ if (data.pdfPath) {
+ state.setPdfPath(data.pdfPath)
+ }
+ state.setCompiling(false)
+ setStatusMessage(data.success ? 'Compiled successfully' : 'Compilation had errors — check Log tab')
+ })
+ return () => { unsubStart(); unsubFinish() }
+ }, [setStatusMessage])
+
// Keyboard shortcuts
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@@ -340,6 +359,11 @@ export default function App() {
useAppStore.getState().setDocVersion(docId, res.version)
}
}
+ // Release back to bridge — we only needed the content for autocomplete,
+ // not an active editor session. Without this, the bridge permanently thinks
+ // the renderer handles OT for .bib files and defers disk changes to a
+ // non-existent docSync.
+ window.api.otLeaveDoc(docId)
}).catch(() => {})
}
}
diff --git a/src/renderer/src/components/Editor.tsx b/src/renderer/src/components/Editor.tsx
index d3a1e3f..97adf1b 100644
--- a/src/renderer/src/components/Editor.tsx
+++ b/src/renderer/src/components/Editor.tsx
@@ -300,7 +300,14 @@ export default function Editor() {
return () => {
if (docSyncRef.current) {
const docId = pathDocMap[activeTab!]
- if (docId) activeDocSyncs.delete(docId)
+ if (docId) {
+ activeDocSyncs.delete(docId)
+ // Tell bridge to take back OT ownership for this doc.
+ // Without this, the bridge thinks the renderer still handles it
+ // and defers disk-change processing — but the docSync is about to
+ // be destroyed, so disk changes would be silently dropped.
+ window.api.otLeaveDoc(docId)
+ }
docSyncRef.current.destroy()
docSyncRef.current = null
}
diff --git a/src/renderer/src/ot/overleafSync.ts b/src/renderer/src/ot/overleafSync.ts
index e9cb749..4a6deda 100644
--- a/src/renderer/src/ot/overleafSync.ts
+++ b/src/renderer/src/ot/overleafSync.ts
@@ -125,16 +125,33 @@ export class OverleafDocSync {
}
}
- /** Replace entire editor content with new content (external edit from disk) */
- replaceContent(newContent: string) {
+ /** Replace entire editor content with new content (external edit from disk).
+ * If baseContent is provided, does a three-way merge to preserve concurrent
+ * remote changes that arrived while the disk edit was being debounced. */
+ replaceContent(newContent: string, baseContent?: string) {
if (!this.view) return
const currentContent = this.view.state.doc.toString()
if (currentContent === newContent) return
- // Use diff to compute minimal changes so comment range positions remap correctly
const dmp = new diff_match_patch()
- const diffs = dmp.diff_main(currentContent, newContent)
+ let targetContent = newContent
+
+ // Three-way merge: if editor has diverged from the base (due to remote edits),
+ // apply only the disk changes (base→new) as patches on top of current editor state
+ if (baseContent !== undefined && currentContent !== baseContent) {
+ const patches = dmp.patch_make(baseContent, newContent)
+ const [merged, results] = dmp.patch_apply(patches, currentContent)
+ if (results.length > 0 && results.every(r => r)) {
+ targetContent = merged
+ }
+ // If patch failed, fall through to two-way diff (full replacement)
+ }
+
+ if (currentContent === targetContent) return
+
+ // Use diff to compute minimal changes so comment range positions remap correctly
+ const diffs = dmp.diff_main(currentContent, targetContent)
dmp.diff_cleanupEfficiency(diffs)
const changes: ChangeSpec[] = []
@@ -156,7 +173,15 @@ export class OverleafDocSync {
}
destroy() {
- if (this.debounceTimer) clearTimeout(this.debounceTimer)
+ // Flush any debounced local changes before destroying, so OT ops are sent
+ // to the server before the bridge takes back ownership of this doc.
+ if (this.debounceTimer) {
+ clearTimeout(this.debounceTimer)
+ this.debounceTimer = null
+ }
+ if (this.pendingChanges && this.view && this.pendingBaseDoc) {
+ this.flushLocalChanges()
+ }
this.view = null
this.pendingChanges = null
this.pendingBaseDoc = null