texlite 0.7.6 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,13 @@ import { gunzipSync, gzipSync } from "node:zlib";
9
9
  import { listProjectFilesAsync, outputRoot, resolveSourcePath, safeRelativePath, texFileStem } from "./files.js";
10
10
  import { parseCompileDiagnostics } from "./compileDiagnostics.js";
11
11
  import { detachedProcessGroup, killProcessGroup } from "./processTree.js";
12
+ /** A controlled stop is a normal compile outcome, never a server error. */
13
+ export class CompileCancelledError extends Error {
14
+ constructor() {
15
+ super("Compilation was cancelled.");
16
+ this.name = "CompileCancelledError";
17
+ }
18
+ }
12
19
  export class CompileQueue {
13
20
  concurrency;
14
21
  running = 0;
@@ -16,16 +23,39 @@ export class CompileQueue {
16
23
  constructor(concurrency) {
17
24
  this.concurrency = concurrency;
18
25
  }
19
- add(job) {
26
+ add(job, signal) {
20
27
  return new Promise((resolve, reject) => {
21
- this.pending.push(async () => {
28
+ let started = false;
29
+ const cancelPending = () => {
30
+ if (started)
31
+ return;
32
+ const index = this.pending.indexOf(task);
33
+ if (index < 0)
34
+ return;
35
+ this.pending.splice(index, 1);
36
+ reject(new CompileCancelledError());
37
+ this.drain();
38
+ };
39
+ const task = async () => {
40
+ started = true;
41
+ signal?.removeEventListener("abort", cancelPending);
42
+ if (signal?.aborted) {
43
+ reject(new CompileCancelledError());
44
+ return;
45
+ }
22
46
  try {
23
47
  resolve(await job());
24
48
  }
25
49
  catch (error) {
26
50
  reject(error);
27
51
  }
28
- });
52
+ };
53
+ if (signal?.aborted) {
54
+ reject(new CompileCancelledError());
55
+ return;
56
+ }
57
+ this.pending.push(task);
58
+ signal?.addEventListener("abort", cancelPending, { once: true });
29
59
  this.drain();
30
60
  });
31
61
  }
@@ -94,8 +124,42 @@ export class ProjectCompileCoordinator {
94
124
  }
95
125
  return managed.promise;
96
126
  }
127
+ /**
128
+ * Stop the active target and discard any newer pending request for it. A
129
+ * target has one writer-visible compile at a time, so cancelling the whole
130
+ * target avoids immediately starting a stale follow-up compilation.
131
+ */
132
+ cancel(projectId, target) {
133
+ const targetKey = `${projectId}\0${target ?? ""}`;
134
+ const state = this.targets.get(targetKey);
135
+ const active = state?.active;
136
+ if (!active)
137
+ return null;
138
+ const status = active.started ? "running" : "queued";
139
+ active.controller.abort();
140
+ if (state?.pending) {
141
+ const pending = state.pending;
142
+ state.pending = null;
143
+ pending.controller.abort();
144
+ pending.input.onDiscarded("cancelled");
145
+ pending.resolve(cancelledCompileResult(pending.input));
146
+ }
147
+ return { runId: active.input.runId, status };
148
+ }
97
149
  start(targetKey, state, job) {
98
- void this.queue.add(job.input.execute).then(job.resolve, job.reject).finally(() => {
150
+ void this.queue.add(async () => {
151
+ job.started = true;
152
+ if (job.controller.signal.aborted)
153
+ throw new CompileCancelledError();
154
+ return await job.input.execute(job.controller.signal);
155
+ }, job.controller.signal).then(job.resolve, (error) => {
156
+ if (error instanceof CompileCancelledError) {
157
+ job.input.onCancelled?.();
158
+ job.resolve(cancelledCompileResult(job.input));
159
+ return;
160
+ }
161
+ job.reject(error);
162
+ }).finally(() => {
99
163
  if (state.active !== job)
100
164
  return;
101
165
  state.active = null;
@@ -122,7 +186,19 @@ function deferredJob(input) {
122
186
  resolve = resolvePromise;
123
187
  reject = rejectPromise;
124
188
  });
125
- return { input, promise, resolve, reject };
189
+ return { input, promise, resolve, reject, controller: new AbortController(), started: false };
190
+ }
191
+ function cancelledCompileResult(input) {
192
+ return {
193
+ runId: input.runId,
194
+ revision: input.revision,
195
+ ok: false,
196
+ cancelled: true,
197
+ log: new CompileCancelledError().message,
198
+ diagnostics: { warnings: [], errors: [] },
199
+ pdfPath: null,
200
+ synctexPath: null
201
+ };
126
202
  }
127
203
  export async function captureCompileSnapshot(config, projectId, runId, settings) {
128
204
  const root = compileRunRoot(config, projectId, runId);
@@ -532,7 +608,10 @@ function rewriteSynctexSource(synctexPath, cacheSource, snapshotSource) {
532
608
  if (rewritten !== original)
533
609
  fs.writeFileSync(synctexPath, gzipSync(Buffer.from(rewritten)), { mode: 0o600 });
534
610
  }
535
- export async function compileProject(config, snapshot, mainFileInput, engine, latexmkrcInput) {
611
+ export async function compileProject(config, snapshot, mainFileInput, engine, latexmkrcInput, options = {}) {
612
+ const { signal } = options;
613
+ if (signal?.aborted)
614
+ return cancelledCompileResultForProject();
536
615
  const mainFile = safeRelativePath(mainFileInput);
537
616
  if (!/\.tex$/i.test(mainFile))
538
617
  throw new Error("The main file must have a .tex extension");
@@ -548,6 +627,10 @@ export async function compileProject(config, snapshot, mainFileInput, engine, la
548
627
  const cacheStartedAt = performance.now();
549
628
  const cache = prepareCompileCache(config, snapshot, mainFile, engine, latexmkrc);
550
629
  const cacheSyncMs = performance.now() - cacheStartedAt;
630
+ if (signal?.aborted)
631
+ return cancelledCompileResultForProject({
632
+ cacheSyncMs, latexmkMs: 0, artifactCopyMs: 0, totalMs: performance.now() - startedAt
633
+ });
551
634
  const cwd = cache.sourceDir;
552
635
  const outDir = cache.outputDir;
553
636
  const engineFlag = engine === "xelatex" ? "-xelatex" : engine === "lualatex" ? "-lualatex" : "-pdf";
@@ -578,6 +661,7 @@ export async function compileProject(config, snapshot, mainFileInput, engine, la
578
661
  stdio: ["ignore", "pipe", "pipe"]
579
662
  });
580
663
  let log = "";
664
+ let cancelled = false;
581
665
  const append = (chunk) => {
582
666
  log += chunk.toString("utf8");
583
667
  if (log.length > 2_000_000)
@@ -585,19 +669,46 @@ export async function compileProject(config, snapshot, mainFileInput, engine, la
585
669
  };
586
670
  child.stdout.on("data", append);
587
671
  child.stderr.on("data", append);
588
- child.on("error", reject);
589
- const timeout = setTimeout(() => {
672
+ let timeout = null;
673
+ const cancel = () => {
674
+ if (cancelled)
675
+ return;
676
+ cancelled = true;
677
+ log += "\nCompilation was cancelled.\n";
678
+ killProcessGroup(child);
679
+ };
680
+ const removeAbortListener = () => signal?.removeEventListener("abort", cancel);
681
+ child.once("error", (error) => {
682
+ if (timeout)
683
+ clearTimeout(timeout);
684
+ removeAbortListener();
685
+ reject(error);
686
+ });
687
+ timeout = setTimeout(() => {
688
+ if (cancelled)
689
+ return;
590
690
  log += `\nCompilation exceeded ${config.compileTimeoutMs / 1000} seconds and was terminated.\n`;
591
691
  killProcessGroup(child);
592
692
  }, config.compileTimeoutMs);
693
+ if (signal?.aborted)
694
+ cancel();
695
+ else
696
+ signal?.addEventListener("abort", cancel, { once: true });
593
697
  child.on("close", (code) => {
594
- clearTimeout(timeout);
595
- resolve({ code, log });
698
+ if (timeout)
699
+ clearTimeout(timeout);
700
+ removeAbortListener();
701
+ resolve({ code, log, cancelled });
596
702
  });
597
703
  });
598
704
  const latexmkMs = performance.now() - latexmkStartedAt;
599
705
  const basename = texFileStem(mainFile);
600
706
  const cachedPdf = path.join(outDir, `${basename}.pdf`);
707
+ if (processResult.cancelled || signal?.aborted) {
708
+ return cancelledCompileResultForProject({
709
+ cacheSyncMs, latexmkMs, artifactCopyMs: 0, totalMs: performance.now() - startedAt
710
+ }, processResult.log);
711
+ }
601
712
  const ok = processResult.code === 0 && fs.existsSync(cachedPdf);
602
713
  const diagnostics = parseCompileDiagnostics(processResult.log, ok ? "succeeded" : "failed");
603
714
  if (!ok) {
@@ -606,6 +717,10 @@ export async function compileProject(config, snapshot, mainFileInput, engine, la
606
717
  timings: { cacheSyncMs, latexmkMs, artifactCopyMs: 0, totalMs: performance.now() - startedAt }
607
718
  };
608
719
  }
720
+ if (signal?.aborted)
721
+ return cancelledCompileResultForProject({
722
+ cacheSyncMs, latexmkMs, artifactCopyMs: 0, totalMs: performance.now() - startedAt
723
+ });
609
724
  const artifactStartedAt = performance.now();
610
725
  const artifacts = materializeCompileArtifacts(cache, snapshot, basename);
611
726
  const artifactCopyMs = performance.now() - artifactStartedAt;
@@ -614,3 +729,17 @@ export async function compileProject(config, snapshot, mainFileInput, engine, la
614
729
  timings: { cacheSyncMs, latexmkMs, artifactCopyMs, totalMs: performance.now() - startedAt }
615
730
  };
616
731
  }
732
+ function cancelledCompileResultForProject(timings, log = "") {
733
+ const message = log.includes("Compilation was cancelled.")
734
+ ? log
735
+ : `${log}${log && !log.endsWith("\n") ? "\n" : ""}Compilation was cancelled.\n`;
736
+ return {
737
+ ok: false,
738
+ cancelled: true,
739
+ log: message,
740
+ diagnostics: parseCompileDiagnostics(message, "failed"),
741
+ pdfPath: null,
742
+ synctexPath: null,
743
+ ...(timings ? { timings } : {})
744
+ };
745
+ }
@@ -5,7 +5,7 @@ import { performance } from "node:perf_hooks";
5
5
  import { requireUser } from "../auth.js";
6
6
  import { accessibleProject, canEdit } from "../projects.js";
7
7
  import { resolveSourcePath, safeRelativePath, texFileStem } from "../files.js";
8
- import { captureCompileSnapshot, cleanCompileArtifacts, cleanCompileCache, compileProject, discardCompileSnapshot, hasCompileCache, publishCompileArtifacts, publishedCompileArtifacts } from "../compiler.js";
8
+ import { captureCompileSnapshot, cleanCompileArtifacts, cleanCompileCache, CompileCancelledError, compileProject, discardCompileSnapshot, hasCompileCache, publishCompileArtifacts, publishedCompileArtifacts } from "../compiler.js";
9
9
  import { parseCompileDiagnostics } from "../compileDiagnostics.js";
10
10
  import { pdfToSource, sourceToPdf } from "../synctex.js";
11
11
  import { availablePdf, compileRunPdf, compileMainFile, compilePdfUrl, isTextCompileArtifact, listCompileArtifacts, pdfLoadingMode, syncArtifacts } from "../compileArtifacts.js";
@@ -32,6 +32,10 @@ function pdfResponseMetadata(config, pdfPath) {
32
32
  function mainDocumentChangedError() {
33
33
  return httpError(409, "MAIN_DOCUMENT_CHANGED");
34
34
  }
35
+ function throwIfCompileCancelled(signal) {
36
+ if (signal.aborted)
37
+ throw new CompileCancelledError();
38
+ }
35
39
  class CompileSnapshotBusyError extends Error {
36
40
  code = "COMPILE_SNAPSHOT_BUSY";
37
41
  constructor() {
@@ -292,6 +296,21 @@ export function registerCompileRoutes(app, context) {
292
296
  throw httpError(404, "PROJECT_NOT_FOUND");
293
297
  } });
294
298
  });
299
+ app.post("/api/projects/:id/compile/cancel", async (request, reply) => {
300
+ const user = requireUser(request, reply, db);
301
+ if (!user)
302
+ return;
303
+ const { id } = request.params;
304
+ const project = accessibleProject(db, id, user);
305
+ if (!project || !canEdit(project))
306
+ return apiError(reply, 403, "COMPILE_FORBIDDEN");
307
+ const body = (request.body ?? {});
308
+ const mainFile = compileMainFile(config, id, project.main_file, body.mainFile);
309
+ if (!mainFile)
310
+ return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
311
+ const cancelled = compileCoordinator.cancel(id, mainFile);
312
+ return { cancelled: Boolean(cancelled), mainFile, runId: cancelled?.runId ?? null, status: cancelled?.status ?? null };
313
+ });
295
314
  app.post("/api/projects/:id/compile", async (request, reply) => {
296
315
  const requestStartedAt = performance.now();
297
316
  const user = requireUser(request, reply, db);
@@ -363,8 +382,8 @@ export function registerCompileRoutes(app, context) {
363
382
  if (currentGeneration !== snapshotGeneration)
364
383
  snapshotStale = true;
365
384
  };
366
- const broadcast = () => collaboration.signalCompileState(id, {
367
- mainFile, runId, status: phase, requestedBy, updatedAt: now(),
385
+ const broadcast = (stateRunId = runId) => collaboration.signalCompileState(id, {
386
+ mainFile, runId: stateRunId, status: phase, requestedBy, updatedAt: now(),
368
387
  ...(phase === "succeeded" && snapshotStale ? { stale: true } : {})
369
388
  });
370
389
  const result = await compileCoordinator.request({
@@ -380,19 +399,28 @@ export function registerCompileRoutes(app, context) {
380
399
  onDiscarded: () => {
381
400
  db.prepare("DELETE FROM compile_runs WHERE id = ? AND status = 'queued'").run(runId);
382
401
  },
402
+ onCancelled: () => {
403
+ phase = "failed";
404
+ db.prepare("UPDATE compile_runs SET status = 'failed', log = ?, finished_at = ? WHERE id = ? AND status IN ('queued', 'running')")
405
+ .run(new CompileCancelledError().message, now(), runId);
406
+ broadcast();
407
+ },
383
408
  // The compile reservation protects the shared compiler cache and
384
409
  // prevents Git/cleanup/deletion from replacing the project while
385
410
  // latexmk is running, but it deliberately does not occupy the ordinary
386
411
  // project queue for the duration of the subprocess.
387
- execute: async () => projectMutations.runCompile(id, async () => {
412
+ execute: async (signal) => projectMutations.runCompile(id, async () => {
388
413
  let snapshot = null;
389
414
  let snapshotMs = 0;
390
415
  const snapshotStartedAt = performance.now();
391
416
  try {
392
- // Refresh the cheap generation after waiting for any queued source
393
- // operation. This makes the no-op check safe when a Git/HTTP write
394
- // completed after admission but before this compile got a worker.
395
- const refreshed = await projectMutations.runSerialized(id, () => {
417
+ throwIfCompileCancelled(signal);
418
+ // Revalidate settings after queued source operations. Do not use the
419
+ // in-memory collaboration revision to skip before a snapshot: it is
420
+ // not a durable content version and can collide after a room restart
421
+ // or reconnect.
422
+ await projectMutations.runSerialized(id, () => {
423
+ throwIfCompileCancelled(signal);
396
424
  const currentProject = accessibleProject(db, id, user);
397
425
  if (!currentProject || !canEdit(currentProject)) {
398
426
  throw new Error("Project access changed while waiting for a source snapshot");
@@ -404,38 +432,9 @@ export function registerCompileRoutes(app, context) {
404
432
  throw mainDocumentChangedError();
405
433
  project = currentProject;
406
434
  mainFile = currentMainFile;
407
- return {
408
- generation: compileRequestGeneration(currentProject, collaboration.currentRevision(id), collaboration.currentGeneration(id), config.extraArgs)
409
- };
410
435
  });
411
- const currentGeneration = refreshed.generation;
412
- const publishedBeforeSnapshot = publishedCompileArtifacts(config, id, mainFile, mainFile === project.main_file);
413
- const publishedRunBeforeSnapshot = publishedBeforeSnapshot ? db.prepare(`SELECT id, status, log, finished_at
414
- FROM compile_runs WHERE id = ? AND project_id = ?`).get(publishedBeforeSnapshot.runId, id) : undefined;
415
- const latestRunBeforeSnapshot = db.prepare(`SELECT id, status FROM compile_runs
416
- WHERE project_id = ? AND main_file = ? AND id <> ? ORDER BY created_at DESC LIMIT 1`).get(id, mainFile, runId);
417
- const activeRunBeforeSnapshot = db.prepare(`SELECT 1 AS active FROM compile_runs
418
- WHERE project_id = ? AND main_file = ? AND id <> ? AND status IN ('queued', 'running') LIMIT 1`).get(id, mainFile, runId);
419
- if (!activeRunBeforeSnapshot && publishedBeforeSnapshot?.generation === currentGeneration
420
- && publishedRunBeforeSnapshot?.status === "succeeded"
421
- && latestRunBeforeSnapshot?.id === publishedRunBeforeSnapshot.id
422
- && hasCompileCache(config, id, mainFile)) {
423
- db.prepare("DELETE FROM compile_runs WHERE id = ? AND status = 'queued'").run(runId);
424
- phase = "succeeded";
425
- broadcast();
426
- return {
427
- runId: publishedBeforeSnapshot.runId,
428
- ok: true,
429
- skipped: true,
430
- log: publishedRunBeforeSnapshot.log ?? "",
431
- diagnostics: parseCompileDiagnostics(publishedRunBeforeSnapshot.log ?? "", "succeeded"),
432
- pdfPath: null,
433
- synctexPath: publishedBeforeSnapshot.synctex,
434
- revision: publishedBeforeSnapshot.revision,
435
- snapshotMs: 0
436
- };
437
- }
438
436
  const captured = await projectMutations.runSerialized(id, async () => {
437
+ throwIfCompileCancelled(signal);
439
438
  const currentProject = accessibleProject(db, id, user);
440
439
  if (!currentProject || !canEdit(currentProject)) {
441
440
  throw new Error("Project access changed while waiting for a source snapshot");
@@ -469,6 +468,7 @@ export function registerCompileRoutes(app, context) {
469
468
  extraArgs: config.extraArgs,
470
469
  generation: generationKey
471
470
  });
471
+ throwIfCompileCancelled(signal);
472
472
  }
473
473
  catch (error) {
474
474
  captureError = error;
@@ -508,6 +508,7 @@ export function registerCompileRoutes(app, context) {
508
508
  throw new CompileSnapshotBusyError();
509
509
  }, { flush: false });
510
510
  snapshot = captured;
511
+ throwIfCompileCancelled(signal);
511
512
  snapshotMs = performance.now() - snapshotStartedAt;
512
513
  metrics.record("compile.snapshot", snapshotMs);
513
514
  const published = publishedCompileArtifacts(config, id, mainFile, mainFile === project.main_file);
@@ -526,7 +527,9 @@ export function registerCompileRoutes(app, context) {
526
527
  snapshot = null;
527
528
  db.prepare("DELETE FROM compile_runs WHERE id = ? AND status = 'queued'").run(runId);
528
529
  phase = "succeeded";
529
- broadcast();
530
+ // The probe row was deleted, so broadcast the retained run that
531
+ // clients can actually resolve from SQLite and the manifest.
532
+ broadcast(published.runId);
530
533
  return {
531
534
  runId: published.runId,
532
535
  ok: true,
@@ -545,15 +548,17 @@ export function registerCompileRoutes(app, context) {
545
548
  broadcast();
546
549
  let compiled;
547
550
  try {
548
- compiled = await compileProject(config, snapshot, mainFile, project.engine, project.latexmkrc);
551
+ compiled = await compileProject(config, snapshot, mainFile, project.engine, project.latexmkrc, { signal });
549
552
  refreshSnapshotStale();
550
553
  if (compiled.ok && compiled.pdfPath) {
554
+ throwIfCompileCancelled(signal);
551
555
  const publishStartedAt = performance.now();
552
556
  // Publishing is short and atomic, but still goes through the
553
557
  // ordinary project queue so it cannot overlap a source-tree
554
558
  // archive/read or another output mutation. The long latexmk
555
559
  // process above remains outside that queue.
556
560
  await projectMutations.runSerialized(id, () => {
561
+ throwIfCompileCancelled(signal);
557
562
  if (!db.prepare("SELECT 1 FROM projects WHERE id = ?").get(id)) {
558
563
  throw new Error("Project was deleted before compile artifacts could be published.");
559
564
  }
@@ -576,12 +581,19 @@ export function registerCompileRoutes(app, context) {
576
581
  snapshot = null;
577
582
  compiled = {
578
583
  ok: false,
584
+ ...(error instanceof CompileCancelledError ? { cancelled: true } : {}),
579
585
  log: error instanceof Error ? error.message : String(error),
580
586
  diagnostics: parseCompileDiagnostics(error instanceof Error ? error.message : String(error), "failed"),
581
587
  pdfPath: null,
582
588
  synctexPath: null
583
589
  };
584
590
  }
591
+ // A killed latexmk can leave an incomplete dependency database in
592
+ // the incremental cache. Keep the last published PDF, but make the
593
+ // next requested compile a clean rebuild instead of trusting that
594
+ // partial state.
595
+ if (compiled.cancelled)
596
+ cleanCompileCache(config, id, mainFile);
585
597
  phase = compiled.ok ? "succeeded" : "failed";
586
598
  db.prepare("UPDATE compile_runs SET status = ?, log = ?, finished_at = ? WHERE id = ?")
587
599
  .run(phase, compiled.log, now(), runId);
@@ -617,6 +629,7 @@ export function registerCompileRoutes(app, context) {
617
629
  broadcast();
618
630
  return {
619
631
  ok: false,
632
+ ...(error instanceof CompileCancelledError ? { cancelled: true } : {}),
620
633
  log,
621
634
  diagnostics: parseCompileDiagnostics(log, "failed"),
622
635
  pdfPath: null,
@@ -660,7 +673,7 @@ export function registerCompileRoutes(app, context) {
660
673
  `total;dur=${timingDuration(requestMs)}`
661
674
  ].filter(Boolean).join(", "));
662
675
  return {
663
- mainFile, runId: result.runId, ok: result.ok, skipped: result.skipped === true, log: result.log, diagnostics: result.diagnostics,
676
+ mainFile, runId: result.runId, ok: result.ok, cancelled: result.cancelled === true, skipped: result.skipped === true, log: result.log, diagnostics: result.diagnostics,
664
677
  pdfUrl: result.ok ? compilePdfUrl(id, mainFile, result.runId) : null,
665
678
  pdfCompiledAt: result.ok ? completed?.finished_at ?? null : null,
666
679
  ...pdfMetadata,
@@ -212,6 +212,14 @@ export function registerProjectFileRoutes(app, context) {
212
212
  return apiError(reply, 404, "PATH_NOT_FOUND", { path: source });
213
213
  if (destination === source)
214
214
  return { ok: true, path: source };
215
+ // Moving the configured root to (for example) main.tex.bak used to
216
+ // silently store that non-TeX path as main_file. The next compilation
217
+ // then failed long after the rename had appeared to succeed. Keep the
218
+ // current root a TeX file; users can first select another root document
219
+ // in project settings when they genuinely want to retire this one.
220
+ if (source === project.main_file && !/\.tex$/i.test(destination)) {
221
+ return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
222
+ }
215
223
  const initialSourceStat = fs.statSync(sourceAbsolute);
216
224
  if (initialSourceStat.isDirectory() && (destinationDirectory === source || destinationDirectory.startsWith(`${source}/`))) {
217
225
  return apiError(reply, 400, "MOVE_INTO_SELF", { path: source });
@@ -229,6 +237,9 @@ export function registerProjectFileRoutes(app, context) {
229
237
  const validateMove = () => {
230
238
  currentProject = requireEditableProject(db, id, user);
231
239
  assertNoSourceSymlinks(config, id);
240
+ if (source === currentProject.main_file && !/\.tex$/i.test(destination)) {
241
+ throw httpError(400, "MAIN_DOCUMENT_INVALID");
242
+ }
232
243
  if (!fs.existsSync(sourceAbsolute)) {
233
244
  throw httpError(404, "PATH_NOT_FOUND", { path: source });
234
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "texlite",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "A lightweight local-first collaborative LaTeX workspace",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- import{u as A,r,j as e}from"./react-BoEjKzFZ.js";import{j as B,a as R,M as F,o as $,L as I,p as J,n as K,F as V,q as G}from"./index-DRShM2ob.js";const L=B("Replace",[["path",{d:"M14 4a2 2 0 0 1 2-2",key:"1w2hp7"}],["path",{d:"M16 10a2 2 0 0 1-2-2",key:"shjach"}],["path",{d:"M20 2a2 2 0 0 1 2 2",key:"188mtx"}],["path",{d:"M22 8a2 2 0 0 1-2 2",key:"ddf4tu"}],["path",{d:"m3 7 3 3 3-3",key:"x25e72"}],["path",{d:"M6 10V5a3 3 0 0 1 3-3h1",key:"3y3t5z"}],["rect",{x:"2",y:"14",width:"8",height:"8",rx:"2",key:"4rksxw"}]]);function Z({open:i,files:u,onOpenChange:l,onOpenFile:p}){const{t:a}=A(),[n,m]=r.useState(""),[d,o]=r.useState(0),f=r.useRef(null),h=r.useMemo(()=>u.filter(t=>t.type==="file").map(t=>({entry:t,score:H(t.path,n)})).filter(t=>t.score>=0).sort((t,c)=>c.score-t.score||t.entry.path.localeCompare(c.entry.path)).slice(0,100),[u,n]);r.useEffect(()=>{i&&(m(""),o(0),window.setTimeout(()=>f.current?.focus(),0))},[i]),r.useEffect(()=>o(t=>Math.min(t,Math.max(0,h.length-1))),[h.length]);const x=t=>{p(t),l(!1)};return e.jsx(F,{open:i,wide:!0,title:a("navigation.quickOpen"),description:a("navigation.quickOpenDescription"),onOpenChange:l,children:e.jsxs("div",{className:"quick-open-dialog",children:[e.jsxs("label",{children:[e.jsx($,{size:16}),e.jsx("input",{ref:f,value:n,placeholder:a("navigation.filePlaceholder"),onChange:t=>{m(t.target.value),o(0)},onKeyDown:t=>{t.key==="ArrowDown"&&(t.preventDefault(),o(c=>Math.min(h.length-1,c+1))),t.key==="ArrowUp"&&(t.preventDefault(),o(c=>Math.max(0,c-1))),t.key==="Enter"&&h[d]&&(t.preventDefault(),x(h[d].entry.path))}})]}),e.jsxs("div",{className:"quick-open-results",children:[h.map(({entry:t},c)=>e.jsxs("button",{className:c===d?"active":"",onMouseEnter:()=>o(c),onClick:()=>x(t.path),children:[e.jsx(V,{size:14}),e.jsx("span",{children:t.path}),c===d&&e.jsx(G,{size:13})]},t.path)),h.length===0&&e.jsx("p",{className:"muted padded",children:a("navigation.noFiles")})]})]})})}function _({open:i,project:u,onOpenChange:l,onJump:p}){const{t:a}=A(),[n,m]=r.useState(""),[d,o]=r.useState(""),[f,h]=r.useState(!1),[x,t]=r.useState(!1),[c,w]=r.useState([]),[v,C]=r.useState(0),[P,D]=r.useState(!1),[b,S]=r.useState(!1),[W,N]=r.useState(!1),[q,M]=r.useState(""),[z,k]=r.useState(""),[O,Q]=r.useState(0),y=r.useRef(null);r.useEffect(()=>{if(!i){y.current?.abort();return}if(M(""),k(""),!n.trim()){w([]),C(0),D(!1);return}const s=window.setTimeout(()=>{y.current?.abort();const j=new AbortController;y.current=j,S(!0);const E=new URLSearchParams({q:n});f&&E.set("caseSensitive","1"),x&&E.set("wholeWord","1"),R(`/api/projects/${u.id}/search?${E}`,{signal:j.signal}).then(g=>{w(g.matches),C(g.total),D(g.truncated)}).catch(g=>{g instanceof DOMException&&g.name==="AbortError"||k(T(g))}).finally(()=>{y.current===j&&(y.current=null,S(!1))})},250);return()=>window.clearTimeout(s)},[i,u.id,n,f,x,O]);const U=async()=>{N(!1),S(!0),k(""),M("");try{const s=await R(`/api/projects/${u.id}/search/replace`,{method:"POST",body:JSON.stringify({query:n,replacement:d,caseSensitive:f,wholeWord:x})});M(a("navigation.replaced",{count:s.replacements,files:s.files.length})),w([]),C(0),Q(j=>j+1)}catch(s){k(T(s))}finally{S(!1)}};return e.jsxs(e.Fragment,{children:[e.jsx(F,{open:i,extraWide:!0,title:a("navigation.projectSearch"),description:a("navigation.projectSearchDescription"),onOpenChange:l,footer:e.jsx("button",{onClick:()=>l(!1),children:a("common.close")}),children:e.jsxs("div",{className:"project-search-dialog",children:[z&&e.jsx("p",{className:"error",children:z}),q&&e.jsx("p",{className:"success",children:q}),e.jsxs("div",{className:"project-search-form",children:[e.jsxs("label",{className:"project-search-query",children:[e.jsx($,{size:16}),e.jsx("input",{autoFocus:!0,value:n,placeholder:a("navigation.searchPlaceholder"),onChange:s=>m(s.target.value)})]}),u.permission!=="read"&&e.jsxs("label",{className:"project-search-query",children:[e.jsx(L,{size:16}),e.jsx("input",{value:d,placeholder:a("navigation.replacePlaceholder"),onChange:s=>o(s.target.value)})]}),e.jsxs("div",{className:"project-search-options",children:[e.jsxs("label",{children:[e.jsx("input",{type:"checkbox",checked:f,onChange:s=>h(s.target.checked)}),a("editor.search.matchCase")]}),e.jsxs("label",{children:[e.jsx("input",{type:"checkbox",checked:x,onChange:s=>t(s.target.checked)}),a("editor.search.wholeWord")]}),u.permission!=="read"&&e.jsxs("button",{className:"danger-text",disabled:!n.trim()||v===0||b,onClick:()=>N(!0),children:[e.jsx(L,{size:13}),a("navigation.replaceAll",{count:v})]})]})]}),e.jsxs("div",{className:"project-search-summary",children:[e.jsx("span",{children:b?e.jsxs(e.Fragment,{children:[e.jsx(I,{className:"spin",size:13}),a("common.loading")]}):a("navigation.matchSummary",{count:v})}),P&&e.jsx("small",{children:a("navigation.truncated",{count:c.length})})]}),e.jsxs("div",{className:"project-search-results",children:[c.map((s,j)=>e.jsxs("button",{onClick:()=>{p(s.path,s.line,s.column),l(!1)},children:[e.jsx(J,{size:14}),e.jsxs("span",{children:[e.jsxs("strong",{children:[s.path,e.jsxs("small",{children:[":",s.line,":",s.column]})]}),e.jsxs("code",{children:[s.preview.slice(0,s.matchStart),e.jsx("mark",{children:s.preview.slice(s.matchStart,s.matchEnd)}),s.preview.slice(s.matchEnd)]})]})]},`${s.path}-${s.line}-${s.column}-${j}`)),!b&&n.trim()&&c.length===0&&e.jsx("p",{className:"muted padded",children:a("editor.search.noMatches")})]})]})}),e.jsx(K,{open:W,title:a("navigation.replaceConfirmTitle"),description:a("navigation.replaceConfirmDescription",{count:v}),confirmLabel:a("navigation.replaceAll",{count:v}),danger:!0,onCancel:()=>N(!1),onConfirm:()=>{U()}})]})}function H(i,u){const l=i.toLocaleLowerCase(),p=u.trim().toLocaleLowerCase();if(!p)return 1;const a=l.indexOf(p);if(a>=0)return 1e4-a-l.length;let n=0,m=0;for(const d of p){const o=l.indexOf(d,n);if(o<0)return-1;m+=o===n?5:1,n=o+1}return m-l.length/100}function T(i){return i instanceof Error?i.message:String(i)}export{_ as ProjectSearchDialog,Z as QuickOpenDialog};