pi-hashline-edit-pro 6.2.0 → 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 +28 -12
- package/package.json +1 -1
- package/src/anchor-registry.ts +22 -5
- package/src/auto-read-all.ts +1 -1
- package/src/batch.ts +127 -41
- package/src/commit.ts +1 -1
- package/src/copy-move.ts +57 -68
- package/src/edit-common.ts +6 -4
- package/src/grep.ts +50 -118
- package/src/hash-store.ts +13 -12
- package/src/hashline/resolve.ts +13 -2
- package/src/insert.ts +37 -31
- package/src/line-endings.ts +10 -1
- package/src/outline/index.ts +1 -0
- package/src/outline/render.ts +6 -2
- package/src/read.ts +17 -12
- package/src/replace-match.ts +39 -38
- package/src/replace-undo.ts +1 -1
- package/src/replace.ts +27 -16
- package/tool-prompts/read-guidelines.md +2 -2
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
|
}
|
|
@@ -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
|
}
|
|
@@ -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[] = [];
|
package/src/hash-store.ts
CHANGED
|
@@ -161,6 +161,15 @@ function openDb(storePath: string): { db: RawDb; stmts: Prepared } {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
function addColumnIfMissing(db: RawDb, statement: string): void {
|
|
165
|
+
try {
|
|
166
|
+
db.exec(statement);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof Error && /duplicate column/i.test(error.message)) return;
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
164
173
|
function buildStore(db: RawDb): { db: RawDb; stmts: Prepared } {
|
|
165
174
|
db.exec("PRAGMA journal_mode = WAL");
|
|
166
175
|
db.exec("PRAGMA synchronous = NORMAL");
|
|
@@ -192,18 +201,10 @@ function buildStore(db: RawDb): { db: RawDb; stmts: Prepared } {
|
|
|
192
201
|
"updated_at INTEGER NOT NULL" +
|
|
193
202
|
")"
|
|
194
203
|
);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
db.exec("ALTER TABLE undo ADD COLUMN mode INTEGER");
|
|
200
|
-
} catch {}
|
|
201
|
-
try {
|
|
202
|
-
db.exec("ALTER TABLE undo ADD COLUMN separators TEXT");
|
|
203
|
-
} catch {}
|
|
204
|
-
try {
|
|
205
|
-
db.exec("ALTER TABLE undo ADD COLUMN result_separators TEXT");
|
|
206
|
-
} catch {}
|
|
204
|
+
addColumnIfMissing(db, "ALTER TABLE snapshots ADD COLUMN line_checksums TEXT");
|
|
205
|
+
addColumnIfMissing(db, "ALTER TABLE undo ADD COLUMN mode INTEGER");
|
|
206
|
+
addColumnIfMissing(db, "ALTER TABLE undo ADD COLUMN separators TEXT");
|
|
207
|
+
addColumnIfMissing(db, "ALTER TABLE undo ADD COLUMN result_separators TEXT");
|
|
207
208
|
const versionRow = db.prepare("SELECT value FROM meta WHERE key = 'version'").get() as { value?: string } | undefined;
|
|
208
209
|
if (versionRow && versionRow.value !== String(HASH_STORE_VERSION)) {
|
|
209
210
|
db.exec("DELETE FROM snapshots");
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -93,7 +93,12 @@ export function fmtMismatchWithHashes(
|
|
|
93
93
|
const out: string[] = [];
|
|
94
94
|
const hashes: string[] = [];
|
|
95
95
|
const servedMap = new Map<string, string>();
|
|
96
|
-
const
|
|
96
|
+
const seen = new Set<string>();
|
|
97
|
+
const notFound = mismatches.filter((mismatch) => {
|
|
98
|
+
if (seen.has(mismatch.ref.hash)) return false;
|
|
99
|
+
seen.add(mismatch.ref.hash);
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
97
102
|
if (notFound.length > 0) {
|
|
98
103
|
const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
|
|
99
104
|
out.push(
|
|
@@ -194,7 +199,13 @@ export function stripAnchorRow(
|
|
|
194
199
|
return match[2]!;
|
|
195
200
|
}
|
|
196
201
|
|
|
197
|
-
export
|
|
202
|
+
export type HTextInput = {
|
|
203
|
+
text: string | string[];
|
|
204
|
+
remove_from: string;
|
|
205
|
+
remove_to: string;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export function resEdit(edit: HTextInput, warnings?: string[]): HEdit {
|
|
198
209
|
if (typeof edit.text === "string") {
|
|
199
210
|
assertPayloadItem(edit as unknown as Record<string, unknown>);
|
|
200
211
|
return resolveParsedEdit(edit.remove_from, edit.remove_to, parsePayloadText(edit.text), warnings);
|