Skip to content

Commit afc8a96

Browse files
authored
Merge pull request #105 from seofernando25/feature-auto-compile
Implement auto-compile on save for LaTeX and Typst
2 parents afaf282 + 4e9a614 commit afc8a96

File tree

5 files changed

+187
-12
lines changed

5 files changed

+187
-12
lines changed

src/components/output/LaTeXCompileButton.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useSettings } from '../../hooks/useSettings';
1010
import type { DocumentList } from '../../types/documents';
1111
import type { FileNode } from '../../types/files';
1212
import { isTemporaryFile } from '../../utils/fileUtils';
13+
import { fileStorageService } from '../../services/FileStorageService';
1314
import { ChevronDownIcon, ClearCompileIcon, PlayIcon, StopIcon, TrashIcon } from '../common/Icons';
1415

1516
interface LaTeXCompileButtonProps {
@@ -59,6 +60,10 @@ const LaTeXCompileButton: React.FC<LaTeXCompileButtonProps> = ({
5960
const compileButtonRef = useRef<{ clearAndCompile: () => void }>();
6061
const dropdownRef = useRef<HTMLDivElement>(null);
6162

63+
const effectiveAutoCompileOnSave = useSharedSettings
64+
? doc?.projectMetadata?.autoCompileOnSave ?? false
65+
: false;
66+
6267
const projectMainFile = useSharedSettings ? doc?.projectMetadata?.mainFile : undefined;
6368
const projectEngine = useSharedSettings ? doc?.projectMetadata?.latexEngine : undefined;
6469
const effectiveEngine = projectEngine || latexEngine;
@@ -128,6 +133,56 @@ const LaTeXCompileButton: React.FC<LaTeXCompileButtonProps> = ({
128133
};
129134
}, []);
130135

136+
// Listen for save events and auto-compile if enabled
137+
useEffect(() => {
138+
if (!useSharedSettings || !effectiveAutoCompileOnSave || !effectiveMainFile) return;
139+
140+
const handleFileSaved = async (event: Event) => {
141+
if (isCompiling) return;
142+
143+
try {
144+
const customEvent = event as CustomEvent;
145+
const detail = customEvent.detail;
146+
147+
if (!detail) return;
148+
149+
const candidatePath = detail.isFile
150+
? detail.fileId
151+
? detail.filePath ||
152+
(await fileStorageService.getFile(detail.fileId))?.path
153+
: undefined
154+
: linkedFileInfo?.filePath ?? detail.filePath;
155+
156+
if (!candidatePath?.endsWith('.tex')) return;
157+
158+
const mainFileToCompile =
159+
detail.isFile ? effectiveMainFile : candidatePath;
160+
161+
setTimeout(async () => {
162+
if (onExpandLatexOutput) {
163+
onExpandLatexOutput();
164+
}
165+
await compileDocument(mainFileToCompile);
166+
}, 120);
167+
} catch (error) {
168+
console.error('Error in auto-compile on save:', error);
169+
}
170+
};
171+
172+
document.addEventListener('file-saved', handleFileSaved);
173+
return () => {
174+
document.removeEventListener('file-saved', handleFileSaved);
175+
};
176+
}, [
177+
useSharedSettings,
178+
effectiveAutoCompileOnSave,
179+
effectiveMainFile,
180+
isCompiling,
181+
compileDocument,
182+
onExpandLatexOutput,
183+
linkedFileInfo,
184+
]);
185+
131186
const shouldNavigateToMain = async (mainFilePath: string): Promise<boolean> => {
132187
const navigationSetting = getSetting('latex-auto-navigate-to-main')?.value as string ?? 'conditional';
133188

@@ -320,6 +375,17 @@ const LaTeXCompileButton: React.FC<LaTeXCompileButtonProps> = ({
320375
});
321376
};
322377

378+
const handleAutoCompileOnSaveChange = (checked: boolean) => {
379+
if (!useSharedSettings || !changeDoc) return;
380+
381+
changeDoc((d) => {
382+
if (!d.projectMetadata) {
383+
d.projectMetadata = { name: '', description: '' };
384+
}
385+
d.projectMetadata.autoCompileOnSave = checked;
386+
});
387+
};
388+
323389
const getFileName = (path?: string) => {
324390
if (!path) return 'No .tex file';
325391
return path.split('/').pop() || path;
@@ -442,6 +508,20 @@ const LaTeXCompileButton: React.FC<LaTeXCompileButtonProps> = ({
442508
)}
443509
</div>
444510

511+
{useSharedSettings && (
512+
<div className="auto-compile-controls">
513+
<label className="auto-compile-checkbox">
514+
<input
515+
type="checkbox"
516+
checked={effectiveAutoCompileOnSave}
517+
onChange={(e) => handleAutoCompileOnSaveChange(e.target.checked)}
518+
disabled={isCompiling}
519+
/>
520+
Auto-compile
521+
</label>
522+
</div>
523+
)}
524+
445525
<div className="cache-controls">
446526
<div
447527
className="cache-item clear-cache"

src/components/output/TypstCompileButton.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { DocumentList } from '../../types/documents';
1111
import type { FileNode } from '../../types/files';
1212
import type { TypstOutputFormat } from '../../types/typst';
1313
import { isTemporaryFile } from '../../utils/fileUtils';
14+
import { fileStorageService } from '../../services/FileStorageService';
1415
import { ChevronDownIcon, ClearCompileIcon, PlayIcon, StopIcon, TrashIcon } from '../common/Icons';
1516

1617
interface TypstCompileButtonProps {
@@ -56,6 +57,9 @@ const TypstCompileButton: React.FC<TypstCompileButtonProps> = ({
5657
const projectFormat = useSharedSettings ? doc?.projectMetadata?.typstOutputFormat : undefined;
5758
const [localFormat, setLocalFormat] = useState<TypstOutputFormat>('pdf');
5859
const effectiveFormat = projectFormat || localFormat;
60+
const effectiveAutoCompileOnSave = useSharedSettings
61+
? doc?.projectMetadata?.typstAutoCompileOnSave ?? false
62+
: false;
5963

6064
useEffect(() => {
6165
const findTypstFiles = (nodes: FileNode[]): string[] => {
@@ -115,6 +119,56 @@ const TypstCompileButton: React.FC<TypstCompileButtonProps> = ({
115119
};
116120
}, []);
117121

122+
useEffect(() => {
123+
if (!useSharedSettings || !effectiveAutoCompileOnSave || !effectiveMainFile) return;
124+
125+
const handleFileSaved = async (event: Event) => {
126+
if (isCompiling) return;
127+
128+
try {
129+
const customEvent = event as CustomEvent;
130+
const detail = customEvent.detail;
131+
132+
if (!detail) return;
133+
134+
const candidatePath = detail.isFile
135+
? detail.fileId
136+
? detail.filePath ||
137+
(await fileStorageService.getFile(detail.fileId))?.path
138+
: undefined
139+
: linkedFileInfo?.filePath ?? detail.filePath;
140+
141+
if (!candidatePath?.endsWith('.typ')) return;
142+
143+
const mainFileToCompile =
144+
detail.isFile ? effectiveMainFile : candidatePath;
145+
const targetFormat = effectiveFormat;
146+
setTimeout(async () => {
147+
if (onExpandTypstOutput) {
148+
onExpandTypstOutput();
149+
}
150+
await compileDocument(mainFileToCompile, targetFormat);
151+
}, 120);
152+
} catch (error) {
153+
console.error('Error in Typst auto-compile on save:', error);
154+
}
155+
};
156+
157+
document.addEventListener('file-saved', handleFileSaved);
158+
return () => {
159+
document.removeEventListener('file-saved', handleFileSaved);
160+
};
161+
}, [
162+
useSharedSettings,
163+
effectiveAutoCompileOnSave,
164+
effectiveMainFile,
165+
effectiveFormat,
166+
isCompiling,
167+
compileDocument,
168+
onExpandTypstOutput,
169+
linkedFileInfo,
170+
]);
171+
118172
const shouldNavigateToMain = async (mainFilePath: string): Promise<boolean> => {
119173
const navigationSetting = getSetting('typst-auto-navigate-to-main')?.value as string ?? 'conditional';
120174

@@ -264,6 +318,17 @@ const TypstCompileButton: React.FC<TypstCompileButtonProps> = ({
264318
});
265319
};
266320

321+
const handleAutoCompileOnSaveChange = (checked: boolean) => {
322+
if (!useSharedSettings || !changeDoc) return;
323+
324+
changeDoc((d) => {
325+
if (!d.projectMetadata) {
326+
d.projectMetadata = { name: '', description: '' };
327+
}
328+
d.projectMetadata.typstAutoCompileOnSave = checked;
329+
});
330+
};
331+
267332
const getFileName = (path?: string) => {
268333
if (!path) return 'No .typ file';
269334
return path.split('/').pop() || path;
@@ -390,6 +455,18 @@ const TypstCompileButton: React.FC<TypstCompileButtonProps> = ({
390455
)}
391456
</div>
392457

458+
{useSharedSettings && (
459+
<label className="auto-compile-checkbox">
460+
<input
461+
type="checkbox"
462+
checked={effectiveAutoCompileOnSave}
463+
onChange={(e) => handleAutoCompileOnSaveChange(e.target.checked)}
464+
disabled={isCompiling}
465+
/>
466+
Auto-compile
467+
</label>
468+
)}
469+
393470
<div className="cache-controls">
394471
<div
395472
className="cache-item clear-cache"

src/services/EditorLoader.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ export const EditorLoader = (
116116

117117
setShowSaveIndicator(true);
118118
setTimeout(() => setShowSaveIndicator(false), 1500);
119+
120+
document.dispatchEvent(
121+
new CustomEvent('file-saved', {
122+
detail: {
123+
isFile: true,
124+
fileId: currentFileId,
125+
},
126+
}),
127+
);
119128
} catch (error) {
120129
console.error('Error saving file:', error);
121130
}
@@ -137,6 +146,17 @@ export const EditorLoader = (
137146

138147
setShowSaveIndicator(true);
139148
setTimeout(() => setShowSaveIndicator(false), 1500);
149+
150+
document.dispatchEvent(
151+
new CustomEvent('file-saved', {
152+
detail: {
153+
isFile: false,
154+
documentId,
155+
fileId: linkedFile.id,
156+
filePath: linkedFile.path,
157+
},
158+
}),
159+
);
140160
}
141161
} catch (error) {
142162
console.error('Error saving document to linked file:', error);
@@ -697,19 +717,12 @@ export const EditorLoader = (
697717
{
698718
enabled: true,
699719
delay: autoSaveDelay,
700-
onSave: async (saveKey, content) => {
720+
onSave: async (_saveKey, content) => {
701721
if (isEditingFile && currentFileId) {
702-
const encoder = new TextEncoder();
703-
const contentBuffer = encoder.encode(content).buffer;
704-
await fileStorageService.updateFileContent(
705-
currentFileId,
706-
contentBuffer,
707-
);
722+
await saveFileToStorage(content);
708723
} else if (!isEditingFile && documentId) {
709724
await saveDocumentToLinkedFile(content);
710725
}
711-
setShowSaveIndicator(true);
712-
setTimeout(() => setShowSaveIndicator(false), 1500);
713726
},
714727
onError: (error) => {
715728
console.error('Auto-save failed:', error);

src/styles/components/latex-typst.css

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ embed {
303303
cursor: not-allowed;
304304
}
305305

306-
.share-checkbox {
306+
.share-checkbox,
307+
.auto-compile-checkbox {
307308
display: flex;
308309
align-items: center;
309310
gap: 0.5rem;
@@ -312,11 +313,13 @@ embed {
312313
cursor: pointer;
313314
}
314315

315-
.share-checkbox input[type="checkbox"] {
316+
.share-checkbox input[type="checkbox"],
317+
.auto-compile-checkbox input[type="checkbox"] {
316318
margin: 0;
317319
}
318320

319-
.share-checkbox:has(input:disabled) {
321+
.share-checkbox:has(input:disabled),
322+
.auto-compile-checkbox:has(input:disabled) {
320323
opacity: 0.6;
321324
cursor: not-allowed;
322325
}

src/types/documents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@ export interface DocumentList {
2020
latexEngine?: 'pdftex' | 'xetex' | 'luatex';
2121
typstEngine?: string;
2222
typstOutputFormat?: 'pdf' | 'svg' | 'canvas';
23+
autoCompileOnSave?: boolean;
24+
typstAutoCompileOnSave?: boolean;
2325
};
2426
}

0 commit comments

Comments
 (0)