pi-hashline-edit-pro 6.1.2 → 6.3.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 +41 -23
- package/index.ts +20 -12
- package/package.json +3 -2
- package/src/anchor-registry.ts +22 -5
- package/src/auto-read-all.ts +39 -21
- package/src/batch.ts +127 -41
- package/src/commit.ts +1 -1
- package/src/config-ui.ts +135 -55
- package/src/config.ts +27 -9
- package/src/copy-move.ts +61 -72
- package/src/edit-common.ts +6 -4
- package/src/grep.ts +53 -121
- package/src/hash-store.ts +13 -12
- package/src/hashline/resolve.ts +13 -2
- package/src/insert.ts +40 -34
- package/src/line-endings.ts +10 -1
- package/src/outline/index.ts +46 -0
- package/src/outline/parsers.ts +42 -0
- package/src/outline/render.ts +170 -0
- package/src/outline/vendor/LICENSE +201 -0
- package/src/outline/vendor/README.md +17 -0
- package/src/outline/vendor/parsers/blade.ts +138 -0
- package/src/outline/vendor/parsers/index.ts +75 -0
- package/src/outline/vendor/parsers/java.ts +501 -0
- package/src/outline/vendor/parsers/manager.ts +1721 -0
- package/src/outline/vendor/parsers/php.ts +332 -0
- package/src/outline/vendor/parsers/sass.ts +416 -0
- package/src/outline/vendor/parsers/scss.ts +566 -0
- package/src/outline/vendor/types.ts +82 -0
- package/src/outline/vendor/wasm/LICENSES.md +37 -0
- package/src/outline/vendor/wasm/tree-sitter-blade.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-go.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-java.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-javascript.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-php.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-python.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-rust.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-sass.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-scss.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-solidity.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-tsx.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-typescript.wasm +0 -0
- package/src/read.ts +118 -41
- package/src/replace-match.ts +42 -41
- package/src/replace-undo.ts +4 -4
- package/src/replace.ts +30 -19
- package/tool-prompts/read-guidelines.md +3 -0
- package/{prompts → tool-prompts}/read.md +2 -0
- package/prompts/read-guidelines.md +0 -1
- package/src/auto-read-all-state.ts +0 -27
- /package/{prompts → tool-prompts}/copy-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/copy-snippet.md +0 -0
- /package/{prompts → tool-prompts}/copy.md +0 -0
- /package/{prompts → tool-prompts}/grep-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/grep-snippet.md +0 -0
- /package/{prompts → tool-prompts}/grep.md +0 -0
- /package/{prompts → tool-prompts}/insert-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/insert-snippet.md +0 -0
- /package/{prompts → tool-prompts}/insert.md +0 -0
- /package/{prompts → tool-prompts}/move-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/move-snippet.md +0 -0
- /package/{prompts → tool-prompts}/move.md +0 -0
- /package/{prompts → tool-prompts}/read-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/replace-match-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/replace-match-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace-match.md +0 -0
- /package/{prompts → tool-prompts}/replace-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace.md +0 -0
- /package/{prompts → tool-prompts}/transfer-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change-snippet.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change.md +0 -0
package/src/config.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import { configPath } from "./paths";
|
|
4
4
|
import { errCode, isRec } from "./utils";
|
|
5
5
|
import { writeAtomic } from "./fs-write";
|
|
6
|
-
export type AutoReadAllMode = "off" | "
|
|
7
|
-
const AUTO_READ_ALL_MODES: AutoReadAllMode[] = ["off", "
|
|
6
|
+
export type AutoReadAllMode = "off" | "outline" | "full";
|
|
7
|
+
const AUTO_READ_ALL_MODES: AutoReadAllMode[] = ["off", "outline", "full"];
|
|
8
8
|
|
|
9
9
|
export type ReadOnDisabledModels = "remove" | "vanilla";
|
|
10
10
|
const READ_ON_DISABLED_MODELS: ReadOnDisabledModels[] = ["remove", "vanilla"];
|
|
@@ -19,6 +19,7 @@ export interface Config {
|
|
|
19
19
|
copyMoveEnabled?: boolean;
|
|
20
20
|
replaceMatchEnabled?: boolean;
|
|
21
21
|
autoReadAll?: AutoReadAllMode;
|
|
22
|
+
autoReadAllRequireGit?: boolean;
|
|
22
23
|
autoReadAllIgnore?: string[];
|
|
23
24
|
requirePath?: boolean;
|
|
24
25
|
strictInput?: boolean;
|
|
@@ -33,6 +34,7 @@ const DEFAULT_CONFIG: Config = {
|
|
|
33
34
|
copyMoveEnabled: true,
|
|
34
35
|
replaceMatchEnabled: true,
|
|
35
36
|
autoReadAll: "off",
|
|
37
|
+
autoReadAllRequireGit: true,
|
|
36
38
|
autoReadAllIgnore: [],
|
|
37
39
|
requirePath: false,
|
|
38
40
|
strictInput: false,
|
|
@@ -42,12 +44,21 @@ const DEFAULT_CONFIG: Config = {
|
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
function parseAutoReadAllMode(value: unknown): AutoReadAllMode {
|
|
45
|
-
if (value === "off" || value === "
|
|
46
|
-
if (value ===
|
|
47
|
+
if (value === "off" || value === "outline" || value === "full") return value;
|
|
48
|
+
if (value === "outline(git)") return "outline";
|
|
49
|
+
if (value === "git") return "full";
|
|
50
|
+
if (value === "on" || value === true) return "full";
|
|
47
51
|
if (value === false) return "off";
|
|
48
52
|
return DEFAULT_CONFIG.autoReadAll ?? "off";
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
function parseAutoReadAllRequireGit(value: unknown, legacyMode: unknown): boolean {
|
|
56
|
+
if (typeof value === "boolean") return value;
|
|
57
|
+
if (legacyMode === "git" || legacyMode === "outline(git)") return true;
|
|
58
|
+
if (legacyMode === "full" || legacyMode === "outline" || legacyMode === "on" || legacyMode === true) return false;
|
|
59
|
+
return DEFAULT_CONFIG.autoReadAllRequireGit ?? true;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
function parseReadOnDisabledModels(value: unknown, legacy: unknown): ReadOnDisabledModels {
|
|
52
63
|
if (value === "remove" || value === "vanilla") return value;
|
|
53
64
|
if (typeof legacy === "boolean") return legacy ? "remove" : "vanilla";
|
|
@@ -100,6 +111,7 @@ function parseConfig(content: string): Config {
|
|
|
100
111
|
const copyMoveEnabled = parsed.copyMoveEnabled;
|
|
101
112
|
const replaceMatchEnabled = parsed.replaceMatchEnabled;
|
|
102
113
|
const autoReadAll = parsed.autoReadAll;
|
|
114
|
+
const autoReadAllRequireGit = parsed.autoReadAllRequireGit;
|
|
103
115
|
const requirePath = parsed.requirePath;
|
|
104
116
|
const strictInput = parsed.strictInput;
|
|
105
117
|
const diffContextLines = parsed.diffContextLines;
|
|
@@ -113,6 +125,7 @@ function parseConfig(content: string): Config {
|
|
|
113
125
|
copyMoveEnabled: typeof copyMoveEnabled === "boolean" ? copyMoveEnabled : DEFAULT_CONFIG.copyMoveEnabled,
|
|
114
126
|
replaceMatchEnabled: typeof replaceMatchEnabled === "boolean" ? replaceMatchEnabled : DEFAULT_CONFIG.replaceMatchEnabled,
|
|
115
127
|
autoReadAll: parseAutoReadAllMode(autoReadAll),
|
|
128
|
+
autoReadAllRequireGit: parseAutoReadAllRequireGit(autoReadAllRequireGit, autoReadAll),
|
|
116
129
|
requirePath: typeof requirePath === "boolean" ? requirePath : DEFAULT_CONFIG.requirePath,
|
|
117
130
|
strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
|
|
118
131
|
diffContextLines: normalizeDiffContextLines(diffContextLines),
|
|
@@ -226,30 +239,35 @@ export async function writeConfig(config: Config): Promise<void> {
|
|
|
226
239
|
}
|
|
227
240
|
|
|
228
241
|
|
|
229
|
-
type ToggleKey = "autoRead" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput";
|
|
242
|
+
type ToggleKey = "autoRead" | "autoReadAllRequireGit" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput";
|
|
230
243
|
|
|
231
244
|
async function toggleFlag(key: ToggleKey): Promise<boolean> {
|
|
232
245
|
const config = await updateConfig((c) => { c[key] = !(c[key] === true); });
|
|
233
246
|
return config[key] === true;
|
|
234
247
|
}
|
|
235
248
|
export const toggleAutoRead = (): Promise<boolean> => toggleFlag("autoRead");
|
|
249
|
+
export const toggleAutoReadAllRequireGit = (): Promise<boolean> => toggleFlag("autoReadAllRequireGit");
|
|
236
250
|
export const toggleAnchorGrep = (): Promise<boolean> => toggleFlag("anchorGrepEnabled");
|
|
237
251
|
export const toggleCopyMove = (): Promise<boolean> => toggleFlag("copyMoveEnabled");
|
|
238
252
|
export const toggleReplaceMatch = (): Promise<boolean> => toggleFlag("replaceMatchEnabled");
|
|
239
|
-
|
|
253
|
+
function steppedIndex(length: number, index: number, delta: number): number {
|
|
254
|
+
return ((index + delta) % length + length) % length;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function cycleReadOnDisabledModels(delta = 1): Promise<ReadOnDisabledModels> {
|
|
240
258
|
let next: ReadOnDisabledModels = "vanilla";
|
|
241
259
|
await updateConfig((c) => {
|
|
242
260
|
const current = c.readOnDisabledModels ?? "vanilla";
|
|
243
|
-
next = READ_ON_DISABLED_MODELS[(READ_ON_DISABLED_MODELS.indexOf(current)
|
|
261
|
+
next = READ_ON_DISABLED_MODELS[steppedIndex(READ_ON_DISABLED_MODELS.length, READ_ON_DISABLED_MODELS.indexOf(current), delta)] ?? "remove";
|
|
244
262
|
c.readOnDisabledModels = next;
|
|
245
263
|
});
|
|
246
264
|
return next;
|
|
247
265
|
}
|
|
248
|
-
export async function cycleAutoReadAllMode(): Promise<AutoReadAllMode> {
|
|
266
|
+
export async function cycleAutoReadAllMode(delta = 1): Promise<AutoReadAllMode> {
|
|
249
267
|
let next: AutoReadAllMode = "off";
|
|
250
268
|
await updateConfig((c) => {
|
|
251
269
|
const current = c.autoReadAll ?? "off";
|
|
252
|
-
next = AUTO_READ_ALL_MODES[(AUTO_READ_ALL_MODES.indexOf(current)
|
|
270
|
+
next = AUTO_READ_ALL_MODES[steppedIndex(AUTO_READ_ALL_MODES.length, AUTO_READ_ALL_MODES.indexOf(current), delta)] ?? "off";
|
|
253
271
|
c.autoReadAll = next;
|
|
254
272
|
});
|
|
255
273
|
return next;
|
package/src/copy-move.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
lineHashes,
|
|
10
10
|
MAX_HASH_LINES,
|
|
11
11
|
parseHashRef,
|
|
12
|
+
planEdit,
|
|
13
|
+
preserveDeletionSeparators,
|
|
12
14
|
resEdit,
|
|
13
15
|
resolveAnchorLine,
|
|
14
16
|
stripAnchorRow,
|
|
@@ -17,7 +19,7 @@ import {
|
|
|
17
19
|
type HTEdit,
|
|
18
20
|
} from "./hashline";
|
|
19
21
|
import { formatAnchorReclaimNotice, servedForPath, takeReclaimedPaths, withAnchorSession } from "./anchor-registry";
|
|
20
|
-
import { batchMemberFor, batchServedFor, ensureBatchBase, executeBatchMember, noteBatchFailure, pendingBatchMemberFor, type BatchBase, type PlannedMember } from "./batch";
|
|
22
|
+
import { batchMemberFor, batchServedFor, ensureBatchBase, executeBatchMember, noteBatchFailure, pendingBatchMemberFor, registerBatchPlanner, type BatchBase, type BatchMemberPlanner, type PlannedMember } from "./batch";
|
|
21
23
|
import { loadP, loadGuide } from "./prompts";
|
|
22
24
|
import { assertTransferReq, normReq, type TransferReq } from "./payload-contract";
|
|
23
25
|
import { abortIf, assertByteLimit, isRec, splitLines } from "./utils";
|
|
@@ -332,13 +334,13 @@ async function commitMovePair(input: {
|
|
|
332
334
|
const destinationSpan = input.destination.pipe.spans?.[0];
|
|
333
335
|
const sourceSpan = input.source.pipe.spans?.[0];
|
|
334
336
|
const destinationSeparators = destinationSpan
|
|
335
|
-
? separatorsForSpans(input.destination.pipe.originalSeparators, input.destination.pipe.originalHashes.length, [destinationSpan], input.destination.pipe.result, input.destination.pipe.originalEnding)
|
|
337
|
+
? separatorsForSpans(input.destination.pipe.originalSeparators, input.destination.pipe.originalHashes.length, [destinationSpan], input.destination.pipe.result, input.destination.pipe.originalEnding, splitLines(input.destination.pipe.originalNormalized))
|
|
336
338
|
: undefined;
|
|
337
339
|
if (destinationSeparators !== undefined && destinationSpan !== undefined) {
|
|
338
340
|
applyEndingOverrides(destinationSeparators, destinationSpan.start, input.destination.endingOverrides);
|
|
339
341
|
}
|
|
340
342
|
const sourceSeparators = sourceSpan
|
|
341
|
-
? separatorsForSpans(input.source.pipe.originalSeparators, input.source.pipe.originalHashes.length, [sourceSpan], input.source.pipe.result, input.source.pipe.originalEnding)
|
|
343
|
+
? separatorsForSpans(input.source.pipe.originalSeparators, input.source.pipe.originalHashes.length, [sourceSpan], input.source.pipe.result, input.source.pipe.originalEnding, splitLines(input.source.pipe.originalNormalized))
|
|
342
344
|
: undefined;
|
|
343
345
|
const destinationBytes = input.destination.pipe.bom + (destinationSeparators !== undefined
|
|
344
346
|
? joinSeparators(input.destination.pipe.result, destinationSeparators)
|
|
@@ -600,50 +602,38 @@ function preloadFromBase(base: BatchBase): NormFile {
|
|
|
600
602
|
};
|
|
601
603
|
}
|
|
602
604
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
return executeBatchMember({
|
|
632
|
-
kind,
|
|
633
|
-
member,
|
|
634
|
-
targetPath,
|
|
635
|
-
mutationTargetPath,
|
|
636
|
-
cwd,
|
|
637
|
-
signal,
|
|
638
|
-
hedit,
|
|
639
|
-
extraWarnings: [...warnings, ...resWarnings],
|
|
640
|
-
foldedLines: plan.foldedAnchorLines,
|
|
641
|
-
...(plan.anchorCarry !== undefined ? { carryIndex: plan.anchorCarry } : {}),
|
|
642
|
-
...(plan.servedOverride !== undefined ? { servedOverride: plan.servedOverride } : {}),
|
|
643
|
-
...(plan.endingOverrides !== undefined ? { contentSeparators: plan.endingOverrides } : {}),
|
|
644
|
-
});
|
|
605
|
+
function planTransferBatchMember(kind: TransferKind): BatchMemberPlanner {
|
|
606
|
+
return (input) => {
|
|
607
|
+
if (input.member.sourceTarget !== undefined && input.member.sourceTarget !== input.member.target) return undefined;
|
|
608
|
+
const req = normReq(input.member.args);
|
|
609
|
+
assertTransferReq(req);
|
|
610
|
+
const { refs, warnings } = parseTransferAnchors(req);
|
|
611
|
+
const preload = preloadFromBase(input.base);
|
|
612
|
+
const transferPlan = buildTransferEdit({ kind, refs, preload, displayPath: input.displayPath, served: input.served });
|
|
613
|
+
const resWarnings: string[] = [];
|
|
614
|
+
const hedit = resEdit(transferPlan.editParams, resWarnings);
|
|
615
|
+
const effectiveHedit = preserveDeletionSeparators(hedit, input.base.baseLines, input.base.hashes);
|
|
616
|
+
const planned = planEdit(input.base.content, effectiveHedit, input.base.hashes, {
|
|
617
|
+
filePath: input.displayPath,
|
|
618
|
+
servedHashes: transferPlan.servedOverride ?? input.served,
|
|
619
|
+
signal: input.signal,
|
|
620
|
+
baseFileLines: input.base.baseLines,
|
|
621
|
+
});
|
|
622
|
+
return {
|
|
623
|
+
hedit,
|
|
624
|
+
warnings: [...warnings, ...resWarnings],
|
|
625
|
+
planned,
|
|
626
|
+
foldedLines: transferPlan.foldedAnchorLines,
|
|
627
|
+
...(transferPlan.anchorCarry !== undefined ? { carryIndex: transferPlan.anchorCarry } : {}),
|
|
628
|
+
...(transferPlan.servedOverride !== undefined ? { servedOverride: transferPlan.servedOverride } : {}),
|
|
629
|
+
...(transferPlan.endingOverrides !== undefined ? { contentSeparators: transferPlan.endingOverrides } : {}),
|
|
630
|
+
};
|
|
631
|
+
};
|
|
645
632
|
}
|
|
646
633
|
|
|
634
|
+
registerBatchPlanner("copy", planTransferBatchMember("copy"));
|
|
635
|
+
registerBatchPlanner("move", planTransferBatchMember("move"));
|
|
636
|
+
|
|
647
637
|
async function executeBatchCrossCopy(
|
|
648
638
|
member: PlannedMember,
|
|
649
639
|
refs: TransferRefs,
|
|
@@ -687,17 +677,18 @@ async function executeBatchCrossCopy(
|
|
|
687
677
|
throw error;
|
|
688
678
|
}
|
|
689
679
|
return executeBatchMember({
|
|
690
|
-
kind: "copy",
|
|
691
680
|
member,
|
|
692
681
|
targetPath: destinationPath,
|
|
693
682
|
mutationTargetPath,
|
|
694
683
|
cwd,
|
|
695
684
|
signal,
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
685
|
+
plan: {
|
|
686
|
+
hedit,
|
|
687
|
+
warnings: [...warnings, ...resWarnings],
|
|
688
|
+
foldedLines: prepared.destinationFolded,
|
|
689
|
+
...(prepared.destinationFolded === 1 ? { carryIndex: 0 } : {}),
|
|
690
|
+
contentSeparators: prepared.endingOverrides,
|
|
691
|
+
},
|
|
701
692
|
});
|
|
702
693
|
}
|
|
703
694
|
|
|
@@ -751,21 +742,22 @@ async function executeBatchCrossMove(
|
|
|
751
742
|
...(sourcePipe.hadUtf8DecodeErrors ? ["Non-UTF-8 bytes were shown as U+FFFD; this edit rewrote the file as UTF-8."] : []),
|
|
752
743
|
];
|
|
753
744
|
return executeBatchMember({
|
|
754
|
-
kind: "move",
|
|
755
745
|
member,
|
|
756
746
|
targetPath: destinationPath,
|
|
757
747
|
mutationTargetPath,
|
|
758
748
|
cwd,
|
|
759
749
|
signal,
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
750
|
+
plan: {
|
|
751
|
+
hedit,
|
|
752
|
+
warnings: [...warnings, ...resWarnings, ...sourceWarnings],
|
|
753
|
+
foldedLines: prepared.destinationFolded,
|
|
754
|
+
...(prepared.destinationFolded === 1 ? { carryIndex: 0 } : {}),
|
|
755
|
+
contentSeparators: prepared.endingOverrides,
|
|
756
|
+
sourceMove: {
|
|
757
|
+
displayPath: prepared.sourceDisplay,
|
|
758
|
+
mutationTargetPath: prepared.sourcePreload.absolutePath,
|
|
759
|
+
pipe: sourcePipe,
|
|
760
|
+
},
|
|
769
761
|
},
|
|
770
762
|
});
|
|
771
763
|
}
|
|
@@ -900,11 +892,11 @@ type TransferToolDef = ToolDefinition<any, ReplaceDetails, RRState> & { renderSh
|
|
|
900
892
|
export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): TransferToolDef {
|
|
901
893
|
const prompted = withTransferPrompts(
|
|
902
894
|
{
|
|
903
|
-
description: loadP(`../prompts/${kind}.md`),
|
|
904
|
-
snippet: loadP(`../prompts/${kind}-snippet.md`),
|
|
895
|
+
description: loadP(`../tool-prompts/${kind}.md`),
|
|
896
|
+
snippet: loadP(`../tool-prompts/${kind}-snippet.md`),
|
|
905
897
|
guidelines: [
|
|
906
|
-
...loadGuide(`../prompts/${kind}-guidelines.md`),
|
|
907
|
-
...loadGuide("../prompts/transfer-guidelines.md"),
|
|
898
|
+
...loadGuide(`../tool-prompts/${kind}-guidelines.md`),
|
|
899
|
+
...loadGuide("../tool-prompts/transfer-guidelines.md"),
|
|
908
900
|
],
|
|
909
901
|
},
|
|
910
902
|
flags,
|
|
@@ -932,20 +924,20 @@ export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags =
|
|
|
932
924
|
const canonical = normReq(params);
|
|
933
925
|
assertTransferReq(canonical);
|
|
934
926
|
const req = canonical;
|
|
927
|
+
const member = batchMemberFor(_toolCallId);
|
|
935
928
|
let refs: TransferRefs;
|
|
936
929
|
let warnings: string[];
|
|
937
930
|
try {
|
|
938
931
|
({ refs, warnings } = parseTransferAnchors(req));
|
|
939
|
-
|
|
932
|
+
const sameFileMember = member !== undefined && (member.sourceTarget === undefined || member.sourceTarget === member.target);
|
|
933
|
+
if (!sameFileMember) await throwIfStrictInput(warnings);
|
|
940
934
|
} catch (error) {
|
|
941
|
-
const member = batchMemberFor(_toolCallId);
|
|
942
935
|
if (member) noteBatchFailure(member, error);
|
|
943
936
|
throw error;
|
|
944
937
|
}
|
|
945
938
|
let sourcePath: string;
|
|
946
939
|
let destinationPath: string;
|
|
947
940
|
try {
|
|
948
|
-
const member = batchMemberFor(_toolCallId);
|
|
949
941
|
if (member) {
|
|
950
942
|
await assertTransferPathOption(req, ctx.cwd, member);
|
|
951
943
|
sourcePath = member.sourceTarget ?? member.target;
|
|
@@ -954,15 +946,13 @@ export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags =
|
|
|
954
946
|
({ sourcePath, destinationPath } = await resolveTransferTargets(req, ctx.cwd));
|
|
955
947
|
}
|
|
956
948
|
} catch (error) {
|
|
957
|
-
const member = batchMemberFor(_toolCallId);
|
|
958
949
|
if (member) noteBatchFailure(member, error);
|
|
959
950
|
throw error;
|
|
960
951
|
}
|
|
961
952
|
if (sourcePath === destinationPath) {
|
|
962
953
|
return queuedEdit(sourcePath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
|
|
963
|
-
const member = batchMemberFor(_toolCallId);
|
|
964
954
|
if (member) {
|
|
965
|
-
return
|
|
955
|
+
return executeBatchMember({ member, targetPath: sourcePath, mutationTargetPath, cwd: ctx.cwd, signal });
|
|
966
956
|
}
|
|
967
957
|
const preload = await readNormFile(sourcePath, ctx.cwd, {
|
|
968
958
|
signal,
|
|
@@ -999,7 +989,6 @@ export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags =
|
|
|
999
989
|
});
|
|
1000
990
|
});
|
|
1001
991
|
}
|
|
1002
|
-
const member = batchMemberFor(_toolCallId);
|
|
1003
992
|
if (member) {
|
|
1004
993
|
return queuedEdit(destinationPath, ctx.cwd, signal, async (_absolutePath, mutationTargetPath) => {
|
|
1005
994
|
return kind === "copy"
|
package/src/edit-common.ts
CHANGED
|
@@ -66,7 +66,7 @@ function batchGuideline(flags: EditToolFlags): string {
|
|
|
66
66
|
const tools = operationNames(SHARED_EDIT_OPS, flags);
|
|
67
67
|
const outcome = flags.autoRead ? "diff" : "result";
|
|
68
68
|
const script = flags.codemode ? ` ${SCRIPT_BATCH_GUIDELINE}` : "";
|
|
69
|
-
return `${tools}: same-file calls in one message are grouped into one batch; earlier calls reply \`In batch N (queued)\` and the last call shows the combined ${outcome}, with one undo for the whole batch.${script}`;
|
|
69
|
+
return `${tools}: same-file calls in one message are grouped into one batch; earlier calls reply \`In batch N (queued)\` and the last applied call shows the combined ${outcome}, with one undo for the whole batch. A call whose own request is invalid (anchors, shape, or \`old_string\`) fails on its own and is left out of the batch; the rest still commits.${script}`;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function diffGuideline(flags: EditToolFlags): string {
|
|
@@ -233,6 +233,7 @@ export interface PathRequirementInput {
|
|
|
233
233
|
anchor?: string;
|
|
234
234
|
providedPath?: unknown;
|
|
235
235
|
cwd: string;
|
|
236
|
+
anchorTarget?: string;
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
export async function resolveEditTargetWithRequirement(input: PathRequirementInput): Promise<string> {
|
|
@@ -243,9 +244,10 @@ export async function resolveEditTargetWithRequirement(input: PathRequirementInp
|
|
|
243
244
|
if (requirePath && (typeof input.providedPath !== "string" || input.providedPath.length === 0)) {
|
|
244
245
|
throw new Error('[E_BAD_SHAPE] Edit request requires a non-empty "path" string when require-path mode is on. Provide `path` matching the file the anchors were served for.');
|
|
245
246
|
}
|
|
246
|
-
const anchorTarget =
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
const anchorTarget = input.anchorTarget
|
|
248
|
+
?? (typeof input.anchor === "string"
|
|
249
|
+
? resolveEditTarget(input.anchor)
|
|
250
|
+
: resolveEditTarget(input.removeFrom as string, input.removeTo));
|
|
249
251
|
if (requirePath) {
|
|
250
252
|
const { resolved } = await resolveInCwd(input.providedPath as string, input.cwd);
|
|
251
253
|
if (resolved !== anchorTarget) {
|
package/src/grep.ts
CHANGED
|
@@ -66,104 +66,22 @@ function compileGrepGlob(glob: string): RegExp {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
function regexSource(pattern: string, literal: boolean): string {
|
|
70
|
+
return literal ? pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : pattern;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertPattern(pattern: string, literal: boolean, ignoreCase: boolean): void {
|
|
72
74
|
try {
|
|
73
|
-
|
|
75
|
+
new RegExp(regexSource(pattern, literal), ignoreCase ? "ui" : "u");
|
|
74
76
|
} catch {
|
|
75
77
|
throw new Error(`[E_BAD_SHAPE] Invalid pattern: ${pattern}`);
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
interface
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
function unsafeRegex(pattern: string): never {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`[E_UNSAFE_REGEX] Refusing potentially exponential regex: ${pattern}. Use literal: true or simplify the expression.`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function assertSafeRegex(pattern: string): void {
|
|
91
|
-
if (pattern.length > 4096) unsafeRegex(pattern);
|
|
92
|
-
const groups: RegexGroupRisk[] = [];
|
|
93
|
-
let inClass = false;
|
|
94
|
-
let escaped = false;
|
|
95
|
-
let variableQuantifiers = 0;
|
|
96
|
-
let lastAtom: { groupRisky: boolean; quantified: boolean } | undefined;
|
|
97
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
98
|
-
const ch = pattern[i]!;
|
|
99
|
-
if (escaped) {
|
|
100
|
-
if (!inClass && (/[1-9]/.test(ch) || (ch === "k" && pattern[i + 1] === "<"))) {
|
|
101
|
-
unsafeRegex(pattern);
|
|
102
|
-
}
|
|
103
|
-
escaped = false;
|
|
104
|
-
lastAtom = { groupRisky: false, quantified: false };
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (ch === "\\") {
|
|
108
|
-
escaped = true;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (inClass) {
|
|
112
|
-
if (ch === "]") {
|
|
113
|
-
inClass = false;
|
|
114
|
-
lastAtom = { groupRisky: false, quantified: false };
|
|
115
|
-
}
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (ch === "[") {
|
|
119
|
-
inClass = true;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (ch === "(") {
|
|
123
|
-
groups.push({ hasQuantifier: false, hasAlternation: false });
|
|
124
|
-
lastAtom = undefined;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (ch === ")") {
|
|
128
|
-
const group = groups.pop();
|
|
129
|
-
if (group) {
|
|
130
|
-
lastAtom = {
|
|
131
|
-
groupRisky: group.hasQuantifier || group.hasAlternation,
|
|
132
|
-
quantified: false,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (ch === "|") {
|
|
138
|
-
const group = groups.at(-1);
|
|
139
|
-
if (group) group.hasAlternation = true;
|
|
140
|
-
lastAtom = undefined;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
let quantifierLength = 0;
|
|
144
|
-
if (ch === "*" || ch === "+" || ch === "?") {
|
|
145
|
-
quantifierLength = 1;
|
|
146
|
-
} else if (ch === "{") {
|
|
147
|
-
quantifierLength = /^\{\d+(?:,\d*)?\}/.exec(pattern.slice(i))?.[0].length ?? 0;
|
|
148
|
-
}
|
|
149
|
-
if (ch === "{" && quantifierLength > 0) {
|
|
150
|
-
const quant = pattern.slice(i, i + quantifierLength);
|
|
151
|
-
const m = /^\{(\d+)/.exec(quant);
|
|
152
|
-
if (m && Number(m[1]) > 1000) unsafeRegex(pattern);
|
|
153
|
-
}
|
|
154
|
-
if (quantifierLength > 0 && lastAtom) {
|
|
155
|
-
if (ch === "?" && lastAtom.quantified) continue;
|
|
156
|
-
const variable = ch !== "{" || pattern.slice(i, i + quantifierLength).includes(",");
|
|
157
|
-
if (variable && ++variableQuantifiers > 1) unsafeRegex(pattern);
|
|
158
|
-
if (lastAtom.groupRisky) unsafeRegex(pattern);
|
|
159
|
-
const group = groups.at(-1);
|
|
160
|
-
if (group) group.hasQuantifier = true;
|
|
161
|
-
lastAtom.quantified = true;
|
|
162
|
-
i += quantifierLength - 1;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
lastAtom = { groupRisky: false, quantified: false };
|
|
166
|
-
}
|
|
81
|
+
interface RgMatchLine {
|
|
82
|
+
line: number;
|
|
83
|
+
start: number;
|
|
84
|
+
end: number;
|
|
167
85
|
}
|
|
168
86
|
|
|
169
87
|
interface FileHit {
|
|
@@ -199,11 +117,23 @@ function snapCharBoundaries(line: string, start: number, end: number): [number,
|
|
|
199
117
|
return [s, e];
|
|
200
118
|
}
|
|
201
119
|
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
120
|
+
function byteOffsetToCharIndex(line: string, byteOffset: number): number {
|
|
121
|
+
if (byteOffset <= 0) return 0;
|
|
122
|
+
let bytes = 0;
|
|
123
|
+
let index = 0;
|
|
124
|
+
for (const char of line) {
|
|
125
|
+
if (bytes >= byteOffset) return index;
|
|
126
|
+
const codePoint = char.codePointAt(0)!;
|
|
127
|
+
bytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : codePoint < 0x10000 ? 3 : 4;
|
|
128
|
+
index += char.length;
|
|
129
|
+
}
|
|
130
|
+
return index;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function grepMatchFragment(line: string, startByte: number, endByte: number): string {
|
|
206
134
|
const budget = GREP_ROW_CONTENT_BYTES - 6;
|
|
135
|
+
const matchStart = byteOffsetToCharIndex(line, startByte);
|
|
136
|
+
const matchLen = Math.max(0, byteOffsetToCharIndex(line, endByte) - matchStart);
|
|
207
137
|
const half = Math.floor((budget - Math.min(matchLen, budget)) / 2);
|
|
208
138
|
const [start, end] = snapCharBoundaries(line, Math.max(0, matchStart - half), Math.min(line.length, matchStart + matchLen + half));
|
|
209
139
|
const content = truncateToBytes(line.slice(start, end), budget);
|
|
@@ -220,20 +150,21 @@ function grepHeadFragment(line: string): string {
|
|
|
220
150
|
function makeHitFromIndices(
|
|
221
151
|
norm: { normalized: string; fileHashes: string[]; absolutePath: string; hadUtf8DecodeErrors: boolean },
|
|
222
152
|
displayPath: string,
|
|
223
|
-
|
|
153
|
+
matches: RgMatchLine[],
|
|
224
154
|
context: number,
|
|
225
|
-
regex: RegExp | undefined,
|
|
226
155
|
totalMatchCount: number,
|
|
227
156
|
keptMatchCount: number,
|
|
228
157
|
): FileHit {
|
|
229
158
|
const lines = visLines(norm.normalized);
|
|
230
159
|
const shown = new Set<number>();
|
|
231
|
-
const kept =
|
|
232
|
-
for (const
|
|
233
|
-
|
|
160
|
+
const kept = matches.slice(0, keptMatchCount);
|
|
161
|
+
for (const match of kept) {
|
|
162
|
+
const lineIndex = match.line - 1;
|
|
163
|
+
for (let j = Math.max(0, lineIndex - context); j <= Math.min(lines.length - 1, lineIndex + context); j++) shown.add(j);
|
|
234
164
|
}
|
|
235
165
|
const sorted = [...shown].sort((a, b) => a - b);
|
|
236
|
-
const
|
|
166
|
+
const matchByLine = new Map<number, RgMatchLine>();
|
|
167
|
+
for (const match of matches) matchByLine.set(match.line - 1, match);
|
|
237
168
|
const rows: string[] = [];
|
|
238
169
|
const hashes: string[] = [];
|
|
239
170
|
const lineNumbers: number[] = [];
|
|
@@ -244,7 +175,8 @@ function makeHitFromIndices(
|
|
|
244
175
|
const line = lines[idx]!;
|
|
245
176
|
const row = fmtRow(hash, line);
|
|
246
177
|
if (Buffer.byteLength(row, "utf-8") > MAX_GREP_LINE_BYTES) {
|
|
247
|
-
const
|
|
178
|
+
const match = matchByLine.get(idx);
|
|
179
|
+
const content = match !== undefined ? grepMatchFragment(line, match.start, match.end) : grepHeadFragment(line);
|
|
248
180
|
rows.push(fmtRow(hash, content));
|
|
249
181
|
hashes.push(hash);
|
|
250
182
|
lineNumbers.push(idx + 1);
|
|
@@ -270,7 +202,7 @@ function makeHitFromIndices(
|
|
|
270
202
|
totalMatchCount,
|
|
271
203
|
fragmented,
|
|
272
204
|
texts,
|
|
273
|
-
matchLines: kept.map((
|
|
205
|
+
matchLines: kept.map((match) => match.line),
|
|
274
206
|
hadUtf8DecodeErrors: norm.hadUtf8DecodeErrors,
|
|
275
207
|
};
|
|
276
208
|
}
|
|
@@ -330,7 +262,7 @@ async function collectRgMatches(
|
|
|
330
262
|
req: GrepReq,
|
|
331
263
|
signal: AbortSignal | undefined,
|
|
332
264
|
repoRooted: boolean,
|
|
333
|
-
): Promise<Map<string,
|
|
265
|
+
): Promise<Map<string, RgMatchLine[]>> {
|
|
334
266
|
const args = ["--json", "--line-number", "--color=never", "--hidden", "--glob", "!.git"];
|
|
335
267
|
if (!repoRooted) args.push("--no-require-git");
|
|
336
268
|
const wanted = req.limit ?? 100;
|
|
@@ -342,8 +274,8 @@ async function collectRgMatches(
|
|
|
342
274
|
if (req.ignoreCase) args.push("--ignore-case");
|
|
343
275
|
if (req.literal) args.push("--fixed-strings");
|
|
344
276
|
args.push("--", pattern, searchPath);
|
|
345
|
-
const result = new Map<string,
|
|
346
|
-
return await new Promise<Map<string,
|
|
277
|
+
const result = new Map<string, RgMatchLine[]>();
|
|
278
|
+
return await new Promise<Map<string, RgMatchLine[]>>((resolve, reject) => {
|
|
347
279
|
const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
348
280
|
const rl = createInterface({ input: child.stdout });
|
|
349
281
|
let stderr = "";
|
|
@@ -367,7 +299,7 @@ async function collectRgMatches(
|
|
|
367
299
|
};
|
|
368
300
|
rl.on("line", (line) => {
|
|
369
301
|
if (!line.trim()) return;
|
|
370
|
-
let event: { type?: string; data?: { path?: { text?: string }; line_number?: number } };
|
|
302
|
+
let event: { type?: string; data?: { path?: { text?: string }; line_number?: number; submatches?: Array<{ start?: number; end?: number }> } };
|
|
371
303
|
try {
|
|
372
304
|
event = JSON.parse(line);
|
|
373
305
|
} catch {
|
|
@@ -383,8 +315,11 @@ async function collectRgMatches(
|
|
|
383
315
|
} catch {
|
|
384
316
|
abs = filePath;
|
|
385
317
|
}
|
|
318
|
+
const submatch = event.data?.submatches?.[0];
|
|
319
|
+
const start = typeof submatch?.start === "number" ? submatch.start : 0;
|
|
320
|
+
const end = typeof submatch?.end === "number" ? submatch.end : start;
|
|
386
321
|
const list = result.get(abs) ?? [];
|
|
387
|
-
list.push(lineNumber);
|
|
322
|
+
list.push({ line: lineNumber, start, end });
|
|
388
323
|
result.set(abs, list);
|
|
389
324
|
}
|
|
390
325
|
}
|
|
@@ -481,8 +416,7 @@ export function fmtGrepCall(args: { pattern?: unknown; path?: unknown; glob?: un
|
|
|
481
416
|
function highlightRegex(args: unknown): RegExp | undefined {
|
|
482
417
|
if (!isRec(args) || typeof args.pattern !== "string" || args.pattern.length === 0) return undefined;
|
|
483
418
|
try {
|
|
484
|
-
|
|
485
|
-
return new RegExp(validated.source, validated.flags.includes("i") ? "giu" : "gu");
|
|
419
|
+
return new RegExp(regexSource(args.pattern, args.literal === true), args.ignoreCase === true ? "giu" : "gu");
|
|
486
420
|
} catch {
|
|
487
421
|
return undefined;
|
|
488
422
|
}
|
|
@@ -535,9 +469,9 @@ export function renderGrepResult(result: { content?: Array<{ type: string; text?
|
|
|
535
469
|
}
|
|
536
470
|
export function regGrep(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
|
|
537
471
|
const prompted = withGrepPrompts({
|
|
538
|
-
description: loadP("../prompts/grep.md"),
|
|
539
|
-
snippet: loadP("../prompts/grep-snippet.md"),
|
|
540
|
-
guidelines: loadGuide("../prompts/grep-guidelines.md"),
|
|
472
|
+
description: loadP("../tool-prompts/grep.md"),
|
|
473
|
+
snippet: loadP("../tool-prompts/grep-snippet.md"),
|
|
474
|
+
guidelines: loadGuide("../tool-prompts/grep-guidelines.md"),
|
|
541
475
|
}, flags);
|
|
542
476
|
pi.registerTool({
|
|
543
477
|
name: "anchor_grep",
|
|
@@ -584,7 +518,7 @@ export function regGrep(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
584
518
|
const globPath = toDisplayPath(globRoot, absPath);
|
|
585
519
|
return globRegex.test(globPath) || globRegex.test(displayPath);
|
|
586
520
|
};
|
|
587
|
-
|
|
521
|
+
assertPattern(req.pattern, req.literal === true, req.ignoreCase === true);
|
|
588
522
|
const rgPath = await resolveRgPath();
|
|
589
523
|
const hits: FileHit[] = [];
|
|
590
524
|
let matches = 0;
|
|
@@ -615,15 +549,13 @@ export function regGrep(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
615
549
|
for (let f = 0; f < sortedFiles.length; f++) {
|
|
616
550
|
abortIf(signal);
|
|
617
551
|
const absPath = sortedFiles[f]!;
|
|
618
|
-
const
|
|
619
|
-
const totalForFile =
|
|
620
|
-
const sortedNums = [...allNums].sort((a, b) => a - b);
|
|
621
|
-
const indices = sortedNums.map((n) => n - 1).filter((n) => n >= 0);
|
|
552
|
+
const fileMatches = [...(rgMatches.get(absPath) ?? [])].sort((a, b) => a.line - b.line);
|
|
553
|
+
const totalForFile = fileMatches.length;
|
|
622
554
|
if (countOnly) {
|
|
623
555
|
if (!matchesGlob(absPath)) continue;
|
|
624
556
|
const norm = await readGrepFileShadow(absPath);
|
|
625
557
|
if (!norm) continue;
|
|
626
|
-
const hit = makeHitFromIndices(norm, toDisplayPath(ctx.cwd, absPath),
|
|
558
|
+
const hit = makeHitFromIndices(norm, toDisplayPath(ctx.cwd, absPath), fileMatches, context, totalForFile, fileMatches.length);
|
|
627
559
|
const display = displayRowsForHit(hit);
|
|
628
560
|
totalRows += display.length;
|
|
629
561
|
for (const r of display) totalBytes += Buffer.byteLength(r, "utf-8") + 1;
|
|
@@ -645,7 +577,7 @@ export function regGrep(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
645
577
|
if (!matchesGlob(absPath)) continue;
|
|
646
578
|
const norm = await readGrepFile(absPath);
|
|
647
579
|
if (!norm) continue;
|
|
648
|
-
const hit = makeHitFromIndices(norm, toDisplayPath(ctx.cwd, absPath),
|
|
580
|
+
const hit = makeHitFromIndices(norm, toDisplayPath(ctx.cwd, absPath), fileMatches, context, totalForFile, Math.min(totalForFile, remaining));
|
|
649
581
|
const display = displayRowsForHit(hit);
|
|
650
582
|
const keptRows: string[] = [];
|
|
651
583
|
const keptHashes: string[] = [];
|