summaryrefslogtreecommitdiff
path: root/src/main/compilationManager.ts
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-03-12 17:52:53 -0500
committerhaoyuren <13851610112@163.com>2026-03-12 17:52:53 -0500
commitb116335f9dbde4f483c0b2b8e7bfca5d321c5dfc (patch)
tree8bd84b0f4a54eb879c8cc5a158002e999b23d57e /src/main/compilationManager.ts
parentebec1a1073f9cc5b69e125d5b284669545ea3d9f (diff)
Add bidirectional file sync, OT system, comments, and real-time collaboration
Implement full Overleaf integration with Socket.IO v0.9 real-time sync: - FileSyncBridge for bidirectional temp dir ↔ Overleaf sync via chokidar + diff-match-patch - OT state machine, transform functions, and CM6 adapter for collaborative editing - Comment system with highlights, tooltips, and review panel - Project list, file tree management, and socket-based compilation - 3-layer loop prevention (write guards, content equality, debounce) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/main/compilationManager.ts')
-rw-r--r--src/main/compilationManager.ts162
1 files changed, 162 insertions, 0 deletions
diff --git a/src/main/compilationManager.ts b/src/main/compilationManager.ts
new file mode 100644
index 0000000..3529345
--- /dev/null
+++ b/src/main/compilationManager.ts
@@ -0,0 +1,162 @@
+// Manages temp directory for Overleaf socket-mode compilation
+import { join, basename } from 'path'
+import { writeFile, mkdir, rm } from 'fs/promises'
+import { existsSync } from 'fs'
+import { spawn } from 'child_process'
+import { net } from 'electron'
+
+export class CompilationManager {
+ private tmpDir: string
+ private projectId: string
+ private cookie: string
+ private docContents = new Map<string, string>() // docPath → content
+ private fileRefCache = new Map<string, boolean>() // fileRefPath → downloaded
+
+ constructor(projectId: string, cookie: string) {
+ this.projectId = projectId
+ this.cookie = cookie
+ this.tmpDir = join(require('os').tmpdir(), `claudetex-${projectId}`)
+ }
+
+ get dir(): string {
+ return this.tmpDir
+ }
+
+ /** Check if a doc is already stored */
+ hasDoc(relativePath: string): boolean {
+ return this.docContents.has(relativePath)
+ }
+
+ /** Store doc content (called when docs are joined/updated) */
+ setDocContent(relativePath: string, content: string) {
+ // Strip C1 control characters (U+0080-U+009F) — Overleaf embeds these as
+ // range markers for tracked changes / comments. They break pdflatex.
+ this.docContents.set(relativePath, content.replace(/[\u0080-\u009F]/g, ''))
+ }
+
+ /** Write all doc contents to disk */
+ async syncDocs(): Promise<void> {
+ await mkdir(this.tmpDir, { recursive: true })
+ for (const [relPath, content] of this.docContents) {
+ const fullPath = join(this.tmpDir, relPath)
+ const dir = fullPath.substring(0, fullPath.lastIndexOf('/'))
+ await mkdir(dir, { recursive: true })
+ await writeFile(fullPath, content, 'utf-8')
+ }
+ }
+
+ /** Download a binary file (image, .bst, etc.) from Overleaf */
+ async downloadFile(fileRefId: string, relativePath: string): Promise<void> {
+ if (this.fileRefCache.has(relativePath)) return
+
+ const fullPath = join(this.tmpDir, relativePath)
+ const dir = fullPath.substring(0, fullPath.lastIndexOf('/'))
+ await mkdir(dir, { recursive: true })
+
+ return new Promise((resolve, reject) => {
+ const url = `https://www.overleaf.com/project/${this.projectId}/file/${fileRefId}`
+ const req = net.request(url)
+ req.setHeader('Cookie', this.cookie)
+ req.setHeader('User-Agent', 'Mozilla/5.0')
+
+ const chunks: Buffer[] = []
+ req.on('response', (res) => {
+ res.on('data', (chunk) => chunks.push(chunk as Buffer))
+ res.on('end', async () => {
+ try {
+ const { writeFile: wf } = await import('fs/promises')
+ await wf(fullPath, Buffer.concat(chunks))
+ this.fileRefCache.set(relativePath, true)
+ resolve()
+ } catch (e) {
+ reject(e)
+ }
+ })
+ })
+ req.on('error', reject)
+ req.end()
+ })
+ }
+
+ /** Download all binary files in the project */
+ async syncBinaries(fileRefs: Array<{ id: string; path: string }>): Promise<void> {
+ for (const ref of fileRefs) {
+ try {
+ await this.downloadFile(ref.id, ref.path)
+ } catch (e) {
+ console.log(`[CompilationManager] failed to download ${ref.path}:`, e)
+ }
+ }
+ }
+
+ /** Run latexmk compilation */
+ async compile(
+ mainTexRelPath: string,
+ onLog: (data: string) => void
+ ): Promise<{ success: boolean; log: string; pdfPath: string }> {
+ await this.syncDocs()
+
+ const texPaths = [
+ '/Library/TeX/texbin',
+ '/usr/local/texlive/2024/bin/universal-darwin',
+ '/usr/texbin',
+ '/opt/homebrew/bin'
+ ]
+ const envPath = texPaths.join(':') + ':' + (process.env.PATH || '')
+
+ // Use // suffix for recursive search of ALL subdirectories in the project tree.
+ // This ensures .sty, .bst, .cls, images, etc. are always found regardless of nesting.
+ const texInputs = `${this.tmpDir}//:`
+ const texBase = basename(mainTexRelPath, '.tex')
+ const pdfPath = join(this.tmpDir, texBase + '.pdf')
+
+ const args = [
+ '-pdf', '-f', '-g', '-bibtex', '-synctex=1',
+ '-interaction=nonstopmode', '-file-line-error',
+ '-outdir=' + this.tmpDir,
+ mainTexRelPath
+ ]
+ console.log('[compile] cwd:', this.tmpDir)
+ console.log('[compile] args:', args.join(' '))
+ console.log('[compile] TEXINPUTS:', texInputs)
+ console.log('[compile] pdfPath:', pdfPath)
+ console.log('[compile] docs synced:', this.docContents.size, 'files:', [...this.docContents.keys()].slice(0, 5))
+
+ return new Promise((resolve) => {
+ let log = ''
+ const proc = spawn('latexmk', args, {
+ cwd: this.tmpDir,
+ env: { ...process.env, PATH: envPath, TEXINPUTS: texInputs, BIBINPUTS: texInputs, BSTINPUTS: texInputs }
+ })
+
+ proc.stdout.on('data', (data) => {
+ const s = data.toString()
+ log += s
+ onLog(s)
+ })
+
+ proc.stderr.on('data', (data) => {
+ const s = data.toString()
+ log += s
+ onLog(s)
+ })
+
+ proc.on('close', (code) => {
+ resolve({ success: code === 0, log, pdfPath })
+ })
+
+ proc.on('error', (err) => {
+ resolve({ success: false, log: log + '\n' + err.message, pdfPath })
+ })
+ })
+ }
+
+ /** Clean up temp directory */
+ async cleanup(): Promise<void> {
+ try {
+ if (existsSync(this.tmpDir)) {
+ await rm(this.tmpDir, { recursive: true })
+ }
+ } catch { /* ignore */ }
+ }
+}