pi-readseek 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.
- package/README.md +28 -46
- package/dist/index.js +7333 -0
- package/package.json +8 -7
- package/index.ts +0 -147
- package/src/binary-detect.ts +0 -22
- package/src/check.ts +0 -92
- package/src/coerce-obvious-int.ts +0 -41
- package/src/confusable-hyphens.ts +0 -6
- package/src/def.ts +0 -143
- package/src/diff-data.ts +0 -302
- package/src/edit-classify.ts +0 -58
- package/src/edit-diff.ts +0 -359
- package/src/edit-output.ts +0 -110
- package/src/edit-render-helpers.ts +0 -141
- package/src/edit-syntax-validate.ts +0 -65
- package/src/edit.ts +0 -686
- package/src/file-map.ts +0 -17
- package/src/fs-error.ts +0 -33
- package/src/grep-budget.ts +0 -35
- package/src/grep-output.ts +0 -181
- package/src/grep-render-helpers.ts +0 -77
- package/src/grep-symbol-scope.ts +0 -184
- package/src/grep.ts +0 -608
- package/src/hashline.ts +0 -736
- package/src/hover.ts +0 -140
- package/src/path-utils.ts +0 -24
- package/src/pending-diff-preview.ts +0 -269
- package/src/read-local-bundle.ts +0 -75
- package/src/read-output.ts +0 -186
- package/src/read-render-helpers.ts +0 -104
- package/src/read.ts +0 -661
- package/src/readseek/constants.ts +0 -17
- package/src/readseek/enums.ts +0 -38
- package/src/readseek/formatter.ts +0 -350
- package/src/readseek/symbol-error-format.ts +0 -18
- package/src/readseek/symbol-lookup.ts +0 -240
- package/src/readseek/symbol-tree.ts +0 -21
- package/src/readseek/types.ts +0 -49
- package/src/readseek-client.ts +0 -1250
- package/src/readseek-params.ts +0 -39
- package/src/readseek-settings.ts +0 -263
- package/src/readseek-value.ts +0 -184
- package/src/refs-output.ts +0 -47
- package/src/refs.ts +0 -161
- package/src/register-tool.ts +0 -29
- package/src/rename.ts +0 -182
- package/src/replace-symbol.ts +0 -77
- package/src/runtime.ts +0 -3
- package/src/search-output.ts +0 -50
- package/src/search.ts +0 -214
- package/src/session-anchors.ts +0 -34
- package/src/stat-search-path.ts +0 -29
- package/src/syntax-validate-mode.ts +0 -15
- package/src/tool-prompt-metadata.ts +0 -107
- package/src/tool-types.ts +0 -12
- package/src/tui-diff-component.ts +0 -108
- package/src/tui-diff-renderer.ts +0 -92
- package/src/tui-render-utils.ts +0 -286
- package/src/view.ts +0 -158
- package/src/write.ts +0 -357
package/src/edit-diff.ts
DELETED
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
import * as Diff from "diff";
|
|
2
|
-
import { computeLineHash } from "./hashline.js";
|
|
3
|
-
import { CONFUSABLE_HYPHENS_RE } from "./confusable-hyphens.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export function detectLineEnding(content: string): "\r\n" | "\n" {
|
|
7
|
-
let crlf = 0;
|
|
8
|
-
let lf = 0;
|
|
9
|
-
for (let i = 0; i < content.length; i++) {
|
|
10
|
-
if (content[i] === "\r" && i + 1 < content.length && content[i + 1] === "\n") {
|
|
11
|
-
crlf++;
|
|
12
|
-
i++;
|
|
13
|
-
} else if (content[i] === "\n") {
|
|
14
|
-
lf++;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
if (lf === 0 && crlf === 0) return "\n";
|
|
18
|
-
if (lf === 0) return "\r\n";
|
|
19
|
-
if (crlf === 0) return "\n";
|
|
20
|
-
return crlf > lf ? "\r\n" : "\n";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function normalizeToLF(text: string): string {
|
|
24
|
-
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string {
|
|
28
|
-
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function stripBom(content: string): { bom: string; text: string } {
|
|
32
|
-
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Detect bare \r characters that are NOT part of \r\n sequences.
|
|
37
|
-
* These cause line-count mismatches between normalizeToLF and external tools (ripgrep, wc).
|
|
38
|
-
*/
|
|
39
|
-
export function hasBareCarriageReturn(content: string): boolean {
|
|
40
|
-
return content.replace(/\r\n/g, "").includes("\r");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const SINGLE_QUOTES_RE = /[\u2018\u2019\u201A\u201B]/g;
|
|
44
|
-
const DOUBLE_QUOTES_RE = /[\u201C\u201D\u201E\u201F]/g;
|
|
45
|
-
const UNICODE_SPACES_RE = /[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g;
|
|
46
|
-
|
|
47
|
-
function normalizeFuzzyChar(ch: string): string {
|
|
48
|
-
return ch.replace(SINGLE_QUOTES_RE, "'").replace(DOUBLE_QUOTES_RE, '"').replace(CONFUSABLE_HYPHENS_RE, "-").replace(UNICODE_SPACES_RE, " ");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function normalizeForFuzzyMatch(text: string): string {
|
|
52
|
-
return text
|
|
53
|
-
.split("\n")
|
|
54
|
-
.map((line) => line.trimEnd())
|
|
55
|
-
.join("\n")
|
|
56
|
-
.replace(SINGLE_QUOTES_RE, "'")
|
|
57
|
-
.replace(DOUBLE_QUOTES_RE, '"')
|
|
58
|
-
.replace(CONFUSABLE_HYPHENS_RE, "-")
|
|
59
|
-
.replace(UNICODE_SPACES_RE, " ");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function buildNormalizedWithMap(text: string): { normalized: string; indexMap: number[] } {
|
|
63
|
-
const lines = text.split("\n");
|
|
64
|
-
const normalizedChars: string[] = [];
|
|
65
|
-
const indexMap: number[] = [];
|
|
66
|
-
let originalOffset = 0;
|
|
67
|
-
|
|
68
|
-
for (let i = 0; i < lines.length; i++) {
|
|
69
|
-
const line = lines[i]!;
|
|
70
|
-
const trimmed = line.replace(/\s+$/u, "");
|
|
71
|
-
|
|
72
|
-
for (let j = 0; j < trimmed.length; j++) {
|
|
73
|
-
normalizedChars.push(normalizeFuzzyChar(trimmed[j]!));
|
|
74
|
-
indexMap.push(originalOffset + j);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (i < lines.length - 1) {
|
|
78
|
-
normalizedChars.push("\n");
|
|
79
|
-
indexMap.push(originalOffset + line.length);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
originalOffset += line.length + 1;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return { normalized: normalizedChars.join(""), indexMap };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function mapNormalizedSpanToOriginal(
|
|
89
|
-
indexMap: number[],
|
|
90
|
-
normalizedStart: number,
|
|
91
|
-
normalizedLength: number,
|
|
92
|
-
): { index: number; matchLength: number } | null {
|
|
93
|
-
if (normalizedStart < 0 || normalizedLength <= 0) return null;
|
|
94
|
-
const normalizedEnd = normalizedStart + normalizedLength;
|
|
95
|
-
if (normalizedEnd > indexMap.length) return null;
|
|
96
|
-
|
|
97
|
-
const start = indexMap[normalizedStart];
|
|
98
|
-
const end = indexMap[normalizedEnd - 1];
|
|
99
|
-
if (start === undefined || end === undefined || end < start) return null;
|
|
100
|
-
|
|
101
|
-
return { index: start, matchLength: end - start + 1 };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Find `oldText` in `content` with optional fuzzy whitespace/unicode matching.
|
|
106
|
-
* Always returns an index/length in the original content.
|
|
107
|
-
*/
|
|
108
|
-
export function fuzzyFindText(
|
|
109
|
-
content: string,
|
|
110
|
-
oldText: string,
|
|
111
|
-
): { found: boolean; index: number; matchLength: number; usedFuzzyMatch: boolean } {
|
|
112
|
-
const exactIndex = content.indexOf(oldText);
|
|
113
|
-
if (exactIndex !== -1) {
|
|
114
|
-
return { found: true, index: exactIndex, matchLength: oldText.length, usedFuzzyMatch: false };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const normalizedNeedle = normalizeForFuzzyMatch(oldText);
|
|
118
|
-
if (!normalizedNeedle.length) return { found: false, index: -1, matchLength: 0, usedFuzzyMatch: false };
|
|
119
|
-
|
|
120
|
-
const { normalized, indexMap } = buildNormalizedWithMap(content);
|
|
121
|
-
const normalizedIndex = normalized.indexOf(normalizedNeedle);
|
|
122
|
-
if (normalizedIndex === -1) {
|
|
123
|
-
return { found: false, index: -1, matchLength: 0, usedFuzzyMatch: false };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const mapped = mapNormalizedSpanToOriginal(indexMap, normalizedIndex, normalizedNeedle.length);
|
|
127
|
-
if (!mapped) {
|
|
128
|
-
return { found: false, index: -1, matchLength: 0, usedFuzzyMatch: false };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return { found: true, index: mapped.index, matchLength: mapped.matchLength, usedFuzzyMatch: true };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Replace `oldText` with `newText` in `content`.
|
|
136
|
-
* Fuzzy matching only determines target spans; replacement always applies to
|
|
137
|
-
* the original content (never normalizes the whole file).
|
|
138
|
-
*/
|
|
139
|
-
export type ReplaceTextResult = { content: string; count: number; usedFuzzyMatch: boolean };
|
|
140
|
-
|
|
141
|
-
export function replaceText(
|
|
142
|
-
content: string,
|
|
143
|
-
oldText: string,
|
|
144
|
-
newText: string,
|
|
145
|
-
opts: { all?: boolean; fuzzy?: boolean },
|
|
146
|
-
): ReplaceTextResult {
|
|
147
|
-
if (!oldText.length) return { content, count: 0, usedFuzzyMatch: false };
|
|
148
|
-
const normalizedNew = normalizeToLF(newText);
|
|
149
|
-
|
|
150
|
-
if (opts.all) {
|
|
151
|
-
const exactCount = content.split(oldText).length - 1;
|
|
152
|
-
if (exactCount > 0) {
|
|
153
|
-
return { content: content.split(oldText).join(normalizedNew), count: exactCount, usedFuzzyMatch: false };
|
|
154
|
-
}
|
|
155
|
-
if (!opts.fuzzy) return { content, count: 0, usedFuzzyMatch: false };
|
|
156
|
-
|
|
157
|
-
const normalizedNeedle = normalizeForFuzzyMatch(oldText);
|
|
158
|
-
if (!normalizedNeedle.length) return { content, count: 0, usedFuzzyMatch: false };
|
|
159
|
-
|
|
160
|
-
const { normalized, indexMap } = buildNormalizedWithMap(content);
|
|
161
|
-
const spans: Array<{ index: number; matchLength: number }> = [];
|
|
162
|
-
let searchFrom = 0;
|
|
163
|
-
|
|
164
|
-
while (searchFrom <= normalized.length - normalizedNeedle.length) {
|
|
165
|
-
const pos = normalized.indexOf(normalizedNeedle, searchFrom);
|
|
166
|
-
if (pos === -1) break;
|
|
167
|
-
const mapped = mapNormalizedSpanToOriginal(indexMap, pos, normalizedNeedle.length);
|
|
168
|
-
if (mapped) {
|
|
169
|
-
const prev = spans[spans.length - 1];
|
|
170
|
-
if (!prev || mapped.index >= prev.index + prev.matchLength) {
|
|
171
|
-
spans.push(mapped);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
searchFrom = pos + Math.max(1, normalizedNeedle.length);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (!spans.length) return { content, count: 0, usedFuzzyMatch: false };
|
|
178
|
-
|
|
179
|
-
let out = content;
|
|
180
|
-
for (let i = spans.length - 1; i >= 0; i--) {
|
|
181
|
-
const span = spans[i]!;
|
|
182
|
-
out = out.substring(0, span.index) + normalizedNew + out.substring(span.index + span.matchLength);
|
|
183
|
-
}
|
|
184
|
-
return { content: out, count: spans.length, usedFuzzyMatch: true };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const exactIndex = content.indexOf(oldText);
|
|
188
|
-
if (exactIndex !== -1) {
|
|
189
|
-
return {
|
|
190
|
-
content: content.substring(0, exactIndex) + normalizedNew + content.substring(exactIndex + oldText.length),
|
|
191
|
-
count: 1,
|
|
192
|
-
usedFuzzyMatch: false,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (!opts.fuzzy) return { content, count: 0, usedFuzzyMatch: false };
|
|
197
|
-
|
|
198
|
-
const result = fuzzyFindText(content, oldText);
|
|
199
|
-
if (!result.found) return { content, count: 0, usedFuzzyMatch: false };
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
content: content.substring(0, result.index) + normalizedNew + content.substring(result.index + result.matchLength),
|
|
203
|
-
count: 1,
|
|
204
|
-
usedFuzzyMatch: result.usedFuzzyMatch,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
export function generateDiffString(
|
|
210
|
-
oldContent: string,
|
|
211
|
-
newContent: string,
|
|
212
|
-
contextLines = 4,
|
|
213
|
-
): { diff: string; firstChangedLine: number | undefined } {
|
|
214
|
-
const parts = Diff.diffLines(oldContent, newContent);
|
|
215
|
-
const output: string[] = [];
|
|
216
|
-
const maxLineNum = Math.max(oldContent.split("\n").length, newContent.split("\n").length);
|
|
217
|
-
const lineNumWidth = String(maxLineNum).length;
|
|
218
|
-
let oldLineNum = 1;
|
|
219
|
-
let newLineNum = 1;
|
|
220
|
-
let lastWasChange = false;
|
|
221
|
-
let firstChangedLine: number | undefined;
|
|
222
|
-
|
|
223
|
-
for (let i = 0; i < parts.length; i++) {
|
|
224
|
-
const part = parts[i]!;
|
|
225
|
-
const raw = part.value.split("\n");
|
|
226
|
-
if (raw[raw.length - 1] === "") raw.pop();
|
|
227
|
-
|
|
228
|
-
if (part.added || part.removed) {
|
|
229
|
-
if (firstChangedLine === undefined) firstChangedLine = newLineNum;
|
|
230
|
-
for (const line of raw) {
|
|
231
|
-
if (part.added) {
|
|
232
|
-
output.push(`+${String(newLineNum).padStart(lineNumWidth, " ")} ${line}`);
|
|
233
|
-
newLineNum++;
|
|
234
|
-
} else {
|
|
235
|
-
output.push(`-${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
|
|
236
|
-
oldLineNum++;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
lastWasChange = true;
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1]!.added || parts[i + 1]!.removed);
|
|
244
|
-
if (lastWasChange || nextPartIsChange) {
|
|
245
|
-
let linesToShow = raw;
|
|
246
|
-
let skipStart = 0;
|
|
247
|
-
let skipEnd = 0;
|
|
248
|
-
|
|
249
|
-
if (!lastWasChange) {
|
|
250
|
-
skipStart = Math.max(0, raw.length - contextLines);
|
|
251
|
-
linesToShow = raw.slice(skipStart);
|
|
252
|
-
}
|
|
253
|
-
if (!nextPartIsChange && linesToShow.length > contextLines) {
|
|
254
|
-
skipEnd = linesToShow.length - contextLines;
|
|
255
|
-
linesToShow = linesToShow.slice(0, contextLines);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (skipStart > 0) {
|
|
259
|
-
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
260
|
-
oldLineNum += skipStart;
|
|
261
|
-
newLineNum += skipStart;
|
|
262
|
-
}
|
|
263
|
-
for (const line of linesToShow) {
|
|
264
|
-
output.push(` ${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
|
|
265
|
-
oldLineNum++;
|
|
266
|
-
newLineNum++;
|
|
267
|
-
}
|
|
268
|
-
if (skipEnd > 0) {
|
|
269
|
-
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
270
|
-
oldLineNum += skipEnd;
|
|
271
|
-
newLineNum += skipEnd;
|
|
272
|
-
}
|
|
273
|
-
} else {
|
|
274
|
-
oldLineNum += raw.length;
|
|
275
|
-
newLineNum += raw.length;
|
|
276
|
-
}
|
|
277
|
-
lastWasChange = false;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return { diff: output.join("\n"), firstChangedLine };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Generate a compact diff for single-line edits, or fall back to the full diff.
|
|
285
|
-
*
|
|
286
|
-
* - Single-line replacement: `LINE:HASH|old → LINE:HASH|new`
|
|
287
|
-
* - Single-line deletion: `LINE:HASH|old → [deleted]`
|
|
288
|
-
* - Multi-line changes: full output from generateDiffString()
|
|
289
|
-
*/
|
|
290
|
-
export function generateCompactOrFullDiff(
|
|
291
|
-
oldContent: string,
|
|
292
|
-
newContent: string,
|
|
293
|
-
contextLines = 4,
|
|
294
|
-
): { diff: string; firstChangedLine: number | undefined } {
|
|
295
|
-
if (oldContent === newContent) return { diff: "", firstChangedLine: undefined };
|
|
296
|
-
|
|
297
|
-
const oldLines = oldContent.split("\n");
|
|
298
|
-
const newLines = newContent.split("\n");
|
|
299
|
-
|
|
300
|
-
// Case 1: Same line count, exactly one changed line → compact replacement.
|
|
301
|
-
if (oldLines.length === newLines.length) {
|
|
302
|
-
let changedIndex = -1;
|
|
303
|
-
let changeCount = 0;
|
|
304
|
-
|
|
305
|
-
for (let i = 0; i < oldLines.length; i++) {
|
|
306
|
-
if (oldLines[i] !== newLines[i]) {
|
|
307
|
-
changedIndex = i;
|
|
308
|
-
changeCount++;
|
|
309
|
-
if (changeCount > 1) break;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (changeCount === 1 && changedIndex >= 0) {
|
|
314
|
-
const lineNum = changedIndex + 1;
|
|
315
|
-
const oldLine = oldLines[changedIndex] ?? "";
|
|
316
|
-
const newLine = newLines[changedIndex] ?? "";
|
|
317
|
-
const oldHash = computeLineHash(oldLine);
|
|
318
|
-
const newHash = computeLineHash(newLine);
|
|
319
|
-
return {
|
|
320
|
-
diff: `${lineNum}:${oldHash}|${oldLine} → ${lineNum}:${newHash}|${newLine}`,
|
|
321
|
-
firstChangedLine: lineNum,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Case 2: Exactly one line deleted.
|
|
327
|
-
// old has one more line than new, and removing a single line makes them equal.
|
|
328
|
-
if (oldLines.length === newLines.length + 1) {
|
|
329
|
-
let deletedIndex = -1;
|
|
330
|
-
let j = 0;
|
|
331
|
-
let failed = false;
|
|
332
|
-
|
|
333
|
-
for (let i = 0; i < oldLines.length; i++) {
|
|
334
|
-
if (j < newLines.length && oldLines[i] === newLines[j]) {
|
|
335
|
-
j++;
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (deletedIndex === -1) {
|
|
339
|
-
deletedIndex = i;
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
failed = true;
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (!failed && deletedIndex !== -1 && j === newLines.length) {
|
|
347
|
-
const lineNum = deletedIndex + 1;
|
|
348
|
-
const oldLine = oldLines[deletedIndex] ?? "";
|
|
349
|
-
const oldHash = computeLineHash(oldLine);
|
|
350
|
-
return {
|
|
351
|
-
diff: `${lineNum}:${oldHash}|${oldLine} → [deleted]`,
|
|
352
|
-
firstChangedLine: lineNum,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Fall back to the full (existing) diff format.
|
|
358
|
-
return generateDiffString(oldContent, newContent, contextLines);
|
|
359
|
-
}
|
package/src/edit-output.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { countEditTypes, parseDiffStats } from "./edit-render-helpers.js";
|
|
2
|
-
import { buildReadSeekEditResult, type SemanticSummary } from "./readseek-value.js";
|
|
3
|
-
|
|
4
|
-
import type { DiffData } from "./diff-data.js";
|
|
5
|
-
export interface BuildEditOutputInput {
|
|
6
|
-
path: string;
|
|
7
|
-
displayPath: string;
|
|
8
|
-
diff: string;
|
|
9
|
-
patch?: string;
|
|
10
|
-
diffData?: DiffData;
|
|
11
|
-
firstChangedLine: number | undefined;
|
|
12
|
-
warnings: string[];
|
|
13
|
-
noopEdits: unknown[];
|
|
14
|
-
semanticSummary?: SemanticSummary;
|
|
15
|
-
edits?: unknown[];
|
|
16
|
-
}
|
|
17
|
-
export interface EditOutputResult {
|
|
18
|
-
text: string;
|
|
19
|
-
patch: string;
|
|
20
|
-
readSeekValue: ReturnType<typeof buildReadSeekEditResult>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const EDIT_OPERATION_NAMES = ["set_line", "replace_lines", "insert_after", "replace"] as const;
|
|
24
|
-
|
|
25
|
-
type EditOperationName = (typeof EDIT_OPERATION_NAMES)[number];
|
|
26
|
-
|
|
27
|
-
type EditOperationWithNewText = {
|
|
28
|
-
new_text: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function hasNewText(value: unknown): value is EditOperationWithNewText {
|
|
32
|
-
return typeof value === "object" && value !== null && typeof (value as { new_text?: unknown }).new_text === "string";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function getVisibleDiffStats(diff: string): { added: number; removed: number } {
|
|
36
|
-
const stats = parseDiffStats(diff);
|
|
37
|
-
if (stats.added > 0 || stats.removed > 0) return stats;
|
|
38
|
-
if (!diff.includes("→")) return stats;
|
|
39
|
-
if (diff.includes("→ [deleted]")) return { added: 0, removed: 1 };
|
|
40
|
-
return { added: 1, removed: 1 };
|
|
41
|
-
}
|
|
42
|
-
function buildVisibleSummary(displayPath: string, diff: string, edits: unknown[] | undefined): string {
|
|
43
|
-
let stats = getVisibleDiffStats(diff);
|
|
44
|
-
const counts = countEditTypes(edits);
|
|
45
|
-
const editCount = counts.total || 1;
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
counts.total > 0 &&
|
|
49
|
-
counts.insert_after === counts.total &&
|
|
50
|
-
stats.removed > 0 &&
|
|
51
|
-
stats.added === stats.removed + counts.insert_after
|
|
52
|
-
) {
|
|
53
|
-
stats = { added: counts.insert_after, removed: 0 };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const changeWord = editCount === 1 ? "change" : "changes";
|
|
57
|
-
const changedLineCount = Math.max(stats.added, stats.removed);
|
|
58
|
-
const lineWord = changedLineCount === 1 ? "line" : "lines";
|
|
59
|
-
return `Edited ${displayPath} (${editCount} ${changeWord}, +${stats.added} -${stats.removed} ${lineWord})`;
|
|
60
|
-
}
|
|
61
|
-
function extractNewTextValues(edits: unknown[] | undefined): string[] {
|
|
62
|
-
const values: string[] = [];
|
|
63
|
-
for (const edit of edits ?? []) {
|
|
64
|
-
if (!edit || typeof edit !== "object") continue;
|
|
65
|
-
|
|
66
|
-
const operations = edit as Partial<Record<EditOperationName, unknown>>;
|
|
67
|
-
for (const operationName of EDIT_OPERATION_NAMES) {
|
|
68
|
-
const operation = operations[operationName];
|
|
69
|
-
if (hasNewText(operation)) values.push(operation.new_text);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return values;
|
|
73
|
-
}
|
|
74
|
-
function formatWhitespaceOnlyWarning(semanticSummary: SemanticSummary | undefined, edits: unknown[] | undefined): string | undefined {
|
|
75
|
-
if (semanticSummary?.classification !== "whitespace-only") return undefined;
|
|
76
|
-
if (!extractNewTextValues(edits).some((text) => /\S/.test(text))) return undefined;
|
|
77
|
-
return "⚠ Edit classified as whitespace-only — if you intended a behavior change, re-read to verify.";
|
|
78
|
-
}
|
|
79
|
-
function formatReplaceHint(edits: unknown[] | undefined, noopEdits: unknown[]): string | undefined {
|
|
80
|
-
if ((noopEdits ?? []).length > 0) return undefined;
|
|
81
|
-
const counts = countEditTypes(edits);
|
|
82
|
-
if (counts.replace === 0) return undefined;
|
|
83
|
-
if (counts.replace !== counts.total) return undefined;
|
|
84
|
-
return "[info: this edit used replace (unverified). For safer future edits, prefer set_line/replace_lines with an anchor from readSeek_read/readSeek_grep/readSeek_search.]";
|
|
85
|
-
}
|
|
86
|
-
export function buildEditOutput(input: BuildEditOutputInput): EditOutputResult {
|
|
87
|
-
const summary = `Updated ${input.displayPath}`;
|
|
88
|
-
const visibleSummary = buildVisibleSummary(input.displayPath, input.diff, input.edits);
|
|
89
|
-
const semanticWarning = formatWhitespaceOnlyWarning(input.semanticSummary, input.edits);
|
|
90
|
-
const warningText = input.warnings.length ? `\n\nWarnings:\n${input.warnings.join("\n")}` : "";
|
|
91
|
-
const replaceHint = formatReplaceHint(input.edits, input.noopEdits);
|
|
92
|
-
let text = visibleSummary;
|
|
93
|
-
if (semanticWarning) text += `\n${semanticWarning}`;
|
|
94
|
-
text += warningText;
|
|
95
|
-
if (replaceHint) text += `\n${replaceHint}`;
|
|
96
|
-
return {
|
|
97
|
-
text,
|
|
98
|
-
patch: input.patch ?? "",
|
|
99
|
-
readSeekValue: buildReadSeekEditResult({
|
|
100
|
-
path: input.path,
|
|
101
|
-
summary,
|
|
102
|
-
diff: input.diff,
|
|
103
|
-
...(input.diffData ? { diffData: input.diffData } : {}),
|
|
104
|
-
firstChangedLine: input.firstChangedLine,
|
|
105
|
-
warnings: input.warnings,
|
|
106
|
-
noopEdits: input.noopEdits,
|
|
107
|
-
...(input.semanticSummary ? { semanticSummary: input.semanticSummary } : {}),
|
|
108
|
-
}),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
const VARIANT_KEYS = ["set_line", "replace_lines", "insert_after", "replace"] as const;
|
|
2
|
-
|
|
3
|
-
export type EditTypeCounts = {
|
|
4
|
-
set_line: number;
|
|
5
|
-
replace_lines: number;
|
|
6
|
-
insert_after: number;
|
|
7
|
-
replace: number;
|
|
8
|
-
total: number;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function countEditTypes(edits: unknown[] | undefined): EditTypeCounts {
|
|
12
|
-
const counts: EditTypeCounts = {
|
|
13
|
-
set_line: 0,
|
|
14
|
-
replace_lines: 0,
|
|
15
|
-
insert_after: 0,
|
|
16
|
-
replace: 0,
|
|
17
|
-
total: 0,
|
|
18
|
-
};
|
|
19
|
-
if (!edits) return counts;
|
|
20
|
-
for (const edit of edits) {
|
|
21
|
-
counts.total++;
|
|
22
|
-
if (edit && typeof edit === "object") {
|
|
23
|
-
for (const key of VARIANT_KEYS) {
|
|
24
|
-
if (key in edit) {
|
|
25
|
-
counts[key]++;
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return counts;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface DiffStats {
|
|
35
|
-
added: number;
|
|
36
|
-
removed: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function parseDiffStats(diff: string | undefined): DiffStats {
|
|
40
|
-
if (!diff) return { added: 0, removed: 0 };
|
|
41
|
-
let added = 0;
|
|
42
|
-
let removed = 0;
|
|
43
|
-
for (const line of diff.split("\n")) {
|
|
44
|
-
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
45
|
-
if (line.startsWith("+")) added++;
|
|
46
|
-
else if (line.startsWith("-")) removed++;
|
|
47
|
-
}
|
|
48
|
-
return { added, removed };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface EditCallTextResult {
|
|
52
|
-
path: string | null;
|
|
53
|
-
suffix: string | undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function formatEditCallText(
|
|
57
|
-
args: Record<string, unknown> | undefined,
|
|
58
|
-
argsComplete: boolean,
|
|
59
|
-
): EditCallTextResult {
|
|
60
|
-
const rawPath = typeof args?.path === "string" ? args.path : null;
|
|
61
|
-
|
|
62
|
-
if (!argsComplete) {
|
|
63
|
-
return { path: rawPath, suffix: undefined };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const edits = args?.edits;
|
|
67
|
-
if (!Array.isArray(edits) || edits.length === 0) {
|
|
68
|
-
return { path: rawPath, suffix: undefined };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const counts = countEditTypes(edits);
|
|
72
|
-
const parts: string[] = [];
|
|
73
|
-
for (const key of VARIANT_KEYS) {
|
|
74
|
-
if (counts[key] > 0) {
|
|
75
|
-
parts.push(`${counts[key]} ${key}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const word = counts.total === 1 ? "edit" : "edits";
|
|
79
|
-
const suffix = `${counts.total} ${word} (${parts.join(", ")})`;
|
|
80
|
-
return { path: rawPath, suffix };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface EditResultTextInput {
|
|
84
|
-
isError: boolean;
|
|
85
|
-
diff: string;
|
|
86
|
-
warnings: string[];
|
|
87
|
-
noopEdits: unknown[];
|
|
88
|
-
errorText: string;
|
|
89
|
-
semanticClassification?: "no-op" | "whitespace-only" | "semantic" | "mixed";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface EditResultTextOutput {
|
|
93
|
-
diffStats: string | undefined;
|
|
94
|
-
noOp: boolean;
|
|
95
|
-
warningsBadge: string | undefined;
|
|
96
|
-
errorText: string | undefined;
|
|
97
|
-
semanticBadge: string | undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function formatEditResultText(input: EditResultTextInput): EditResultTextOutput {
|
|
101
|
-
const { isError, diff, warnings, noopEdits, errorText } = input;
|
|
102
|
-
|
|
103
|
-
// No-op detection: error + noopEdits present OR error text contains "No changes made"
|
|
104
|
-
const isNoOp = isError && (
|
|
105
|
-
(Array.isArray(noopEdits) && noopEdits.length > 0) ||
|
|
106
|
-
errorText.includes("No changes made")
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Diff stats
|
|
110
|
-
const stats = parseDiffStats(diff);
|
|
111
|
-
const hasDiffStats = stats.added > 0 || stats.removed > 0;
|
|
112
|
-
const diffStats = hasDiffStats ? `+${stats.added} / -${stats.removed}` : undefined;
|
|
113
|
-
|
|
114
|
-
// Warnings badge
|
|
115
|
-
let warningsBadge: string | undefined;
|
|
116
|
-
if (warnings.length === 1) {
|
|
117
|
-
warningsBadge = "\u26a0 1 warning";
|
|
118
|
-
} else if (warnings.length > 1) {
|
|
119
|
-
warningsBadge = `\u26a0 ${warnings.length} warnings`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Error text (only for errors)
|
|
123
|
-
const showErrorText = isError ? errorText : undefined;
|
|
124
|
-
|
|
125
|
-
// Semantic classification badge (success only)
|
|
126
|
-
let semanticBadge: string | undefined;
|
|
127
|
-
if (!isError && !isNoOp && input.semanticClassification) {
|
|
128
|
-
switch (input.semanticClassification) {
|
|
129
|
-
case "whitespace-only": semanticBadge = "ws-only"; break;
|
|
130
|
-
case "semantic": semanticBadge = "\u2713 semantic"; break;
|
|
131
|
-
case "mixed": semanticBadge = "mixed"; break;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return {
|
|
135
|
-
diffStats,
|
|
136
|
-
noOp: isNoOp,
|
|
137
|
-
warningsBadge,
|
|
138
|
-
errorText: showErrorText,
|
|
139
|
-
semanticBadge,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { readSeekCheck, type ReadSeekCheckOutput, type ReadSeekDiagnostic } from "./readseek-client.js";
|
|
2
|
-
|
|
3
|
-
export interface ValidateInput {
|
|
4
|
-
filePath: string;
|
|
5
|
-
before: string | undefined;
|
|
6
|
-
after: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ValidateResult {
|
|
10
|
-
errorLines: string[];
|
|
11
|
-
newErrorCount: number;
|
|
12
|
-
newMissingCount: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function dedupeSortLines(diagnostics: ReadSeekDiagnostic[]): string[] {
|
|
16
|
-
const seen = new Set<string>();
|
|
17
|
-
const out: Array<{ key: string; start: number }> = [];
|
|
18
|
-
for (const diagnostic of diagnostics) {
|
|
19
|
-
const key =
|
|
20
|
-
diagnostic.start_line === diagnostic.end_line
|
|
21
|
-
? String(diagnostic.start_line)
|
|
22
|
-
: `${diagnostic.start_line}-${diagnostic.end_line}`;
|
|
23
|
-
if (!seen.has(key)) {
|
|
24
|
-
seen.add(key);
|
|
25
|
-
out.push({ key, start: diagnostic.start_line });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
out.sort((a, b) => a.start - b.start);
|
|
29
|
-
return out.map((o) => o.key);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const EMPTY: ReadSeekCheckOutput = { errorCount: 0, missingCount: 0, diagnostics: [] };
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Compare parse diagnostics between `before` and `after` and report any newly
|
|
36
|
-
* introduced syntax errors. Uses readseek's native `check`, so every language
|
|
37
|
-
* with a tree-sitter parser is validated.
|
|
38
|
-
*
|
|
39
|
-
* Returns `null` when nothing new appears or when readseek cannot parse the
|
|
40
|
-
* file, so a validator failure never blocks an edit.
|
|
41
|
-
*/
|
|
42
|
-
export async function validateSyntaxRegression(
|
|
43
|
-
input: ValidateInput,
|
|
44
|
-
options: { signal?: AbortSignal } = {},
|
|
45
|
-
): Promise<ValidateResult | null> {
|
|
46
|
-
let before: ReadSeekCheckOutput;
|
|
47
|
-
let after: ReadSeekCheckOutput;
|
|
48
|
-
try {
|
|
49
|
-
before = input.before === undefined ? EMPTY : await readSeekCheck(input.filePath, input.before, { signal: options.signal });
|
|
50
|
-
after = await readSeekCheck(input.filePath, input.after, { signal: options.signal });
|
|
51
|
-
} catch {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const newErrorCount = Math.max(0, after.errorCount - before.errorCount);
|
|
56
|
-
const newMissingCount = Math.max(0, after.missingCount - before.missingCount);
|
|
57
|
-
|
|
58
|
-
if (newErrorCount === 0 && newMissingCount === 0) return null;
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
errorLines: dedupeSortLines(after.diagnostics),
|
|
62
|
-
newErrorCount,
|
|
63
|
-
newMissingCount,
|
|
64
|
-
};
|
|
65
|
-
}
|