@oh-my-pi/hashline 15.13.2 → 16.0.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/CHANGELOG.md +6 -0
- package/README.md +3 -3
- package/dist/types/format.d.ts +2 -2
- package/dist/types/messages.d.ts +3 -3
- package/dist/types/types.d.ts +3 -3
- package/package.json +1 -1
- package/src/apply.ts +2 -2
- package/src/block.ts +2 -2
- package/src/format.ts +2 -2
- package/src/grammar.lark +1 -1
- package/src/messages.ts +11 -8
- package/src/parser.ts +13 -11
- package/src/prompt.md +14 -14
- package/src/tokenizer.ts +8 -3
- package/src/types.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.13.3] - 2026-06-15
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Changed the recommended hashline range separator from `..` to `.=` (e.g. `SWAP 1.=3:`, `DEL 4.=5`) so the inclusive `<=`-style end is self-evident. `HL_RANGE_SEP` is now `.=`; the prompt, grammar, error messages, and emitted headers all use it. The lenient parser still accepts the legacy `..` (and `-`/`…`/space) forms.
|
|
10
|
+
|
|
5
11
|
## [15.13.2] - 2026-06-15
|
|
6
12
|
|
|
7
13
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ await fs.writeText("hello.ts", before);
|
|
|
26
26
|
const tag = snapshots.record("hello.ts", before);
|
|
27
27
|
const patcher = new Patcher({ fs, snapshots });
|
|
28
28
|
const patch = Patch.parse(String.raw`[hello.ts#${tag}]
|
|
29
|
-
SWAP 1
|
|
29
|
+
SWAP 1.=1:
|
|
30
30
|
+const greeting = "hello";`);
|
|
31
31
|
const result = await patcher.apply(patch);
|
|
32
32
|
|
|
@@ -47,9 +47,9 @@ still matches the recorded content hash, and refusing or attempting
|
|
|
47
47
|
session-aware recovery on mismatch.
|
|
48
48
|
|
|
49
49
|
Inside a section:
|
|
50
|
-
- `SWAP A
|
|
50
|
+
- `SWAP A.=B:` — replace lines A.=B with following `+TEXT` body rows.
|
|
51
51
|
- `SWAP.BLK A:` — replace the syntactic block beginning on line A.
|
|
52
|
-
- `DEL A
|
|
52
|
+
- `DEL A.=B` / `DEL.BLK A` — delete concrete lines or a resolved block.
|
|
53
53
|
- `INS.PRE A:` / `INS.POST A:` / `INS.HEAD:` / `INS.TAIL:` — insert following body rows.
|
|
54
54
|
- `INS.BLK.POST A:` — insert following body rows after the resolved block's last line.
|
|
55
55
|
- `+TEXT` — literal body row (use `+` alone for a blank line).
|
package/dist/types/format.d.ts
CHANGED
|
@@ -32,8 +32,8 @@ export declare const HL_INSERT_AFTER_BLOCK_KEYWORD = "INS.BLK.POST";
|
|
|
32
32
|
export declare const HL_HEADER_COLON = ":";
|
|
33
33
|
/** Separator between a hashline file path and its opaque snapshot tag. */
|
|
34
34
|
export declare const HL_FILE_HASH_SEP = "#";
|
|
35
|
-
/** Separator between two line numbers in a range, e.g. `5
|
|
36
|
-
export declare const HL_RANGE_SEP = "
|
|
35
|
+
/** Separator between two line numbers in a range, e.g. `5.=10`. */
|
|
36
|
+
export declare const HL_RANGE_SEP = ".=";
|
|
37
37
|
/** Separator between a line number and displayed line content in hashline mode. */
|
|
38
38
|
export declare const HL_LINE_BODY_SEP = ":";
|
|
39
39
|
/** Bare positive line-number Lid (no decorations, no captures, no anchors). */
|
package/dist/types/messages.d.ts
CHANGED
|
@@ -17,13 +17,13 @@ export declare const END_PATCH_MARKER = "*** End Patch";
|
|
|
17
17
|
*/
|
|
18
18
|
export declare const ABORT_MARKER = "*** Abort";
|
|
19
19
|
/** Two consecutive hunks targeted the exact same concrete range. */
|
|
20
|
-
export declare const REPLACE_PAIR_COALESCED_WARNING = "Two hunks targeted the same range; kept only the second. One `SWAP N
|
|
20
|
+
export declare const REPLACE_PAIR_COALESCED_WARNING = "Two hunks targeted the same range; kept only the second. One `SWAP N.=M:` hunk per range \u2014 the body is the final content, never old+new.";
|
|
21
21
|
/** Bare body rows auto-converted to literal `+` rows. */
|
|
22
22
|
export declare const BARE_BODY_AUTO_PIPED_WARNING = "Auto-prefixed bare body row(s) with `+`. Body rows must be `+TEXT` literal lines.";
|
|
23
23
|
/** Unified-diff-style `-` row in a hunk body. */
|
|
24
24
|
export declare const MINUS_ROW_REJECTED = "`-` rows are not valid; the range already names the lines being changed. For a literal `-` line, write `+-\u2026`.";
|
|
25
25
|
/** Replace hunk with no body. */
|
|
26
|
-
export declare const EMPTY_REPLACE = "`SWAP N
|
|
26
|
+
export declare const EMPTY_REPLACE = "`SWAP N.=M:` needs at least one `+TEXT` body row. To delete lines, use `DEL N.=M`.";
|
|
27
27
|
/** `replace_block N:` hunk with no body. */
|
|
28
28
|
export declare const EMPTY_BLOCK = "`SWAP.BLK N:` needs at least one `+TEXT` body row. To delete a block, use `DEL.BLK N`.";
|
|
29
29
|
/**
|
|
@@ -55,7 +55,7 @@ export declare function insertAfterBlockUnresolvedLoweredWarning(line: number):
|
|
|
55
55
|
*/
|
|
56
56
|
export declare const UNRESOLVED_BLOCK_INTERNAL = "internal error: unresolved `SWAP.BLK` edit reached the applier (resolveBlockEdits was not run).";
|
|
57
57
|
/** Delete hunk received a body row. */
|
|
58
|
-
export declare const DELETE_TAKES_NO_BODY = "`DEL N
|
|
58
|
+
export declare const DELETE_TAKES_NO_BODY = "`DEL N.=M` does not take body rows. Remove the body, or use `SWAP N.=M:`.";
|
|
59
59
|
/** `delete_block N` hunk received a body row. */
|
|
60
60
|
export declare const DELETE_BLOCK_TAKES_NO_BODY = "`DEL.BLK N` does not take body rows. Remove the body, or use `SWAP.BLK N:`.";
|
|
61
61
|
/** Insert hunk with no body. */
|
package/dist/types/types.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export type Edit = {
|
|
|
53
53
|
* time — it is computed by {@link resolveBlockEdits} once file text +
|
|
54
54
|
* path (→ language) are available, then expanded into concrete edits:
|
|
55
55
|
* a non-empty `payloads` without `mode` (from `replace_block`) becomes
|
|
56
|
-
* the same `replacement` inserts + deletes that `replace start
|
|
56
|
+
* the same `replacement` inserts + deletes that `replace start.=end:`
|
|
57
57
|
* produces; an empty `payloads` (from `delete_block`) becomes a pure
|
|
58
58
|
* range deletion; `mode: "insert_after"` becomes plain `after_anchor`
|
|
59
59
|
* inserts at the block's last line. `applyEdits` never sees this
|
|
@@ -82,7 +82,7 @@ export interface ApplyResult {
|
|
|
82
82
|
*/
|
|
83
83
|
blockResolutions?: BlockResolution[];
|
|
84
84
|
}
|
|
85
|
-
/** A parsed `[A
|
|
85
|
+
/** A parsed `[A.=B]` line range. */
|
|
86
86
|
export interface ParsedRange {
|
|
87
87
|
start: Anchor;
|
|
88
88
|
end: Anchor;
|
|
@@ -132,7 +132,7 @@ export interface BlockSpan {
|
|
|
132
132
|
/**
|
|
133
133
|
* One `replace_block N:` / `delete_block N` / `insert_after_block N:` anchor
|
|
134
134
|
* resolved to its concrete line span. Surfaced on {@link ApplyResult} so the
|
|
135
|
-
* host can echo "block N → lines start
|
|
135
|
+
* host can echo "block N → lines start.=end" and let the model catch a wrong
|
|
136
136
|
* opener — e.g. a decorator or doc-comment that sits in a separate node
|
|
137
137
|
* outside the resolved block.
|
|
138
138
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/hashline",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "16.0.0",
|
|
5
5
|
"description": "Hashline: a compact, line-anchored patch language and applier. Pluggable FS/IO so it works over disk, in-memory, or any custom backend.",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
package/src/apply.ts
CHANGED
|
@@ -232,7 +232,7 @@ interface ReplacementGroup {
|
|
|
232
232
|
* Detect a replacement group starting at `start`: a run of `before_anchor`
|
|
233
233
|
* replacement inserts sharing one source op line, immediately followed by the
|
|
234
234
|
* contiguous range deletes for that same op. Mirrors how the parser lowers an
|
|
235
|
-
* `replace N
|
|
235
|
+
* `replace N.=M:` hunk with a body.
|
|
236
236
|
*/
|
|
237
237
|
function findReplacementGroup(edits: readonly AppliedEdit[], start: number): ReplacementGroup | undefined {
|
|
238
238
|
const first = edits[start];
|
|
@@ -453,7 +453,7 @@ function describeBoundaryRepair(group: ReplacementGroup, action: string): string
|
|
|
453
453
|
* by {@link findDuplicateSuffix}/{@link findDuplicatePrefix}.
|
|
454
454
|
*
|
|
455
455
|
* Scoped to multi-line ranges (a construct rewrite) on purpose: a single-line
|
|
456
|
-
* `replace N
|
|
456
|
+
* `replace N.=N` expanding into several lines is an *expansion* where every
|
|
457
457
|
* payload line is intentional new content, so a payload line that happens to
|
|
458
458
|
* equal a neighbor stays — only a genuine block rewrite retypes a boundary
|
|
459
459
|
* keeper by mistake. The dropped lines must be delimiter-neutral so removing the
|
package/src/block.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* runs at every apply/preview boundary that has text: it calls the injected
|
|
8
8
|
* {@link BlockResolver} to resolve each block's `[start, end]` span, then emits
|
|
9
9
|
* the exact same edits the concrete form produces in the parser: `replace
|
|
10
|
-
* start
|
|
10
|
+
* start.=end:` inserts + deletes for a replace, a pure range delete for a
|
|
11
11
|
* delete, and plain `after_anchor` inserts at `end` for an insert-after. After
|
|
12
12
|
* it runs, no `block` edits remain, so {@link applyEdits} (and recovery) only
|
|
13
13
|
* ever see resolved edits.
|
|
@@ -145,7 +145,7 @@ export function resolveBlockEdits(
|
|
|
145
145
|
}
|
|
146
146
|
continue;
|
|
147
147
|
}
|
|
148
|
-
// Mirror the parser's `replace start
|
|
148
|
+
// Mirror the parser's `replace start.=end:` expansion exactly: one
|
|
149
149
|
// `before_anchor` replacement insert per payload row at `span.start`,
|
|
150
150
|
// then one delete per line across `[span.start, span.end]`. An empty
|
|
151
151
|
// `payloads` (from `delete_block N`) emits no inserts — a pure deletion.
|
package/src/format.ts
CHANGED
|
@@ -38,8 +38,8 @@ export const HL_HEADER_COLON = ":";
|
|
|
38
38
|
/** Separator between a hashline file path and its opaque snapshot tag. */
|
|
39
39
|
export const HL_FILE_HASH_SEP = "#";
|
|
40
40
|
|
|
41
|
-
/** Separator between two line numbers in a range, e.g. `5
|
|
42
|
-
export const HL_RANGE_SEP = "
|
|
41
|
+
/** Separator between two line numbers in a range, e.g. `5.=10`. */
|
|
42
|
+
export const HL_RANGE_SEP = ".=";
|
|
43
43
|
|
|
44
44
|
/** Separator between a line number and displayed line content in hashline mode. */
|
|
45
45
|
export const HL_LINE_BODY_SEP = ":";
|
package/src/grammar.lark
CHANGED
package/src/messages.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Centralized error/warning text for the hashline parser, applier, and patcher. */
|
|
2
2
|
|
|
3
|
-
import { formatNumberedLine, HL_FILE_HASH_SEP, HL_FILE_PREFIX, HL_FILE_SUFFIX } from "./format";
|
|
3
|
+
import { formatNumberedLine, HL_FILE_HASH_SEP, HL_FILE_PREFIX, HL_FILE_SUFFIX, HL_RANGE_SEP } from "./format";
|
|
4
4
|
|
|
5
5
|
/** Lines of context shown either side of a hash mismatch. */
|
|
6
6
|
export const MISMATCH_CONTEXT = 2;
|
|
@@ -43,11 +43,10 @@ export const END_PATCH_MARKER = "*** End Patch";
|
|
|
43
43
|
export const ABORT_MARKER = "*** Abort";
|
|
44
44
|
|
|
45
45
|
/** Two consecutive hunks targeted the exact same concrete range. */
|
|
46
|
-
export const REPLACE_PAIR_COALESCED_WARNING =
|
|
47
|
-
"Two hunks targeted the same range; kept only the second. One `SWAP N..M:` hunk per range — the body is the final content, never old+new.";
|
|
46
|
+
export const REPLACE_PAIR_COALESCED_WARNING = `Two hunks targeted the same range; kept only the second. One \`SWAP N${HL_RANGE_SEP}M:\` hunk per range — the body is the final content, never old+new.`;
|
|
48
47
|
|
|
49
48
|
/** Bare bodyless hunk followed by an overlapping concrete hunk. */
|
|
50
|
-
|
|
49
|
+
`Dropped a bare hunk overlapped by the concrete hunk after it. One \`SWAP N${HL_RANGE_SEP}M:\` hunk per range — the body is the final content, never old+new.`;
|
|
51
50
|
|
|
52
51
|
/** Bare body rows auto-converted to literal `+` rows. */
|
|
53
52
|
export const BARE_BODY_AUTO_PIPED_WARNING =
|
|
@@ -58,7 +57,7 @@ export const MINUS_ROW_REJECTED =
|
|
|
58
57
|
"`-` rows are not valid; the range already names the lines being changed. For a literal `-` line, write `+-…`.";
|
|
59
58
|
|
|
60
59
|
/** Replace hunk with no body. */
|
|
61
|
-
export const EMPTY_REPLACE =
|
|
60
|
+
export const EMPTY_REPLACE = `\`SWAP N${HL_RANGE_SEP}M:\` needs at least one \`+TEXT\` body row. To delete lines, use \`DEL N${HL_RANGE_SEP}M\`.`;
|
|
62
61
|
|
|
63
62
|
/** `replace_block N:` hunk with no body. */
|
|
64
63
|
export const EMPTY_BLOCK = "`SWAP.BLK N:` needs at least one `+TEXT` body row. To delete a block, use `DEL.BLK N`.";
|
|
@@ -77,7 +76,7 @@ export function blockUnresolvedMessage(
|
|
|
77
76
|
fileLines?: readonly string[],
|
|
78
77
|
): string {
|
|
79
78
|
const phrase = op === "delete" ? `DEL.BLK ${line}` : `SWAP.BLK ${line}:`;
|
|
80
|
-
const fallback = op === "delete" ? `DEL ${line}
|
|
79
|
+
const fallback = op === "delete" ? `DEL ${line}${HL_RANGE_SEP}M` : `SWAP ${line}${HL_RANGE_SEP}M:`;
|
|
81
80
|
let message =
|
|
82
81
|
`\`${phrase}\` could not resolve a syntactic block beginning on line ${line} ` +
|
|
83
82
|
`(unsupported language, blank/closer line, or parse error). Use \`${fallback}\` with explicit lines.`;
|
|
@@ -118,7 +117,7 @@ export const UNRESOLVED_BLOCK_INTERNAL =
|
|
|
118
117
|
"internal error: unresolved `SWAP.BLK` edit reached the applier (resolveBlockEdits was not run).";
|
|
119
118
|
|
|
120
119
|
/** Delete hunk received a body row. */
|
|
121
|
-
export const DELETE_TAKES_NO_BODY =
|
|
120
|
+
export const DELETE_TAKES_NO_BODY = `\`DEL N${HL_RANGE_SEP}M\` does not take body rows. Remove the body, or use \`SWAP N${HL_RANGE_SEP}M:\`.`;
|
|
122
121
|
|
|
123
122
|
/** `delete_block N` hunk received a body row. */
|
|
124
123
|
export const DELETE_BLOCK_TAKES_NO_BODY = "`DEL.BLK N` does not take body rows. Remove the body, or use `SWAP.BLK N:`.";
|
|
@@ -226,7 +225,11 @@ export type BlockOp = "replace" | "delete" | "insert_after";
|
|
|
226
225
|
export function blockSingleLineMessage(line: number, op: BlockOp): string {
|
|
227
226
|
const blockForm = op === "insert_after" ? "INS.BLK.POST" : op === "delete" ? "DEL.BLK" : "SWAP.BLK";
|
|
228
227
|
const plainForm =
|
|
229
|
-
op === "insert_after"
|
|
228
|
+
op === "insert_after"
|
|
229
|
+
? `INS.POST ${line}:`
|
|
230
|
+
: op === "delete"
|
|
231
|
+
? `DEL ${line}`
|
|
232
|
+
: `SWAP ${line}${HL_RANGE_SEP}${line}:`;
|
|
230
233
|
return (
|
|
231
234
|
`\`${blockForm} ${line}\` resolved a single-line block — line ${line} is a bare statement, not the opening line ` +
|
|
232
235
|
`of a multi-line construct. For that one line use \`${plainForm}\`; to act on an enclosing construct, anchor ${blockForm} ` +
|
package/src/parser.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* flat list of {@link Edit}s. Sits between the {@link Tokenizer} and the
|
|
4
4
|
* applier.
|
|
5
5
|
*/
|
|
6
|
-
import { HL_PAYLOAD_REPLACE } from "./format";
|
|
6
|
+
import { HL_PAYLOAD_REPLACE, HL_RANGE_SEP } from "./format";
|
|
7
7
|
import {
|
|
8
8
|
BARE_BODY_AUTO_PIPED_WARNING,
|
|
9
9
|
DELETE_BLOCK_TAKES_NO_BODY,
|
|
@@ -18,7 +18,9 @@ import type { Anchor, Cursor, Edit } from "./types";
|
|
|
18
18
|
|
|
19
19
|
function validateRangeOrder(range: ParsedRange, lineNum: number): void {
|
|
20
20
|
if (range.end.line < range.start.line) {
|
|
21
|
-
throw new Error(
|
|
21
|
+
throw new Error(
|
|
22
|
+
`line ${lineNum}: range ${range.start.line}${HL_RANGE_SEP}${range.end.line} ends before it starts.`,
|
|
23
|
+
);
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -52,33 +54,33 @@ function detectApplyPatchContamination(text: string, _hasPending: boolean): stri
|
|
|
52
54
|
return (
|
|
53
55
|
`apply_patch sentinel ${JSON.stringify(preview)} is not valid in hashline. ` +
|
|
54
56
|
"File sections start with `[path#HASH]` (no `Update File:` / `Add File:` keyword). " +
|
|
55
|
-
|
|
57
|
+
`Use \`SWAP N${HL_RANGE_SEP}M:\`, \`DEL N${HL_RANGE_SEP}M\`, or \`INS.PRE|POST|HEAD|TAIL:\` ops.`
|
|
56
58
|
);
|
|
57
59
|
}
|
|
58
60
|
if (/^@@\s+[-+]?\d+,\d+\s+[-+]?\d+,\d+\s+@@/.test(trimmed)) {
|
|
59
61
|
return (
|
|
60
62
|
"unified-diff hunk header (`@@ -N,M +N,M @@`) is not valid in hashline. " +
|
|
61
|
-
|
|
63
|
+
`Use \`SWAP N${HL_RANGE_SEP}M:\`, \`DEL N${HL_RANGE_SEP}M\`, or \`INS.PRE|POST|HEAD|TAIL:\` ops.`
|
|
62
64
|
);
|
|
63
65
|
}
|
|
64
66
|
if (trimmed.startsWith("@@")) {
|
|
65
67
|
const preview = trimmed.length > 48 ? `${trimmed.slice(0, 48)}…` : trimmed;
|
|
66
68
|
return (
|
|
67
69
|
`\`@@\`-bracketed hunk header ${JSON.stringify(preview)} is not valid in hashline. ` +
|
|
68
|
-
|
|
70
|
+
`Drop the \`@@ ... @@\` brackets and write a verb header such as \`SWAP N${HL_RANGE_SEP}M:\`.`
|
|
69
71
|
);
|
|
70
72
|
}
|
|
71
|
-
if (/^DEL\s+[1-9]\d*(?:\s*(
|
|
72
|
-
return
|
|
73
|
+
if (/^DEL\s+[1-9]\d*(?:\s*(?:\.\.|\.=|-|…|\s)\s*[1-9]\d*)?\s*:/.test(trimmed)) {
|
|
74
|
+
return `\`DEL N${HL_RANGE_SEP}M\` has no colon and no body. Remove the colon and body rows.`;
|
|
73
75
|
}
|
|
74
76
|
if (/^[1-9]\d*\s*$/.test(trimmed)) {
|
|
75
|
-
return `hunk headers need a verb. Use \`SWAP ${trimmed}
|
|
77
|
+
return `hunk headers need a verb. Use \`SWAP ${trimmed}${HL_RANGE_SEP}${trimmed}:\` to replace, or \`DEL ${trimmed}\` to delete.`;
|
|
76
78
|
}
|
|
77
|
-
const bareRange = /^([1-9]\d*)\s*[-.
|
|
79
|
+
const bareRange = /^([1-9]\d*)\s*[-. …=]+\s*([1-9]\d*)\s*:?$/.exec(trimmed);
|
|
78
80
|
if (bareRange !== null) {
|
|
79
81
|
return (
|
|
80
82
|
`bare range hunk header ${JSON.stringify(trimmed)} is not valid. ` +
|
|
81
|
-
`Hunk headers need a verb: write \`SWAP ${bareRange[1]}
|
|
83
|
+
`Hunk headers need a verb: write \`SWAP ${bareRange[1]}${HL_RANGE_SEP}${bareRange[2]}:\` or \`DEL ${bareRange[1]}${HL_RANGE_SEP}${bareRange[2]}\`.`
|
|
82
84
|
);
|
|
83
85
|
}
|
|
84
86
|
return null;
|
|
@@ -253,7 +255,7 @@ export class Executor {
|
|
|
253
255
|
if (text.trim().length === 0) return;
|
|
254
256
|
throw new Error(
|
|
255
257
|
`line ${lineNum}: payload line has no preceding hunk header. ` +
|
|
256
|
-
`Use \`SWAP N
|
|
258
|
+
`Use \`SWAP N${HL_RANGE_SEP}M:\`, \`DEL N${HL_RANGE_SEP}M\`, or \`INS.PRE|POST|HEAD|TAIL:\` above the body. Got ${JSON.stringify(text)}.`,
|
|
257
259
|
);
|
|
258
260
|
}
|
|
259
261
|
|
package/src/prompt.md
CHANGED
|
@@ -5,16 +5,16 @@ Every file section starts with `[PATH#TAG]`. `TAG` is the 4-hex snapshot tag fro
|
|
|
5
5
|
</headers>
|
|
6
6
|
|
|
7
7
|
<ops>
|
|
8
|
-
`SWAP N
|
|
8
|
+
`SWAP N.=M:` — replace original lines N.=M with the body rows below. INCLUSIVE — line M is consumed too.
|
|
9
9
|
`SWAP.BLK N:` — replace the whole syntactic block that BEGINS on line N; tree-sitter resolves the closing line. Body rows below.
|
|
10
|
-
`DEL N
|
|
10
|
+
`DEL N.=M` — delete original lines N.=M. No body.
|
|
11
11
|
`DEL.BLK N` — delete the whole syntactic block that BEGINS on line N.
|
|
12
12
|
`INS.PRE N:` — insert the body rows immediately before line N.
|
|
13
13
|
`INS.POST N:` — insert the body rows immediately after line N.
|
|
14
14
|
`INS.BLK.POST N:` — insert the body rows after the END of the block that BEGINS on line N — outside it, at sibling depth. To append inside a block, use `INS.POST`.
|
|
15
15
|
`INS.HEAD:` — insert the body rows at the very start of the file.
|
|
16
16
|
`INS.TAIL:` — insert the body rows at the very end of the file.
|
|
17
|
-
Single line: `SWAP N
|
|
17
|
+
Single line: `SWAP N.=N:` / `DEL N`. The range is the ORIGINAL lines you touch; body length is irrelevant (replacing 1 line with 10 is still `SWAP N.=N:`).
|
|
18
18
|
</ops>
|
|
19
19
|
|
|
20
20
|
<body-rows>
|
|
@@ -34,9 +34,9 @@ There is NO other body row kind. NEVER write `-old` or a bare/context line. To k
|
|
|
34
34
|
- On a stale-tag rejection or any surprising result: STOP and re-`read` before further edits.
|
|
35
35
|
- One hunk per range; the body is the final content, never an old/new pair.
|
|
36
36
|
- Ranges cover ONLY lines whose content changes. Never widen over unchanged lines — a stale wide range shreds everything it spans.
|
|
37
|
-
- Whole construct → `SWAP.BLK N` (tree-sitter resolves the end); lines inside it → `SWAP N
|
|
38
|
-
- `SWAP.BLK N` resolves EXACTLY the node at N. Leading decorators/attributes/doc-comments are separate nodes: point N at the FIRST decorator to sweep both; standalone line-comments are never swept — use `SWAP N
|
|
39
|
-
- Block ops (`SWAP.BLK`/`DEL.BLK`/`INS.BLK.POST`) anchor the OPENING line of a MULTI-LINE construct — never its closer, its last line, or a bare statement inside it. Anchoring a single statement resolves to ONE line and is REJECTED: use the plain op (`SWAP N
|
|
37
|
+
- Whole construct → `SWAP.BLK N` (tree-sitter resolves the end); lines inside it → `SWAP N.=M`.
|
|
38
|
+
- `SWAP.BLK N` resolves EXACTLY the node at N. Leading decorators/attributes/doc-comments are separate nodes: point N at the FIRST decorator to sweep both; standalone line-comments are never swept — use `SWAP N.=M`.
|
|
39
|
+
- Block ops (`SWAP.BLK`/`DEL.BLK`/`INS.BLK.POST`) anchor the OPENING line of a MULTI-LINE construct — never its closer, its last line, or a bare statement inside it. Anchoring a single statement resolves to ONE line and is REJECTED: use the plain op (`SWAP N.=N` / `DEL N` / `INS.POST N`) for one line, or point N at the real opener. Saw the closer? Use plain `INS.POST M:`.
|
|
40
40
|
- Non-adjacent changes = separate hunks; untouched lines stay out of every range.
|
|
41
41
|
- Pure additions use `INS.PRE` / `INS.POST` / `INS.HEAD` / `INS.TAIL`, never a widened `SWAP` — retyped keepers are exactly what gets dropped. A multi-line `SWAP` whose body restates the line just outside the range is auto-dropped as an off-by-one keeper (with a warning), but issue the payload as the final content for the range only and never lean on the repair.
|
|
42
42
|
- NEVER format/restyle code with this tool; run the project formatter instead.
|
|
@@ -62,7 +62,7 @@ INS.POST 1:
|
|
|
62
62
|
Replace line 2 with two lines:
|
|
63
63
|
```
|
|
64
64
|
[greet.py#A1B2]
|
|
65
|
-
SWAP 2
|
|
65
|
+
SWAP 2.=2:
|
|
66
66
|
+ greeting = "Hi"
|
|
67
67
|
+ msg = f"{greeting}, {name}"
|
|
68
68
|
```
|
|
@@ -102,24 +102,24 @@ SWAP.BLK 1:
|
|
|
102
102
|
|
|
103
103
|
<anti-patterns>
|
|
104
104
|
# WRONG — empty `SWAP` to delete. RIGHT: DEL 4
|
|
105
|
-
SWAP 4
|
|
105
|
+
SWAP 4.=4:
|
|
106
106
|
|
|
107
|
-
# WRONG — range describes post-edit size. RIGHT: SWAP 1
|
|
108
|
-
SWAP 1
|
|
107
|
+
# WRONG — range describes post-edit size. RIGHT: SWAP 1.=1: (body length is irrelevant)
|
|
108
|
+
SWAP 1.=2:
|
|
109
109
|
+def greet(name):
|
|
110
110
|
|
|
111
111
|
# WRONG — `-` rows / bare context lines do not exist. The range deletes; the body is only the new content.
|
|
112
|
-
SWAP 3
|
|
112
|
+
SWAP 3.=3:
|
|
113
113
|
msg = "Hello, " + name
|
|
114
114
|
- print(msg)
|
|
115
115
|
+ return msg
|
|
116
116
|
# RIGHT
|
|
117
|
-
SWAP 3
|
|
117
|
+
SWAP 3.=3:
|
|
118
118
|
+ return msg
|
|
119
119
|
|
|
120
120
|
# WRONG — a pure insertion done as a widened `SWAP`: you only want to add one line after 2,
|
|
121
|
-
# but you replace 2
|
|
122
|
-
SWAP 2
|
|
121
|
+
# but you replace 2.=4, retype the keepers in the body, and drop one (here line 4, `greet("world")`).
|
|
122
|
+
SWAP 2.=4:
|
|
123
123
|
+ msg = "Hello, " + name
|
|
124
124
|
+ extra = compute(name)
|
|
125
125
|
+ print(msg)
|
package/src/tokenizer.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Format shape:
|
|
5
5
|
* ```
|
|
6
6
|
* [path/to/file.ts#1A2B]
|
|
7
|
-
* replace 5
|
|
7
|
+
* replace 5.=7:
|
|
8
8
|
* +literal new line
|
|
9
9
|
* ```
|
|
10
10
|
*/
|
|
@@ -40,6 +40,7 @@ const CHAR_SPACE = 32;
|
|
|
40
40
|
const CHAR_DOT = 46;
|
|
41
41
|
const CHAR_HYPHEN = 45;
|
|
42
42
|
const CHAR_ELLIPSIS = 0x2026;
|
|
43
|
+
const CHAR_EQUALS = 61;
|
|
43
44
|
|
|
44
45
|
const CHAR_UPPER_A = 65;
|
|
45
46
|
const CHAR_UPPER_F = 70;
|
|
@@ -167,7 +168,11 @@ function scanRangeSeparator(line: string, index: number, end: number): number |
|
|
|
167
168
|
consumedSeparator = true;
|
|
168
169
|
continue;
|
|
169
170
|
}
|
|
170
|
-
if (
|
|
171
|
+
if (
|
|
172
|
+
code === CHAR_DOT &&
|
|
173
|
+
cursor + 1 < end &&
|
|
174
|
+
(line.charCodeAt(cursor + 1) === CHAR_DOT || line.charCodeAt(cursor + 1) === CHAR_EQUALS)
|
|
175
|
+
) {
|
|
171
176
|
cursor += 2;
|
|
172
177
|
consumedSeparator = true;
|
|
173
178
|
continue;
|
|
@@ -277,7 +282,7 @@ function scanHunkAnchor(line: string, start: number, end: number): TargetScan |
|
|
|
277
282
|
};
|
|
278
283
|
}
|
|
279
284
|
// `delete_block N` — resolve N to a tree-sitter block range at apply time
|
|
280
|
-
// and delete its whole span. Like `delete N
|
|
285
|
+
// and delete its whole span. Like `delete N.=M`, it takes no body and no
|
|
281
286
|
// trailing colon.
|
|
282
287
|
const deleteBlockEnd = scanKeyword(line, cursor, end, HL_DELETE_BLOCK_KEYWORD);
|
|
283
288
|
if (deleteBlockEnd !== null) {
|
package/src/types.ts
CHANGED
|
@@ -47,7 +47,7 @@ export type Edit =
|
|
|
47
47
|
* time — it is computed by {@link resolveBlockEdits} once file text +
|
|
48
48
|
* path (→ language) are available, then expanded into concrete edits:
|
|
49
49
|
* a non-empty `payloads` without `mode` (from `replace_block`) becomes
|
|
50
|
-
* the same `replacement` inserts + deletes that `replace start
|
|
50
|
+
* the same `replacement` inserts + deletes that `replace start.=end:`
|
|
51
51
|
* produces; an empty `payloads` (from `delete_block`) becomes a pure
|
|
52
52
|
* range deletion; `mode: "insert_after"` becomes plain `after_anchor`
|
|
53
53
|
* inserts at the block's last line. `applyEdits` never sees this
|
|
@@ -78,7 +78,7 @@ export interface ApplyResult {
|
|
|
78
78
|
blockResolutions?: BlockResolution[];
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** A parsed `[A
|
|
81
|
+
/** A parsed `[A.=B]` line range. */
|
|
82
82
|
export interface ParsedRange {
|
|
83
83
|
start: Anchor;
|
|
84
84
|
end: Anchor;
|
|
@@ -134,7 +134,7 @@ export interface BlockSpan {
|
|
|
134
134
|
/**
|
|
135
135
|
* One `replace_block N:` / `delete_block N` / `insert_after_block N:` anchor
|
|
136
136
|
* resolved to its concrete line span. Surfaced on {@link ApplyResult} so the
|
|
137
|
-
* host can echo "block N → lines start
|
|
137
|
+
* host can echo "block N → lines start.=end" and let the model catch a wrong
|
|
138
138
|
* opener — e.g. a decorator or doc-comment that sits in a separate node
|
|
139
139
|
* outside the resolved block.
|
|
140
140
|
*/
|