From e1d33fe4a85facba8666eddf7ac0d2c1d18eb763 Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 14 Nov 2025 04:22:32 -0500 Subject: [PATCH 1/4] Implement auto-compile for LaTeX and Typst --- src/components/output/LaTeXCompileButton.tsx | 108 +++++++++++++++++++ src/components/output/TypstCompileButton.tsx | 93 ++++++++++++++++ src/services/EditorLoader.ts | 50 +++++++-- src/styles/components/latex-typst.css | 9 +- src/types/documents.ts | 2 + 5 files changed, 248 insertions(+), 14 deletions(-) diff --git a/src/components/output/LaTeXCompileButton.tsx b/src/components/output/LaTeXCompileButton.tsx index 5764359..72e9609 100644 --- a/src/components/output/LaTeXCompileButton.tsx +++ b/src/components/output/LaTeXCompileButton.tsx @@ -10,6 +10,7 @@ import { useSettings } from '../../hooks/useSettings'; import type { DocumentList } from '../../types/documents'; import type { FileNode } from '../../types/files'; import { isTemporaryFile } from '../../utils/fileUtils'; +import { fileStorageService } from '../../services/FileStorageService'; import { ChevronDownIcon, ClearCompileIcon, PlayIcon, StopIcon, TrashIcon } from '../common/Icons'; interface LaTeXCompileButtonProps { @@ -59,6 +60,10 @@ const LaTeXCompileButton: React.FC = ({ const compileButtonRef = useRef<{ clearAndCompile: () => void }>(); const dropdownRef = useRef(null); + const effectiveAutoCompileOnSave = useSharedSettings + ? doc?.projectMetadata?.autoCompileOnSave ?? false + : false; + const projectMainFile = useSharedSettings ? doc?.projectMetadata?.mainFile : undefined; const projectEngine = useSharedSettings ? doc?.projectMetadata?.latexEngine : undefined; const effectiveEngine = projectEngine || latexEngine; @@ -128,6 +133,84 @@ const LaTeXCompileButton: React.FC = ({ }; }, []); + // Listen for save events and auto-compile if enabled + useEffect(() => { + if (!useSharedSettings || !effectiveAutoCompileOnSave || !effectiveMainFile) return; + + const handleFileSaved = async (event: Event) => { + const customEvent = event as CustomEvent; + const detail = customEvent.detail; + + if (!detail) return; + + const { + fileId: eventFileId, + documentId: eventDocumentId, + isFile, + filePath: savedFilePath, + } = detail as { + fileId?: string; + documentId?: string; + isFile?: boolean; + filePath?: string; + }; + + // Skip if already compiling + if (isCompiling) return; + + try { + let shouldCompile = false; + let mainFileToCompile = effectiveMainFile; + + if (isFile && eventFileId) { + let candidatePath = savedFilePath; + + if (!candidatePath) { + const file = await fileStorageService.getFile(eventFileId); + candidatePath = file?.path; + } + + if (candidatePath?.endsWith('.tex')) { + shouldCompile = true; + } + } else if (!isFile && eventDocumentId) { + const candidatePath = linkedFileInfo?.filePath ?? savedFilePath; + + if (candidatePath?.endsWith('.tex')) { + shouldCompile = true; + mainFileToCompile = candidatePath; + } + } + + if (shouldCompile && mainFileToCompile) { + const targetFile = mainFileToCompile; + // Small delay to ensure save completes + setTimeout(async () => { + if (onExpandLatexOutput) { + onExpandLatexOutput(); + } + await compileDocument(targetFile); + }, 120); + } + } catch (error) { + console.error('Error in auto-compile on save:', error); + } + }; + + document.addEventListener('file-saved', handleFileSaved); + return () => { + document.removeEventListener('file-saved', handleFileSaved); + }; + }, [ + useSharedSettings, + effectiveAutoCompileOnSave, + effectiveMainFile, + isCompiling, + compileDocument, + onExpandLatexOutput, + linkedFileInfo, + ]); + const shouldNavigateToMain = async (mainFilePath: string): Promise => { const navigationSetting = getSetting('latex-auto-navigate-to-main')?.value as string ?? 'conditional'; @@ -320,6 +403,17 @@ const LaTeXCompileButton: React.FC = ({ }); }; + const handleAutoCompileOnSaveChange = (checked: boolean) => { + if (!useSharedSettings || !changeDoc) return; + + changeDoc((d) => { + if (!d.projectMetadata) { + d.projectMetadata = { name: '', description: '' }; + } + d.projectMetadata.autoCompileOnSave = checked; + }); + }; + const getFileName = (path?: string) => { if (!path) return 'No .tex file'; return path.split('/').pop() || path; @@ -442,6 +536,20 @@ const LaTeXCompileButton: React.FC = ({ )} + {useSharedSettings && ( +
+ +
+ )} +
= ({ const projectFormat = useSharedSettings ? doc?.projectMetadata?.typstOutputFormat : undefined; const [localFormat, setLocalFormat] = useState('pdf'); const effectiveFormat = projectFormat || localFormat; + const effectiveAutoCompileOnSave = useSharedSettings + ? doc?.projectMetadata?.typstAutoCompileOnSave ?? false + : false; useEffect(() => { const findTypstFiles = (nodes: FileNode[]): string[] => { @@ -115,6 +119,72 @@ const TypstCompileButton: React.FC = ({ }; }, []); + useEffect(() => { + if (!useSharedSettings || !effectiveAutoCompileOnSave || !effectiveMainFile) return; + + const handleFileSaved = async (event: Event) => { + const customEvent = event as CustomEvent; + const detail = customEvent.detail as { + fileId?: string; + documentId?: string; + isFile?: boolean; + filePath?: string; + }; + + if (!detail || isCompiling) return; + + try { + let shouldCompile = false; + let mainFileToCompile = effectiveMainFile; + + if (detail.isFile && detail.fileId) { + let candidatePath = detail.filePath; + if (!candidatePath) { + const file = await fileStorageService.getFile(detail.fileId); + candidatePath = file?.path; + } + + if (candidatePath?.endsWith('.typ')) { + shouldCompile = true; + } + } else if (!detail.isFile && detail.documentId) { + const candidatePath = linkedFileInfo?.filePath ?? detail.filePath; + if (candidatePath?.endsWith('.typ')) { + shouldCompile = true; + mainFileToCompile = candidatePath; + } + } + + if (shouldCompile && mainFileToCompile) { + const targetMainFile = mainFileToCompile; + const targetFormat = effectiveFormat; + setTimeout(async () => { + if (onExpandTypstOutput) { + onExpandTypstOutput(); + } + await compileDocument(targetMainFile, targetFormat); + }, 120); + } + } catch (error) { + console.error('Error in Typst auto-compile on save:', error); + } + }; + + document.addEventListener('file-saved', handleFileSaved); + return () => { + document.removeEventListener('file-saved', handleFileSaved); + }; + }, [ + useSharedSettings, + effectiveAutoCompileOnSave, + effectiveMainFile, + effectiveFormat, + isCompiling, + compileDocument, + onExpandTypstOutput, + linkedFileInfo, + ]); + const shouldNavigateToMain = async (mainFilePath: string): Promise => { const navigationSetting = getSetting('typst-auto-navigate-to-main')?.value as string ?? 'conditional'; @@ -264,6 +334,17 @@ const TypstCompileButton: React.FC = ({ }); }; + const handleAutoCompileOnSaveChange = (checked: boolean) => { + if (!useSharedSettings || !changeDoc) return; + + changeDoc((d) => { + if (!d.projectMetadata) { + d.projectMetadata = { name: '', description: '' }; + } + d.projectMetadata.typstAutoCompileOnSave = checked; + }); + }; + const getFileName = (path?: string) => { if (!path) return 'No .typ file'; return path.split('/').pop() || path; @@ -390,6 +471,18 @@ const TypstCompileButton: React.FC = ({ )}
+ {useSharedSettings && ( + + )} +
setShowSaveIndicator(false), 1500); + + document.dispatchEvent( + new CustomEvent('file-saved', { + detail: { + isFile: true, + fileId: currentFileId, + }, + }), + ); } catch (error) { console.error('Error saving file:', error); } @@ -130,6 +139,17 @@ export const EditorLoader = ( setShowSaveIndicator(true); setTimeout(() => setShowSaveIndicator(false), 1500); + + document.dispatchEvent( + new CustomEvent('file-saved', { + detail: { + isFile: false, + documentId, + fileId: linkedFile.id, + filePath: linkedFile.path, + }, + }), + ); } } catch (error) { console.error('Error saving document to linked file:', error); @@ -675,19 +695,12 @@ export const EditorLoader = ( { enabled: true, delay: autoSaveDelay, - onSave: async (saveKey, content) => { + onSave: async (_saveKey, content) => { if (isEditingFile && currentFileId) { - const encoder = new TextEncoder(); - const contentBuffer = encoder.encode(content).buffer; - await fileStorageService.updateFileContent( - currentFileId, - contentBuffer, - ); + await saveFileToStorage(content); } else if (!isEditingFile && documentId) { await saveDocumentToLinkedFile(content); } - setShowSaveIndicator(true); - setTimeout(() => setShowSaveIndicator(false), 1500); }, onError: (error) => { console.error('Auto-save failed:', error); @@ -955,9 +968,24 @@ export const EditorLoader = ( const handleFileSaved = (event: Event) => { const customEvent = event as CustomEvent; - const { fileId: eventFileId } = customEvent.detail; + const detail = customEvent.detail; + if (!detail) return; + + const { + fileId: eventFileId, + documentId: eventDocumentId, + isFile, + } = detail as { + fileId?: string; + documentId?: string; + isFile?: boolean; + }; + + const shouldShow = + (isFile && eventFileId === currentFileId && isEditingFile) || + (!isFile && eventDocumentId === documentId && !isEditingFile); - if (eventFileId === currentFileId && isEditingFile) { + if (shouldShow) { setShowSaveIndicator(true); setTimeout(() => setShowSaveIndicator(false), 1500); } diff --git a/src/styles/components/latex-typst.css b/src/styles/components/latex-typst.css index 8766cdc..0618a4d 100644 --- a/src/styles/components/latex-typst.css +++ b/src/styles/components/latex-typst.css @@ -303,7 +303,8 @@ embed { cursor: not-allowed; } -.share-checkbox { +.share-checkbox, +.auto-compile-checkbox { display: flex; align-items: center; gap: 0.5rem; @@ -312,11 +313,13 @@ embed { cursor: pointer; } -.share-checkbox input[type="checkbox"] { +.share-checkbox input[type="checkbox"], +.auto-compile-checkbox input[type="checkbox"] { margin: 0; } -.share-checkbox:has(input:disabled) { +.share-checkbox:has(input:disabled), +.auto-compile-checkbox:has(input:disabled) { opacity: 0.6; cursor: not-allowed; } diff --git a/src/types/documents.ts b/src/types/documents.ts index cbe9c5e..4437c6c 100644 --- a/src/types/documents.ts +++ b/src/types/documents.ts @@ -20,5 +20,7 @@ export interface DocumentList { latexEngine?: 'pdftex' | 'xetex' | 'luatex'; typstEngine?: string; typstOutputFormat?: 'pdf' | 'svg' | 'canvas'; + autoCompileOnSave?: boolean; + typstAutoCompileOnSave?: boolean; }; } From 17321406b7c4ec543d97261ceecae49a04591d0c Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 14 Nov 2025 05:08:26 -0500 Subject: [PATCH 2/4] Refactor auto-compile logic --- src/components/output/LaTeXCompileButton.tsx | 77 +++++++------------- src/components/output/TypstCompileButton.tsx | 67 +++++++---------- 2 files changed, 55 insertions(+), 89 deletions(-) diff --git a/src/components/output/LaTeXCompileButton.tsx b/src/components/output/LaTeXCompileButton.tsx index 72e9609..0d6c416 100644 --- a/src/components/output/LaTeXCompileButton.tsx +++ b/src/components/output/LaTeXCompileButton.tsx @@ -138,60 +138,37 @@ const LaTeXCompileButton: React.FC = ({ if (!useSharedSettings || !effectiveAutoCompileOnSave || !effectiveMainFile) return; const handleFileSaved = async (event: Event) => { - const customEvent = event as CustomEvent; - const detail = customEvent.detail; - - if (!detail) return; - - const { - fileId: eventFileId, - documentId: eventDocumentId, - isFile, - filePath: savedFilePath, - } = detail as { - fileId?: string; - documentId?: string; - isFile?: boolean; - filePath?: string; - }; - - // Skip if already compiling if (isCompiling) return; try { - let shouldCompile = false; - let mainFileToCompile = effectiveMainFile; - - if (isFile && eventFileId) { - let candidatePath = savedFilePath; - - if (!candidatePath) { - const file = await fileStorageService.getFile(eventFileId); - candidatePath = file?.path; - } - - if (candidatePath?.endsWith('.tex')) { - shouldCompile = true; - } - } else if (!isFile && eventDocumentId) { - const candidatePath = linkedFileInfo?.filePath ?? savedFilePath; - - if (candidatePath?.endsWith('.tex')) { - shouldCompile = true; - mainFileToCompile = candidatePath; + const customEvent = event as CustomEvent; + const detail = customEvent.detail as { + fileId?: string; + documentId?: string; + isFile?: boolean; + filePath?: string; + }; + + if (!detail) return; + + const candidatePath = detail.isFile + ? detail.fileId + ? detail.filePath || + (await fileStorageService.getFile(detail.fileId))?.path + : undefined + : linkedFileInfo?.filePath ?? detail.filePath; + + if (!candidatePath?.endsWith('.tex')) return; + + const mainFileToCompile = + detail.isFile ? effectiveMainFile : candidatePath; + + setTimeout(async () => { + if (onExpandLatexOutput) { + onExpandLatexOutput(); } - } - - if (shouldCompile && mainFileToCompile) { - const targetFile = mainFileToCompile; - // Small delay to ensure save completes - setTimeout(async () => { - if (onExpandLatexOutput) { - onExpandLatexOutput(); - } - await compileDocument(targetFile); - }, 120); - } + await compileDocument(mainFileToCompile); + }, 120); } catch (error) { console.error('Error in auto-compile on save:', error); } diff --git a/src/components/output/TypstCompileButton.tsx b/src/components/output/TypstCompileButton.tsx index 93084da..7248929 100644 --- a/src/components/output/TypstCompileButton.tsx +++ b/src/components/output/TypstCompileButton.tsx @@ -123,48 +123,37 @@ const TypstCompileButton: React.FC = ({ if (!useSharedSettings || !effectiveAutoCompileOnSave || !effectiveMainFile) return; const handleFileSaved = async (event: Event) => { - const customEvent = event as CustomEvent; - const detail = customEvent.detail as { - fileId?: string; - documentId?: string; - isFile?: boolean; - filePath?: string; - }; - - if (!detail || isCompiling) return; + if (isCompiling) return; try { - let shouldCompile = false; - let mainFileToCompile = effectiveMainFile; - - if (detail.isFile && detail.fileId) { - let candidatePath = detail.filePath; - if (!candidatePath) { - const file = await fileStorageService.getFile(detail.fileId); - candidatePath = file?.path; - } - - if (candidatePath?.endsWith('.typ')) { - shouldCompile = true; - } - } else if (!detail.isFile && detail.documentId) { - const candidatePath = linkedFileInfo?.filePath ?? detail.filePath; - if (candidatePath?.endsWith('.typ')) { - shouldCompile = true; - mainFileToCompile = candidatePath; + const customEvent = event as CustomEvent; + const detail = customEvent.detail as { + fileId?: string; + documentId?: string; + isFile?: boolean; + filePath?: string; + }; + + if (!detail) return; + + const candidatePath = detail.isFile + ? detail.fileId + ? detail.filePath || + (await fileStorageService.getFile(detail.fileId))?.path + : undefined + : linkedFileInfo?.filePath ?? detail.filePath; + + if (!candidatePath?.endsWith('.typ')) return; + + const mainFileToCompile = + detail.isFile ? effectiveMainFile : candidatePath; + const targetFormat = effectiveFormat; + setTimeout(async () => { + if (onExpandTypstOutput) { + onExpandTypstOutput(); } - } - - if (shouldCompile && mainFileToCompile) { - const targetMainFile = mainFileToCompile; - const targetFormat = effectiveFormat; - setTimeout(async () => { - if (onExpandTypstOutput) { - onExpandTypstOutput(); - } - await compileDocument(targetMainFile, targetFormat); - }, 120); - } + await compileDocument(mainFileToCompile, targetFormat); + }, 120); } catch (error) { console.error('Error in Typst auto-compile on save:', error); } From 98c827e55f28a9d7f8057396922064ad4a2016d5 Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 14 Nov 2025 05:18:18 -0500 Subject: [PATCH 3/4] Refactor event detail --- src/components/output/LaTeXCompileButton.tsx | 7 +--- src/components/output/TypstCompileButton.tsx | 7 +--- src/services/EditorLoader.ts | 34 ++++++++------------ 3 files changed, 15 insertions(+), 33 deletions(-) diff --git a/src/components/output/LaTeXCompileButton.tsx b/src/components/output/LaTeXCompileButton.tsx index 0d6c416..d6e88dc 100644 --- a/src/components/output/LaTeXCompileButton.tsx +++ b/src/components/output/LaTeXCompileButton.tsx @@ -142,12 +142,7 @@ const LaTeXCompileButton: React.FC = ({ try { const customEvent = event as CustomEvent; - const detail = customEvent.detail as { - fileId?: string; - documentId?: string; - isFile?: boolean; - filePath?: string; - }; + const detail = customEvent.detail; if (!detail) return; diff --git a/src/components/output/TypstCompileButton.tsx b/src/components/output/TypstCompileButton.tsx index 7248929..f509f35 100644 --- a/src/components/output/TypstCompileButton.tsx +++ b/src/components/output/TypstCompileButton.tsx @@ -127,12 +127,7 @@ const TypstCompileButton: React.FC = ({ try { const customEvent = event as CustomEvent; - const detail = customEvent.detail as { - fileId?: string; - documentId?: string; - isFile?: boolean; - filePath?: string; - }; + const detail = customEvent.detail; if (!detail) return; diff --git a/src/services/EditorLoader.ts b/src/services/EditorLoader.ts index 9228d6d..7b991f0 100644 --- a/src/services/EditorLoader.ts +++ b/src/services/EditorLoader.ts @@ -967,28 +967,20 @@ export const EditorLoader = ( }; const handleFileSaved = (event: Event) => { - const customEvent = event as CustomEvent; - const detail = customEvent.detail; - if (!detail) return; - - const { - fileId: eventFileId, - documentId: eventDocumentId, - isFile, - } = detail as { - fileId?: string; - documentId?: string; - isFile?: boolean; - }; - - const shouldShow = - (isFile && eventFileId === currentFileId && isEditingFile) || - (!isFile && eventDocumentId === documentId && !isEditingFile); + const detail = (event as CustomEvent).detail; + + // Ignore file-saved events for other files + if ( + !detail || + !( + (detail.isFile && detail.fileId === currentFileId && isEditingFile) || + (!detail.isFile && detail.documentId === documentId && !isEditingFile) + ) + ) + return; - if (shouldShow) { - setShowSaveIndicator(true); - setTimeout(() => setShowSaveIndicator(false), 1500); - } + setShowSaveIndicator(true); + setTimeout(() => setShowSaveIndicator(false), 1500); }; const handleTriggerSave = (event: Event) => { From 4e9a614c6aeea143e96fad31622e2f958f0bb727 Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 14 Nov 2025 05:35:25 -0500 Subject: [PATCH 4/4] revert handleFileSaved --- src/services/EditorLoader.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/services/EditorLoader.ts b/src/services/EditorLoader.ts index 7b991f0..617e5e7 100644 --- a/src/services/EditorLoader.ts +++ b/src/services/EditorLoader.ts @@ -967,20 +967,13 @@ export const EditorLoader = ( }; const handleFileSaved = (event: Event) => { - const detail = (event as CustomEvent).detail; - - // Ignore file-saved events for other files - if ( - !detail || - !( - (detail.isFile && detail.fileId === currentFileId && isEditingFile) || - (!detail.isFile && detail.documentId === documentId && !isEditingFile) - ) - ) - return; + const customEvent = event as CustomEvent; + const { fileId: eventFileId } = customEvent.detail; - setShowSaveIndicator(true); - setTimeout(() => setShowSaveIndicator(false), 1500); + if (eventFileId === currentFileId && isEditingFile) { + setShowSaveIndicator(true); + setTimeout(() => setShowSaveIndicator(false), 1500); + } }; const handleTriggerSave = (event: Event) => {