pi-hashline-edit-pro 4.5.2 → 4.5.3
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 +5 -3
- package/index.ts +2 -1
- package/package.json +1 -1
- package/prompts/replace-guidelines.md +1 -1
- package/src/batch.ts +29 -7
- package/src/commit.ts +16 -13
- package/src/copy-move.ts +4 -0
- package/src/hashline/apply.ts +7 -5
- package/src/hashline/parse.ts +8 -4
- package/src/hashline/resolve.ts +2 -0
- package/src/insert.ts +3 -1
- package/src/replace.ts +18 -2
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ Edge cases:
|
|
|
186
186
|
| --- | --- |
|
|
187
187
|
| `remove_from` | 4-char anchor marking the FIRST line to remove (inclusive). |
|
|
188
188
|
| `remove_to` | 4-char anchor marking the LAST line to remove (inclusive). |
|
|
189
|
-
| `replacement_lines` | Replacement lines, one element per line. Mirror the removed lines exactly, blank lines included: `[]` deletes the range, `[""]` is a single blank line, `["a", ""]` is a line followed by a blank line. One element is one line: a real line-break character (`\n`, `\r\n`, or `\r`) splits it and sets
|
|
189
|
+
| `replacement_lines` | Replacement lines, one element per line. Mirror the removed lines exactly, blank lines included: `[]` deletes the range, `[""]` is a single blank line, `["a", ""]` is a line followed by a blank line. One element is one line: a real line-break character (`\n`, `\r\n`, or `\r`) splits it and sets the line's ending; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. A trailing line-break sets the ending of the element's last line instead of adding a blank line, so `["b\n"]` sets that line's ending to LF, `["b\r\n"]` to CRLF, and on a file without a final newline it adds it. A lone string is accepted too: it is split on newlines, and stringified array text is unwrapped. |
|
|
190
190
|
|
|
191
191
|
Example: read showed `Hasu│old` and `arvm│old2`; to replace both:
|
|
192
192
|
|
|
@@ -208,7 +208,7 @@ Content containing a NUL byte (`U+0000`) is rejected with `[E_BAD_SHAPE]` before
|
|
|
208
208
|
|
|
209
209
|
Every line in the removed range must match what was last shown to you. The extension records the `anchor│content` rows it serves (`read` output, `anchor_grep` output, the auto-read block after `write`, the `+anchor│` and ` anchor│` rows of post-edit diffs, the current-range rows of `[E_RANGE_STALE]` feedback, and the context rows of stale-anchor feedback) and verifies the whole range against that record before writing. A line that changed on disk since it was shown, or an anchor that is not owned in this session, refuses the edit with `[E_RANGE_STALE]` or `[E_STALE_ANCHOR]` and returns the current range with fresh anchors, so the retry needs no `read`. An owned anchor enters the served record when its row is shown (after a restart, restored ownership counts as shown), so a file with no owned anchors cannot be edited by anchor at all; call `read` first. An owned line that was never shown, for example beyond an auto-read preview's truncation cap, is refused with `[E_RANGE_STALE]` and returns the current range, so the retry still needs no `read`.
|
|
210
210
|
|
|
211
|
-
An edit that
|
|
211
|
+
An edit that changes neither content nor line endings reports `No changes made` and leaves the anchors alone.
|
|
212
212
|
|
|
213
213
|
After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as a marker that keeps the row's anchor, and only the rows shown in the capped diff are recorded as served. The same caps apply to the `insert` and `undo_last_change` diffs, to the interactive previews, and to `details.patch`.
|
|
214
214
|
|
|
@@ -220,7 +220,7 @@ After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as
|
|
|
220
220
|
| --- | --- |
|
|
221
221
|
| `anchor` | 4-char anchor marking the line next to which the lines go. The anchor line is preserved. A pasted `+Hasu│x` diff row or `anchor│` prefix is stripped automatically with a warning. |
|
|
222
222
|
| `direction` | `"after"` inserts below the anchor line, `"before"` above it. |
|
|
223
|
-
| `lines` | Lines to insert, one element per line. `[""]` is a blank line. Never include the anchor line. One element is one line: a real line-break character (`\n`, `\r\n`, or `\r`) splits it and sets
|
|
223
|
+
| `lines` | Lines to insert, one element per line. `[""]` is a blank line. Never include the anchor line. One element is one line: a real line-break character (`\n`, `\r\n`, or `\r`) splits it and sets the line's ending; escapes decode once — `\uXXXX` is the character, `\\uXXXX` the literal text. A trailing line-break sets the ending of the element's last line instead of adding a blank line, so `["b\n"]` inserts `b` with an LF ending, and on a file without a final newline it adds it. A lone string is split on newlines, and stringified array text is unwrapped. |
|
|
224
224
|
|
|
225
225
|
Nothing is removed and the inserted lines are written exactly as given; the anchor line and every other line stay in place. Inserting nothing (`lines: []`) reports a noop. To seed an empty file, read it and insert after the `anchor│` empty-line row.
|
|
226
226
|
|
|
@@ -314,6 +314,8 @@ Auto-read is enabled by default. After a successful `write`, the extension reads
|
|
|
314
314
|
|
|
315
315
|
After `replace`, `insert`, `copy`, `move`, and `undo_last_change`, the result shows the post-edit diff. Inside a same-message batch, only the batch's last call shows the combined diff, headed by a `batch N:` line; earlier calls reply `In batch N`. The `+anchor│` and ` anchor│` rows carry the current anchors, so follow-up edits can anchor on the diff directly. The `-anchor│` rows show removed lines with their old anchors, which are stale after the edit. When the context line next to a change is blank or whitespace-only, one more context line is shown in that direction, so the change stays anchored to visible content. Call `read` when you want the full file's anchors.
|
|
316
316
|
|
|
317
|
+
An edit that changes only line endings has no content diff; the result still reports `applied`, and one `undo_last_change` reverts it.
|
|
318
|
+
|
|
317
319
|
Auto-read keeps the same 50KB and 2000-line budget as `read`. Auto-read and Diff context live in `/hashline-config` and persist across sessions. The post-edit diff shows 1 surrounding line by default; change Diff context in `/hashline-config` (0-10, needs Auto-read) to show more or fewer.
|
|
318
320
|
|
|
319
321
|
## Auto-read all
|
package/index.ts
CHANGED
|
@@ -280,7 +280,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
280
280
|
const warnings = detailWarnings.length ? `Warnings:\n${detailWarnings.join("\n")}` : extractWarnings(rendered);
|
|
281
281
|
const hints = detailHints.length ? `Hints:\n${detailHints.join("\n")}` : extractHints(rendered);
|
|
282
282
|
const notices = [warnings, hints].filter((part): part is string => part !== undefined).join("\n\n");
|
|
283
|
-
const
|
|
283
|
+
const emptyDiffNotice = "[post-edit] applied successfully; the diff is empty (no content change: whitespace or line endings only).";
|
|
284
|
+
const noticeText = hasDiff ? (notices ? `${diff}\n\n${notices}` : diff) : notices ? `${emptyDiffNotice}\n\n${notices}` : emptyDiffNotice;
|
|
284
285
|
return {
|
|
285
286
|
content: [
|
|
286
287
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
- `replace`: `-anchor│` rows in a post-edit diff are dead anchors; only `+anchor│` and ` anchor│` rows are live. Check the post-edit diff before the next turn's edits on that file.
|
|
2
2
|
- `replace`: same-file same-message calls batch: disjoint ranges, one undo. A call whose anchors resolve nowhere fails alone; its file's batch still commits.
|
|
3
|
-
- `replace`: use `replace`/`insert
|
|
3
|
+
- `replace`: use `replace`/`insert` for structural or multi-line edits — anchored, verified, undoable.
|
|
4
4
|
- `replace`: `replacement_lines`: one string per line, `[""]` is one blank line, `[]` deletes. Pasted `anchor│` prefixes are stripped. Single line: same anchor for `remove_from` and `remove_to`.
|
package/src/batch.ts
CHANGED
|
@@ -543,13 +543,31 @@ export async function executeBatchMember(input: BatchMemberInput): Promise<TResu
|
|
|
543
543
|
|
|
544
544
|
function composeBatchLines(baseContent: string, pieces: BatchPiece[]): string {
|
|
545
545
|
const lines = splitLines(baseContent);
|
|
546
|
+
const baseLineCount = lines.length;
|
|
546
547
|
const descending = [...pieces].sort((a, b) => b.start - a.start);
|
|
547
548
|
for (const piece of descending) lines.splice(piece.start - 1, piece.end - piece.start + 1, ...piece.newLines);
|
|
548
549
|
let composed = lines.join("\n");
|
|
549
|
-
if (lines.length > 0 && (baseContent.endsWith("\n") || lines[lines.length - 1] === ""))
|
|
550
|
+
if (lines.length > 0 && (baseContent.endsWith("\n") || lines[lines.length - 1] === "")) {
|
|
551
|
+
composed += "\n";
|
|
552
|
+
} else if (lines.length > 0) {
|
|
553
|
+
const trailing = pieces.reduce<BatchPiece | undefined>((best, piece) => (best === undefined || piece.end > best.end ? piece : best), undefined);
|
|
554
|
+
const lastEnding = trailing?.separators?.[trailing.newLines.length - 1];
|
|
555
|
+
if (trailing !== undefined && trailing.end === baseLineCount && lastEnding !== undefined) composed += "\n";
|
|
556
|
+
}
|
|
550
557
|
return composed;
|
|
551
558
|
}
|
|
552
559
|
|
|
560
|
+
function changesEnding(piece: BatchPiece, baseSeparators: LineEnding[]): boolean {
|
|
561
|
+
if (piece.separators === undefined) return false;
|
|
562
|
+
for (let index = 0; index < piece.separators.length; index++) {
|
|
563
|
+
const ending = piece.separators[index];
|
|
564
|
+
if (ending === undefined) continue;
|
|
565
|
+
const baseIndex = piece.start - 1 + index;
|
|
566
|
+
if (baseIndex >= baseSeparators.length || baseSeparators[baseIndex] !== ending) return true;
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
|
|
553
571
|
function mergeInsertPairs(pieces: BatchPiece[]): BatchPiece[] {
|
|
554
572
|
const byAnchor = new Map<number, BatchPiece[]>();
|
|
555
573
|
for (const piece of pieces) {
|
|
@@ -624,11 +642,12 @@ async function finishBatch(member: PlannedMember, signal?: AbortSignal): Promise
|
|
|
624
642
|
}
|
|
625
643
|
}
|
|
626
644
|
const appliedPieces = runtime.pieces.filter((piece) => !piece.noop);
|
|
627
|
-
|
|
645
|
+
const candidatePieces = runtime.pieces.filter((piece) => !piece.noop || changesEnding(piece, base.separators));
|
|
646
|
+
if (candidatePieces.length === 0) {
|
|
628
647
|
const snapshotId = await safeSnapId(paths.absolutePath, "noop edit");
|
|
629
648
|
return combinedNoop(paths.displayPath, member, runtime, snapshotId);
|
|
630
649
|
}
|
|
631
|
-
const effectivePieces = mergeInsertPairs(
|
|
650
|
+
const effectivePieces = mergeInsertPairs(candidatePieces);
|
|
632
651
|
const ordered = [...effectivePieces].sort((a, b) => a.start - b.start);
|
|
633
652
|
for (let i = 1; i < ordered.length; i++) {
|
|
634
653
|
const prev = ordered[i - 1]!;
|
|
@@ -649,11 +668,12 @@ async function finishBatch(member: PlannedMember, signal?: AbortSignal): Promise
|
|
|
649
668
|
})));
|
|
650
669
|
const warnings = [...runtime.warnings];
|
|
651
670
|
if (base.hadUtf8DecodeErrors) warnings.push("Non-UTF-8 bytes were shown as U+FFFD; this edit rewrote the file as UTF-8.");
|
|
671
|
+
const finalBytes = base.bom + joinSeparators(composed, resultSeparators);
|
|
672
|
+
const originalBytes = base.bom + joinSeparators(base.content, base.separators);
|
|
652
673
|
try {
|
|
653
674
|
await throwIfStrictInput(dedupeWarnings(warnings));
|
|
654
675
|
assertNotEmpty(base.content, composed);
|
|
655
676
|
assertLineLimit(composed, paths.displayPath, MAX_HASH_LINES);
|
|
656
|
-
const finalBytes = base.bom + joinSeparators(composed, resultSeparators);
|
|
657
677
|
assertByteLimit(finalBytes, paths.displayPath);
|
|
658
678
|
} catch (error) {
|
|
659
679
|
discardBatchState(runtime);
|
|
@@ -662,7 +682,7 @@ async function finishBatch(member: PlannedMember, signal?: AbortSignal): Promise
|
|
|
662
682
|
}
|
|
663
683
|
const reclaimNotice = formatAnchorReclaimNotice(takeReclaimedPaths());
|
|
664
684
|
if (reclaimNotice !== undefined) warnings.push(reclaimNotice);
|
|
665
|
-
if (
|
|
685
|
+
if (finalBytes === originalBytes) {
|
|
666
686
|
const snapshotId = await safeSnapId(paths.absolutePath, "noop edit");
|
|
667
687
|
return combinedNoop(paths.displayPath, member, runtime, snapshotId);
|
|
668
688
|
}
|
|
@@ -756,8 +776,10 @@ async function finishBatch(member: PlannedMember, signal?: AbortSignal): Promise
|
|
|
756
776
|
batchVerb(runtime),
|
|
757
777
|
await getDiffContextLines(),
|
|
758
778
|
);
|
|
759
|
-
changed.details.diff
|
|
760
|
-
|
|
779
|
+
if (changed.details.diff.length > 0) {
|
|
780
|
+
changed.details.diff = `${header}\n${changed.details.diff}`;
|
|
781
|
+
changed.details.diffLineNumbers?.unshift(null);
|
|
782
|
+
}
|
|
761
783
|
try {
|
|
762
784
|
serveRows(runtime.target, resultHashes, splitLines(composed), servedHashesFromDiff(changed.details.diff));
|
|
763
785
|
} catch (error) {
|
package/src/commit.ts
CHANGED
|
@@ -33,7 +33,22 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
|
|
|
33
33
|
const readReclaim = formatAnchorReclaimNotice(takeReclaimedPaths());
|
|
34
34
|
if (readReclaim !== undefined) warnings.push(readReclaim);
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
const span = pipe.spans?.[0] ?? (meta.editAnchors ? spanForEdit(pipe.originalHashes, meta.editAnchors[0], meta.editAnchors[1], pipe.result) : undefined);
|
|
37
|
+
if (span && meta.anchorCarry !== undefined) span.carry = meta.anchorCarry;
|
|
38
|
+
const resultSeparators = span
|
|
39
|
+
? separatorsForSpans(pipe.originalSeparators, pipe.originalHashes.length, [span], pipe.result, pipe.originalEnding)
|
|
40
|
+
: undefined;
|
|
41
|
+
if (resultSeparators !== undefined && span !== undefined) {
|
|
42
|
+
applyEndingOverrides(resultSeparators, span.start, pipe.contentSeparators);
|
|
43
|
+
applyEndingOverrides(resultSeparators, span.start, meta.endingOverrides);
|
|
44
|
+
}
|
|
45
|
+
const finalFileBytes = pipe.bom + (resultSeparators !== undefined
|
|
46
|
+
? joinSeparators(pipe.result, resultSeparators)
|
|
47
|
+
: restoreEndings(pipe.result, pipe.originalEnding));
|
|
48
|
+
const contentUnchanged = pipe.result === pipe.originalNormalized;
|
|
49
|
+
const bytesUnchanged = contentUnchanged && (span === undefined || finalFileBytes === pipe.bom + joinSeparators(pipe.originalNormalized, pipe.originalSeparators));
|
|
50
|
+
|
|
51
|
+
if (bytesUnchanged) {
|
|
37
52
|
const noopSnapshotId = await safeSnapId(absolutePath, "noop edit");
|
|
38
53
|
return buildNoop(
|
|
39
54
|
{
|
|
@@ -52,18 +67,6 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
|
|
|
52
67
|
);
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
const span = pipe.spans?.[0] ?? (meta.editAnchors ? spanForEdit(pipe.originalHashes, meta.editAnchors[0], meta.editAnchors[1], pipe.result) : undefined);
|
|
56
|
-
if (span && meta.anchorCarry !== undefined) span.carry = meta.anchorCarry;
|
|
57
|
-
const resultSeparators = span
|
|
58
|
-
? separatorsForSpans(pipe.originalSeparators, pipe.originalHashes.length, [span], pipe.result, pipe.originalEnding)
|
|
59
|
-
: undefined;
|
|
60
|
-
if (resultSeparators !== undefined && span !== undefined) {
|
|
61
|
-
applyEndingOverrides(resultSeparators, span.start, pipe.contentSeparators);
|
|
62
|
-
applyEndingOverrides(resultSeparators, span.start, meta.endingOverrides);
|
|
63
|
-
}
|
|
64
|
-
const finalFileBytes = pipe.bom + (resultSeparators !== undefined
|
|
65
|
-
? joinSeparators(pipe.result, resultSeparators)
|
|
66
|
-
: restoreEndings(pipe.result, pipe.originalEnding));
|
|
67
70
|
assertByteLimit(finalFileBytes, path);
|
|
68
71
|
|
|
69
72
|
if (pipe.hadUtf8DecodeErrors) {
|
package/src/copy-move.ts
CHANGED
|
@@ -544,6 +544,7 @@ async function executeCrossFile(
|
|
|
544
544
|
accessMode: constants.R_OK | constants.W_OK,
|
|
545
545
|
signal,
|
|
546
546
|
preloadedNorm: prepared.destinationPreload,
|
|
547
|
+
endingOverrides: prepared.endingOverrides,
|
|
547
548
|
});
|
|
548
549
|
if (kind === "copy") {
|
|
549
550
|
return commitEdit(destinationPipe, {
|
|
@@ -610,6 +611,7 @@ export async function transferPreview(kind: TransferKind, request: unknown, cwd:
|
|
|
610
611
|
noPersist: true,
|
|
611
612
|
preloadedNorm: preload,
|
|
612
613
|
served: plan.servedOverride,
|
|
614
|
+
endingOverrides: plan.endingOverrides,
|
|
613
615
|
signal,
|
|
614
616
|
});
|
|
615
617
|
return previewFromPipe(pipe);
|
|
@@ -619,6 +621,7 @@ export async function transferPreview(kind: TransferKind, request: unknown, cwd:
|
|
|
619
621
|
accessMode: constants.R_OK,
|
|
620
622
|
noPersist: true,
|
|
621
623
|
preloadedNorm: prepared.destinationPreload,
|
|
624
|
+
endingOverrides: prepared.endingOverrides,
|
|
622
625
|
signal,
|
|
623
626
|
});
|
|
624
627
|
if (kind === "copy") return previewFromPipe(destinationPipe);
|
|
@@ -764,6 +767,7 @@ export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags =
|
|
|
764
767
|
signal,
|
|
765
768
|
preloadedNorm: preload,
|
|
766
769
|
served: plan.servedOverride,
|
|
770
|
+
endingOverrides: plan.endingOverrides,
|
|
767
771
|
});
|
|
768
772
|
return commitEdit(pipe, {
|
|
769
773
|
path: pipe.path,
|
package/src/hashline/apply.ts
CHANGED
|
@@ -68,12 +68,12 @@ function resToSpan(
|
|
|
68
68
|
const startLine = edit.hash_bounds[0].line;
|
|
69
69
|
const endLine = edit.hash_bounds[1].line;
|
|
70
70
|
const originalLines = fileLines.slice(startLine - 1, endLine);
|
|
71
|
-
|
|
71
|
+
const contentUnchanged =
|
|
72
72
|
originalLines.length === edit.content_lines.length &&
|
|
73
73
|
originalLines.every(
|
|
74
74
|
(line, lineIndex) => line === edit.content_lines[lineIndex],
|
|
75
|
-
)
|
|
76
|
-
) {
|
|
75
|
+
);
|
|
76
|
+
if (contentUnchanged && edit.content_separators === undefined) {
|
|
77
77
|
return {
|
|
78
78
|
kind: "noop",
|
|
79
79
|
loc: edit.hash_bounds[0].hash,
|
|
@@ -82,7 +82,9 @@ function resToSpan(
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
if (edit.content_lines.length > 0) {
|
|
85
|
-
const
|
|
85
|
+
const lastIndex = edit.content_lines.length - 1;
|
|
86
|
+
const lastReplacementLine = edit.content_lines[lastIndex]!;
|
|
87
|
+
const lastEnding = edit.content_separators?.[lastIndex];
|
|
86
88
|
const endsWithBlank = lastReplacementLine.length === 0;
|
|
87
89
|
const endsAtEofWithoutNewline =
|
|
88
90
|
endLine === fileLines.length && !content.endsWith("\n");
|
|
@@ -92,7 +94,7 @@ function resToSpan(
|
|
|
92
94
|
start: lineStarts[startLine - 1]!,
|
|
93
95
|
end: lineStarts[endLine - 1]! + fileLines[endLine - 1]!.length,
|
|
94
96
|
replacement:
|
|
95
|
-
endsAtEofWithoutNewline && endsWithBlank
|
|
97
|
+
endsAtEofWithoutNewline && (endsWithBlank || lastEnding !== undefined)
|
|
96
98
|
? `${replacement}\n`
|
|
97
99
|
: replacement,
|
|
98
100
|
};
|
package/src/hashline/parse.ts
CHANGED
|
@@ -81,10 +81,14 @@ export function parseTextWithSeparators(edit: string[]): ParsedText {
|
|
|
81
81
|
}
|
|
82
82
|
const lines: string[] = [];
|
|
83
83
|
const separators: (LineEnding | undefined)[] = [];
|
|
84
|
-
for (const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
for (const element of edit) {
|
|
85
|
+
const text = unwrapJsonEnvelope(element);
|
|
86
|
+
const parsed = splitWithEndings(text);
|
|
87
|
+
const lineCount = text.endsWith("\n") || text.endsWith("\r") ? parsed.lines.length - 1 : parsed.lines.length;
|
|
88
|
+
for (let index = 0; index < lineCount; index++) {
|
|
89
|
+
lines.push(parsed.lines[index]!);
|
|
90
|
+
separators.push(parsed.endings[index]!);
|
|
91
|
+
}
|
|
88
92
|
}
|
|
89
93
|
return { lines, separators };
|
|
90
94
|
}
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type HEdit = {
|
|
|
18
18
|
export type RHEdit = {
|
|
19
19
|
content_lines: string[];
|
|
20
20
|
hash_bounds: [RAnchor, RAnchor];
|
|
21
|
+
content_separators?: (LineEnding | undefined)[];
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
interface HMismatch {
|
|
@@ -288,6 +289,7 @@ export function valEdit(
|
|
|
288
289
|
resolved: {
|
|
289
290
|
content_lines: edit.content_lines,
|
|
290
291
|
hash_bounds: [startResolved, endResolved],
|
|
292
|
+
...(edit.content_separators !== undefined ? { content_separators: edit.content_separators } : {}),
|
|
291
293
|
},
|
|
292
294
|
mismatches,
|
|
293
295
|
};
|
package/src/insert.ts
CHANGED
|
@@ -125,13 +125,14 @@ export async function insertPreview(request: unknown, cwd: string, signal?: Abor
|
|
|
125
125
|
allocation: "shadow",
|
|
126
126
|
signal,
|
|
127
127
|
});
|
|
128
|
-
const { editParams, anchorLine } = buildInsertEdit(normalized, preload, ref, targetPath);
|
|
128
|
+
const { editParams, anchorLine, contentSeparators } = buildInsertEdit(normalized, preload, ref, targetPath);
|
|
129
129
|
const pipe = await execPipeline(targetPath, editParams, cwd, {
|
|
130
130
|
accessMode: constants.R_OK,
|
|
131
131
|
noPersist: true,
|
|
132
132
|
preloadedNorm: preload,
|
|
133
133
|
signal,
|
|
134
134
|
stripWarning: insertStripWarning(anchorLine, normalized.direction),
|
|
135
|
+
endingOverrides: contentSeparators,
|
|
135
136
|
});
|
|
136
137
|
return previewFromPipe(pipe);
|
|
137
138
|
} catch (error: unknown) {
|
|
@@ -253,6 +254,7 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
|
|
|
253
254
|
signal,
|
|
254
255
|
preloadedNorm: preload,
|
|
255
256
|
stripWarning: insertStripWarning(anchorLine, req.direction),
|
|
257
|
+
endingOverrides: contentSeparators,
|
|
256
258
|
});
|
|
257
259
|
return commitEdit(pipe, {
|
|
258
260
|
path: pipe.path,
|
package/src/replace.ts
CHANGED
|
@@ -83,6 +83,7 @@ export interface ExecPipelineOptions {
|
|
|
83
83
|
served?: ReadonlyMap<string, string>;
|
|
84
84
|
allowEmpty?: boolean;
|
|
85
85
|
stripWarning?: StripWarningLocation;
|
|
86
|
+
endingOverrides?: (LineEnding | undefined)[];
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
export function hashSpan(hashes: string[], from: string, to: string): [number, number] | undefined {
|
|
@@ -134,6 +135,17 @@ export function buildReplaceHEdit(params: ReqParams): { edit: HEdit; warnings: s
|
|
|
134
135
|
return { edit, warnings: editWarnings };
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
function withEndingOverrides(edit: HEdit, overrides: (LineEnding | undefined)[] | undefined): HEdit {
|
|
139
|
+
if (overrides === undefined) return edit;
|
|
140
|
+
const length = Math.max(edit.content_separators?.length ?? 0, overrides.length);
|
|
141
|
+
const separators: (LineEnding | undefined)[] = new Array(length);
|
|
142
|
+
for (let index = 0; index < length; index++) {
|
|
143
|
+
separators[index] = overrides[index] ?? edit.content_separators?.[index];
|
|
144
|
+
}
|
|
145
|
+
if (separators.every((ending) => ending === undefined)) return edit;
|
|
146
|
+
return { ...edit, content_separators: separators };
|
|
147
|
+
}
|
|
148
|
+
|
|
137
149
|
export async function execPipeline(
|
|
138
150
|
targetPath: string,
|
|
139
151
|
params: ReqParams,
|
|
@@ -142,6 +154,7 @@ export async function execPipeline(
|
|
|
142
154
|
): Promise<PipelineResult> {
|
|
143
155
|
|
|
144
156
|
const { edit, warnings: editWarnings } = buildReplaceHEdit(params);
|
|
157
|
+
const anchoredEdit = withEndingOverrides(edit, options?.endingOverrides);
|
|
145
158
|
const hashStore = options?.store ?? await loadHashStore();
|
|
146
159
|
const preResolvedPath = await resolveTarget(toCwd(targetPath, cwd));
|
|
147
160
|
const served = options?.served ?? servedForPath(preResolvedPath);
|
|
@@ -154,7 +167,7 @@ export async function execPipeline(
|
|
|
154
167
|
try {
|
|
155
168
|
anchorResult = applyEdit(
|
|
156
169
|
originalNormalized,
|
|
157
|
-
|
|
170
|
+
anchoredEdit,
|
|
158
171
|
options?.signal,
|
|
159
172
|
originalHashes,
|
|
160
173
|
displayPath,
|
|
@@ -202,12 +215,15 @@ export async function execPipeline(
|
|
|
202
215
|
totalRemovedLines,
|
|
203
216
|
identity,
|
|
204
217
|
...(pipeSpans ? { spans: pipeSpans } : {}),
|
|
205
|
-
...(
|
|
218
|
+
...(anchoredEdit.content_separators !== undefined ? { contentSeparators: anchoredEdit.content_separators } : {}),
|
|
206
219
|
};
|
|
207
220
|
}
|
|
208
221
|
|
|
209
222
|
export function previewFromPipe(pipe: PipelineResult): RPreview {
|
|
210
223
|
if (pipe.originalNormalized === pipe.result) {
|
|
224
|
+
if (pipe.contentSeparators !== undefined) {
|
|
225
|
+
return { diff: "", path: pipe.path };
|
|
226
|
+
}
|
|
211
227
|
return {
|
|
212
228
|
error: `No changes made to ${pipe.path}. The edit produced identical content.`,
|
|
213
229
|
path: pipe.path,
|