texlite 0.7.7 → 0.8.0
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.
- package/DESIGN.md +460 -0
- package/NPM_TESTING.md +130 -0
- package/OPERATIONS.md +246 -0
- package/README.md +78 -306
- package/README.zh-CN.md +61 -330
- package/dist/client/assets/{GitDialog-Bm5yMpQK.js → GitDialog-DdLrbz_H.js} +1 -1
- package/dist/client/assets/{HistoryDialog-Po4KHT_2.js → HistoryDialog-AlF9iozX.js} +1 -1
- package/dist/client/assets/{LatexEditor-CnKCXaHr.js → LatexEditor-DH9v1wgS.js} +22 -22
- package/dist/client/assets/{PdfPreview-Cd20VIjT.js → PdfPreview-CanYnyfJ.js} +1 -1
- package/dist/client/assets/ProjectNavigationDialogs-DiXtkTj1.js +1 -0
- package/dist/client/assets/{SystemMetricsDialog-DdOlcqqK.js → SystemMetricsDialog-DimBI7er.js} +1 -1
- package/dist/client/assets/index-CdiutUgf.css +1 -0
- package/dist/client/assets/index-vpWlpUOr.js +64 -0
- package/dist/client/assets/{minus-O6pK-6mA.js → minus-DR-ttrx3.js} +1 -1
- package/dist/client/assets/spellCheck-Wn7_eF9j.js +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/app.js +3 -3
- package/dist/server/compiler.js +139 -10
- package/dist/server/harper.js +210 -119
- package/dist/server/latexSpellMask.js +257 -0
- package/dist/server/projects.js +0 -12
- package/dist/server/routes/compile.js +54 -41
- package/dist/server/routes/projectFiles.js +11 -0
- package/dist/server/routes/projectShared.js +1 -1
- package/dist/server/routes/projects.js +20 -19
- package/package.json +4 -2
- package/dist/client/assets/ProjectNavigationDialogs-BDOtIoTy.js +0 -1
- package/dist/client/assets/index-MDwr9W9R.css +0 -1
- package/dist/client/assets/index-zIjCeZFK.js +0 -64
- package/dist/client/assets/spellCheck-Ce3o_xO-.js +0 -4
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// This intentionally recognizes only the parts of LaTeX that are clearly not
|
|
2
|
+
// prose. It is a single-pass lexer, not a parser: malformed delimiters remain
|
|
3
|
+
// narrow, so they never hide the rest of a document or cause backtracking.
|
|
4
|
+
const nonProseEnvironments = new Set([
|
|
5
|
+
"math", "displaymath", "equation", "equation*", "align", "align*", "alignat", "alignat*", "gather", "gather*", "multline", "multline*", "flalign", "flalign*",
|
|
6
|
+
"tikzpicture", "axis", "scope", "pgfonlayer", "pgfpicture",
|
|
7
|
+
"tabular", "tabularx", "tabulary", "longtable", "array", "matrix", "pmatrix", "bmatrix", "vmatrix",
|
|
8
|
+
"verbatim", "lstlisting", "minted", "comment", "algorithmic"
|
|
9
|
+
]);
|
|
10
|
+
const opaqueArgumentCommands = new Set([
|
|
11
|
+
"documentclass", "usepackage", "requirepackage", "includegraphics", "tikzset", "pgfplotsset", "hypersetup", "lstset",
|
|
12
|
+
"label", "hypertarget", "ref", "pageref", "autoref", "nameref", "hyperref", "index", "gls", "glspl",
|
|
13
|
+
"bibliography", "addbibresource", "input", "include", "url", "path", "setlength", "setcounter", "color", "textcolor", "colorbox", "pagecolor"
|
|
14
|
+
]);
|
|
15
|
+
function addRange(ranges, from, to) {
|
|
16
|
+
if (to > from)
|
|
17
|
+
ranges.push({ from, to });
|
|
18
|
+
}
|
|
19
|
+
function isWhitespace(character) {
|
|
20
|
+
return character === " " || character === "\t" || character === "\r" || character === "\n";
|
|
21
|
+
}
|
|
22
|
+
function isCommandNameCharacter(character) {
|
|
23
|
+
if (!character)
|
|
24
|
+
return false;
|
|
25
|
+
const code = character.charCodeAt(0);
|
|
26
|
+
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57)
|
|
27
|
+
|| character === "@" || character === ":" || character === "_";
|
|
28
|
+
}
|
|
29
|
+
function balancedArgument(source, start, open, close) {
|
|
30
|
+
if (source[start] !== open)
|
|
31
|
+
return null;
|
|
32
|
+
let depth = 1;
|
|
33
|
+
for (let index = start + 1; index < source.length; index += 1) {
|
|
34
|
+
if (source[index] === "\\") {
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (source[index] === open)
|
|
39
|
+
depth += 1;
|
|
40
|
+
else if (source[index] === close && --depth === 0)
|
|
41
|
+
return { from: start, to: index + 1 };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function skipWhitespace(source, start) {
|
|
46
|
+
let index = start;
|
|
47
|
+
while (index < source.length && isWhitespace(source[index]))
|
|
48
|
+
index += 1;
|
|
49
|
+
return index;
|
|
50
|
+
}
|
|
51
|
+
function readCommand(source, start) {
|
|
52
|
+
let end = start + 1;
|
|
53
|
+
if (isCommandNameCharacter(source[end]))
|
|
54
|
+
while (isCommandNameCharacter(source[end]))
|
|
55
|
+
end += 1;
|
|
56
|
+
else if (end < source.length)
|
|
57
|
+
end += 1;
|
|
58
|
+
const name = source.slice(start + 1, end);
|
|
59
|
+
if (source[end] === "*")
|
|
60
|
+
end += 1;
|
|
61
|
+
return { name, end };
|
|
62
|
+
}
|
|
63
|
+
function isCitationCommand(name) {
|
|
64
|
+
const normalized = name.toLocaleLowerCase("en-US");
|
|
65
|
+
return normalized === "cite" || normalized.startsWith("cite") || normalized.endsWith("cite");
|
|
66
|
+
}
|
|
67
|
+
function masksOpaqueArguments(name) {
|
|
68
|
+
const normalized = name.toLocaleLowerCase("en-US");
|
|
69
|
+
return opaqueArgumentCommands.has(normalized) || isCitationCommand(normalized) || normalized === "definecolor" || normalized === "colorlet" || normalized === "href";
|
|
70
|
+
}
|
|
71
|
+
function maskOpaqueArguments(source, start, name, ranges) {
|
|
72
|
+
let cursor = start;
|
|
73
|
+
const normalized = name.toLocaleLowerCase("en-US");
|
|
74
|
+
const count = normalized === "definecolor" || normalized === "colorlet" ? 3 : 1;
|
|
75
|
+
for (let index = 0; index < count; index += 1) {
|
|
76
|
+
const argument = balancedArgument(source, cursor, "{", "}");
|
|
77
|
+
if (!argument)
|
|
78
|
+
break;
|
|
79
|
+
addRange(ranges, argument.from, argument.to);
|
|
80
|
+
cursor = skipWhitespace(source, argument.to);
|
|
81
|
+
}
|
|
82
|
+
return cursor;
|
|
83
|
+
}
|
|
84
|
+
function rawUrlEnd(source, start) {
|
|
85
|
+
const first = source[start];
|
|
86
|
+
if (first !== "h" && first !== "H" && first !== "f" && first !== "F")
|
|
87
|
+
return null;
|
|
88
|
+
for (const prefix of ["http://", "https://", "ftp://"]) {
|
|
89
|
+
if (source.slice(start, start + prefix.length).toLowerCase() !== prefix)
|
|
90
|
+
continue;
|
|
91
|
+
let end = start + prefix.length;
|
|
92
|
+
while (end < source.length && !isWhitespace(source[end]))
|
|
93
|
+
end += 1;
|
|
94
|
+
return end;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
function mergeOrderedRanges(ranges) {
|
|
99
|
+
const merged = [];
|
|
100
|
+
for (const range of ranges) {
|
|
101
|
+
const previous = merged.at(-1);
|
|
102
|
+
if (previous && range.from <= previous.to)
|
|
103
|
+
previous.to = Math.max(previous.to, range.to);
|
|
104
|
+
else
|
|
105
|
+
merged.push({ ...range });
|
|
106
|
+
}
|
|
107
|
+
return merged;
|
|
108
|
+
}
|
|
109
|
+
/** Return ordered source spans that should not be sent to a prose checker. */
|
|
110
|
+
export function ignoredLatexRanges(source) {
|
|
111
|
+
const ranges = [];
|
|
112
|
+
// Each non-prose environment installs its span at \begin and extends it at
|
|
113
|
+
// \end. This preserves source order, so final merging stays linear.
|
|
114
|
+
const nonProseStack = [];
|
|
115
|
+
// The same technique keeps unmatched $ / \[ delimiters cheap and local.
|
|
116
|
+
let pendingMath = null;
|
|
117
|
+
for (let index = 0; index < source.length;) {
|
|
118
|
+
if (pendingMath?.close.startsWith("\\") && source.startsWith(pendingMath.close, index)) {
|
|
119
|
+
pendingMath.range.to = index + pendingMath.close.length;
|
|
120
|
+
index += pendingMath.close.length;
|
|
121
|
+
pendingMath = null;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (source[index] === "%") {
|
|
125
|
+
const end = source.indexOf("\n", index);
|
|
126
|
+
addRange(ranges, index, end < 0 ? source.length : end);
|
|
127
|
+
index = end < 0 ? source.length : end + 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (source[index] === "$") {
|
|
131
|
+
if (pendingMath) {
|
|
132
|
+
if (pendingMath.close === "$" || (pendingMath.close === "$$" && source.startsWith("$$", index))) {
|
|
133
|
+
pendingMath.range.to = index + pendingMath.close.length;
|
|
134
|
+
index += pendingMath.close.length;
|
|
135
|
+
pendingMath = null;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
index += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const delimiter = source[index + 1] === "$" ? "$$" : "$";
|
|
143
|
+
const range = { from: index, to: index + delimiter.length };
|
|
144
|
+
ranges.push(range);
|
|
145
|
+
pendingMath = { close: delimiter, range };
|
|
146
|
+
index += delimiter.length;
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const urlEnd = rawUrlEnd(source, index);
|
|
151
|
+
if (urlEnd !== null) {
|
|
152
|
+
addRange(ranges, index, urlEnd);
|
|
153
|
+
index = urlEnd;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (source[index] !== "\\") {
|
|
157
|
+
index += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const commandStart = index;
|
|
161
|
+
if (source[index + 1] === "(" || source[index + 1] === "[") {
|
|
162
|
+
if (pendingMath) {
|
|
163
|
+
addRange(ranges, commandStart, index + 2);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const range = { from: commandStart, to: index + 2 };
|
|
167
|
+
ranges.push(range);
|
|
168
|
+
pendingMath = { close: source[index + 1] === "(" ? "\\)" : "\\]", range };
|
|
169
|
+
}
|
|
170
|
+
index += 2;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const command = readCommand(source, commandStart);
|
|
174
|
+
let cursor = skipWhitespace(source, command.end);
|
|
175
|
+
if (command.name === "verb") {
|
|
176
|
+
addRange(ranges, commandStart, command.end);
|
|
177
|
+
const delimiter = source[cursor];
|
|
178
|
+
if (delimiter && !isWhitespace(delimiter)) {
|
|
179
|
+
const end = source.indexOf(delimiter, cursor + 1);
|
|
180
|
+
if (end >= 0) {
|
|
181
|
+
addRange(ranges, cursor, end + 1);
|
|
182
|
+
cursor = end + 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
index = Math.max(command.end, cursor);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (command.name === "begin" || command.name === "end") {
|
|
189
|
+
const environment = balancedArgument(source, cursor, "{", "}");
|
|
190
|
+
if (!environment) {
|
|
191
|
+
addRange(ranges, commandStart, command.end);
|
|
192
|
+
index = command.end;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const environmentName = source.slice(environment.from + 1, environment.to - 1).trim().toLocaleLowerCase("en-US");
|
|
196
|
+
cursor = skipWhitespace(source, environment.to);
|
|
197
|
+
let options = null;
|
|
198
|
+
if (command.name === "begin") {
|
|
199
|
+
options = balancedArgument(source, cursor, "[", "]");
|
|
200
|
+
if (options)
|
|
201
|
+
cursor = skipWhitespace(source, options.to);
|
|
202
|
+
if (nonProseEnvironments.has(environmentName)) {
|
|
203
|
+
const range = { from: commandStart, to: cursor };
|
|
204
|
+
ranges.push(range);
|
|
205
|
+
nonProseStack.push({ name: environmentName, range });
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
addRange(ranges, commandStart, command.end);
|
|
209
|
+
addRange(ranges, environment.from, environment.to);
|
|
210
|
+
if (options)
|
|
211
|
+
addRange(ranges, options.from, options.to);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const open = nonProseStack.at(-1);
|
|
216
|
+
if (open?.name === environmentName) {
|
|
217
|
+
nonProseStack.pop();
|
|
218
|
+
open.range.to = cursor;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
addRange(ranges, commandStart, command.end);
|
|
222
|
+
addRange(ranges, environment.from, environment.to);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
index = Math.max(command.end, cursor);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
addRange(ranges, commandStart, command.end);
|
|
229
|
+
const options = balancedArgument(source, cursor, "[", "]");
|
|
230
|
+
if (options) {
|
|
231
|
+
addRange(ranges, options.from, options.to);
|
|
232
|
+
cursor = skipWhitespace(source, options.to);
|
|
233
|
+
}
|
|
234
|
+
if (masksOpaqueArguments(command.name))
|
|
235
|
+
cursor = maskOpaqueArguments(source, cursor, command.name, ranges);
|
|
236
|
+
index = Math.max(command.end, cursor);
|
|
237
|
+
}
|
|
238
|
+
return mergeOrderedRanges(ranges);
|
|
239
|
+
}
|
|
240
|
+
/** Replace syntax with spaces while retaining Unicode-scalar offsets and line breaks. */
|
|
241
|
+
export function maskLatexSource(source) {
|
|
242
|
+
const ranges = ignoredLatexRanges(source);
|
|
243
|
+
const chars = [];
|
|
244
|
+
let offset = 0;
|
|
245
|
+
let rangeIndex = 0;
|
|
246
|
+
for (const character of source) {
|
|
247
|
+
while (rangeIndex < ranges.length && ranges[rangeIndex].to <= offset)
|
|
248
|
+
rangeIndex += 1;
|
|
249
|
+
const range = ranges[rangeIndex];
|
|
250
|
+
const masked = range && range.from <= offset && offset < range.to && character !== "\n" && character !== "\r";
|
|
251
|
+
// `harper-cli` reports Unicode scalar positions. One space for every
|
|
252
|
+
// source scalar keeps diagnostics aligned after masked astral characters.
|
|
253
|
+
chars.push(masked ? " " : character);
|
|
254
|
+
offset += character.length;
|
|
255
|
+
}
|
|
256
|
+
return chars.join("");
|
|
257
|
+
}
|
package/dist/server/projects.js
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
export function accessibleProject(db, projectId, user) {
|
|
2
|
-
if (user.role === "admin") {
|
|
3
|
-
const row = db.prepare(`
|
|
4
|
-
SELECT p.*, 'owner' AS permission, owner.username AS owner_username,
|
|
5
|
-
owner.display_name AS owner_display_name,
|
|
6
|
-
modifier.username AS last_modified_username, modifier.display_name AS last_modified_display_name
|
|
7
|
-
FROM projects p
|
|
8
|
-
JOIN users owner ON owner.id = p.owner_id
|
|
9
|
-
LEFT JOIN users modifier ON modifier.id = p.last_modified_by
|
|
10
|
-
WHERE p.id = ?
|
|
11
|
-
`).get(projectId);
|
|
12
|
-
return row ?? null;
|
|
13
|
-
}
|
|
14
2
|
const row = db.prepare(`
|
|
15
3
|
SELECT p.*,
|
|
16
4
|
CASE WHEN p.owner_id = ? THEN 'owner' ELSE pm.permission END AS permission,
|
|
@@ -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
|
-
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
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
|
}
|
|
@@ -129,7 +129,7 @@ export function requireEditableProject(db, projectId, user) {
|
|
|
129
129
|
throw httpError(403, "PROJECT_EDIT_FORBIDDEN");
|
|
130
130
|
return project;
|
|
131
131
|
}
|
|
132
|
-
/** Owner permission
|
|
132
|
+
/** Owner permission is granted only to the stored project owner. */
|
|
133
133
|
export function requireProjectOwnerPermission(db, projectId, user) {
|
|
134
134
|
requireActiveUser(db, user);
|
|
135
135
|
const project = accessibleProject(db, projectId, user);
|
|
@@ -9,6 +9,7 @@ import { isMainDocumentCandidate } from "../latexRoot.js";
|
|
|
9
9
|
import { accessibleProject, canEdit } from "../projects.js";
|
|
10
10
|
import { writeProjectArchive } from "../archive.js";
|
|
11
11
|
import { extractProjectZip, ZipValidationError } from "../zip.js";
|
|
12
|
+
import { HarperUnavailableError } from "../harper.js";
|
|
12
13
|
import { commentsSummaryForProject, commentsSummaryForProjects, dictionaryWord, escapeLikePattern, now, projectJson, requireProjectOwnerPermission, tagColors, tagsForProject, tagsForProjects, text } from "./projectShared.js";
|
|
13
14
|
/** Register project catalog, metadata, archive, dictionary, tag, export, and deletion routes. */
|
|
14
15
|
export function registerProjectCatalogRoutes(app, context) {
|
|
@@ -50,14 +51,11 @@ export function registerProjectCatalogRoutes(app, context) {
|
|
|
50
51
|
const archiveCondition = archivedOnly
|
|
51
52
|
? "EXISTS (SELECT 1 FROM user_project_archives archive WHERE archive.project_id = p.id AND archive.user_id = :userId)"
|
|
52
53
|
: "NOT EXISTS (SELECT 1 FROM user_project_archives archive WHERE archive.project_id = p.id AND archive.user_id = :userId)";
|
|
53
|
-
const from =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
: `FROM projects p JOIN users owner ON owner.id = p.owner_id
|
|
57
|
-
LEFT JOIN users modifier ON modifier.id = p.last_modified_by
|
|
58
|
-
LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = :userId`;
|
|
54
|
+
const from = `FROM projects p JOIN users owner ON owner.id = p.owner_id
|
|
55
|
+
LEFT JOIN users modifier ON modifier.id = p.last_modified_by
|
|
56
|
+
LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = :userId`;
|
|
59
57
|
const conditions = [
|
|
60
|
-
|
|
58
|
+
"(p.owner_id = :userId OR pm.user_id = :userId)",
|
|
61
59
|
archiveCondition
|
|
62
60
|
];
|
|
63
61
|
const params = { userId: user.id };
|
|
@@ -79,14 +77,10 @@ export function registerProjectCatalogRoutes(app, context) {
|
|
|
79
77
|
const totalPages = Math.ceil(total / pageSize);
|
|
80
78
|
const currentPage = totalPages === 0 ? 1 : Math.min(page, totalPages);
|
|
81
79
|
const rowsParams = { ...params, limit: pageSize, offset: (currentPage - 1) * pageSize };
|
|
82
|
-
const select =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
: `SELECT DISTINCT p.*,
|
|
87
|
-
CASE WHEN p.owner_id = :userId THEN 'owner' ELSE pm.permission END AS permission,
|
|
88
|
-
owner.username AS owner_username, owner.display_name AS owner_display_name,
|
|
89
|
-
modifier.username AS last_modified_username, modifier.display_name AS last_modified_display_name`;
|
|
80
|
+
const select = `SELECT DISTINCT p.*,
|
|
81
|
+
CASE WHEN p.owner_id = :userId THEN 'owner' ELSE pm.permission END AS permission,
|
|
82
|
+
owner.username AS owner_username, owner.display_name AS owner_display_name,
|
|
83
|
+
modifier.username AS last_modified_username, modifier.display_name AS last_modified_display_name`;
|
|
90
84
|
const rows = db.prepare(`${select} ${from} ${where}
|
|
91
85
|
ORDER BY ${sortColumn} DESC, p.name COLLATE NOCASE ASC
|
|
92
86
|
LIMIT :limit OFFSET :offset`).all(rowsParams);
|
|
@@ -259,16 +253,21 @@ export function registerProjectCatalogRoutes(app, context) {
|
|
|
259
253
|
if (!accessibleProject(db, id, user))
|
|
260
254
|
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
261
255
|
const body = request.body;
|
|
262
|
-
if (typeof body?.source !== "string")
|
|
256
|
+
if (typeof body?.source !== "string" || typeof body.path !== "string")
|
|
263
257
|
return apiError(reply, 400, "SPELLCHECK_SOURCE_INVALID");
|
|
264
258
|
if (Buffer.byteLength(body.source, "utf8") > maxCollaborativeFileBytes(config)) {
|
|
265
259
|
return apiError(reply, 413, "SPELLCHECK_SOURCE_TOO_LARGE");
|
|
266
260
|
}
|
|
267
261
|
try {
|
|
268
|
-
return { lints: await harper.lint(body.source) };
|
|
262
|
+
return { lints: await harper.lint(body.source, body.path) };
|
|
269
263
|
}
|
|
270
264
|
catch (error) {
|
|
271
|
-
|
|
265
|
+
// A missing optional command is an expected fallback condition. Keep it
|
|
266
|
+
// out of normal logs while preserving diagnostics for an actual failure.
|
|
267
|
+
if (error instanceof HarperUnavailableError)
|
|
268
|
+
request.log.debug({ projectId: id }, "Host Harper CLI unavailable; using browser fallback");
|
|
269
|
+
else
|
|
270
|
+
request.log.error({ err: error, projectId: id }, "Harper writing check failed");
|
|
272
271
|
return apiError(reply, 503, "HARPER_UNAVAILABLE");
|
|
273
272
|
}
|
|
274
273
|
});
|
|
@@ -459,7 +458,9 @@ export function registerProjectCatalogRoutes(app, context) {
|
|
|
459
458
|
return;
|
|
460
459
|
const { id } = request.params;
|
|
461
460
|
const project = accessibleProject(db, id, user);
|
|
462
|
-
if (!project
|
|
461
|
+
if (!project)
|
|
462
|
+
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
463
|
+
if (project.permission !== "owner")
|
|
463
464
|
return apiError(reply, 403, "PROJECT_DELETE_FORBIDDEN");
|
|
464
465
|
return await projectMutations.runExclusive(id, "project deletion", () => {
|
|
465
466
|
collaboration.resetProject(id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "texlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A lightweight local-first collaborative LaTeX workspace",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
"dist",
|
|
25
25
|
"README.md",
|
|
26
26
|
"README.zh-CN.md",
|
|
27
|
+
"OPERATIONS.md",
|
|
28
|
+
"DESIGN.md",
|
|
29
|
+
"NPM_TESTING.md",
|
|
27
30
|
"LICENSE",
|
|
28
31
|
"preview.png",
|
|
29
32
|
"texlite.config.example.json"
|
|
@@ -72,7 +75,6 @@
|
|
|
72
75
|
"bibtex-tidy": "1.14.0",
|
|
73
76
|
"diff-match-patch": "^1.0.5",
|
|
74
77
|
"fastify": "^5.4.0",
|
|
75
|
-
"harper.js": "^2.7.0",
|
|
76
78
|
"i18next": "^25.3.2",
|
|
77
79
|
"i18next-browser-languagedetector": "^8.2.0",
|
|
78
80
|
"lib0": "^0.2.117",
|
|
@@ -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-zIjCeZFK.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};
|