From 57dbdd5d02b49b09b1bfcae8aae9f3ce1a50362c Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Sat, 5 Sep 2026 22:07:21 -0400 Subject: [PATCH] Compile only when the document changes --- server/src/handlers.rs | 14 ++++++++++---- src/lib/components/Editor.svelte | 15 +++++++++++++-- src/lib/ts/editor-lsp.ts | 19 +++++++++++++++++++ src/routes/doc/[id]/+page.svelte | 6 ++++-- src/routes/project/[id]/+page.svelte | 21 ++++++++++++++++----- 5 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 src/lib/ts/editor-lsp.ts diff --git a/server/src/handlers.rs b/server/src/handlers.rs index 53cb1bb..d032dc8 100644 --- a/server/src/handlers.rs +++ b/server/src/handlers.rs @@ -726,8 +726,10 @@ pub async fn lsp_handler( .arg("--font-path") .arg(temp_dir.path()) .current_dir(temp_dir.path()) + .env("RUST_LOG", "warn") .stdin(Stdio::piped()) .stdout(Stdio::piped()) + .kill_on_drop(true) .spawn() .expect("Failed to start tinymist lsp"); @@ -745,7 +747,7 @@ pub async fn lsp_handler( use futures_util::SinkExt; let _ = ws_tx.send(axum::extract::ws::Message::Text(init_msg.to_string().into())).await; - let ws_to_lsp = tokio::spawn(async move { + let mut ws_to_lsp = tokio::spawn(async move { while let Some(Ok(axum::extract::ws::Message::Text(msg))) = ws_rx.next().await { let content_length = format!("Content-Length: {}\r\n\r\n", msg.len()); if stdin.write_all(content_length.as_bytes()).await.is_err() { @@ -757,7 +759,7 @@ pub async fn lsp_handler( } }); - let lsp_to_ws = tokio::spawn(async move { + let mut lsp_to_ws = tokio::spawn(async move { loop { let mut content_length = 0; let mut header = String::new(); @@ -797,9 +799,13 @@ pub async fn lsp_handler( }); tokio::select! { - _ = ws_to_lsp => {} - _ = lsp_to_ws => {} + _ = &mut ws_to_lsp => {} + _ = &mut lsp_to_ws => {} _ = child.wait() => {} } + + ws_to_lsp.abort(); + lsp_to_ws.abort(); + let _ = child.kill().await; }) } diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index bfd6ed3..9722be1 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -12,7 +12,8 @@ import { getThemeExtension } from '../ts/themes'; import { themeStore, darkModeStore, editorViewStore, editorErrors, triggerLspReconnect } from '../ts/store'; import { page } from '$app/stores'; - import { LSPClient, languageServerExtensions } from "@codemirror/lsp-client"; + import { LSPClient } from "@codemirror/lsp-client"; + import { typstLspExtensions } from '../ts/editor-lsp'; import { setDiagnostics, lintGutter } from '@codemirror/lint'; import { bracketExtensions, typstBracketSettings } from '../ts/editor-brackets'; @@ -202,6 +203,7 @@ editorViewStore.set(view); + let lastCompilerDiagnostics = ''; unsubscribeErrors = editorErrors.subscribe((errors) => { if (view) { const docLen = view.state.doc.length; @@ -218,6 +220,9 @@ message: e.message }; }); + const snapshot = JSON.stringify(safeDiagnostics); + if (snapshot === lastCompilerDiagnostics) return; + lastCompilerDiagnostics = snapshot; view.dispatch(setDiagnostics(view.state, safeDiagnostics)); } }); @@ -246,6 +251,7 @@ let lsHandlers: ((value: string) => void)[] = []; let lspInitialized = false; + let lastServerDiagnostics = ''; const transport = { send(message: string) { if (lsSocket?.readyState === WebSocket.OPEN) lsSocket.send(message); }, @@ -261,6 +267,7 @@ lspInitialized = false; lsHandlers = []; + lastServerDiagnostics = ''; lsSocket = new WebSocket(`${protocol}//${host}/api/lsp/${docId}`); @@ -274,7 +281,7 @@ client = new LSPClient({ rootUri: msg.rootUri, timeout: 10000, - extensions: languageServerExtensions() + extensions: typstLspExtensions() }).connect(transport); view.dispatch({ @@ -292,6 +299,10 @@ if (msg.method === 'textDocument/publishDiagnostics' && msg.params && msg.params.diagnostics) { msg.params.diagnostics = msg.params.diagnostics.filter((d: any) => !d.message.toLowerCase().includes('unknown font family')); processedData = JSON.stringify(msg); + + const snapshot = `${msg.params.uri}:${msg.params.version ?? ''}:${JSON.stringify(msg.params.diagnostics)}`; + if (snapshot === lastServerDiagnostics) return; + lastServerDiagnostics = snapshot; } } catch (err) {} } diff --git a/src/lib/ts/editor-lsp.ts b/src/lib/ts/editor-lsp.ts new file mode 100644 index 0000000..1c54ab2 --- /dev/null +++ b/src/lib/ts/editor-lsp.ts @@ -0,0 +1,19 @@ +import { + serverCompletion, + serverDiagnostics, + signatureHelp, + formatKeymap, + renameKeymap, + jumpToDefinitionKeymap, + findReferencesKeymap +} from '@codemirror/lsp-client'; +import { keymap } from '@codemirror/view'; + +export function typstLspExtensions() { + return [ + serverCompletion(), + signatureHelp(), + serverDiagnostics(), + keymap.of([...formatKeymap, ...renameKeymap, ...jumpToDefinitionKeymap, ...findReferencesKeymap]) + ]; +} diff --git a/src/routes/doc/[id]/+page.svelte b/src/routes/doc/[id]/+page.svelte index 7d9db2e..4d0144f 100644 --- a/src/routes/doc/[id]/+page.svelte +++ b/src/routes/doc/[id]/+page.svelte @@ -14,6 +14,7 @@ let svgs = $state([]); let errors = $state([]); let timeoutId: number | undefined; + let lastCompiledContent: string | null = null; let initialized = $state(false); let documentTitle = $state('Untitled Document'); let isViewer = $state(false); @@ -54,6 +55,8 @@ function triggerCompile() { if (!text || !$previewOpenStore) return; const content = text.toString(); + if (content === lastCompiledContent) return; + lastCompiledContent = content; const docId = $page.params.id; compileTypst(content, docId) .then((res) => { @@ -71,6 +74,7 @@ }) .catch((e) => { console.error('Compilation fetch failed', e); + lastCompiledContent = null; errors = [{ message: 'Network or Server Error compiling document.', severity: 'error' }]; }); } @@ -106,8 +110,6 @@ timeoutId = window.setTimeout(triggerCompile, 500); }); - triggerCompile(); - return () => { if (timeoutId) clearTimeout(timeoutId); cleanupYjs(); diff --git a/src/routes/project/[id]/+page.svelte b/src/routes/project/[id]/+page.svelte index 291545c..8514dbd 100644 --- a/src/routes/project/[id]/+page.svelte +++ b/src/routes/project/[id]/+page.svelte @@ -31,6 +31,7 @@ let showPublish = $state(false); let ready = $state(false); let timeoutId: number | undefined; + let lastCompiledSources: string | null = null; let contextMenu = $state({ show: false, x: 0, y: 0, text: '' }); @@ -43,9 +44,18 @@ timeoutId = window.setTimeout(triggerCompile, 500); } + function recompileAfterFileChange() { + lastCompiledSources = null; + scheduleCompile(); + } + function triggerCompile() { if (!$previewOpenStore) return; - compileProject(projectId, getAllText()) + const sources = getAllText(); + const fingerprint = JSON.stringify(sources); + if (fingerprint === lastCompiledSources) return; + lastCompiledSources = fingerprint; + compileProject(projectId, sources) .then((res) => { if (res.stats) $documentStatsStore = res.stats; if (res.svgs) { @@ -58,6 +68,7 @@ } }) .catch(() => { + lastCompiledSources = null; errors = [{ message: 'Network or server error compiling project.', severity: 'error' }]; }); } @@ -106,7 +117,7 @@ const res = await fetch(`/api/projects/${projectId}/files/upload`, { method: 'POST', body: form }); if (res.ok) { await loadFiles(); - triggerCompile(); + recompileAfterFileChange(); } } @@ -119,7 +130,7 @@ if (res.ok) { files = files.map((f) => (f.id === file.id ? { ...f, path } : f)); renameOpenFile(file.id, path); - scheduleCompile(); + recompileAfterFileChange(); } } @@ -132,7 +143,7 @@ if (activeFileId === file.id) { activeFileId = files.find((f) => f.kind === 'text')?.id ?? ''; } - scheduleCompile(); + recompileAfterFileChange(); } } @@ -144,7 +155,7 @@ }); if (res.ok) { entrypoint = file.path; - scheduleCompile(); + recompileAfterFileChange(); } }