@mrclrchtr/supi-code-intelligence 4.9.0 → 4.10.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/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +155 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +70 -34
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +104 -5
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +10 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +4 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +106 -2
- package/package.json +5 -5
- package/src/analysis/refactor/apply.ts +14 -19
- package/src/analysis/refactor/position.ts +47 -0
- package/src/analysis/refactor/safety.ts +18 -10
- package/src/analysis/search/ast-scan-timing.ts +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-tree-sitter",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.0",
|
|
4
4
|
"description": "Structural AST analysis for SuPi code intelligence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"web-tree-sitter": "^0.26.8",
|
|
37
|
-
"@mrclrchtr/supi-code-runtime": "4.
|
|
38
|
-
"@mrclrchtr/supi-core": "4.
|
|
37
|
+
"@mrclrchtr/supi-code-runtime": "4.10.0",
|
|
38
|
+
"@mrclrchtr/supi-core": "4.10.0"
|
|
39
39
|
},
|
|
40
40
|
"bundledDependencies": [
|
|
41
41
|
"@mrclrchtr/supi-code-runtime",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"./provider/tree-sitter-provider": "./src/provider/tree-sitter-provider.ts"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
|
+
"bench:structural": "vitest bench __tests__/benchmark/structural-baseline.bench.ts --run",
|
|
52
53
|
"vendor:wasm": "node scripts/vendor-wasm.mjs",
|
|
53
54
|
"check:wasm": "node scripts/vendor-wasm.mjs --check",
|
|
54
55
|
"generate:kotlin-wasm": "node scripts/generate-kotlin-wasm.mjs",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"releaseAsset": "https://github.com/fwcd/tree-sitter-kotlin/releases/download/0.3.8/tree-sitter-kotlin.wasm"
|
|
7
7
|
},
|
|
8
8
|
"generatedWith": {
|
|
9
|
-
"treeSitterCli": "0.26.
|
|
9
|
+
"treeSitterCli": "0.26.12"
|
|
10
10
|
},
|
|
11
11
|
"sha256": "e08447e36ad5d86f39f91f13defce0636aea1490b8d65dafde6fa72649422061"
|
|
12
12
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Tree-sitter runtime — parser management, parse, and query services.
|
|
2
2
|
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import * as fs from "node:fs";
|
|
4
5
|
import * as path from "node:path";
|
|
6
|
+
import { startDebugTimer } from "@mrclrchtr/supi-core/debug";
|
|
5
7
|
import { resolveToolPath } from "@mrclrchtr/supi-core/path";
|
|
6
8
|
import type { Language, Parser, Tree } from "web-tree-sitter";
|
|
7
9
|
import { nodeToRange } from "../coordinates.ts";
|
|
@@ -126,11 +128,25 @@ export class TreeSitterRuntime {
|
|
|
126
128
|
};
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
const timer = startDebugTimer();
|
|
132
|
+
const parserState = this.parsers.has(grammarId)
|
|
133
|
+
? "reused"
|
|
134
|
+
: this.parserPromises.has(grammarId)
|
|
135
|
+
? "initializing"
|
|
136
|
+
: "cold";
|
|
137
|
+
|
|
129
138
|
// Read the file
|
|
130
139
|
let source: string;
|
|
131
140
|
try {
|
|
132
141
|
source = fs.readFileSync(resolvedPath, "utf-8");
|
|
133
142
|
} catch (err: unknown) {
|
|
143
|
+
finishStructuralTiming(timer, {
|
|
144
|
+
operation: "parse",
|
|
145
|
+
grammar: grammarId,
|
|
146
|
+
parserState,
|
|
147
|
+
outcome: "file-access-error",
|
|
148
|
+
finalPhase: "file-read",
|
|
149
|
+
});
|
|
134
150
|
const message = err instanceof Error ? err.message : "File could not be read";
|
|
135
151
|
return {
|
|
136
152
|
kind: "file-access-error",
|
|
@@ -138,15 +154,42 @@ export class TreeSitterRuntime {
|
|
|
138
154
|
message,
|
|
139
155
|
};
|
|
140
156
|
}
|
|
157
|
+
timer.mark("file-read");
|
|
158
|
+
|
|
159
|
+
if (timer.enabled) {
|
|
160
|
+
try {
|
|
161
|
+
createHash("sha256").update(source).digest();
|
|
162
|
+
} catch {
|
|
163
|
+
// Timing-only hashing must not change structural behavior.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
timer.mark("content-hash");
|
|
141
167
|
|
|
168
|
+
let parserReady = false;
|
|
142
169
|
try {
|
|
143
170
|
const entry = await this.ensureGrammarParser(grammarId);
|
|
171
|
+
timer.mark("parser-setup");
|
|
172
|
+
parserReady = true;
|
|
144
173
|
const tree = entry.parser.parse(source);
|
|
174
|
+
finishStructuralTiming(timer, {
|
|
175
|
+
operation: "parse",
|
|
176
|
+
grammar: grammarId,
|
|
177
|
+
parserState,
|
|
178
|
+
outcome: "completed",
|
|
179
|
+
finalPhase: "parse",
|
|
180
|
+
});
|
|
145
181
|
return {
|
|
146
182
|
kind: "success",
|
|
147
183
|
data: { tree: tree as Tree, source, resolvedPath, grammarId },
|
|
148
184
|
};
|
|
149
185
|
} catch (err: unknown) {
|
|
186
|
+
finishStructuralTiming(timer, {
|
|
187
|
+
operation: "parse",
|
|
188
|
+
grammar: grammarId,
|
|
189
|
+
parserState,
|
|
190
|
+
outcome: "runtime-error",
|
|
191
|
+
finalPhase: parserReady ? "parse" : "parser-setup",
|
|
192
|
+
});
|
|
150
193
|
return { kind: "runtime-error", message: formatError(err, "Parser initialization failed") };
|
|
151
194
|
}
|
|
152
195
|
}
|
|
@@ -169,16 +212,27 @@ export class TreeSitterRuntime {
|
|
|
169
212
|
const parseResult = await this.parseFile(filePath);
|
|
170
213
|
if (parseResult.kind !== "success") return parseResult;
|
|
171
214
|
|
|
172
|
-
const { tree, source } = parseResult.data;
|
|
215
|
+
const { grammarId, tree, source } = parseResult.data;
|
|
216
|
+
const timer = startDebugTimer();
|
|
217
|
+
let compiled = false;
|
|
173
218
|
|
|
174
219
|
try {
|
|
175
|
-
const entry = await this.ensureGrammarParser(
|
|
220
|
+
const entry = await this.ensureGrammarParser(grammarId);
|
|
176
221
|
const mod = await this.ensureParserInit();
|
|
177
222
|
let query: InstanceType<typeof mod.Query>;
|
|
178
223
|
|
|
179
224
|
try {
|
|
180
225
|
query = new mod.Query(entry.language, queryString);
|
|
226
|
+
compiled = true;
|
|
227
|
+
timer.mark("query-compilation");
|
|
181
228
|
} catch (err: unknown) {
|
|
229
|
+
finishStructuralTiming(timer, {
|
|
230
|
+
operation: "query",
|
|
231
|
+
grammar: grammarId,
|
|
232
|
+
outcome: "validation-error",
|
|
233
|
+
captureCount: 0,
|
|
234
|
+
finalPhase: "query-compilation",
|
|
235
|
+
});
|
|
182
236
|
return { kind: "validation-error", message: `Invalid query: ${formatError(err)}` };
|
|
183
237
|
}
|
|
184
238
|
|
|
@@ -195,11 +249,25 @@ export class TreeSitterRuntime {
|
|
|
195
249
|
});
|
|
196
250
|
}
|
|
197
251
|
}
|
|
252
|
+
finishStructuralTiming(timer, {
|
|
253
|
+
operation: "query",
|
|
254
|
+
grammar: grammarId,
|
|
255
|
+
outcome: "completed",
|
|
256
|
+
captureCount: captures.length,
|
|
257
|
+
finalPhase: "query-execution",
|
|
258
|
+
});
|
|
198
259
|
return { kind: "success", data: captures };
|
|
199
260
|
} finally {
|
|
200
261
|
query.delete();
|
|
201
262
|
}
|
|
202
263
|
} catch (err: unknown) {
|
|
264
|
+
finishStructuralTiming(timer, {
|
|
265
|
+
operation: "query",
|
|
266
|
+
grammar: grammarId,
|
|
267
|
+
outcome: "runtime-error",
|
|
268
|
+
captureCount: 0,
|
|
269
|
+
finalPhase: compiled ? "query-execution" : "query-compilation",
|
|
270
|
+
});
|
|
203
271
|
return { kind: "runtime-error", message: formatError(err, "Query execution failed") };
|
|
204
272
|
} finally {
|
|
205
273
|
tree.delete();
|
|
@@ -239,6 +307,42 @@ export class TreeSitterRuntime {
|
|
|
239
307
|
/** Max query string length to prevent ReDoS via overly complex Tree-sitter patterns. */
|
|
240
308
|
const MAX_QUERY_LENGTH = 10_000;
|
|
241
309
|
|
|
310
|
+
type StructuralTimer = ReturnType<typeof startDebugTimer>;
|
|
311
|
+
type ParserState = "cold" | "initializing" | "reused";
|
|
312
|
+
|
|
313
|
+
type StructuralTimingObservation =
|
|
314
|
+
| {
|
|
315
|
+
readonly operation: "parse";
|
|
316
|
+
readonly grammar: GrammarId;
|
|
317
|
+
readonly parserState: ParserState;
|
|
318
|
+
readonly outcome: "completed" | "file-access-error" | "runtime-error";
|
|
319
|
+
readonly finalPhase: "file-read" | "parse" | "parser-setup";
|
|
320
|
+
}
|
|
321
|
+
| {
|
|
322
|
+
readonly operation: "query";
|
|
323
|
+
readonly grammar: GrammarId;
|
|
324
|
+
readonly outcome: "completed" | "runtime-error" | "validation-error";
|
|
325
|
+
readonly captureCount: number;
|
|
326
|
+
readonly finalPhase: "query-compilation" | "query-execution";
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
function finishStructuralTiming(
|
|
330
|
+
timer: StructuralTimer,
|
|
331
|
+
observation: StructuralTimingObservation,
|
|
332
|
+
): void {
|
|
333
|
+
const { finalPhase, ...data } = observation;
|
|
334
|
+
timer.finish(
|
|
335
|
+
() => ({
|
|
336
|
+
source: "tree-sitter",
|
|
337
|
+
level: "debug",
|
|
338
|
+
category: `structural.${data.operation}.timing`,
|
|
339
|
+
message: `Tree-sitter ${data.operation} ${data.outcome}`,
|
|
340
|
+
data: { ...data },
|
|
341
|
+
}),
|
|
342
|
+
finalPhase,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
242
346
|
/** Format errors with their cause chain's first message for user-facing tool output. */
|
|
243
347
|
function formatError(err: unknown, fallback = "Operation failed"): string {
|
|
244
348
|
if (!(err instanceof Error)) return String(err || fallback);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-code-intelligence",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.0",
|
|
4
4
|
"description": "LSP and AST navigation, search, diagnostics, and refactoring",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"yaml": "^2.9.0",
|
|
38
|
-
"@mrclrchtr/supi-code-runtime": "4.
|
|
39
|
-
"@mrclrchtr/supi-
|
|
40
|
-
"@mrclrchtr/supi-
|
|
41
|
-
"@mrclrchtr/supi-
|
|
38
|
+
"@mrclrchtr/supi-code-runtime": "4.10.0",
|
|
39
|
+
"@mrclrchtr/supi-tree-sitter": "4.10.0",
|
|
40
|
+
"@mrclrchtr/supi-lsp": "4.10.0",
|
|
41
|
+
"@mrclrchtr/supi-core": "4.10.0"
|
|
42
42
|
},
|
|
43
43
|
"bundledDependencies": [
|
|
44
44
|
"@mrclrchtr/supi-code-runtime",
|
|
@@ -16,7 +16,7 @@ import { readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:
|
|
|
16
16
|
import { basename, dirname, join } from "node:path";
|
|
17
17
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import type { FileEdit, WorkspaceEdit } from "@mrclrchtr/supi-code-runtime/api";
|
|
19
|
-
import { compareCodePositions } from "./position.ts";
|
|
19
|
+
import { compareCodePositions, createLogicalLineIndex } from "./position.ts";
|
|
20
20
|
import { validateEditAgainstFiles } from "./safety.ts";
|
|
21
21
|
|
|
22
22
|
export type ApplyResult =
|
|
@@ -158,18 +158,21 @@ function buildTransformedContents(
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
function applyEditsToContent(content: string, edits: FileEdit[]): string {
|
|
161
|
-
const
|
|
162
|
-
const sortedEdits =
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
161
|
+
const lineIndex = createLogicalLineIndex(content);
|
|
162
|
+
const sortedEdits = edits
|
|
163
|
+
.map((edit, protocolIndex) => ({ edit, protocolIndex }))
|
|
164
|
+
.sort(
|
|
165
|
+
(left, right) =>
|
|
166
|
+
// Descending positions prevent shifts. Reverse protocol order at one
|
|
167
|
+
// position so sequential inserts appear in their supplied order.
|
|
168
|
+
compareCodePositions(right.edit.range.start, left.edit.range.start) ||
|
|
169
|
+
right.protocolIndex - left.protocolIndex,
|
|
170
|
+
);
|
|
168
171
|
|
|
169
172
|
let updated = content;
|
|
170
|
-
for (const fileEdit of sortedEdits) {
|
|
171
|
-
const startOffset =
|
|
172
|
-
const endOffset =
|
|
173
|
+
for (const { edit: fileEdit } of sortedEdits) {
|
|
174
|
+
const startOffset = lineIndex.offsetAt(fileEdit.range.start);
|
|
175
|
+
const endOffset = lineIndex.offsetAt(fileEdit.range.end);
|
|
173
176
|
updated = updated.slice(0, startOffset) + fileEdit.newText + updated.slice(endOffset);
|
|
174
177
|
}
|
|
175
178
|
|
|
@@ -266,14 +269,6 @@ function rollbackCommittedFiles(
|
|
|
266
269
|
}
|
|
267
270
|
}
|
|
268
271
|
|
|
269
|
-
function toOffset(lines: string[], line: number, character: number): number {
|
|
270
|
-
let offset = 0;
|
|
271
|
-
for (let index = 0; index < line && index < lines.length; index++) {
|
|
272
|
-
offset += lines[index].length + 1;
|
|
273
|
-
}
|
|
274
|
-
return offset + character;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
272
|
function toErrorMessage(error: unknown): string {
|
|
278
273
|
return error instanceof Error ? error.message : String(error);
|
|
279
274
|
}
|
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
import type { CodePosition } from "@mrclrchtr/supi-code-runtime/api";
|
|
2
2
|
|
|
3
|
+
interface IndexedLine {
|
|
4
|
+
readonly startOffset: number;
|
|
5
|
+
readonly length: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Logical source lines with offsets into the unchanged source string. */
|
|
9
|
+
export interface LogicalLineIndex {
|
|
10
|
+
readonly lineCount: number;
|
|
11
|
+
/** Get one logical line length in UTF-16 code units. */
|
|
12
|
+
lineLength(line: number): number | undefined;
|
|
13
|
+
/** Convert a validated zero-based UTF-16 position to a source-string offset. */
|
|
14
|
+
offsetAt(position: CodePosition): number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Index LF, CRLF, and bare-CR lines without normalizing source bytes.
|
|
19
|
+
*
|
|
20
|
+
* JavaScript string offsets and lengths use UTF-16 code units, which are the
|
|
21
|
+
* only LSP coordinate encoding that the refactor path supports.
|
|
22
|
+
*/
|
|
23
|
+
export function createLogicalLineIndex(source: string): LogicalLineIndex {
|
|
24
|
+
const lines: IndexedLine[] = [];
|
|
25
|
+
let lineStart = 0;
|
|
26
|
+
|
|
27
|
+
for (let offset = 0; offset < source.length; offset++) {
|
|
28
|
+
const codeUnit = source.charCodeAt(offset);
|
|
29
|
+
if (codeUnit !== 0x0a && codeUnit !== 0x0d) continue;
|
|
30
|
+
|
|
31
|
+
lines.push({ startOffset: lineStart, length: offset - lineStart });
|
|
32
|
+
if (codeUnit === 0x0d && source.charCodeAt(offset + 1) === 0x0a) offset++;
|
|
33
|
+
lineStart = offset + 1;
|
|
34
|
+
}
|
|
35
|
+
lines.push({ startOffset: lineStart, length: source.length - lineStart });
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
lineCount: lines.length,
|
|
39
|
+
lineLength: (line) => lines[line]?.length,
|
|
40
|
+
offsetAt: (position) => {
|
|
41
|
+
const line = lines[position.line];
|
|
42
|
+
if (!line || position.character < 0 || position.character > line.length) {
|
|
43
|
+
throw new RangeError("Position is outside the logical source lines");
|
|
44
|
+
}
|
|
45
|
+
return line.startOffset + position.character;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
3
50
|
/** Compare zero-based source positions lexicographically by line, then character. */
|
|
4
51
|
export function compareCodePositions(left: CodePosition, right: CodePosition): number {
|
|
5
52
|
return left.line - right.line || left.character - right.character;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import type { FileEdit, WorkspaceEdit } from "@mrclrchtr/supi-code-runtime/api";
|
|
10
|
-
import { compareCodePositions } from "./position.ts";
|
|
10
|
+
import { compareCodePositions, createLogicalLineIndex, type LogicalLineIndex } from "./position.ts";
|
|
11
11
|
|
|
12
12
|
export type ValidationResult = { safe: true } | { safe: false; reason: string };
|
|
13
13
|
|
|
@@ -76,12 +76,12 @@ export function validateEditAgainstFiles(edit: WorkspaceEdit): ValidationResult
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const
|
|
79
|
+
const lineIndex = createLogicalLineIndex(content);
|
|
80
80
|
for (const fileEdit of fileEdits) {
|
|
81
|
-
const lineValidation = validateLineBounds(file, fileEdit,
|
|
81
|
+
const lineValidation = validateLineBounds(file, fileEdit, lineIndex);
|
|
82
82
|
if (!lineValidation.safe) return lineValidation;
|
|
83
83
|
|
|
84
|
-
const characterValidation = validateCharacterBounds(file, fileEdit,
|
|
84
|
+
const characterValidation = validateCharacterBounds(file, fileEdit, lineIndex);
|
|
85
85
|
if (!characterValidation.safe) return characterValidation;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -89,19 +89,27 @@ export function validateEditAgainstFiles(edit: WorkspaceEdit): ValidationResult
|
|
|
89
89
|
return { safe: true };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function validateLineBounds(
|
|
93
|
-
|
|
92
|
+
function validateLineBounds(
|
|
93
|
+
file: string,
|
|
94
|
+
edit: FileEdit,
|
|
95
|
+
lineIndex: LogicalLineIndex,
|
|
96
|
+
): ValidationResult {
|
|
97
|
+
const maxLine = lineIndex.lineCount - 1;
|
|
94
98
|
if (edit.range.start.line > maxLine || edit.range.end.line > maxLine) {
|
|
95
99
|
return {
|
|
96
100
|
safe: false,
|
|
97
|
-
reason: `Edit in file "${file}" references line ${Math.max(edit.range.start.line, edit.range.end.line)}, but the file has only ${
|
|
101
|
+
reason: `Edit in file "${file}" references line ${Math.max(edit.range.start.line, edit.range.end.line)}, but the file has only ${lineIndex.lineCount} line${lineIndex.lineCount === 1 ? "" : "s"}`,
|
|
98
102
|
};
|
|
99
103
|
}
|
|
100
104
|
return { safe: true };
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
function validateCharacterBounds(
|
|
104
|
-
|
|
107
|
+
function validateCharacterBounds(
|
|
108
|
+
file: string,
|
|
109
|
+
edit: FileEdit,
|
|
110
|
+
lineIndex: LogicalLineIndex,
|
|
111
|
+
): ValidationResult {
|
|
112
|
+
const startLineLength = lineIndex.lineLength(edit.range.start.line) ?? 0;
|
|
105
113
|
if (edit.range.start.character > startLineLength) {
|
|
106
114
|
return {
|
|
107
115
|
safe: false,
|
|
@@ -109,7 +117,7 @@ function validateCharacterBounds(file: string, edit: FileEdit, lines: string[]):
|
|
|
109
117
|
};
|
|
110
118
|
}
|
|
111
119
|
|
|
112
|
-
const endLineLength =
|
|
120
|
+
const endLineLength = lineIndex.lineLength(edit.range.end.line) ?? 0;
|
|
113
121
|
if (edit.range.end.character > endLineLength) {
|
|
114
122
|
return {
|
|
115
123
|
safe: false,
|
|
@@ -3,7 +3,6 @@ import type { StructuralSearchOperation } from "@mrclrchtr/supi-tree-sitter/api"
|
|
|
3
3
|
import type { StructuredFileAnalysis, StructuredPatternParams } from "./pattern-analysis.ts";
|
|
4
4
|
|
|
5
5
|
interface ScanContext {
|
|
6
|
-
readonly cwd: string;
|
|
7
6
|
readonly params: StructuredPatternParams;
|
|
8
7
|
}
|
|
9
8
|
|
|
@@ -40,7 +39,6 @@ export function startAstScanTimer(): AstScanTimer {
|
|
|
40
39
|
level: "debug",
|
|
41
40
|
category: "ast-scan.timing",
|
|
42
41
|
message: `AST ${input.context.params.kind} scan analyzed ${input.analysis.analyzedFileCount} files`,
|
|
43
|
-
cwd: input.context.cwd,
|
|
44
42
|
data: {
|
|
45
43
|
kind: input.context.params.kind,
|
|
46
44
|
operation: input.operation,
|