diff options
| author | haoyuren <13851610112@163.com> | 2026-03-15 04:27:43 -0500 |
|---|---|---|
| committer | haoyuren <13851610112@163.com> | 2026-03-15 04:27:43 -0500 |
| commit | d1ee677591bd5e6e8b1726b2281621adf6131332 (patch) | |
| tree | 609785e6022fe41f8809ab172003928165a1bb72 /src/main | |
| parent | 6aea514b38a8b36882d39aec30fd12e997f82611 (diff) | |
v0.3.0: Fix sync exclusions, add editor zoom, cached PDF loadingv0.3.0
- Exclude CLAUDE.md from sync (move to .claude/ dotfile dir, clean up root copy)
- Add Ctrl+wheel font zoom for editor (capture phase, disable Electron built-in zoom)
- Load cached PDF on project connect (avoid recompile to see last PDF)
- Add synctex debug logging for PDF↔source navigation troubleshooting
- Fix .claude/ dir creation order (mkdir before write)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/main')
| -rw-r--r-- | src/main/fileSyncBridge.ts | 13 | ||||
| -rw-r--r-- | src/main/index.ts | 72 |
2 files changed, 62 insertions, 23 deletions
diff --git a/src/main/fileSyncBridge.ts b/src/main/fileSyncBridge.ts index fb45208..7d7274a 100644 --- a/src/main/fileSyncBridge.ts +++ b/src/main/fileSyncBridge.ts @@ -138,7 +138,8 @@ export class FileSyncBridge { atomic: true, ignored: [ /(^|[/\\])\../, // dotfiles - /\.(aux|log|fls|fdb_latexmk|synctex\.gz|bbl|blg|out|toc|lof|lot|nav|snm|vrb|pdf|pdfxref|stderr|stdout|chktex)$/ // LaTeX output files + /\.(aux|log|fls|fdb_latexmk|synctex\.gz|bbl|blg|out|toc|lof|lot|nav|snm|vrb|pdf|pdfxref|stderr|stdout|chktex)$/, // LaTeX output files + /(?:^|[/\\])(?:CLAUDE\.md|\.mcp\.json)$/ // App-generated config files ] }) @@ -449,6 +450,10 @@ export class FileSyncBridge { private onFileChanged(relPath: string): void { if (this.stopped) return + // Skip app-generated config files that should not be synced to Overleaf + const basename = relPath.split('/').pop() || relPath + if (basename === 'CLAUDE.md' || basename === '.mcp.json') return + // Layer 1: Skip if bridge is currently writing this file if (this.writesInProgress.has(relPath)) { bridgeLog(`[FileSyncBridge] skipping ${relPath} (write in progress)`) @@ -831,9 +836,10 @@ export class FileSyncBridge { for (const relPath of allFiles) { if (this.pathDocMap[relPath] || this.pathFileRefMap[relPath]) continue - // Skip LaTeX output files + // Skip LaTeX output files and app-generated config files if (/\.(aux|log|fls|fdb_latexmk|synctex\.gz|bbl|blg|out|toc|lof|lot|nav|snm|vrb|pdf|pdfxref|stderr|stdout|chktex|synctex)/.test(relPath)) continue if (/(^|[/\\])\./.test(relPath)) continue + if (/(?:^|[/\\])(?:CLAUDE\.md|\.mcp\.json)$/.test(relPath)) continue bridgeLog(`[FileSyncBridge] orphaned file found: ${relPath}`) this.onNewLocalFile(relPath) @@ -850,9 +856,10 @@ export class FileSyncBridge { if (this.stopped) return if (this.writesInProgress.has(relPath)) return - // Skip LaTeX output files and dotfiles (same as chokidar ignored) + // Skip LaTeX output files, dotfiles, and app-generated config files (same as chokidar ignored) if (/\.(aux|log|fls|fdb_latexmk|synctex\.gz|bbl|blg|out|toc|lof|lot|nav|snm|vrb|pdf|pdfxref|stderr|stdout|chktex)$/.test(relPath)) return if (/(^|[/\\])\./.test(relPath)) return + if (/(?:^|[/\\])(?:CLAUDE\.md|\.mcp\.json)$/.test(relPath)) return // Debounce 1s to let the tool finish writing const key = 'new:' + relPath diff --git a/src/main/index.ts b/src/main/index.ts index 2be5ab8..fa5c4b1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -53,6 +53,9 @@ function createWindow(): void { } }) + // Disable Electron's built-in pinch/Ctrl+wheel zoom so editor can handle it + mainWindow.webContents.setVisualZoomLevelLimits(1, 1) + if (process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) } else { @@ -92,33 +95,44 @@ for (const p of texPaths) { ipcMain.handle('synctex:editFromPdf', async (_e, pdfPath: string, page: number, x: number, y: number) => { return new Promise<{ file: string; line: number } | null>((resolve) => { const pdfDir = pdfPath.substring(0, pdfPath.lastIndexOf('/')) + console.log(`[synctex] edit -o ${page}:${x}:${y}:${pdfPath} (cwd: ${pdfDir})`) const proc = spawn('synctex', ['edit', '-o', `${page}:${x}:${y}:${pdfPath}`], { env: process.env, cwd: pdfDir }) - let out = '' - proc.stdout?.on('data', (d) => { out += d.toString() }) - proc.stderr?.on('data', (d) => { out += d.toString() }) - proc.on('close', () => { + let stdout = '' + let stderr = '' + proc.stdout?.on('data', (d) => { stdout += d.toString() }) + proc.stderr?.on('data', (d) => { stderr += d.toString() }) + proc.on('close', (code) => { + console.log(`[synctex] exit=${code} stdout=${stdout.slice(0, 300)} stderr=${stderr.slice(0, 200)}`) // Parse output: Input:filename\nLine:123\n... - const fileMatch = out.match(/Input:(.+)/) - const lineMatch = out.match(/Line:(\d+)/) + const fileMatch = stdout.match(/Input:(.+)/) + const lineMatch = stdout.match(/Line:(\d+)/) if (fileMatch && lineMatch) { let filePath = fileMatch[1].trim() - // Convert absolute path to relative (strip tmpDir prefix) + // Strip CLSI compilation prefix (server compile uses /compile/ as cwd) + if (filePath.startsWith('/compile/')) { + filePath = filePath.slice('/compile/'.length) + } + // Convert absolute path to relative (strip tmpDir prefix for local compile) const syncDir = compilationManager?.dir if (syncDir && filePath.startsWith(syncDir)) { filePath = filePath.slice(syncDir.length).replace(/^\//, '') } // Strip leading ./ if (filePath.startsWith('./')) filePath = filePath.slice(2) + console.log(`[synctex] resolved: file=${filePath} line=${lineMatch[1]}`) resolve({ file: filePath, line: parseInt(lineMatch[1]) }) } else { - console.log('[synctex] no result:', out.slice(0, 200)) + console.log('[synctex] no match in output') resolve(null) } }) - proc.on('error', () => resolve(null)) + proc.on('error', (err) => { + console.log(`[synctex] spawn error: ${err.message}`) + resolve(null) + }) }) }) @@ -648,8 +662,11 @@ ipcMain.handle('ot:connect', async (_e, projectId: string) => { } } }, null, 2)).catch(() => {}) - // Write CLAUDE.md with project context - writeFile(join(tmpDir, 'CLAUDE.md'), `# LatteX Project — Overleaf Integration + // Clean up old root-level CLAUDE.md (was incorrectly placed there before) + require('fs').unlink(join(tmpDir, 'CLAUDE.md'), () => {}) + // Write .claude/ dir with CLAUDE.md + settings (dotfile dir = excluded from sync) + mkdirAsync(join(tmpDir, '.claude'), { recursive: true }).then(async () => { + await writeFile(join(tmpDir, '.claude', 'CLAUDE.md'), `# LatteX Project — Overleaf Integration This is a LaTeX project synced from Overleaf via LatteX. Files here are bidirectionally synced — edits you make will appear on Overleaf. @@ -690,10 +707,8 @@ You have MCP tools to interact with Overleaf. Use them proactively. 2. Use \`compile_latex\` to compile 3. If errors: use \`get_compile_errors\` for details, fix them, recompile 4. If warnings: use \`get_compile_warnings\` to review -`).catch(() => {}) - // Write .claude/settings.json to auto-allow MCP tools - mkdirAsync(join(tmpDir, '.claude'), { recursive: true }).then(() => - writeFile(join(tmpDir, '.claude', 'settings.json'), JSON.stringify({ +`) + await writeFile(join(tmpDir, '.claude', 'settings.json'), JSON.stringify({ permissions: { allow: [ 'mcp__lattex__get_comments', @@ -711,7 +726,7 @@ You have MCP tools to interact with Overleaf. Use them proactively. ] } }, null, 2)) - ).catch(() => {}) + }).catch(() => {}) // Fetch resolved thread IDs immediately (fast REST call) so editor highlights // don't flash resolved comments while waiting for background fetch @@ -749,6 +764,15 @@ You have MCP tools to interact with Overleaf. Use them proactively. sendToRenderer('comments:initContexts', { contexts }) }, 3000) + // Check for cached PDF from previous compile + const buildDir = join(tmpDir, '.build') + const cachedPdf = join(buildDir, 'output.pdf') + let cachedPdfPath: string | undefined + try { + const stat = await require('fs').promises.stat(cachedPdf) + if (stat.size > 0) cachedPdfPath = cachedPdf + } catch { /* no cached PDF */ } + return { success: true, files, @@ -760,7 +784,8 @@ You have MCP tools to interact with Overleaf. Use them proactively. pathDocMap, fileRefs, rootFolderId, - syncDir: tmpDir + syncDir: tmpDir, + cachedPdfPath } } catch (e) { console.log('[ot:connect] error:', e) @@ -1262,13 +1287,20 @@ ipcMain.handle('overleaf:serverCompile', async (_e, rootDocId?: string) => { } } - // Grab synctex.gz + // Grab synctex.gz (needed for PDF↔source navigation) const synctexFile = (data.outputFiles || []).find((f: any) => f.path === 'output.synctex.gz') if (synctexFile) { try { - const d = await fetchBinary(buildOutputUrl(synctexFile)) + const synctexUrl = buildOutputUrl(synctexFile) + console.log(`[compile] downloading synctex.gz from ${synctexUrl.slice(0, 80)}...`) + const d = await fetchBinary(synctexUrl) await writeFile(join(buildDir, 'output.synctex.gz'), Buffer.from(d)) - } catch { /* optional */ } + console.log(`[compile] synctex.gz saved (${d.byteLength} bytes)`) + } catch (e) { + console.log(`[compile] synctex.gz download failed: ${e}`) + } + } else { + console.log('[compile] no synctex.gz in compile output') } // Download PDF — first check outputFiles, then try direct URL from build ID |
