pi-hashline-edit-pro 4.4.4 → 4.5.1
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 +49 -16
- package/index.ts +25 -7
- package/package.json +4 -3
- package/prompts/copy-guidelines.md +1 -0
- package/prompts/copy-snippet.md +1 -0
- package/prompts/copy.md +3 -0
- package/prompts/grep.md +1 -1
- package/prompts/insert.md +1 -1
- package/prompts/move-guidelines.md +1 -0
- package/prompts/move-snippet.md +1 -0
- package/prompts/move.md +3 -0
- package/prompts/replace-guidelines.md +0 -2
- package/prompts/replace.md +1 -1
- package/prompts/undo-last-change-guidelines.md +2 -1
- package/prompts/undo-last-change-snippet.md +1 -1
- package/prompts/undo-last-change.md +1 -1
- package/src/batch.ts +28 -6
- package/src/commit.ts +17 -4
- package/src/config-ui.ts +2 -1
- package/src/config.ts +6 -1
- package/src/constants.ts +1 -1
- package/src/copy-move.ts +793 -0
- package/src/edit-common.ts +19 -0
- package/src/file-reader.ts +4 -0
- package/src/hash-store/validation.ts +22 -0
- package/src/hash-store.ts +32 -4
- package/src/hashline/apply.ts +8 -3
- package/src/hashline/index.ts +3 -0
- package/src/hashline/parse.ts +22 -10
- package/src/hashline/resolve.ts +23 -8
- package/src/insert.ts +28 -10
- package/src/line-endings.ts +151 -0
- package/src/paths.ts +18 -2
- package/src/payload-contract.ts +29 -1
- package/src/replace-render.ts +15 -7
- package/src/replace-response.ts +23 -8
- package/src/replace-undo.ts +9 -2
- package/src/replace.ts +18 -5
- package/src/utils.ts +40 -5
package/src/paths.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { isAbsolute, relative, resolve as resolvePath, join, dirname } from "node:path";
|
|
2
|
+
import { isAbsolute, relative, resolve as resolvePath, join, dirname, parse } from "node:path";
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
function homeBase(): string {
|
|
@@ -55,6 +55,22 @@ export function toCwd(filePath: string, cwd: string): string {
|
|
|
55
55
|
return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function pathDepth(path: string): number {
|
|
59
|
+
const root = parse(path).root;
|
|
60
|
+
return path.slice(root.length).split(/[\\/]+/).filter((part) => part.length > 0).length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function climbsToRoot(cwd: string, relativePath: string): boolean {
|
|
64
|
+
let climbs = 0;
|
|
65
|
+
for (const part of relativePath.split("/")) {
|
|
66
|
+
if (part !== "..") break;
|
|
67
|
+
climbs += 1;
|
|
68
|
+
}
|
|
69
|
+
return climbs > 0 && climbs >= pathDepth(resolvePath(cwd));
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
export function toDisplayPath(cwd: string, absolutePath: string, fallback?: string): string {
|
|
59
|
-
|
|
73
|
+
const rel = relative(cwd, absolutePath).replace(/\\/g, "/");
|
|
74
|
+
if (rel.length === 0 || climbsToRoot(cwd, rel)) return fallback ?? absolutePath;
|
|
75
|
+
return rel;
|
|
60
76
|
}
|
package/src/payload-contract.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { isRec, normalizeRequest, rejectUnknownFields, assertNoNul } from "./uti
|
|
|
4
4
|
const replacementLinesSchema = Type.Array(
|
|
5
5
|
Type.String({
|
|
6
6
|
description:
|
|
7
|
-
"One replacement line
|
|
7
|
+
"One replacement line. A real line break (\\n, \\r\\n, or \\r) splits it into lines and sets their endings.",
|
|
8
8
|
}),
|
|
9
9
|
{
|
|
10
10
|
description:
|
|
@@ -131,3 +131,31 @@ export function assertInsertReq(request: unknown): asserts request is InsertReq
|
|
|
131
131
|
}
|
|
132
132
|
assertNoNul(request.lines);
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
const TRANSFER_KEYS = new Set(["path", "source_from", "source_to", "insert_after"]);
|
|
136
|
+
|
|
137
|
+
export interface TransferReq {
|
|
138
|
+
path?: string;
|
|
139
|
+
source_from: string;
|
|
140
|
+
source_to: string;
|
|
141
|
+
insert_after: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function assertTransferReq(request: unknown): asserts request is TransferReq {
|
|
145
|
+
if (!isRec(request)) {
|
|
146
|
+
throw new Error("[E_BAD_SHAPE] Copy/move request must be an object.");
|
|
147
|
+
}
|
|
148
|
+
rejectUnknownFields(request, TRANSFER_KEYS, "Copy/move request");
|
|
149
|
+
if (request.path !== undefined && typeof request.path !== "string") {
|
|
150
|
+
throw new Error('[E_BAD_SHAPE] Copy/move request field "path" must be a string when provided.');
|
|
151
|
+
}
|
|
152
|
+
if (typeof request.source_from !== "string" || request.source_from.length === 0) {
|
|
153
|
+
throw new Error('[E_BAD_SHAPE] Copy/move request requires a "source_from" string (4-char anchor from read output).');
|
|
154
|
+
}
|
|
155
|
+
if (typeof request.source_to !== "string" || request.source_to.length === 0) {
|
|
156
|
+
throw new Error('[E_BAD_SHAPE] Copy/move request requires a "source_to" string (4-char anchor from read output).');
|
|
157
|
+
}
|
|
158
|
+
if (typeof request.insert_after !== "string" || request.insert_after.length === 0) {
|
|
159
|
+
throw new Error('[E_BAD_SHAPE] Copy/move request requires an "insert_after" string (4-char anchor from read output).');
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/replace-render.ts
CHANGED
|
@@ -81,7 +81,7 @@ export function highlightBatchRefs(text: string, theme: FgT): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export function fmtCall(
|
|
84
|
-
args: { path?: string; remove_from?: string; remove_to?: string; anchor?: string } | undefined,
|
|
84
|
+
args: { path?: string; remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string; insert_after?: string } | undefined,
|
|
85
85
|
state: RRState,
|
|
86
86
|
expanded: boolean,
|
|
87
87
|
theme: CallT,
|
|
@@ -89,7 +89,7 @@ export function fmtCall(
|
|
|
89
89
|
): string {
|
|
90
90
|
const previewPath = state.preview && "path" in state.preview ? state.preview.path : undefined;
|
|
91
91
|
const path = args?.path ?? state.resolvedPath ?? previewPath;
|
|
92
|
-
const anchorFallback = typeof args?.remove_from === "string" && typeof args?.remove_to === "string" ? `${args.remove_from}→${args.remove_to}` : typeof args?.anchor === "string" ? args.anchor : undefined;
|
|
92
|
+
const anchorFallback = typeof args?.remove_from === "string" && typeof args?.remove_to === "string" ? `${args.remove_from}→${args.remove_to}` : typeof args?.anchor === "string" ? args.anchor : typeof args?.source_from === "string" && typeof args?.insert_after === "string" ? `${args.source_from}→${args.insert_after}` : undefined;
|
|
93
93
|
const pathDisplay =
|
|
94
94
|
typeof path === "string" && path.length > 0
|
|
95
95
|
? theme.fg("accent", path)
|
|
@@ -126,7 +126,13 @@ export function getResultText(result: {
|
|
|
126
126
|
export function extractWarnings(
|
|
127
127
|
text: string | undefined,
|
|
128
128
|
): string | undefined {
|
|
129
|
-
return text?.match(/(?:^|\n)Warnings:\n[\s\S]
|
|
129
|
+
return text?.match(/(?:^|\n)Warnings:\n[\s\S]*?(?=\n\nHints:\n|$)/)?.[0]?.trimStart();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function extractHints(
|
|
133
|
+
text: string | undefined,
|
|
134
|
+
): string | undefined {
|
|
135
|
+
return text?.match(/(?:^|\n)Hints:\n[\s\S]*$/)?.[0]?.trimStart();
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
export function isApplied(
|
|
@@ -153,8 +159,8 @@ export function expandHint(): string {
|
|
|
153
159
|
function extractSummary(text: string | undefined): string | undefined {
|
|
154
160
|
if (!text) return undefined;
|
|
155
161
|
if (text.includes("│")) return undefined;
|
|
156
|
-
const
|
|
157
|
-
const summary =
|
|
162
|
+
const noticesIdx = text.search(/\n\n(?:Warnings|Hints):\n/);
|
|
163
|
+
const summary = noticesIdx >= 0 ? text.slice(0, noticesIdx) : text;
|
|
158
164
|
return summary.length > 0 ? summary : undefined;
|
|
159
165
|
}
|
|
160
166
|
|
|
@@ -190,6 +196,8 @@ export function buildAppliedText(
|
|
|
190
196
|
}
|
|
191
197
|
const warnings = details?.warnings?.length ? `Warnings:\n${details.warnings.join("\n")}` : extractWarnings(text);
|
|
192
198
|
if (warnings) sections.push(warnings);
|
|
199
|
+
const hints = details?.hints?.length ? `Hints:\n${details.hints.join("\n")}` : extractHints(text);
|
|
200
|
+
if (hints) sections.push(hints);
|
|
193
201
|
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
194
202
|
}
|
|
195
203
|
|
|
@@ -264,9 +272,9 @@ export function reuseMarkdown(context: any, content: string, theme: any): Markdo
|
|
|
264
272
|
export function makeRenderCall(
|
|
265
273
|
preview: (args: unknown, cwd: string, signal?: AbortSignal) => Promise<RPreview>,
|
|
266
274
|
options: {
|
|
267
|
-
getInput?: (args: unknown) => { path?: string; remove_from?: string; remove_to?: string; anchor?: string } | null;
|
|
275
|
+
getInput?: (args: unknown) => { path?: string; remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string; insert_after?: string } | null;
|
|
268
276
|
toolName?: string;
|
|
269
|
-
resolveTarget?: (input: { remove_from?: string; remove_to?: string; anchor?: string }) => string | undefined;
|
|
277
|
+
resolveTarget?: (input: { remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string; insert_after?: string }) => string | undefined;
|
|
270
278
|
} = {},
|
|
271
279
|
) {
|
|
272
280
|
const getInput = options.getInput ?? getPreviewInput;
|
package/src/replace-response.ts
CHANGED
|
@@ -85,6 +85,17 @@ function warnBlock(warnings: string[] | undefined): string {
|
|
|
85
85
|
return warnings?.length ? `\n\nWarnings:\n${warnings.join("\n")}` : "";
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function hintBlock(hints: string[] | undefined): string {
|
|
89
|
+
return hints?.length ? `\n\nHints:\n${hints.join("\n")}` : "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function splitNotices(notices: string[] | undefined): { warnings: string[]; hints: string[] } {
|
|
93
|
+
const warnings: string[] = [];
|
|
94
|
+
const hints: string[] = [];
|
|
95
|
+
for (const notice of notices ?? []) (notice.startsWith("[H_") ? hints : warnings).push(notice);
|
|
96
|
+
return { warnings, hints };
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
|
|
89
100
|
const {
|
|
90
101
|
path,
|
|
@@ -97,13 +108,14 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
|
|
|
97
108
|
const noopDetailsText = noopEdit
|
|
98
109
|
? `${noopNoun} for ${noopEdit.loc} is identical to current content:\n ${noopEdit.loc}: ${clipLine(noopEdit.currentContent)}`
|
|
99
110
|
: "The edit produced identical content.";
|
|
100
|
-
const
|
|
111
|
+
const { warnings: noticeWarnings, hints } = splitNotices(warnings);
|
|
112
|
+
const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}${warnBlock(noticeWarnings)}${hintBlock(hints)}`;
|
|
101
113
|
|
|
102
114
|
const metrics = buildMetrics({
|
|
103
115
|
classification: "noop",
|
|
104
116
|
editsAttempted: editMeta.editsAttempted,
|
|
105
117
|
noopEditsCount: editMeta.noopEditsCount,
|
|
106
|
-
warningsCount:
|
|
118
|
+
warningsCount: noticeWarnings.length,
|
|
107
119
|
});
|
|
108
120
|
|
|
109
121
|
return {
|
|
@@ -115,7 +127,8 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
|
|
|
115
127
|
snapshotId,
|
|
116
128
|
classification: "noop" as const,
|
|
117
129
|
metrics,
|
|
118
|
-
...(
|
|
130
|
+
...(noticeWarnings.length ? { warnings: [...noticeWarnings] } : {}),
|
|
131
|
+
...(hints.length ? { hints: [...hints] } : {}),
|
|
119
132
|
},
|
|
120
133
|
};
|
|
121
134
|
}
|
|
@@ -126,22 +139,23 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
|
|
|
126
139
|
const diffResult = genDiff(originalNormalized, result, diffContextLines, resultHashes, originalHashes, undefined, spans);
|
|
127
140
|
const addedLines = editMeta.addedLines;
|
|
128
141
|
const removedLines = editMeta.removedLines;
|
|
129
|
-
const
|
|
142
|
+
const { warnings: noticeWarnings, hints } = splitNotices(warnings);
|
|
143
|
+
const noticesBlock = `${warnBlock(noticeWarnings)}${hintBlock(hints)}`;
|
|
130
144
|
const successPrefix = `Successfully ${verb} in ${path}.`;
|
|
131
145
|
const lineSummary = addedLines > 0 || removedLines > 0
|
|
132
146
|
? ` Added ${addedLines} line(s), removed ${removedLines} line(s).`
|
|
133
147
|
: "";
|
|
134
148
|
const text = resultLines.length === 0
|
|
135
149
|
? "File is empty. Use replace to insert content."
|
|
136
|
-
:
|
|
137
|
-
? `${successPrefix}${lineSummary}${
|
|
150
|
+
: noticesBlock
|
|
151
|
+
? `${successPrefix}${lineSummary}${noticesBlock}`
|
|
138
152
|
: `${successPrefix}${lineSummary}`;
|
|
139
153
|
|
|
140
154
|
const metrics = buildMetrics({
|
|
141
155
|
classification: "applied",
|
|
142
156
|
editsAttempted: editMeta.editsAttempted,
|
|
143
157
|
noopEditsCount: editMeta.noopEditsCount,
|
|
144
|
-
warningsCount:
|
|
158
|
+
warningsCount: noticeWarnings.length,
|
|
145
159
|
firstChangedLine: editMeta.firstChangedLine,
|
|
146
160
|
lastChangedLine: editMeta.lastChangedLine,
|
|
147
161
|
addedLines,
|
|
@@ -160,7 +174,8 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
|
|
|
160
174
|
snapshotId,
|
|
161
175
|
metrics,
|
|
162
176
|
diffLineNumbers: diffResult.lineNumbers.map((line) => line ?? null),
|
|
163
|
-
...(
|
|
177
|
+
...(noticeWarnings.length ? { warnings: [...noticeWarnings] } : {}),
|
|
178
|
+
...(hints.length ? { hints: [...hints] } : {}),
|
|
164
179
|
},
|
|
165
180
|
};
|
|
166
181
|
}
|
package/src/replace-undo.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { lineChecksum } from "./hashline";
|
|
|
9
9
|
import { freeAnchors, adoptAnchors, withAnchorSession, formatAnchorReclaimNotice, takeReclaimedPaths } from "./anchor-registry";
|
|
10
10
|
import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
|
|
11
11
|
import { toLF, stripBOM, restoreEndings, type LineEnding } from "./normalize";
|
|
12
|
+
import { joinSeparators } from "./line-endings";
|
|
12
13
|
import { genDiff, genPatch, spansFromHashes } from "./replace-diff";
|
|
13
14
|
import { getDiffContextLines } from "./config";
|
|
14
15
|
import { cntDiff, errCode, makePrepareArguments, splitLines } from "./utils";
|
|
@@ -22,8 +23,10 @@ export interface UndoEntry {
|
|
|
22
23
|
content: string;
|
|
23
24
|
bom: string;
|
|
24
25
|
originalEnding: LineEnding;
|
|
26
|
+
separators?: LineEnding[];
|
|
25
27
|
hashes: string[];
|
|
26
28
|
resultContent: string;
|
|
29
|
+
resultSeparators?: LineEnding[];
|
|
27
30
|
mode?: number;
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -44,8 +47,10 @@ export async function saveUndo(
|
|
|
44
47
|
content: entry.content,
|
|
45
48
|
bom: entry.bom,
|
|
46
49
|
ending: entry.originalEnding,
|
|
50
|
+
...(entry.separators !== undefined ? { separators: entry.separators } : {}),
|
|
47
51
|
hashes: entry.hashes,
|
|
48
52
|
resultContent: entry.resultContent,
|
|
53
|
+
...(entry.resultSeparators !== undefined ? { resultSeparators: entry.resultSeparators } : {}),
|
|
49
54
|
...(mode !== undefined ? { mode } : {}),
|
|
50
55
|
});
|
|
51
56
|
} catch (error) {
|
|
@@ -80,8 +85,10 @@ export async function getUndo(path: string): Promise<UndoEntry | undefined> {
|
|
|
80
85
|
content: record.content,
|
|
81
86
|
bom: record.bom,
|
|
82
87
|
originalEnding,
|
|
88
|
+
...(record.separators !== undefined ? { separators: record.separators as LineEnding[] } : {}),
|
|
83
89
|
hashes: record.hashes,
|
|
84
90
|
resultContent: record.resultContent,
|
|
91
|
+
...(record.resultSeparators !== undefined ? { resultSeparators: record.resultSeparators as LineEnding[] } : {}),
|
|
85
92
|
...(record.mode !== undefined ? { mode: record.mode } : {}),
|
|
86
93
|
};
|
|
87
94
|
} catch (error) {
|
|
@@ -171,7 +178,7 @@ export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
171
178
|
|
|
172
179
|
if (
|
|
173
180
|
currentRaw !== undefined &&
|
|
174
|
-
currentRaw !== undo.bom + restoreEndings(undo.resultContent, undo.originalEnding)
|
|
181
|
+
currentRaw !== undo.bom + (undo.resultSeparators !== undefined ? joinSeparators(undo.resultContent, undo.resultSeparators) : restoreEndings(undo.resultContent, undo.originalEnding))
|
|
175
182
|
) {
|
|
176
183
|
return {
|
|
177
184
|
content: [
|
|
@@ -187,7 +194,7 @@ export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
187
194
|
|
|
188
195
|
await writeAtomic(
|
|
189
196
|
mutationTargetPath,
|
|
190
|
-
undo.bom + restoreEndings(undo.content, undo.originalEnding),
|
|
197
|
+
undo.bom + (undo.separators !== undefined ? joinSeparators(undo.content, undo.separators) : restoreEndings(undo.content, undo.originalEnding)),
|
|
191
198
|
currentIdentity,
|
|
192
199
|
undo.mode ?? fallbackFileMode(),
|
|
193
200
|
);
|
package/src/replace.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./replace-diff";
|
|
11
11
|
import { readNormFile, type NormFile } from "./file-reader";
|
|
12
12
|
import { editToolSchema, buildEditToolSchema, type ReqParams, assertReq, normReq } from "./payload-contract";
|
|
13
|
-
import { splitLines } from "./utils";
|
|
13
|
+
import { literalEscapeHint, splitLines } from "./utils";
|
|
14
14
|
import { loadP, loadGuide } from "./prompts";
|
|
15
15
|
import { type FileIdentity } from "./fs-write";
|
|
16
16
|
import { applyEdit,
|
|
@@ -21,6 +21,7 @@ import { applyEdit,
|
|
|
21
21
|
AnchorMismatchError,
|
|
22
22
|
type HEdit,
|
|
23
23
|
type NEdit,
|
|
24
|
+
type StripWarningLocation,
|
|
24
25
|
} from "./hashline";
|
|
25
26
|
import { type RMetrics } from "./replace-response";
|
|
26
27
|
import {
|
|
@@ -47,6 +48,7 @@ export type ReplaceDetails = {
|
|
|
47
48
|
metrics?: RMetrics;
|
|
48
49
|
diffLineNumbers?: (number | null)[];
|
|
49
50
|
warnings?: string[];
|
|
51
|
+
hints?: string[];
|
|
50
52
|
batch?: { id: number; size: number; last: boolean; total: number; aborted?: boolean; abortMessage?: string };
|
|
51
53
|
};
|
|
52
54
|
|
|
@@ -56,6 +58,7 @@ export interface PipelineResult {
|
|
|
56
58
|
result: string;
|
|
57
59
|
bom: string;
|
|
58
60
|
originalEnding: LineEnding;
|
|
61
|
+
originalSeparators: LineEnding[];
|
|
59
62
|
hadUtf8DecodeErrors: boolean;
|
|
60
63
|
warnings: string[];
|
|
61
64
|
noopEdit?: NEdit;
|
|
@@ -67,6 +70,7 @@ export interface PipelineResult {
|
|
|
67
70
|
totalRemovedLines: number;
|
|
68
71
|
identity: FileIdentity;
|
|
69
72
|
spans?: DiffSpan[];
|
|
73
|
+
contentSeparators?: (LineEnding | undefined)[];
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
|
|
@@ -76,6 +80,9 @@ export interface ExecPipelineOptions {
|
|
|
76
80
|
store?: HashStore;
|
|
77
81
|
noPersist?: boolean;
|
|
78
82
|
preloadedNorm?: NormFile;
|
|
83
|
+
served?: ReadonlyMap<string, string>;
|
|
84
|
+
allowEmpty?: boolean;
|
|
85
|
+
stripWarning?: StripWarningLocation;
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
export function hashSpan(hashes: string[], from: string, to: string): [number, number] | undefined {
|
|
@@ -91,7 +98,7 @@ export function spanForEdit(originalHashes: string[], from: string, to: string,
|
|
|
91
98
|
const replacementCount = splitLines(resultContent).length - (originalHashes.length - (span[1] - span[0] + 1));
|
|
92
99
|
return { start: span[0], end: span[1], replacementCount };
|
|
93
100
|
}
|
|
94
|
-
async function noteAnchorError(absolutePath: string, error: unknown, noPersist?: boolean): Promise<void> {
|
|
101
|
+
export async function noteAnchorError(absolutePath: string, error: unknown, noPersist?: boolean): Promise<void> {
|
|
95
102
|
if (noPersist === true) return;
|
|
96
103
|
if (error instanceof RangeStaleError) {
|
|
97
104
|
adoptAnchors(absolutePath, error.rangeServedMap);
|
|
@@ -137,8 +144,8 @@ export async function execPipeline(
|
|
|
137
144
|
const { edit, warnings: editWarnings } = buildReplaceHEdit(params);
|
|
138
145
|
const hashStore = options?.store ?? await loadHashStore();
|
|
139
146
|
const preResolvedPath = await resolveTarget(toCwd(targetPath, cwd));
|
|
140
|
-
const served = servedForPath(preResolvedPath);
|
|
141
|
-
const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath, identity } = await readNormFile(
|
|
147
|
+
const served = options?.served ?? servedForPath(preResolvedPath);
|
|
148
|
+
const { normalized: originalNormalized, bom, originalEnding, endingSeparators: originalSeparators, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath, identity } = await readNormFile(
|
|
142
149
|
targetPath, cwd, { signal: options?.signal, accessMode: options?.accessMode, maxLines: MAX_HASH_LINES, store: hashStore, noPersist: options?.noPersist, allocation: options?.noPersist ? "shadow" : "real", preloadedNorm: options?.preloadedNorm },
|
|
143
150
|
);
|
|
144
151
|
const displayPath = toDisplayPath(cwd, absolutePath, targetPath);
|
|
@@ -152,6 +159,8 @@ export async function execPipeline(
|
|
|
152
159
|
originalHashes,
|
|
153
160
|
displayPath,
|
|
154
161
|
served,
|
|
162
|
+
options?.allowEmpty,
|
|
163
|
+
options?.stripWarning,
|
|
155
164
|
);
|
|
156
165
|
} catch (error) {
|
|
157
166
|
await noteAnchorError(absolutePath, error, options?.noPersist);
|
|
@@ -181,6 +190,7 @@ export async function execPipeline(
|
|
|
181
190
|
result,
|
|
182
191
|
bom,
|
|
183
192
|
originalEnding,
|
|
193
|
+
originalSeparators,
|
|
184
194
|
hadUtf8DecodeErrors,
|
|
185
195
|
warnings,
|
|
186
196
|
noopEdit: anchorResult.noopEdit,
|
|
@@ -192,6 +202,7 @@ export async function execPipeline(
|
|
|
192
202
|
totalRemovedLines,
|
|
193
203
|
identity,
|
|
194
204
|
...(pipeSpans ? { spans: pipeSpans } : {}),
|
|
205
|
+
...(edit.content_separators !== undefined ? { contentSeparators: edit.content_separators } : {}),
|
|
195
206
|
};
|
|
196
207
|
}
|
|
197
208
|
|
|
@@ -263,6 +274,7 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
|
|
|
263
274
|
const canonical = normReq(params);
|
|
264
275
|
assertReq(canonical);
|
|
265
276
|
const normalizedParams = canonical;
|
|
277
|
+
const literalEscape = literalEscapeHint(normalizedParams.replacement_lines, "replacement_lines");
|
|
266
278
|
const targetPath = await resolveEditTargetWithRequirement({
|
|
267
279
|
removeFrom: normalizedParams.remove_from,
|
|
268
280
|
removeTo: normalizedParams.remove_to,
|
|
@@ -287,6 +299,7 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
|
|
|
287
299
|
absolutePath,
|
|
288
300
|
mutationTargetPath,
|
|
289
301
|
editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
|
|
302
|
+
prefixWarnings: literalEscape !== undefined ? [literalEscape] : [],
|
|
290
303
|
signal,
|
|
291
304
|
});
|
|
292
305
|
}
|
|
@@ -305,7 +318,7 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
|
|
|
305
318
|
cwd: ctx.cwd,
|
|
306
319
|
signal,
|
|
307
320
|
hedit: built.edit,
|
|
308
|
-
extraWarnings: built.warnings,
|
|
321
|
+
extraWarnings: [...(literalEscape !== undefined ? [literalEscape] : []), ...built.warnings],
|
|
309
322
|
});
|
|
310
323
|
});
|
|
311
324
|
});
|
package/src/utils.ts
CHANGED
|
@@ -342,10 +342,6 @@ export function decodeStringArray(value: unknown, warnings?: string[], label = "
|
|
|
342
342
|
return undefined;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
function splitEditLines(text: string): string[] {
|
|
346
|
-
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
347
|
-
}
|
|
348
|
-
|
|
349
345
|
function normalizeLineFieldValue(value: unknown): string[] | undefined {
|
|
350
346
|
const candidate = typeof value === "string"
|
|
351
347
|
? value
|
|
@@ -356,7 +352,7 @@ function normalizeLineFieldValue(value: unknown): string[] | undefined {
|
|
|
356
352
|
const decoded = decodeStringArray(candidate);
|
|
357
353
|
if (decoded !== undefined) return decoded;
|
|
358
354
|
if (/^\[\s*\]$/.test(stripTrailingMemberCall(stripCodeFence(candidate)))) return [];
|
|
359
|
-
return
|
|
355
|
+
return [candidate];
|
|
360
356
|
}
|
|
361
357
|
|
|
362
358
|
function normalizeEditLines(record: Record<string, unknown>): void {
|
|
@@ -366,3 +362,42 @@ function normalizeEditLines(record: Record<string, unknown>): void {
|
|
|
366
362
|
if (lines !== undefined) record[key] = lines;
|
|
367
363
|
}
|
|
368
364
|
}
|
|
365
|
+
|
|
366
|
+
const LITERAL_ESCAPE_RE = /\\(?:u([0-9a-fA-F]{4})|([ntr"]))/g;
|
|
367
|
+
|
|
368
|
+
const REAL_LINE_BREAK_RE = /[\n\r]/;
|
|
369
|
+
|
|
370
|
+
function isSurrogateEscapePair(line: string, index: number, hex: string): boolean {
|
|
371
|
+
const code = Number.parseInt(hex, 16);
|
|
372
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
373
|
+
const next = /^\\u([0-9a-fA-F]{4})/.exec(line.slice(index + 6))?.[1];
|
|
374
|
+
if (next === undefined) return false;
|
|
375
|
+
const nextCode = Number.parseInt(next, 16);
|
|
376
|
+
return nextCode >= 0xdc00 && nextCode <= 0xdfff;
|
|
377
|
+
}
|
|
378
|
+
if (code >= 0xdc00 && code <= 0xdfff) {
|
|
379
|
+
const previous = /\\u([0-9a-fA-F]{4})$/.exec(line.slice(0, index))?.[1];
|
|
380
|
+
if (previous === undefined) return false;
|
|
381
|
+
const previousCode = Number.parseInt(previous, 16);
|
|
382
|
+
return previousCode >= 0xd800 && previousCode <= 0xdbff;
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function literalEscapeHint(lines: string[], label: string): string | undefined {
|
|
388
|
+
for (const line of lines) {
|
|
389
|
+
if (!line.includes("\\")) continue;
|
|
390
|
+
const hasRealBreak = REAL_LINE_BREAK_RE.test(line);
|
|
391
|
+
for (const match of line.matchAll(LITERAL_ESCAPE_RE)) {
|
|
392
|
+
const hex = match[1];
|
|
393
|
+
const simple = match[2];
|
|
394
|
+
if (simple !== undefined && hasRealBreak) continue;
|
|
395
|
+
if (hex !== undefined) {
|
|
396
|
+
if (hex.toLowerCase() === "dddd") continue;
|
|
397
|
+
if (isSurrogateEscapePair(line, match.index, hex)) continue;
|
|
398
|
+
}
|
|
399
|
+
return `[H_LITERAL_ESCAPE] "${label}" contains the literal escaped text "${match[0]}"`;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|