pi-hashline-edit-pro 2.1.1 → 2.2.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/README.md +2 -2
- package/package.json +1 -1
- package/prompts/replace-guidelines.md +1 -1
- package/prompts/undo-last-replace-guidelines.md +1 -1
- package/src/hashline/apply.ts +19 -18
- package/src/hashline/hash.ts +1 -1
- package/src/hashline/index.ts +3 -1
- package/src/hashline/resolve.ts +123 -39
- package/src/replace-diff.ts +9 -1
- package/src/replace-response.ts +2 -3
- package/src/replace-undo.ts +4 -3
- package/src/replace.ts +2 -3
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ One edit per call, with `hash_bounds` and `new_content` at the top level:
|
|
|
91
91
|
Notes:
|
|
92
92
|
|
|
93
93
|
- The request is checked before any file I/O, so a bad request never touches the file.
|
|
94
|
-
- Common copy-paste slips are fixed automatically and reported: a leftover `HASH│` prefix in `new_content` or `hash_bounds`, diff-preview rows pasted into the replacement, a reversed range, or a boundary line pasted twice. `file_path` works as an alias for `path` in all three tools.
|
|
94
|
+
- Common copy-paste slips are fixed automatically and reported: a leftover `HASH│` prefix in `new_content` or `hash_bounds`, diff-preview rows pasted into the replacement, a reversed range, or a boundary line pasted twice. New lines that duplicate unique lines adjacent to the range are stripped automatically — consecutive duplicates are stripped as a run, so re-including a whole unchanged block next to the range never duplicates it. `file_path` works as an alias for `path` in all three tools.
|
|
95
95
|
- An edit that produces identical content reports `No changes made` and leaves the anchors alone.
|
|
96
96
|
- After a successful edit you get the post-edit diff with fresh anchors, so you can keep editing without re-reading.
|
|
97
97
|
|
|
@@ -110,7 +110,7 @@ Notes:
|
|
|
110
110
|
Enabled by default. After a successful `write` that changes the file, the extension reads the file and appends an `--- Auto-read (hashline anchors) ---` block to the result, so you get fresh `HASH│content` anchors without a separate `read` call.
|
|
111
111
|
|
|
112
112
|
- After `replace` and `undo_last_replace`, the result shows the post-edit diff. The `+HASH│` and ` HASH│` rows carry the current hashes, so follow-up edits can anchor on the diff directly. Call `read` when you want the full file's anchors.
|
|
113
|
-
- After `
|
|
113
|
+
- After `replace` and `undo_last_replace`, the result shows the post-edit diff. The `+HASH│` and ` HASH│` rows carry the current hashes, so follow-up edits can anchor on the diff directly. The `-HASH│` rows show removed lines with their old hashes, so you can see exactly which anchors were deleted (those hashes are stale after the edit). Call `read` when you want the full file's anchors.
|
|
114
114
|
- Auto-read keeps a 50KB display budget. Lines over 50KB are skipped with a marker instead of their content (use `read` for lines up to 200KB).
|
|
115
115
|
- Toggle at runtime with `/toggle-auto-read`; the setting persists across sessions.
|
|
116
116
|
|
package/package.json
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
- `replace`: to replace a single line, repeat its hash in both positions of hash_bounds: ["<HASH>", "<HASH>"] — never extend the range to neighboring lines for a one-line edit.
|
|
4
4
|
- `replace`: when copying a line from read output, remove its HASH│ prefix and keep the leading whitespace exactly as shown.
|
|
5
5
|
- `replace`: to add a blank line, end new_content with an explicit empty line (e.g. ending with \n\n).
|
|
6
|
-
- `replace`: when auto-read shows the post-edit diff, its rows are the fresh anchors for the new file — `+HASH│` and ` HASH│` rows carry current hashes and unchanged lines keep their previous hashes, so you can anchor follow-up edits on the diff without re-reading.
|
|
6
|
+
- `replace`: when auto-read shows the post-edit diff, its rows are the fresh anchors for the new file — `+HASH│` and ` HASH│` rows carry current hashes and unchanged lines keep their previous hashes, so you can anchor follow-up edits on the diff without re-reading. `-HASH│` rows show removed lines with their old hashes; those hashes are stale after the edit.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
- `undo_last_replace`: reverts only the most recent replace on the file — any write to the file clears the undo history, so call it immediately after a bad replace.
|
|
2
|
-
- `undo_last_replace`: when auto-read shows the post-edit diff, its `+HASH│` and ` HASH│` rows are the fresh anchors for the restored file, so follow-up edits can anchor on the diff without re-reading.
|
|
2
|
+
- `undo_last_replace`: when auto-read shows the post-edit diff, its `+HASH│` and ` HASH│` rows are the fresh anchors for the restored file, so follow-up edits can anchor on the diff without re-reading. `-HASH│` rows show removed lines with their old hashes; those hashes are stale after the undo.
|
package/src/hashline/apply.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { abortIf, splitLines
|
|
1
|
+
import { abortIf, splitLines } from "../utils";
|
|
2
2
|
import { _lineHashesPure, HASH_SEP } from "./hash";
|
|
3
3
|
import {
|
|
4
4
|
valEdit,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type NEdit,
|
|
12
12
|
type HEdit,
|
|
13
13
|
type AutoFix,
|
|
14
|
+
type BDup,
|
|
14
15
|
} from "./resolve";
|
|
15
16
|
|
|
16
17
|
type LIdx = {
|
|
@@ -49,7 +50,6 @@ type NoopSpan = {
|
|
|
49
50
|
loc: string;
|
|
50
51
|
currentContent: string;
|
|
51
52
|
};
|
|
52
|
-
|
|
53
53
|
function assertNotEmpty(originalContent: string, result: string): void {
|
|
54
54
|
if (originalContent.length > 0 && result.length === 0) {
|
|
55
55
|
throw new Error(
|
|
@@ -164,7 +164,7 @@ export function applyEdit(
|
|
|
164
164
|
warnings,
|
|
165
165
|
);
|
|
166
166
|
|
|
167
|
-
const { resolved: initialResolved, mismatches,
|
|
167
|
+
const { resolved: initialResolved, mismatches, boundaryDups } = valEdit(
|
|
168
168
|
prefixFixed,
|
|
169
169
|
lineIndex.fileLines,
|
|
170
170
|
fileHashes,
|
|
@@ -181,26 +181,27 @@ export function applyEdit(
|
|
|
181
181
|
|
|
182
182
|
let resolved = initialResolved;
|
|
183
183
|
let autoFixes: AutoFix[] | undefined;
|
|
184
|
-
if (
|
|
184
|
+
if (boundaryDups.length > 0) {
|
|
185
185
|
autoFixes = [];
|
|
186
186
|
const correctedEdit: HEdit = {
|
|
187
187
|
...prefixFixed,
|
|
188
188
|
content_lines: [...prefixFixed.content_lines],
|
|
189
189
|
};
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
190
|
+
const seen = new Set<number>();
|
|
191
|
+
const uniqueDups: BDup[] = [];
|
|
192
|
+
for (const dup of boundaryDups) {
|
|
193
|
+
if (seen.has(dup.replacementLineIndex)) continue;
|
|
194
|
+
seen.add(dup.replacementLineIndex);
|
|
195
|
+
uniqueDups.push(dup);
|
|
196
|
+
}
|
|
197
|
+
const dupsByIndex = uniqueDups.sort(
|
|
198
|
+
(a, b) => b.replacementLineIndex - a.replacementLineIndex,
|
|
199
|
+
);
|
|
200
|
+
for (const dup of dupsByIndex) {
|
|
201
|
+
const idx = dup.replacementLineIndex;
|
|
202
|
+
if (idx < 0 || idx >= correctedEdit.content_lines.length) continue;
|
|
203
|
+
const removed = correctedEdit.content_lines.splice(idx, 1)[0];
|
|
204
|
+
autoFixes.push({ kind: dup.kind, removedLine: removed, removedLineIndex: idx });
|
|
204
205
|
}
|
|
205
206
|
const correctedResult = valEdit(
|
|
206
207
|
correctedEdit,
|
package/src/hashline/hash.ts
CHANGED
|
@@ -47,7 +47,7 @@ export const HL_PREFIX_MINUS_RE = new RegExp(
|
|
|
47
47
|
|
|
48
48
|
export const HL_BARE_PREFIX_RE = new RegExp(`^\\s*(${HASH_CLASS})│`);
|
|
49
49
|
|
|
50
|
-
function canon(line: string): string {
|
|
50
|
+
export function canon(line: string): string {
|
|
51
51
|
return line.replace(/\r/g, "").trimEnd();
|
|
52
52
|
}
|
|
53
53
|
|
package/src/hashline/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export {
|
|
|
12
12
|
lineHashes,
|
|
13
13
|
_lineHashesPure,
|
|
14
14
|
initHasher,
|
|
15
|
+
canon,
|
|
15
16
|
} from "./hash";
|
|
16
17
|
|
|
17
18
|
export {
|
|
@@ -26,7 +27,7 @@ export {
|
|
|
26
27
|
type RHEdit,
|
|
27
28
|
type HTEdit,
|
|
28
29
|
type NEdit,
|
|
29
|
-
type
|
|
30
|
+
type BDup,
|
|
30
31
|
type AutoFix,
|
|
31
32
|
resEdit,
|
|
32
33
|
valEdit,
|
|
@@ -34,6 +35,7 @@ export {
|
|
|
34
35
|
stripDiffPrefixes,
|
|
35
36
|
swapReversedRanges,
|
|
36
37
|
fmtMismatch,
|
|
38
|
+
findNewEdge,
|
|
37
39
|
} from "./resolve";
|
|
38
40
|
|
|
39
41
|
export {
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { abortIf, rejectUnknownFields,
|
|
2
|
-
import { HASH_CLASS, HL_BARE_PREFIX_RE, HL_PREFIX_PLUS_RE, HL_PREFIX_MINUS_RE } from "./hash";
|
|
1
|
+
import { abortIf, rejectUnknownFields, firstNonEmptyIndex, lastNonEmptyIndex, clipLine } from "../utils";
|
|
2
|
+
import { HASH_CLASS, HL_BARE_PREFIX_RE, HL_PREFIX_PLUS_RE, HL_PREFIX_MINUS_RE, canon } from "./hash";
|
|
3
3
|
import { parseHashRef, parseText, type Anchor } from "./parse";
|
|
4
4
|
import { NEW_CONTENT_NOT_STRING_MSG } from "../constants";
|
|
5
5
|
|
|
@@ -22,16 +22,15 @@ interface HMismatch {
|
|
|
22
22
|
context?: RAnchor;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export interface
|
|
26
|
-
kind: "trailing" | "leading";
|
|
27
|
-
|
|
28
|
-
survivingLineIndex: number;
|
|
29
|
-
replacementLineContent: string;
|
|
25
|
+
export interface BDup {
|
|
26
|
+
kind: "trailing" | "leading" | "first-new-after" | "last-new-before";
|
|
27
|
+
replacementLineIndex: number;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
export interface AutoFix {
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
kind: "trailing" | "leading" | "first-new-after" | "last-new-before";
|
|
32
|
+
removedLine: string;
|
|
33
|
+
removedLineIndex: number;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
export interface NEdit {
|
|
@@ -284,24 +283,109 @@ export function swapReversedRanges(
|
|
|
284
283
|
return { ...edit, hash_bounds: [endRef, startRef] as [Anchor, Anchor] };
|
|
285
284
|
}
|
|
286
285
|
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
286
|
+
function trailingDups(
|
|
287
|
+
contentLines: string[],
|
|
288
|
+
fileLines: string[],
|
|
289
|
+
endLine: number,
|
|
290
|
+
): BDup[] {
|
|
291
|
+
const start = lastNonEmptyIndex(contentLines);
|
|
292
|
+
if (start < 0) return [];
|
|
293
|
+
const dups: BDup[] = [];
|
|
294
|
+
const maxK = Math.min(start + 1, fileLines.length - endLine);
|
|
295
|
+
for (let k = 0; k < maxK; k++) {
|
|
296
|
+
if (contentLines[start - k] !== fileLines[endLine + k]) break;
|
|
297
|
+
dups.push({ kind: "trailing", replacementLineIndex: start - k });
|
|
298
|
+
}
|
|
299
|
+
return dups;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function leadingDups(
|
|
303
|
+
contentLines: string[],
|
|
304
|
+
fileLines: string[],
|
|
305
|
+
startLine: number,
|
|
306
|
+
): BDup[] {
|
|
307
|
+
const start = firstNonEmptyIndex(contentLines);
|
|
308
|
+
if (start < 0) return [];
|
|
309
|
+
const dups: BDup[] = [];
|
|
310
|
+
const maxK = Math.min(contentLines.length - start, startLine - 1);
|
|
311
|
+
for (let k = 0; k < maxK; k++) {
|
|
312
|
+
if (contentLines[start + k] !== fileLines[startLine - 2 - k]) break;
|
|
313
|
+
dups.push({ kind: "leading", replacementLineIndex: start + k });
|
|
314
|
+
}
|
|
315
|
+
return dups;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function firstNewAfterDups(
|
|
319
|
+
contentLines: string[],
|
|
320
|
+
rangeLines: string[],
|
|
321
|
+
fileLines: string[],
|
|
322
|
+
endLine: number,
|
|
323
|
+
fileCounts: Map<string, number>,
|
|
324
|
+
): BDup[] {
|
|
325
|
+
const firstNew = findNewEdge(contentLines, rangeLines, false);
|
|
326
|
+
if (!firstNew) return [];
|
|
327
|
+
const dups: BDup[] = [];
|
|
328
|
+
const maxK = Math.min(contentLines.length - firstNew.index, fileLines.length - endLine);
|
|
329
|
+
for (let k = 0; k < maxK; k++) {
|
|
330
|
+
const newLine = contentLines[firstNew.index + k]!;
|
|
331
|
+
const fileLine = fileLines[endLine + k]!;
|
|
332
|
+
if (canon(newLine) !== canon(fileLine)) break;
|
|
333
|
+
if ((fileCounts.get(canon(fileLine)) ?? 0) !== 1) break;
|
|
334
|
+
dups.push({ kind: "first-new-after", replacementLineIndex: firstNew.index + k });
|
|
335
|
+
}
|
|
336
|
+
return dups;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function lastNewBeforeDups(
|
|
340
|
+
contentLines: string[],
|
|
341
|
+
rangeLines: string[],
|
|
342
|
+
fileLines: string[],
|
|
343
|
+
startLine: number,
|
|
344
|
+
fileCounts: Map<string, number>,
|
|
345
|
+
): BDup[] {
|
|
346
|
+
const lastNew = findNewEdge(contentLines, rangeLines, true);
|
|
347
|
+
if (!lastNew) return [];
|
|
348
|
+
const dups: BDup[] = [];
|
|
349
|
+
const maxK = Math.min(lastNew.index + 1, startLine - 1);
|
|
350
|
+
for (let k = 0; k < maxK; k++) {
|
|
351
|
+
const newLine = contentLines[lastNew.index - k]!;
|
|
352
|
+
const fileLine = fileLines[startLine - 2 - k]!;
|
|
353
|
+
if (canon(newLine) !== canon(fileLine)) break;
|
|
354
|
+
if ((fileCounts.get(canon(fileLine)) ?? 0) !== 1) break;
|
|
355
|
+
dups.push({ kind: "last-new-before", replacementLineIndex: lastNew.index - k });
|
|
356
|
+
}
|
|
357
|
+
return dups;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function canonCounts(lines: string[]): Map<string, number> {
|
|
361
|
+
const counts = new Map<string, number>();
|
|
362
|
+
for (const line of lines) {
|
|
363
|
+
const key = canon(line);
|
|
364
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
365
|
+
}
|
|
366
|
+
return counts;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function findNewEdge(
|
|
370
|
+
contentLines: string[],
|
|
371
|
+
rangeLines: string[],
|
|
372
|
+
fromEnd: boolean,
|
|
373
|
+
): { index: number; line: string } | undefined {
|
|
374
|
+
const multiset = canonCounts(rangeLines);
|
|
375
|
+
const step = fromEnd ? -1 : 1;
|
|
376
|
+
const start = fromEnd ? contentLines.length - 1 : 0;
|
|
377
|
+
for (let i = start; i >= 0 && i < contentLines.length; i += step) {
|
|
378
|
+
const line = contentLines[i]!;
|
|
379
|
+
if (line.length === 0) continue;
|
|
380
|
+
const key = canon(line);
|
|
381
|
+
const count = multiset.get(key) ?? 0;
|
|
382
|
+
if (count > 0) {
|
|
383
|
+
multiset.set(key, count - 1);
|
|
384
|
+
} else {
|
|
385
|
+
return { index: i, line };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return undefined;
|
|
305
389
|
}
|
|
306
390
|
|
|
307
391
|
export function valEdit(
|
|
@@ -310,10 +394,10 @@ export function valEdit(
|
|
|
310
394
|
fileHashes: string[],
|
|
311
395
|
warnings: string[],
|
|
312
396
|
signal: AbortSignal | undefined,
|
|
313
|
-
): { resolved: RHEdit | undefined; mismatches: HMismatch[];
|
|
397
|
+
): { resolved: RHEdit | undefined; mismatches: HMismatch[]; boundaryDups: BDup[] } {
|
|
314
398
|
assertAligned(fileLines, fileHashes, "valEdit");
|
|
315
399
|
const mismatches: HMismatch[] = [];
|
|
316
|
-
const
|
|
400
|
+
const boundaryDups: BDup[] = [];
|
|
317
401
|
|
|
318
402
|
const hashIndex = new Map<string, number[]>();
|
|
319
403
|
for (let i = 0; i < fileHashes.length; i++) {
|
|
@@ -343,7 +427,7 @@ export function valEdit(
|
|
|
343
427
|
const endMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[1]);
|
|
344
428
|
if (endMismatch && endMismatch.kind === "not_found") endMismatch.context = startResolved;
|
|
345
429
|
}
|
|
346
|
-
return { resolved: undefined, mismatches,
|
|
430
|
+
return { resolved: undefined, mismatches, boundaryDups };
|
|
347
431
|
}
|
|
348
432
|
if (startResolved.line > endResolved.line) {
|
|
349
433
|
throw new Error(
|
|
@@ -351,14 +435,14 @@ export function valEdit(
|
|
|
351
435
|
);
|
|
352
436
|
}
|
|
353
437
|
const endLine = endResolved.line;
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
438
|
+
const rangeLines = fileLines.slice(startResolved.line - 1, endLine);
|
|
439
|
+
const fileCounts = canonCounts(fileLines);
|
|
440
|
+
boundaryDups.push(
|
|
441
|
+
...trailingDups(edit.content_lines, fileLines, endLine),
|
|
442
|
+
...leadingDups(edit.content_lines, fileLines, startResolved.line),
|
|
443
|
+
...firstNewAfterDups(edit.content_lines, rangeLines, fileLines, endLine, fileCounts),
|
|
444
|
+
...lastNewBeforeDups(edit.content_lines, rangeLines, fileLines, startResolved.line, fileCounts),
|
|
445
|
+
);
|
|
362
446
|
|
|
363
447
|
return {
|
|
364
448
|
resolved: {
|
|
@@ -366,7 +450,7 @@ export function valEdit(
|
|
|
366
450
|
hash_bounds: [startResolved, endResolved],
|
|
367
451
|
},
|
|
368
452
|
mismatches,
|
|
369
|
-
|
|
453
|
+
boundaryDups,
|
|
370
454
|
};
|
|
371
455
|
}
|
|
372
456
|
|
package/src/replace-diff.ts
CHANGED
|
@@ -56,12 +56,14 @@ export function genDiff(
|
|
|
56
56
|
newContent: string,
|
|
57
57
|
contextLines = 2,
|
|
58
58
|
newContentHashes?: string[],
|
|
59
|
+
oldContentHashes?: string[],
|
|
59
60
|
): { diff: string; firstChangedLine: number | undefined } {
|
|
60
61
|
const effectiveNewHashes = newContentHashes ?? _lineHashesPure(newContent);
|
|
61
62
|
|
|
62
63
|
const parts = Diff.diffLines(oldContent, newContent);
|
|
63
64
|
const output: string[] = [];
|
|
64
65
|
let newLineNum = 1;
|
|
66
|
+
let oldLineNum = 1;
|
|
65
67
|
let lastWasChange = false;
|
|
66
68
|
let firstChangedLine: number | undefined;
|
|
67
69
|
|
|
@@ -79,7 +81,9 @@ export function genDiff(
|
|
|
79
81
|
output.push(fmtDiffLine("+", displayLines[k]!, hash));
|
|
80
82
|
newLineNum++;
|
|
81
83
|
} else {
|
|
82
|
-
|
|
84
|
+
const hash = oldContentHashes?.[oldLineNum - 1];
|
|
85
|
+
output.push(fmtDiffLine("-", displayLines[k]!, hash));
|
|
86
|
+
oldLineNum++;
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
lastWasChange = true;
|
|
@@ -107,19 +111,23 @@ export function genDiff(
|
|
|
107
111
|
if (skipStart > 0) {
|
|
108
112
|
output.push(" ...");
|
|
109
113
|
newLineNum += skipStart;
|
|
114
|
+
oldLineNum += skipStart;
|
|
110
115
|
}
|
|
111
116
|
for (const line of linesToShow) {
|
|
112
117
|
if (isEllipsisMarker(line)) {
|
|
113
118
|
output.push(" ...");
|
|
114
119
|
newLineNum += skipMiddle;
|
|
120
|
+
oldLineNum += skipMiddle;
|
|
115
121
|
continue;
|
|
116
122
|
}
|
|
117
123
|
const hash = effectiveNewHashes[newLineNum - 1];
|
|
118
124
|
output.push(fmtDiffLine(" ", line, hash));
|
|
119
125
|
newLineNum++;
|
|
126
|
+
oldLineNum++;
|
|
120
127
|
}
|
|
121
128
|
} else {
|
|
122
129
|
newLineNum += displayLines.length;
|
|
130
|
+
oldLineNum += displayLines.length;
|
|
123
131
|
}
|
|
124
132
|
lastWasChange = false;
|
|
125
133
|
}
|
package/src/replace-response.ts
CHANGED
|
@@ -123,10 +123,9 @@ export function buildNoop(input: NoopInput): TResult {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
export function buildChanged(input: SuccessInput): TResult {
|
|
126
|
-
const { path, result, warnings, snapshotId, originalNormalized, editMeta, resultHashes } = input;
|
|
127
|
-
|
|
126
|
+
const { path, result, warnings, snapshotId, originalNormalized, originalHashes, editMeta, resultHashes } = input;
|
|
128
127
|
const resultLines = visLines(result);
|
|
129
|
-
const diffResult = genDiff(originalNormalized, result, 1, resultHashes);
|
|
128
|
+
const diffResult = genDiff(originalNormalized, result, 1, resultHashes, originalHashes);
|
|
130
129
|
const addedLines = editMeta.addedLines;
|
|
131
130
|
const removedLines = editMeta.removedLines;
|
|
132
131
|
const warningsBlock = warnBlock(warnings);
|
package/src/replace-undo.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { toLF, stripBOM, genDiff, restoreEndings, type LineEnding } from "./repl
|
|
|
10
10
|
import { cntDiff, splitLines, errCode, isRec, normalizeFilePath } from "./utils";
|
|
11
11
|
import { loadP, loadGuide } from "./prompts";
|
|
12
12
|
import { buildMetrics } from "./replace-response";
|
|
13
|
-
import { changedRange } from "./hashline";
|
|
13
|
+
import { changedRange, lineHashes } from "./hashline";
|
|
14
14
|
export interface UndoEntry {
|
|
15
15
|
content: string;
|
|
16
16
|
bom: string;
|
|
@@ -159,11 +159,12 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
|
|
|
159
159
|
|
|
160
160
|
const { text: currentStripped } = stripBOM(currentRaw);
|
|
161
161
|
const currentNormalized = toLF(currentStripped);
|
|
162
|
-
const
|
|
162
|
+
const currentHashes = await lineHashes(currentNormalized, mutationTargetPath);
|
|
163
|
+
const diffResult = genDiff(undo.content, currentNormalized, 0, undefined, undo.hashes);
|
|
163
164
|
const linesAddedByReplace = cntDiff(diffResult.diff, "+");
|
|
164
165
|
const linesRemovedByReplace = cntDiff(diffResult.diff, "-");
|
|
165
166
|
const restoredRange = changedRange(currentNormalized, undo.content);
|
|
166
|
-
const undoDiff = genDiff(currentNormalized, undo.content, 1, undo.hashes).diff;
|
|
167
|
+
const undoDiff = genDiff(currentNormalized, undo.content, 1, undo.hashes, currentHashes).diff;
|
|
167
168
|
|
|
168
169
|
await writeAtomic(
|
|
169
170
|
mutationTargetPath,
|
package/src/replace.ts
CHANGED
|
@@ -241,19 +241,18 @@ export async function compPreview(
|
|
|
241
241
|
try {
|
|
242
242
|
const normalized = normReq(request);
|
|
243
243
|
assertReq(normalized);
|
|
244
|
-
const { path, originalNormalized, result, resultHashes } = await execPipeline(
|
|
244
|
+
const { path, originalNormalized, result, resultHashes, originalHashes } = await execPipeline(
|
|
245
245
|
normalized,
|
|
246
246
|
cwd,
|
|
247
247
|
{ accessMode: constants.R_OK, noPersist: true },
|
|
248
248
|
);
|
|
249
|
-
|
|
250
249
|
if (originalNormalized === result) {
|
|
251
250
|
return {
|
|
252
251
|
error: `No changes made to ${path}. The edit produced identical content.`,
|
|
253
252
|
};
|
|
254
253
|
}
|
|
255
254
|
|
|
256
|
-
return { diff: genDiff(originalNormalized, result, 4, resultHashes).diff };
|
|
255
|
+
return { diff: genDiff(originalNormalized, result, 4, resultHashes, originalHashes).diff };
|
|
257
256
|
} catch (error: unknown) {
|
|
258
257
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
259
258
|
}
|