@oh-my-pi/pi-coding-agent 15.5.2 → 15.5.4
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/CHANGELOG.md +38 -0
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/config.d.ts +31 -5
- package/dist/types/edit/file-snapshot-store.d.ts +18 -0
- package/dist/types/edit/hashline/diff.d.ts +30 -0
- package/dist/types/edit/hashline/execute.d.ts +29 -0
- package/dist/types/edit/hashline/filesystem.d.ts +57 -0
- package/dist/types/edit/hashline/index.d.ts +4 -0
- package/dist/types/edit/hashline/params.d.ts +12 -0
- package/dist/types/edit/index.d.ts +4 -3
- package/dist/types/edit/normalize.d.ts +4 -16
- package/dist/types/index.d.ts +0 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +18 -0
- package/dist/types/utils/changelog.d.ts +8 -3
- package/package.json +8 -15
- package/src/config/settings-schema.ts +32 -0
- package/src/config.ts +42 -15
- package/src/edit/file-snapshot-store.ts +22 -0
- package/src/edit/hashline/diff.ts +88 -0
- package/src/edit/hashline/execute.ts +188 -0
- package/src/edit/hashline/filesystem.ts +129 -0
- package/src/edit/hashline/index.ts +4 -0
- package/src/edit/hashline/params.ts +11 -0
- package/src/edit/index.ts +7 -15
- package/src/edit/normalize.ts +11 -41
- package/src/edit/renderer.ts +1 -1
- package/src/edit/streaming.ts +8 -9
- package/src/index.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/sdk.ts +8 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +3 -3
- package/src/tools/bash.ts +74 -10
- package/src/tools/index.ts +6 -5
- package/src/tools/path-utils.ts +81 -0
- package/src/tools/read.ts +14 -72
- package/src/tools/search.ts +136 -17
- package/src/tools/write.ts +3 -3
- package/src/utils/changelog.ts +11 -3
- package/src/utils/file-mentions.ts +1 -1
- package/dist/types/edit/file-read-cache.d.ts +0 -36
- package/dist/types/hashline/anchors.d.ts +0 -26
- package/dist/types/hashline/apply.d.ts +0 -14
- package/dist/types/hashline/constants.d.ts +0 -40
- package/dist/types/hashline/diff-preview.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +0 -16
- package/dist/types/hashline/execute.d.ts +0 -4
- package/dist/types/hashline/executor.d.ts +0 -56
- package/dist/types/hashline/hash.d.ts +0 -76
- package/dist/types/hashline/index.d.ts +0 -14
- package/dist/types/hashline/input.d.ts +0 -4
- package/dist/types/hashline/prefixes.d.ts +0 -7
- package/dist/types/hashline/recovery.d.ts +0 -21
- package/dist/types/hashline/stream.d.ts +0 -2
- package/dist/types/hashline/tokenizer.d.ts +0 -94
- package/dist/types/hashline/types.d.ts +0 -75
- package/src/edit/file-read-cache.ts +0 -138
- package/src/hashline/anchors.ts +0 -104
- package/src/hashline/apply.ts +0 -790
- package/src/hashline/bigrams.json +0 -649
- package/src/hashline/constants.ts +0 -51
- package/src/hashline/diff-preview.ts +0 -42
- package/src/hashline/diff.ts +0 -82
- package/src/hashline/execute.ts +0 -334
- package/src/hashline/executor.ts +0 -334
- package/src/hashline/grammar.lark +0 -23
- package/src/hashline/hash.ts +0 -131
- package/src/hashline/index.ts +0 -14
- package/src/hashline/input.ts +0 -137
- package/src/hashline/prefixes.ts +0 -111
- package/src/hashline/recovery.ts +0 -139
- package/src/hashline/stream.ts +0 -123
- package/src/hashline/tokenizer.ts +0 -473
- package/src/hashline/types.ts +0 -66
- package/src/prompts/tools/hashline.md +0 -63
|
@@ -1,473 +0,0 @@
|
|
|
1
|
-
import { ABORT_MARKER, BEGIN_PATCH_MARKER, END_PATCH_MARKER } from "./constants";
|
|
2
|
-
import {
|
|
3
|
-
describeAnchorExamples,
|
|
4
|
-
HL_FILE_HASH_SEP,
|
|
5
|
-
HL_FILE_PREFIX,
|
|
6
|
-
HL_OP_DELETE,
|
|
7
|
-
HL_OP_INSERT_AFTER,
|
|
8
|
-
HL_OP_INSERT_BEFORE,
|
|
9
|
-
HL_OP_REPLACE,
|
|
10
|
-
HL_PAYLOAD_PREFIX,
|
|
11
|
-
} from "./hash";
|
|
12
|
-
import type { Anchor, HashlineCursor } from "./types";
|
|
13
|
-
|
|
14
|
-
const CHAR_LINE_FEED = 10;
|
|
15
|
-
const CHAR_CARRIAGE_RETURN = 13;
|
|
16
|
-
const CHAR_ZERO = 48;
|
|
17
|
-
const CHAR_NINE = 57;
|
|
18
|
-
const CHAR_HASH = 35;
|
|
19
|
-
const CHAR_TAB = 9;
|
|
20
|
-
const CHAR_SPACE = 32;
|
|
21
|
-
const CHAR_LOWER_A = 97;
|
|
22
|
-
const CHAR_LOWER_F = 102;
|
|
23
|
-
const CHAR_PILCROW = HL_FILE_PREFIX.charCodeAt(0);
|
|
24
|
-
const CHAR_PAYLOAD_PREFIX = HL_PAYLOAD_PREFIX.charCodeAt(0);
|
|
25
|
-
const FILE_HASH_LENGTH = 4;
|
|
26
|
-
|
|
27
|
-
function isDigitCode(code: number): boolean {
|
|
28
|
-
return code >= CHAR_ZERO && code <= CHAR_NINE;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function isNonZeroDigitCode(code: number): boolean {
|
|
32
|
-
return code > CHAR_ZERO && code <= CHAR_NINE;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isDecorationCode(code: number): boolean {
|
|
36
|
-
return code === 42 || code === 45 || code === 62;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isHexDigitCode(code: number): boolean {
|
|
40
|
-
return isDigitCode(code) || (code >= CHAR_LOWER_A && code <= CHAR_LOWER_F);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function skipWhitespace(line: string, index: number, end = line.length): number {
|
|
44
|
-
return end - line.slice(index, end).trimStart().length;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function trimEndIndex(line: string): number {
|
|
48
|
-
return line.trimEnd().length;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isEmptyLine(line: string): boolean {
|
|
52
|
-
return line.length === 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function markerLineEquals(line: string, marker: string): boolean {
|
|
56
|
-
return line.trimEnd() === marker;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Split a hashline diff into individual lines without losing the trailing
|
|
61
|
-
* empty line that callers may rely on for explicit blank payloads. CRLF pairs
|
|
62
|
-
* are normalized to a single line break.
|
|
63
|
-
*
|
|
64
|
-
* This mirrors the line-splitting performed by {@link HashlineTokenizer}'s
|
|
65
|
-
* streaming drain loop and is kept for non-streaming callers that prefer
|
|
66
|
-
* a single-shot split.
|
|
67
|
-
*/
|
|
68
|
-
export function splitHashlineLines(text: string): string[] {
|
|
69
|
-
if (text.length === 0) return [""];
|
|
70
|
-
|
|
71
|
-
const lines: string[] = [];
|
|
72
|
-
let start = 0;
|
|
73
|
-
for (let index = 0; index < text.length; index++) {
|
|
74
|
-
if (text.charCodeAt(index) !== CHAR_LINE_FEED) continue;
|
|
75
|
-
let end = index;
|
|
76
|
-
if (end > start && text.charCodeAt(end - 1) === CHAR_CARRIAGE_RETURN) end--;
|
|
77
|
-
lines.push(text.slice(start, end));
|
|
78
|
-
start = index + 1;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (start < text.length) {
|
|
82
|
-
let end = text.length;
|
|
83
|
-
if (end > start && text.charCodeAt(end - 1) === CHAR_CARRIAGE_RETURN) end--;
|
|
84
|
-
lines.push(text.slice(start, end));
|
|
85
|
-
}
|
|
86
|
-
return lines;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function cloneCursor(cursor: HashlineCursor): HashlineCursor {
|
|
90
|
-
if (cursor.kind === "before_anchor") return { kind: "before_anchor", anchor: { ...cursor.anchor } };
|
|
91
|
-
if (cursor.kind === "after_anchor") return { kind: "after_anchor", anchor: { ...cursor.anchor } };
|
|
92
|
-
return cursor;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Leniently accept anchors copied from read/search output:
|
|
96
|
-
// - optional leading line-marker decoration (`*`, `>`, `-`)
|
|
97
|
-
// - the required bare line number
|
|
98
|
-
function skipDecoratedAnchorPrefix(line: string, end = trimEndIndex(line)): number {
|
|
99
|
-
let index = skipWhitespace(line, 0, end);
|
|
100
|
-
while (index < end && isDecorationCode(line.charCodeAt(index))) index++;
|
|
101
|
-
return skipWhitespace(line, index, end);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
interface NumberScan {
|
|
105
|
-
line: number;
|
|
106
|
-
nextIndex: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function scanLineNumber(line: string, index: number, end: number): NumberScan | null {
|
|
110
|
-
if (index >= end || !isNonZeroDigitCode(line.charCodeAt(index))) return null;
|
|
111
|
-
|
|
112
|
-
let lineNumber = 0;
|
|
113
|
-
let nextIndex = index;
|
|
114
|
-
while (nextIndex < end) {
|
|
115
|
-
const code = line.charCodeAt(nextIndex);
|
|
116
|
-
if (!isDigitCode(code)) break;
|
|
117
|
-
lineNumber = lineNumber * 10 + (code - CHAR_ZERO);
|
|
118
|
-
nextIndex++;
|
|
119
|
-
}
|
|
120
|
-
return { line: lineNumber, nextIndex };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Parse a bare line-number anchor (used by insert ops). Throws on malformed input. */
|
|
124
|
-
export function parseLid(raw: string, lineNum: number): Anchor {
|
|
125
|
-
const end = trimEndIndex(raw);
|
|
126
|
-
const numberStart = skipDecoratedAnchorPrefix(raw, end);
|
|
127
|
-
const number = scanLineNumber(raw, numberStart, end);
|
|
128
|
-
if (number === null || skipWhitespace(raw, number.nextIndex, end) !== end) {
|
|
129
|
-
throw new Error(
|
|
130
|
-
`line ${lineNum}: expected a line number such as ${describeAnchorExamples("119")}; ` +
|
|
131
|
-
`got ${JSON.stringify(raw)}. Use ${HL_FILE_PREFIX}PATH${HL_FILE_HASH_SEP}hash from your latest read for file-version binding.`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
return { line: number.line };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export interface ParsedRange {
|
|
138
|
-
start: Anchor;
|
|
139
|
-
end: Anchor;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
interface RangeScan {
|
|
143
|
-
range: ParsedRange;
|
|
144
|
-
nextIndex: number;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function scanRange(line: string, end = trimEndIndex(line)): RangeScan | null {
|
|
148
|
-
const numberStart = skipDecoratedAnchorPrefix(line, end);
|
|
149
|
-
const start = scanLineNumber(line, numberStart, end);
|
|
150
|
-
if (start === null) return null;
|
|
151
|
-
|
|
152
|
-
let nextIndex = start.nextIndex;
|
|
153
|
-
let rangeEnd = start.line;
|
|
154
|
-
if (nextIndex < end && line.charCodeAt(nextIndex) === 45) {
|
|
155
|
-
const endNumber = scanLineNumber(line, nextIndex + 1, end);
|
|
156
|
-
if (endNumber === null) return null;
|
|
157
|
-
rangeEnd = endNumber.line;
|
|
158
|
-
nextIndex = endNumber.nextIndex;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
range: { start: { line: start.line }, end: { line: rangeEnd } },
|
|
163
|
-
nextIndex: skipWhitespace(line, nextIndex, end),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function startsWithWord(line: string, index: number, end: number, word: string): boolean {
|
|
168
|
-
if (index + word.length > end) return false;
|
|
169
|
-
for (let offset = 0; offset < word.length; offset++) {
|
|
170
|
-
if (line.charCodeAt(index + offset) !== word.charCodeAt(offset)) return false;
|
|
171
|
-
}
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function parseInsertTarget(raw: string, lineNum: number, kind: "before" | "after"): HashlineCursor {
|
|
176
|
-
const end = trimEndIndex(raw);
|
|
177
|
-
const targetStart = skipDecoratedAnchorPrefix(raw, end);
|
|
178
|
-
|
|
179
|
-
if (startsWithWord(raw, targetStart, end, "BOF") && skipWhitespace(raw, targetStart + 3, end) === end) {
|
|
180
|
-
return { kind: "bof" };
|
|
181
|
-
}
|
|
182
|
-
if (startsWithWord(raw, targetStart, end, "EOF") && skipWhitespace(raw, targetStart + 3, end) === end) {
|
|
183
|
-
return { kind: "eof" };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const cursorKind = kind === "before" ? "before_anchor" : "after_anchor";
|
|
187
|
-
return { kind: cursorKind, anchor: parseLid(raw, lineNum) };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function scanInlineBody(line: string, index: number): string | undefined {
|
|
191
|
-
const end = trimEndIndex(line);
|
|
192
|
-
return index < end ? line.slice(index, end) : undefined;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
interface ParsedInsertOp {
|
|
196
|
-
kind: "insert";
|
|
197
|
-
cursor: HashlineCursor;
|
|
198
|
-
inlineBody: string | undefined;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
interface ParsedReplaceOp {
|
|
202
|
-
kind: "replace";
|
|
203
|
-
range: ParsedRange;
|
|
204
|
-
inlineBody: string | undefined;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
interface ParsedDeleteOp {
|
|
208
|
-
kind: "delete";
|
|
209
|
-
range: ParsedRange;
|
|
210
|
-
trailingPayload: boolean;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
type ParsedOp = ParsedInsertOp | ParsedReplaceOp | ParsedDeleteOp;
|
|
214
|
-
|
|
215
|
-
function tryParseInsertOp(line: string, sigil: string, kind: "before" | "after"): ParsedInsertOp | null {
|
|
216
|
-
const end = trimEndIndex(line);
|
|
217
|
-
const targetStart = skipDecoratedAnchorPrefix(line, end);
|
|
218
|
-
|
|
219
|
-
let targetEnd: number;
|
|
220
|
-
if (startsWithWord(line, targetStart, end, "BOF") || startsWithWord(line, targetStart, end, "EOF")) {
|
|
221
|
-
targetEnd = targetStart + 3;
|
|
222
|
-
} else {
|
|
223
|
-
const anchor = scanLineNumber(line, targetStart, end);
|
|
224
|
-
if (anchor === null) return null;
|
|
225
|
-
targetEnd = anchor.nextIndex;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const opIndex = skipWhitespace(line, targetEnd, end);
|
|
229
|
-
if (opIndex >= end || line[opIndex] !== sigil) return null;
|
|
230
|
-
|
|
231
|
-
// parseInsertTarget can only throw on inputs that already passed the
|
|
232
|
-
// BOF/EOF/line-number scan above, but guard the throw anyway — the
|
|
233
|
-
// tokenizer contract forbids it and a future refactor of the prefix
|
|
234
|
-
// scan must not silently start raising here.
|
|
235
|
-
try {
|
|
236
|
-
return {
|
|
237
|
-
kind: "insert",
|
|
238
|
-
cursor: parseInsertTarget(line.slice(0, opIndex), 0, kind),
|
|
239
|
-
inlineBody: scanInlineBody(line, opIndex + sigil.length),
|
|
240
|
-
};
|
|
241
|
-
} catch {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function tryParseReplaceOp(line: string): ParsedReplaceOp | null {
|
|
247
|
-
const end = trimEndIndex(line);
|
|
248
|
-
const range = scanRange(line, end);
|
|
249
|
-
if (range === null || range.nextIndex >= end || line[range.nextIndex] !== HL_OP_REPLACE) return null;
|
|
250
|
-
return {
|
|
251
|
-
kind: "replace",
|
|
252
|
-
range: range.range,
|
|
253
|
-
inlineBody: scanInlineBody(line, range.nextIndex + HL_OP_REPLACE.length),
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function tryParseDeleteOp(line: string): ParsedDeleteOp | null {
|
|
258
|
-
const end = trimEndIndex(line);
|
|
259
|
-
const range = scanRange(line, end);
|
|
260
|
-
if (range === null || range.nextIndex >= end || line[range.nextIndex] !== HL_OP_DELETE) return null;
|
|
261
|
-
const afterSigil = range.nextIndex + HL_OP_DELETE.length;
|
|
262
|
-
return { kind: "delete", range: range.range, trailingPayload: afterSigil !== end };
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function tryParseOp(line: string): ParsedOp | null {
|
|
266
|
-
return (
|
|
267
|
-
tryParseInsertOp(line, HL_OP_INSERT_BEFORE, "before") ??
|
|
268
|
-
tryParseInsertOp(line, HL_OP_INSERT_AFTER, "after") ??
|
|
269
|
-
tryParseReplaceOp(line) ??
|
|
270
|
-
tryParseDeleteOp(line)
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Strict header scan: `¶+` prefix, optional whitespace, path body that excludes
|
|
276
|
-
* whitespace, `#`, and `¶`, optional `#[0-9a-f]{4}` hash suffix, optional
|
|
277
|
-
* trailing whitespace. Returns `null` when any byte deviates from the shape.
|
|
278
|
-
*/
|
|
279
|
-
function tryParseHeader(line: string): { path: string; fileHash?: string } | null {
|
|
280
|
-
const end = trimEndIndex(line);
|
|
281
|
-
if (end === 0 || line.charCodeAt(0) !== CHAR_PILCROW) return null;
|
|
282
|
-
|
|
283
|
-
let index = 0;
|
|
284
|
-
while (index < end && line.charCodeAt(index) === CHAR_PILCROW) index++;
|
|
285
|
-
index = skipWhitespace(line, index, end);
|
|
286
|
-
if (index >= end) return null;
|
|
287
|
-
|
|
288
|
-
const pathStart = index;
|
|
289
|
-
while (index < end) {
|
|
290
|
-
const code = line.charCodeAt(index);
|
|
291
|
-
if (code === CHAR_HASH || code === CHAR_PILCROW || code === CHAR_SPACE || code === CHAR_TAB) break;
|
|
292
|
-
index++;
|
|
293
|
-
}
|
|
294
|
-
if (index === pathStart) return null;
|
|
295
|
-
const path = line.slice(pathStart, index);
|
|
296
|
-
|
|
297
|
-
let fileHash: string | undefined;
|
|
298
|
-
if (index < end && line.charCodeAt(index) === CHAR_HASH) {
|
|
299
|
-
const hashStart = index + 1;
|
|
300
|
-
const hashEnd = hashStart + FILE_HASH_LENGTH;
|
|
301
|
-
if (hashEnd > end) return null;
|
|
302
|
-
for (let probe = hashStart; probe < hashEnd; probe++) {
|
|
303
|
-
if (!isHexDigitCode(line.charCodeAt(probe))) return null;
|
|
304
|
-
}
|
|
305
|
-
fileHash = line.slice(hashStart, hashEnd);
|
|
306
|
-
index = hashEnd;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Anything other than trailing whitespace disqualifies the header.
|
|
310
|
-
if (skipWhitespace(line, index, end) !== end) return null;
|
|
311
|
-
|
|
312
|
-
return fileHash !== undefined ? { path, fileHash } : { path };
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Returns true when the line scans as `LINE!payload` (delete sigil followed by
|
|
317
|
-
* additional content). The executor uses this for the dedicated "deletes only"
|
|
318
|
-
* diagnostic, separate from the standard "unrecognized op" path.
|
|
319
|
-
*/
|
|
320
|
-
export function isDeleteOpWithPayload(line: string): boolean {
|
|
321
|
-
const range = scanRange(line, line.length);
|
|
322
|
-
return (
|
|
323
|
-
range !== null &&
|
|
324
|
-
range.nextIndex < line.length &&
|
|
325
|
-
line[range.nextIndex] === HL_OP_DELETE &&
|
|
326
|
-
range.nextIndex + HL_OP_DELETE.length < line.length
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
interface TokenBase {
|
|
331
|
-
/** 1-indexed line number in the original input stream. */
|
|
332
|
-
lineNum: number;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export type HashlineToken =
|
|
336
|
-
| (TokenBase & { kind: "blank" })
|
|
337
|
-
| (TokenBase & { kind: "envelope-begin" })
|
|
338
|
-
| (TokenBase & { kind: "envelope-end" })
|
|
339
|
-
| (TokenBase & { kind: "abort" })
|
|
340
|
-
| (TokenBase & { kind: "header"; path: string; fileHash?: string })
|
|
341
|
-
| (TokenBase & { kind: "op-insert"; cursor: HashlineCursor; inlineBody: string | undefined })
|
|
342
|
-
| (TokenBase & { kind: "op-replace"; range: ParsedRange; inlineBody: string | undefined })
|
|
343
|
-
| (TokenBase & { kind: "op-delete"; range: ParsedRange; trailingPayload: boolean })
|
|
344
|
-
| (TokenBase & { kind: "payload"; text: string })
|
|
345
|
-
| (TokenBase & { kind: "raw"; text: string });
|
|
346
|
-
|
|
347
|
-
function classifyLine(line: string, lineNum: number): HashlineToken {
|
|
348
|
-
if (isEmptyLine(line)) return { kind: "blank", lineNum };
|
|
349
|
-
if (markerLineEquals(line, BEGIN_PATCH_MARKER)) return { kind: "envelope-begin", lineNum };
|
|
350
|
-
if (markerLineEquals(line, END_PATCH_MARKER)) return { kind: "envelope-end", lineNum };
|
|
351
|
-
if (markerLineEquals(line, ABORT_MARKER)) return { kind: "abort", lineNum };
|
|
352
|
-
|
|
353
|
-
if (line.charCodeAt(0) === CHAR_PILCROW) {
|
|
354
|
-
const header = tryParseHeader(line);
|
|
355
|
-
if (header !== null) {
|
|
356
|
-
return header.fileHash !== undefined
|
|
357
|
-
? { kind: "header", lineNum, path: header.path, fileHash: header.fileHash }
|
|
358
|
-
: { kind: "header", lineNum, path: header.path };
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (line.charCodeAt(0) === CHAR_PAYLOAD_PREFIX) {
|
|
363
|
-
return { kind: "payload", lineNum, text: line.slice(HL_PAYLOAD_PREFIX.length) };
|
|
364
|
-
}
|
|
365
|
-
const op = tryParseOp(line);
|
|
366
|
-
if (op !== null) {
|
|
367
|
-
if (op.kind === "insert") {
|
|
368
|
-
return { kind: "op-insert", lineNum, cursor: op.cursor, inlineBody: op.inlineBody };
|
|
369
|
-
}
|
|
370
|
-
if (op.kind === "replace") {
|
|
371
|
-
return { kind: "op-replace", lineNum, range: op.range, inlineBody: op.inlineBody };
|
|
372
|
-
}
|
|
373
|
-
return { kind: "op-delete", lineNum, range: op.range, trailingPayload: op.trailingPayload };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return { kind: "raw", lineNum, text: line };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Stateful, line-oriented classifier for hashline diff text. Use the streaming
|
|
381
|
-
* {@link feed}/{@link end} pair to ingest text in chunks (each completed line
|
|
382
|
-
* emits exactly one token; a trailing partial line stays buffered until the
|
|
383
|
-
* next chunk or {@link end}). Use the stateless {@link tokenize}/predicate
|
|
384
|
-
* methods for callers that already hold whole lines and only need
|
|
385
|
-
* classification without buffering.
|
|
386
|
-
*/
|
|
387
|
-
export class HashlineTokenizer {
|
|
388
|
-
#buffer = "";
|
|
389
|
-
#nextLineNum = 1;
|
|
390
|
-
#closed = false;
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Ingest a chunk of input text. Each newline-terminated line in the
|
|
394
|
-
* combined buffer produces one token. A trailing partial line (no `\n`
|
|
395
|
-
* yet, possibly ending in a lone `\r`) stays buffered until the next
|
|
396
|
-
* `feed`/`end` call so CRLF pairs that straddle chunk boundaries are
|
|
397
|
-
* still normalized correctly.
|
|
398
|
-
*/
|
|
399
|
-
feed(chunk: string): HashlineToken[] {
|
|
400
|
-
if (this.#closed) throw new Error("HashlineTokenizer is closed; call reset() before reusing.");
|
|
401
|
-
if (chunk.length === 0) return [];
|
|
402
|
-
this.#buffer = this.#buffer ? this.#buffer + chunk : chunk;
|
|
403
|
-
return this.#drainCompleteLines();
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Flush any buffered residual line (the last line of input when it lacks
|
|
408
|
-
* a trailing newline) and mark the tokenizer closed. Calling `end` a
|
|
409
|
-
* second time returns `[]`; reuse requires `reset`.
|
|
410
|
-
*/
|
|
411
|
-
end(): HashlineToken[] {
|
|
412
|
-
if (this.#closed) return [];
|
|
413
|
-
this.#closed = true;
|
|
414
|
-
const buf = this.#buffer;
|
|
415
|
-
this.#buffer = "";
|
|
416
|
-
if (buf.length === 0) return [];
|
|
417
|
-
let stop = buf.length;
|
|
418
|
-
if (buf.charCodeAt(stop - 1) === CHAR_CARRIAGE_RETURN) stop--;
|
|
419
|
-
const token = classifyLine(buf.slice(0, stop), this.#nextLineNum++);
|
|
420
|
-
return [token];
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/** Discard any buffered text and reset the line counter to 1. */
|
|
424
|
-
reset(): void {
|
|
425
|
-
this.#buffer = "";
|
|
426
|
-
this.#nextLineNum = 1;
|
|
427
|
-
this.#closed = false;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/** Convenience: feed an entire text and immediately flush. */
|
|
431
|
-
tokenizeAll(text: string): HashlineToken[] {
|
|
432
|
-
this.reset();
|
|
433
|
-
const first = this.feed(text);
|
|
434
|
-
const last = this.end();
|
|
435
|
-
return last.length === 0 ? first : first.concat(last);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/** Stateless one-shot classification. Does not touch the streaming buffer. */
|
|
439
|
-
tokenize(line: string, lineNum = 0): HashlineToken {
|
|
440
|
-
return classifyLine(line, lineNum);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
isOp(line: string): boolean {
|
|
444
|
-
return tryParseOp(line) !== null;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
isHeader(line: string): boolean {
|
|
448
|
-
return tryParseHeader(line) !== null;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
isEnvelopeMarker(line: string): boolean {
|
|
452
|
-
return (
|
|
453
|
-
markerLineEquals(line, BEGIN_PATCH_MARKER) ||
|
|
454
|
-
markerLineEquals(line, END_PATCH_MARKER) ||
|
|
455
|
-
markerLineEquals(line, ABORT_MARKER)
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
#drainCompleteLines(): HashlineToken[] {
|
|
460
|
-
const tokens: HashlineToken[] = [];
|
|
461
|
-
const buf = this.#buffer;
|
|
462
|
-
let start = 0;
|
|
463
|
-
for (let index = 0; index < buf.length; index++) {
|
|
464
|
-
if (buf.charCodeAt(index) !== CHAR_LINE_FEED) continue;
|
|
465
|
-
let stop = index;
|
|
466
|
-
if (stop > start && buf.charCodeAt(stop - 1) === CHAR_CARRIAGE_RETURN) stop--;
|
|
467
|
-
tokens.push(classifyLine(buf.slice(start, stop), this.#nextLineNum++));
|
|
468
|
-
start = index + 1;
|
|
469
|
-
}
|
|
470
|
-
this.#buffer = start < buf.length ? buf.slice(start) : "";
|
|
471
|
-
return tokens;
|
|
472
|
-
}
|
|
473
|
-
}
|
package/src/hashline/types.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import * as z from "zod/v4";
|
|
2
|
-
import type { LspBatchRequest } from "../edit/renderer";
|
|
3
|
-
import type { WritethroughCallback, WritethroughDeferredHandle } from "../lsp";
|
|
4
|
-
import type { ToolSession } from "../tools";
|
|
5
|
-
|
|
6
|
-
export type Anchor = {
|
|
7
|
-
line: number;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export type HashlineCursor =
|
|
11
|
-
| { kind: "bof" }
|
|
12
|
-
| { kind: "eof" }
|
|
13
|
-
| { kind: "before_anchor"; anchor: Anchor }
|
|
14
|
-
| { kind: "after_anchor"; anchor: Anchor };
|
|
15
|
-
|
|
16
|
-
export type HashlineEdit =
|
|
17
|
-
| { kind: "insert"; cursor: HashlineCursor; text: string; lineNum: number; index: number }
|
|
18
|
-
| { kind: "delete"; anchor: Anchor; lineNum: number; index: number; oldAssertion?: string };
|
|
19
|
-
|
|
20
|
-
export interface HashlineInputSection {
|
|
21
|
-
path: string;
|
|
22
|
-
fileHash?: string;
|
|
23
|
-
diff: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** `path` is accepted by the edit tool runtime; other extra keys are preserved. */
|
|
27
|
-
export const hashlineEditParamsSchema = z.object({ input: z.string(), path: z.string().optional() }).passthrough();
|
|
28
|
-
export type HashlineParams = z.infer<typeof hashlineEditParamsSchema>;
|
|
29
|
-
|
|
30
|
-
export interface HashlineStreamOptions {
|
|
31
|
-
/** First line number to use when formatting (1-indexed). */
|
|
32
|
-
startLine?: number;
|
|
33
|
-
/** Maximum formatted lines per yielded chunk (default: 200). */
|
|
34
|
-
maxChunkLines?: number;
|
|
35
|
-
/** Maximum UTF-8 bytes per yielded chunk (default: 64 KiB). */
|
|
36
|
-
maxChunkBytes?: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface CompactHashlineDiffPreview {
|
|
40
|
-
preview: string;
|
|
41
|
-
addedLines: number;
|
|
42
|
-
removedLines: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface CompactHashlineDiffOptions {
|
|
46
|
-
/** Maximum entries kept on each side of an unchanged-context truncation (default: 2). */
|
|
47
|
-
maxUnchangedRun?: number;
|
|
48
|
-
}
|
|
49
|
-
export interface HashlineApplyOptions {
|
|
50
|
-
autoDropPureInsertDuplicates?: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface SplitHashlineOptions {
|
|
54
|
-
cwd?: string;
|
|
55
|
-
path?: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface ExecuteHashlineSingleOptions {
|
|
59
|
-
session: ToolSession;
|
|
60
|
-
input: string;
|
|
61
|
-
path?: string;
|
|
62
|
-
signal?: AbortSignal;
|
|
63
|
-
batchRequest?: LspBatchRequest;
|
|
64
|
-
writethrough: WritethroughCallback;
|
|
65
|
-
beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
|
|
66
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
Your patch language is a compact, line-anchored edit format.
|
|
2
|
-
|
|
3
|
-
<payload>
|
|
4
|
-
Patch payload is a series of hunks: `¶PATH#HASH` header followed by any number of operations. `HASH` should be copied as is from read/search. Missing? Re-`read`.
|
|
5
|
-
- No context rows, no gutters.
|
|
6
|
-
- NEVER restate unchanged lines "for context".
|
|
7
|
-
- Inline payload after an op is literal. Additional payload lines MUST start with `+`; that delimiter is stripped.
|
|
8
|
-
- Payload indentation after the op sigil or after `+` is literal.
|
|
9
|
-
</payload>
|
|
10
|
-
|
|
11
|
-
<ops>
|
|
12
|
-
LINE↑PAYLOAD insert before (or BOF↑)
|
|
13
|
-
LINE↓PAYLOAD insert after (or EOF↓)
|
|
14
|
-
A-B:PAYLOAD replace A..B (or A: == A..A)
|
|
15
|
-
A-B! delete A..B (or A! == A..A)
|
|
16
|
-
+PAYLOAD continuation payload line; leading `+` is not written
|
|
17
|
-
</ops>
|
|
18
|
-
|
|
19
|
-
<rules>
|
|
20
|
-
- **Payload is only what's NEW.** `:` replaces inside; `↑`/`↓` add at anchor. NEVER repeat anchor lines or neighbors.
|
|
21
|
-
- **Continuation lines require `+`.** Use `+` for a blank payload line; use `++text` to write a line starting with `+text`.
|
|
22
|
-
- **Go small.** Add → `↑`/`↓`; replace → `:`; delete → `!`.
|
|
23
|
-
- **Line numbers are frozen references to what you have seen.** Later ops still use original line numbers.
|
|
24
|
-
</rules>
|
|
25
|
-
|
|
26
|
-
<common-failures>
|
|
27
|
-
- **NEVER replay past your range.** Stop before B+1; extend B if needed.
|
|
28
|
-
- **Read lines look like replace ops.** `84:content` = "make line 84 content" — don't echo context before it.
|
|
29
|
-
- **NEVER fabricate file hashes.** Missing? Re-`read`.
|
|
30
|
-
</common-failures>
|
|
31
|
-
|
|
32
|
-
<example>
|
|
33
|
-
```a.ts#1a2b
|
|
34
|
-
1:const X = "a";
|
|
35
|
-
2:export function f() { return X; }
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
# replace with a continuation line, insert after, delete
|
|
39
|
-
```
|
|
40
|
-
¶a.ts#1a2b
|
|
41
|
-
1:const X = "b";
|
|
42
|
-
+export const Y = X;
|
|
43
|
-
1↓const Z = Y;
|
|
44
|
-
2!
|
|
45
|
-
```
|
|
46
|
-
</example>
|
|
47
|
-
|
|
48
|
-
<anti-pattern>
|
|
49
|
-
# WRONG — INSERT used to change a line (old line survives)
|
|
50
|
-
1↓const X = "b";
|
|
51
|
-
# WRONG — echoing read-style lines as context before the real op
|
|
52
|
-
1:const X = "a";
|
|
53
|
-
1-2:const X = "b";
|
|
54
|
-
export const Y = X; # raw continuation line missing required `+`
|
|
55
|
-
</anti-pattern>
|
|
56
|
-
|
|
57
|
-
<critical>
|
|
58
|
-
- One op per range, ever.
|
|
59
|
-
- Pick op precisely. Update: `:`, add: `↑`/`↓`, remove: `!`.
|
|
60
|
-
- Payload is only what's NEW; never repeat anchor lines or neighbors.
|
|
61
|
-
- Continuation payload lines after the op line must start with `+`.
|
|
62
|
-
- Anchor exactly; don't anchor neighbors.
|
|
63
|
-
</critical>
|