From 52a5c24f5e28a4b2ba8ffb006874cd7b552d60f7 Mon Sep 17 00:00:00 2001 From: haoyuren <13851610112@163.com> Date: Fri, 13 Mar 2026 00:52:59 -0500 Subject: Rename to LatteX, add LaTeX autocomplete, fix comment highlight positions - Rename project from ClaudeTeX to LatteX (Cosmic Latte theme pun) - Add new coffee cup logo and branded welcome/project screens - Add 5-source LaTeX autocomplete: commands, environments, \ref, \cite, file paths - Fix comment highlight drift by decoding WebSocket-encoded UTF-8 lines and range text via decodeURIComponent(escape()), matching Overleaf's client implementation - Remove hacky byte-offset position remapping from index.ts - Add pinch-to-zoom on PDF viewer Co-Authored-By: Claude Opus 4.6 --- electron-builder.yml | 4 +- package-lock.json | 170 +++++++++-- package.json | 6 +- resources/logo.svg | 28 ++ src/main/compilationManager.ts | 2 +- src/main/index.ts | 4 +- src/main/overleafSocket.ts | 31 +- src/renderer/index.html | 2 +- src/renderer/src/App.css | 73 ++++- src/renderer/src/App.tsx | 34 ++- src/renderer/src/components/Editor.tsx | 5 +- src/renderer/src/components/PdfViewer.tsx | 15 + src/renderer/src/components/ProjectList.tsx | 2 +- src/renderer/src/data/latexCommands.ts | 300 +++++++++++++++++++ src/renderer/src/data/latexEnvironments.ts | 98 +++++++ src/renderer/src/extensions/commentHighlights.ts | 43 ++- src/renderer/src/extensions/latexAutocomplete.ts | 349 +++++++++++++++++++++++ 17 files changed, 1117 insertions(+), 49 deletions(-) create mode 100644 resources/logo.svg create mode 100644 src/renderer/src/data/latexCommands.ts create mode 100644 src/renderer/src/data/latexEnvironments.ts create mode 100644 src/renderer/src/extensions/latexAutocomplete.ts diff --git a/electron-builder.yml b/electron-builder.yml index bd020a0..9152755 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -1,5 +1,5 @@ -appId: com.claudetex.app -productName: ClaudeTeX +appId: com.lattex.app +productName: LatteX directories: buildResources: resources output: dist diff --git a/package-lock.json b/package-lock.json index a2e21b0..4a4b460 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "claude-tex", + "name": "lattex", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "claude-tex", + "name": "lattex", "version": "0.1.0", "hasInstallScript": true, "dependencies": { @@ -18,7 +18,7 @@ "@codemirror/view": "^6.34.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", - "chokidar": "^5.0.0", + "chokidar": "^3.6.0", "diff-match-patch": "^1.0.5", "node-pty": "^1.0.0", "pdfjs-dist": "^4.9.155", @@ -2514,6 +2514,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/app-builder-bin": { "version": "5.0.0-alpha.10", "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz", @@ -2828,6 +2841,18 @@ "node": ">=6.0.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -2879,6 +2904,18 @@ "node": "18 || 20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -3215,18 +3252,27 @@ } }, "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { - "readdirp": "^5.0.0" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">= 20.19.0" + "node": ">= 8.10.0" }, "funding": { "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, "node_modules/chownr": { @@ -4444,6 +4490,18 @@ "node": ">=10" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -4538,7 +4596,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4677,6 +4734,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/glob/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5078,6 +5147,18 @@ "node": ">= 12" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-ci": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", @@ -5091,6 +5172,15 @@ "is-ci": "bin.js" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -5101,6 +5191,18 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -5118,6 +5220,15 @@ "dev": true, "license": "MIT" }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -5928,9 +6039,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6170,6 +6279,18 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -6405,16 +6526,15 @@ } }, "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", - "engines": { - "node": ">= 20.19.0" + "dependencies": { + "picomatch": "^2.2.1" }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=8.10.0" } }, "node_modules/require-directory": { @@ -7080,6 +7200,18 @@ "tmp": "^0.2.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", diff --git a/package.json b/package.json index ffcf155..6e6563c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "claude-tex", + "name": "lattex", "version": "0.1.0", - "description": "LaTeX editor with Claude AI integration and Overleaf sync", + "description": "LaTeX editor with real-time Overleaf sync", "main": "./out/main/index.js", "scripts": { "dev": "electron-vite dev", @@ -21,7 +21,7 @@ "@codemirror/view": "^6.34.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", - "chokidar": "^5.0.0", + "chokidar": "^3.6.0", "diff-match-patch": "^1.0.5", "node-pty": "^1.0.0", "pdfjs-dist": "^4.9.155", diff --git a/resources/logo.svg b/resources/logo.svg new file mode 100644 index 0000000..bd5af43 --- /dev/null +++ b/resources/logo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/compilationManager.ts b/src/main/compilationManager.ts index 3529345..8fbd946 100644 --- a/src/main/compilationManager.ts +++ b/src/main/compilationManager.ts @@ -15,7 +15,7 @@ export class CompilationManager { constructor(projectId: string, cookie: string) { this.projectId = projectId this.cookie = cookie - this.tmpDir = join(require('os').tmpdir(), `claudetex-${projectId}`) + this.tmpDir = join(require('os').tmpdir(), `lattex-${projectId}`) } get dir(): string { diff --git a/src/main/index.ts b/src/main/index.ts index 89a04b0..7fc1c4d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -211,9 +211,9 @@ ipcMain.handle('overleaf:webLogin', async () => { // Inject a floating back button when navigated away from overleaf.com const injectBackButton = () => { loginWindow.webContents.executeJavaScript(` - if (!document.getElementById('claudetex-back-btn')) { + if (!document.getElementById('lattex-back-btn')) { const btn = document.createElement('div'); - btn.id = 'claudetex-back-btn'; + btn.id = 'lattex-back-btn'; btn.innerHTML = '← Back'; btn.style.cssText = 'position:fixed;top:8px;left:8px;z-index:999999;padding:6px 14px;' + 'background:#333;color:#fff;border-radius:6px;cursor:pointer;font:13px -apple-system,sans-serif;' + diff --git a/src/main/overleafSocket.ts b/src/main/overleafSocket.ts index 52ac20f..1a12260 100644 --- a/src/main/overleafSocket.ts +++ b/src/main/overleafSocket.ts @@ -9,6 +9,15 @@ import { encodeHeartbeat } from './overleafProtocol' +/** Decode WebSocket-encoded UTF-8 text (reverses server's unescape(encodeURIComponent(text))) */ +function decodeUtf8(text: string): string { + try { + return decodeURIComponent(escape(text)) + } catch { + return text // already decoded or pure ASCII + } +} + export interface JoinProjectResult { publicId: string project: { @@ -252,14 +261,30 @@ export class OverleafSocket extends EventEmitter { this.joinedDocs.add(docId) // Ack response format: [error, docLines, version, updates, ranges, pathname] - // First element is error (null = success) const err = result[0] if (err) throw new Error(`joinDoc failed: ${JSON.stringify(err)}`) - const docLines = (result[1] as string[]) || [] + // Server encodes lines + range text via unescape(encodeURIComponent(text)) + // for safe WebSocket transport. Decode with decodeURIComponent(escape(text)). + const rawLines = (result[1] as string[]) || [] + const docLines = rawLines.map(line => decodeUtf8(line)) const version = (result[2] as number) || 0 const updates = (result[3] as unknown[]) || [] - const ranges = (result[4] || { comments: [], changes: [] }) as JoinDocResult['ranges'] + const rawRanges = result[4] as JoinDocResult['ranges'] | undefined + + // Decode range text (op.c, op.i, op.d) — positions (op.p) stay as-is + const ranges = rawRanges || { comments: [], changes: [] } + if (ranges.comments) { + for (const c of ranges.comments) { + if (c.op?.c) c.op.c = decodeUtf8(c.op.c) + } + } + if (ranges.changes) { + for (const ch of ranges.changes as any[]) { + if (ch.op?.i) ch.op.i = decodeUtf8(ch.op.i) + if (ch.op?.d) ch.op.d = decodeUtf8(ch.op.d) + } + } return { docLines, version, updates, ranges } } diff --git a/src/renderer/index.html b/src/renderer/index.html index 1fb4779..9fa1aa8 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -3,7 +3,7 @@ - ClaudeTeX + LatteX
diff --git a/src/renderer/src/App.css b/src/renderer/src/App.css index ec9cf88..2a681a5 100644 --- a/src/renderer/src/App.css +++ b/src/renderer/src/App.css @@ -83,12 +83,23 @@ html, body, #root { text-align: center; } +.welcome-logo { + margin-bottom: 20px; +} + .welcome-content h1 { - font-size: 48px; + font-size: 52px; font-weight: 700; letter-spacing: -1px; margin-bottom: 8px; color: var(--accent); + font-family: "Georgia", "Times New Roman", serif; +} + +.lattex-x { + font-weight: 800; + font-style: italic; + color: var(--accent-blue); } .welcome-content p { @@ -767,7 +778,8 @@ html, body, #root { .projects-header h1 { font-size: 22px; font-weight: 700; - color: var(--text-primary); + color: var(--accent); + font-family: "Georgia", "Times New Roman", serif; } .projects-header-actions { @@ -2039,3 +2051,60 @@ html, body, #root { opacity: 0.5; cursor: not-allowed; } + +/* ── Autocomplete ─────────────────────────────────────────────── */ + +.cm-tooltip-autocomplete { + background: var(--bg-primary) !important; + border: 1px solid var(--border) !important; + border-radius: var(--radius) !important; + box-shadow: var(--shadow-md) !important; + font-family: var(--font-mono) !important; + font-size: 12.5px !important; +} + +.cm-tooltip-autocomplete > ul { + max-height: 250px !important; +} + +.cm-tooltip-autocomplete > ul > li { + padding: 3px 8px !important; + color: var(--text-primary) !important; + line-height: 1.5; +} + +.cm-tooltip-autocomplete > ul > li[aria-selected] { + background: var(--accent-blue) !important; + color: #fff !important; +} + +.cm-tooltip-autocomplete .cm-completionLabel { + font-weight: 500; +} + +.cm-tooltip-autocomplete .cm-completionDetail { + color: var(--text-muted); + font-style: italic; + margin-left: 8px; + font-size: 11.5px; +} + +.cm-tooltip-autocomplete > ul > li[aria-selected] .cm-completionDetail { + color: rgba(255, 255, 255, 0.7); +} + +.cm-tooltip-autocomplete .cm-completionIcon { + padding: 0 4px 0 0; + opacity: 0.6; +} + +.cm-completionInfo { + background: var(--bg-secondary) !important; + border: 1px solid var(--border) !important; + border-radius: var(--radius) !important; + color: var(--text-primary) !important; + padding: 6px 10px !important; + font-family: var(--font-sans) !important; + font-size: 12px !important; + max-width: 350px; +} diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 1eb1d76..fb24a36 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -242,6 +242,21 @@ export default function App() { } } } + + // Pre-load .bib files in background for citation autocomplete + const st = useAppStore.getState() + for (const [docId, relPath] of Object.entries(st.docPathMap)) { + if (relPath.endsWith('.bib') && !st.fileContents[relPath]) { + window.api.otJoinDoc(docId).then((res) => { + if (res.success && res.content !== undefined) { + useAppStore.getState().setFileContent(relPath, res.content) + if (res.version !== undefined) { + useAppStore.getState().setDocVersion(docId, res.version) + } + } + }).catch(() => {}) + } + } } const handleBackToProjects = async () => { @@ -271,7 +286,24 @@ export default function App() {
-

ClaudeTeX

+
+ + + + + + + + + + + + + + + +
+

LatteX

LaTeX editor with real-time Overleaf sync