Compile only when the document changes

This commit is contained in:
2026-09-05 22:07:21 -04:00
parent 9d3966772b
commit 57dbdd5d02
5 changed files with 62 additions and 13 deletions
+10 -4
View File
@@ -726,8 +726,10 @@ pub async fn lsp_handler(
.arg("--font-path") .arg("--font-path")
.arg(temp_dir.path()) .arg(temp_dir.path())
.current_dir(temp_dir.path()) .current_dir(temp_dir.path())
.env("RUST_LOG", "warn")
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.kill_on_drop(true)
.spawn() .spawn()
.expect("Failed to start tinymist lsp"); .expect("Failed to start tinymist lsp");
@@ -745,7 +747,7 @@ pub async fn lsp_handler(
use futures_util::SinkExt; use futures_util::SinkExt;
let _ = ws_tx.send(axum::extract::ws::Message::Text(init_msg.to_string().into())).await; 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 { 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()); let content_length = format!("Content-Length: {}\r\n\r\n", msg.len());
if stdin.write_all(content_length.as_bytes()).await.is_err() { 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 { loop {
let mut content_length = 0; let mut content_length = 0;
let mut header = String::new(); let mut header = String::new();
@@ -797,9 +799,13 @@ pub async fn lsp_handler(
}); });
tokio::select! { tokio::select! {
_ = ws_to_lsp => {} _ = &mut ws_to_lsp => {}
_ = lsp_to_ws => {} _ = &mut lsp_to_ws => {}
_ = child.wait() => {} _ = child.wait() => {}
} }
ws_to_lsp.abort();
lsp_to_ws.abort();
let _ = child.kill().await;
}) })
} }
+13 -2
View File
@@ -12,7 +12,8 @@
import { getThemeExtension } from '../ts/themes'; import { getThemeExtension } from '../ts/themes';
import { themeStore, darkModeStore, editorViewStore, editorErrors, triggerLspReconnect } from '../ts/store'; import { themeStore, darkModeStore, editorViewStore, editorErrors, triggerLspReconnect } from '../ts/store';
import { page } from '$app/stores'; 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 { setDiagnostics, lintGutter } from '@codemirror/lint';
import { bracketExtensions, typstBracketSettings } from '../ts/editor-brackets'; import { bracketExtensions, typstBracketSettings } from '../ts/editor-brackets';
@@ -202,6 +203,7 @@
editorViewStore.set(view); editorViewStore.set(view);
let lastCompilerDiagnostics = '';
unsubscribeErrors = editorErrors.subscribe((errors) => { unsubscribeErrors = editorErrors.subscribe((errors) => {
if (view) { if (view) {
const docLen = view.state.doc.length; const docLen = view.state.doc.length;
@@ -218,6 +220,9 @@
message: e.message message: e.message
}; };
}); });
const snapshot = JSON.stringify(safeDiagnostics);
if (snapshot === lastCompilerDiagnostics) return;
lastCompilerDiagnostics = snapshot;
view.dispatch(setDiagnostics(view.state, safeDiagnostics)); view.dispatch(setDiagnostics(view.state, safeDiagnostics));
} }
}); });
@@ -246,6 +251,7 @@
let lsHandlers: ((value: string) => void)[] = []; let lsHandlers: ((value: string) => void)[] = [];
let lspInitialized = false; let lspInitialized = false;
let lastServerDiagnostics = '';
const transport = { const transport = {
send(message: string) { if (lsSocket?.readyState === WebSocket.OPEN) lsSocket.send(message); }, send(message: string) { if (lsSocket?.readyState === WebSocket.OPEN) lsSocket.send(message); },
@@ -261,6 +267,7 @@
lspInitialized = false; lspInitialized = false;
lsHandlers = []; lsHandlers = [];
lastServerDiagnostics = '';
lsSocket = new WebSocket(`${protocol}//${host}/api/lsp/${docId}`); lsSocket = new WebSocket(`${protocol}//${host}/api/lsp/${docId}`);
@@ -274,7 +281,7 @@
client = new LSPClient({ client = new LSPClient({
rootUri: msg.rootUri, rootUri: msg.rootUri,
timeout: 10000, timeout: 10000,
extensions: languageServerExtensions() extensions: typstLspExtensions()
}).connect(transport); }).connect(transport);
view.dispatch({ view.dispatch({
@@ -292,6 +299,10 @@
if (msg.method === 'textDocument/publishDiagnostics' && msg.params && msg.params.diagnostics) { 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')); msg.params.diagnostics = msg.params.diagnostics.filter((d: any) => !d.message.toLowerCase().includes('unknown font family'));
processedData = JSON.stringify(msg); 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) {} } catch (err) {}
} }
+19
View File
@@ -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])
];
}
+4 -2
View File
@@ -14,6 +14,7 @@
let svgs = $state<string[]>([]); let svgs = $state<string[]>([]);
let errors = $state<Diagnostic[]>([]); let errors = $state<Diagnostic[]>([]);
let timeoutId: number | undefined; let timeoutId: number | undefined;
let lastCompiledContent: string | null = null;
let initialized = $state(false); let initialized = $state(false);
let documentTitle = $state('Untitled Document'); let documentTitle = $state('Untitled Document');
let isViewer = $state(false); let isViewer = $state(false);
@@ -54,6 +55,8 @@
function triggerCompile() { function triggerCompile() {
if (!text || !$previewOpenStore) return; if (!text || !$previewOpenStore) return;
const content = text.toString(); const content = text.toString();
if (content === lastCompiledContent) return;
lastCompiledContent = content;
const docId = $page.params.id; const docId = $page.params.id;
compileTypst(content, docId) compileTypst(content, docId)
.then((res) => { .then((res) => {
@@ -71,6 +74,7 @@
}) })
.catch((e) => { .catch((e) => {
console.error('Compilation fetch failed', e); console.error('Compilation fetch failed', e);
lastCompiledContent = null;
errors = [{ message: 'Network or Server Error compiling document.', severity: 'error' }]; errors = [{ message: 'Network or Server Error compiling document.', severity: 'error' }];
}); });
} }
@@ -106,8 +110,6 @@
timeoutId = window.setTimeout(triggerCompile, 500); timeoutId = window.setTimeout(triggerCompile, 500);
}); });
triggerCompile();
return () => { return () => {
if (timeoutId) clearTimeout(timeoutId); if (timeoutId) clearTimeout(timeoutId);
cleanupYjs(); cleanupYjs();
+16 -5
View File
@@ -31,6 +31,7 @@
let showPublish = $state(false); let showPublish = $state(false);
let ready = $state(false); let ready = $state(false);
let timeoutId: number | undefined; let timeoutId: number | undefined;
let lastCompiledSources: string | null = null;
let contextMenu = $state({ show: false, x: 0, y: 0, text: '' }); let contextMenu = $state({ show: false, x: 0, y: 0, text: '' });
@@ -43,9 +44,18 @@
timeoutId = window.setTimeout(triggerCompile, 500); timeoutId = window.setTimeout(triggerCompile, 500);
} }
function recompileAfterFileChange() {
lastCompiledSources = null;
scheduleCompile();
}
function triggerCompile() { function triggerCompile() {
if (!$previewOpenStore) return; 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) => { .then((res) => {
if (res.stats) $documentStatsStore = res.stats; if (res.stats) $documentStatsStore = res.stats;
if (res.svgs) { if (res.svgs) {
@@ -58,6 +68,7 @@
} }
}) })
.catch(() => { .catch(() => {
lastCompiledSources = null;
errors = [{ message: 'Network or server error compiling project.', severity: 'error' }]; 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 }); const res = await fetch(`/api/projects/${projectId}/files/upload`, { method: 'POST', body: form });
if (res.ok) { if (res.ok) {
await loadFiles(); await loadFiles();
triggerCompile(); recompileAfterFileChange();
} }
} }
@@ -119,7 +130,7 @@
if (res.ok) { if (res.ok) {
files = files.map((f) => (f.id === file.id ? { ...f, path } : f)); files = files.map((f) => (f.id === file.id ? { ...f, path } : f));
renameOpenFile(file.id, path); renameOpenFile(file.id, path);
scheduleCompile(); recompileAfterFileChange();
} }
} }
@@ -132,7 +143,7 @@
if (activeFileId === file.id) { if (activeFileId === file.id) {
activeFileId = files.find((f) => f.kind === 'text')?.id ?? ''; activeFileId = files.find((f) => f.kind === 'text')?.id ?? '';
} }
scheduleCompile(); recompileAfterFileChange();
} }
} }
@@ -144,7 +155,7 @@
}); });
if (res.ok) { if (res.ok) {
entrypoint = file.path; entrypoint = file.path;
scheduleCompile(); recompileAfterFileChange();
} }
} }