pi-hashline-edit-pro 2.1.2 → 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 +1 -1
- package/package.json +1 -1
- package/src/hashline/apply.ts +9 -1
- package/src/hashline/resolve.ts +75 -52
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.
|
|
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
|
|
package/package.json
CHANGED
package/src/hashline/apply.ts
CHANGED
|
@@ -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 = {
|
|
@@ -186,7 +187,14 @@ export function applyEdit(
|
|
|
186
187
|
...prefixFixed,
|
|
187
188
|
content_lines: [...prefixFixed.content_lines],
|
|
188
189
|
};
|
|
189
|
-
const
|
|
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(
|
|
190
198
|
(a, b) => b.replacementLineIndex - a.replacementLineIndex,
|
|
191
199
|
);
|
|
192
200
|
for (const dup of dupsByIndex) {
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { abortIf, rejectUnknownFields,
|
|
1
|
+
import { abortIf, rejectUnknownFields, firstNonEmptyIndex, lastNonEmptyIndex, clipLine } from "../utils";
|
|
2
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";
|
|
@@ -283,38 +283,78 @@ export function swapReversedRanges(
|
|
|
283
283
|
return { ...edit, hash_bounds: [endRef, startRef] as [Anchor, Anchor] };
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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;
|
|
291
300
|
}
|
|
292
301
|
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return null;
|
|
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 });
|
|
306
314
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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 });
|
|
313
335
|
}
|
|
314
|
-
return
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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;
|
|
318
358
|
}
|
|
319
359
|
|
|
320
360
|
function canonCounts(lines: string[]): Map<string, number> {
|
|
@@ -348,16 +388,6 @@ export function findNewEdge(
|
|
|
348
388
|
return undefined;
|
|
349
389
|
}
|
|
350
390
|
|
|
351
|
-
function newEdgeLines(
|
|
352
|
-
contentLines: string[],
|
|
353
|
-
rangeLines: string[],
|
|
354
|
-
): { firstNew: { index: number; line: string } | undefined; lastNew: { index: number; line: string } | undefined } {
|
|
355
|
-
return {
|
|
356
|
-
firstNew: findNewEdge(contentLines, rangeLines, false),
|
|
357
|
-
lastNew: findNewEdge(contentLines, rangeLines, true),
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
391
|
export function valEdit(
|
|
362
392
|
edit: HEdit,
|
|
363
393
|
fileLines: string[],
|
|
@@ -405,21 +435,14 @@ export function valEdit(
|
|
|
405
435
|
);
|
|
406
436
|
}
|
|
407
437
|
const endLine = endResolved.line;
|
|
408
|
-
const nextLine = fileLines[endLine];
|
|
409
|
-
const replacementLastLine = lastNonEmpty(edit.content_lines);
|
|
410
|
-
const trailing = checkBoundaryDup(nextLine, edgeRef(replacementLastLine, lastNonEmptyIndex(edit.content_lines)), "trailing");
|
|
411
|
-
if (trailing) boundaryDups.push(trailing);
|
|
412
|
-
const prevLine = fileLines[startResolved.line - 2];
|
|
413
|
-
const replacementFirstLine = firstNonEmpty(edit.content_lines);
|
|
414
|
-
const leading = checkBoundaryDup(prevLine, edgeRef(replacementFirstLine, firstNonEmptyIndex(edit.content_lines)), "leading");
|
|
415
|
-
if (leading) boundaryDups.push(leading);
|
|
416
438
|
const rangeLines = fileLines.slice(startResolved.line - 1, endLine);
|
|
417
|
-
const { firstNew, lastNew } = newEdgeLines(edit.content_lines, rangeLines);
|
|
418
439
|
const fileCounts = canonCounts(fileLines);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
);
|
|
423
446
|
|
|
424
447
|
return {
|
|
425
448
|
resolved: {
|