fix(hashline): clarified insert-after-block anchors
Updated the hashline edit prompt and internal edit docs to make insert-after-block anchors opener-only, and added regression coverage for closing-delimiter anchors and prompt guidance.\n\nFixes #2292
This commit is contained in:
+4
-2
@@ -34,6 +34,7 @@ Patch language inside `input`:
|
||||
- `delete block N` — delete the whole tree-sitter block beginning on line N (resolved like `replace block N`, with the same decorator/comment caveat). No body. On success the result echoes the matched span (`delete block N → resolved lines A-B`). Same resolution failure modes and `delete N..M` fallback.
|
||||
- `insert before N:` — insert body rows immediately before line N.
|
||||
- `insert after N:` — insert body rows immediately after line N.
|
||||
- `insert after block N:` — insert body rows after the last line of the tree-sitter block beginning on line N. Point N at the line that opens the construct, never its closing delimiter / last visible line; if you can see the last line already, use plain `insert after M:`. Same resolution failure modes and `insert after M:` fallback.
|
||||
- `insert head:` — insert body rows at the start of the file.
|
||||
- `insert tail:` — insert body rows at the end of the file.
|
||||
- **Body rows**:
|
||||
@@ -62,7 +63,7 @@ The canonical grammar is strict, but the hand parser accepts a few non-dangerous
|
||||
- `delete N..M:` and any body rows under `delete` / `delete block` are rejected.
|
||||
- Empty `replace` / `insert` / `replace block` hunks are rejected.
|
||||
- `-` body rows are rejected with `MINUS_ROW_REJECTED`.
|
||||
- `replace block N:` / `delete block N` require a wired tree-sitter resolver; `replace block` additionally needs at least one `+TEXT` body row, while `delete block` takes none. An unresolvable block (unsupported language, blank/closing-delimiter line, no node beginning on N, or a syntax error in the resolved block) is rejected on the apply/final-preview path; the streaming preview silently drops it instead.
|
||||
- `replace block N:` / `delete block N` / `insert after block N:` require a wired tree-sitter resolver; `replace block` and `insert after block` additionally need at least one `+TEXT` body row, while `delete block` takes none. An unresolvable block (unsupported language, blank/closing-delimiter line, no node beginning on N, or a syntax error in the resolved block) is rejected on the apply/final-preview path; the streaming preview silently drops it instead.
|
||||
|
||||
## Outputs
|
||||
- Single-shot tool result; hashline mode does not use a `resolve` preview/apply handshake.
|
||||
@@ -171,8 +172,9 @@ delete 20
|
||||
- `line N: \`replace N..M:\` needs at least one \`+TEXT\` body row. To delete lines, use \`delete N..M\`.`
|
||||
- `line N: \`insert\` needs at least one \`+TEXT\` body row.`
|
||||
- `line N: \`replace block N:\` needs at least one \`+TEXT\` body row. To delete a block, use \`delete N..M\` with the block's line range.`
|
||||
- Unresolvable `replace block N:` (apply / final-preview path only):
|
||||
- Unresolvable block anchor (apply / final-preview path only):
|
||||
- `line N: \`replace block X:\` could not resolve a syntactic block beginning on line X. The language may be unsupported, the line may be blank or a closing delimiter, or the block may not parse. Use \`replace X..M:\` with the block's explicit end line instead.` — followed by a blank line and numbered `*`-marked context rows around line X (same shape as the mismatch preview).
|
||||
- `line N: \`insert after block X:\` could not resolve a syntactic block beginning on line X. The language may be unsupported, the line may be blank or a closing delimiter, or the block may not parse. Use \`insert after M:\` with the block's explicit last line instead.` — same context preview.
|
||||
- Delete with body:
|
||||
- `line N: \`delete N..M\` does not take body rows. Remove the body, or use \`replace N..M:\`.`
|
||||
- `line N: \`delete block N\` does not take body rows. Remove the body, or use \`replace block N:\` to replace the block.`
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the edit-tool hashline prompt to stop steering agents toward `insert after block N:` on closing delimiter lines; opener-only block anchors now point visible closing-line insertions to plain `insert after M:`. ([#2292](https://github.com/can1357/oh-my-pi/issues/2292))
|
||||
- Fixed `irc` live message delivery so successfully handed-off messages are no longer enqueued as mailbox mail, so they do not inflate unread `irc` counts
|
||||
- Fixed `irc send` with `await: true` to wait for a fresh reply to the current call instead of consuming previously buffered messages
|
||||
- Fixed main-session chat output to stop duplicating outbound `irc` sends from the main agent as relay cards
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the `insert after block N:` prompt guidance so it explicitly says N must be the block opener, not the closing delimiter or last visible line, and points visible closing-line edits to plain `insert after M:`. ([#2292](https://github.com/can1357/oh-my-pi/issues/2292))
|
||||
|
||||
## [15.11.0] - 2026-06-10
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -11,7 +11,7 @@ Every file section starts with `[PATH#TAG]`. `TAG` is the 4-hex snapshot tag fro
|
||||
`delete block N` — delete the whole syntactic block that BEGINS on line N.
|
||||
`insert before N:` — insert the body rows immediately before line N.
|
||||
`insert after N:` — insert the body rows immediately after line N.
|
||||
`insert after block N:` — insert the body rows after the END of the syntactic block that BEGINS on line N (resolved like `replace block`).
|
||||
`insert after block N:` — insert the body rows after the END of the syntactic block that BEGINS on line N. Point N at the line that OPENS the construct (the `if`/`function`/`def`/`{`-bearing line), not the closing delimiter / last visible line; if you have the last line, use plain `insert after M:` instead.
|
||||
`insert head:` — insert the body rows at the very start of the file.
|
||||
`insert tail:` — insert the body rows at the very end of the file.
|
||||
Single line: `replace N..N:` / `delete N`. The range is the ORIGINAL lines you touch; body length is irrelevant (replacing 1 line with 10 is still `replace N..N:`).
|
||||
@@ -36,6 +36,7 @@ There is NO other body row kind. NEVER write `-old` or a bare/context line. To k
|
||||
- Keep every range as tight as the change: a range covers ONLY lines whose content actually changes. Never widen it to swallow an unchanged signature, brace, or neighboring statement just to rewrite a few lines inside — change one line with `replace N..N`, not the whole block around it. Tightness means excluding unchanged lines, not being short: a range where every line genuinely changes is correctly long. Tight ranges bound the blast radius of a stale number: a stale one-line range corrupts one line; a stale wide range shreds every line it spans. This applies to hand-counted `replace N..M` ranges; `replace block N` is exempt — tree-sitter fixes the end.
|
||||
- `replace block N` vs `replace N..M`: use `replace block N` to rewrite a WHOLE construct (function / `if` / loop / class body) — tree-sitter resolves its closing line, so a long body can't be mis-counted and a stale end can't clip it mid-block. The edit result echoes the span it matched (`replace block N → resolved lines A-B`); glance at it to confirm you got what you meant. Use `replace N..M` to change specific lines inside a construct.
|
||||
- The resolved span of `replace block N` is EXACTLY the node beginning on line N. A leading decorator, attribute, or doc-comment is a separate node and is NOT included; to take a decorated definition together with its decorator, point N at the FIRST decorator line (Python parses `@dec` + `def` as one block). A leading line-comment that parses as its own node (e.g. Rust `///`) is not captured by any single opener — use `replace N..M` spanning the comment and the construct.
|
||||
- `insert after block N` follows the same opener-only anchor rule as `replace block N`: N is the first line of the syntactic block, never a line inside it, its closing delimiter, or its last visible line. To append after a closing delimiter you can see, use plain `insert after M:`.
|
||||
- To change lines 2 and 5 while keeping 3–4, issue two hunks (`replace 2..2:` and `replace 5..5:`). Untouched lines are simply absent from every range.
|
||||
- Pure additions use `insert`, never a widened `replace`. If the change only adds lines, `insert before/after` the spot and keep every existing line out of all ranges. Do NOT `replace` a span of keepers and retype them around the new line "to preserve" them — those retyped keepers are exactly what gets silently dropped when one is forgotten. A keeper that never enters your body cannot be lost. `replace` is only for lines whose own text changes.
|
||||
- NEVER use this tool to format code — reordering imports, re-indenting, aligning columns, or any mechanical restyling. That is the project formatter's job; run it instead of hand-editing layout here.
|
||||
@@ -125,6 +126,13 @@ replace 2..4:
|
||||
# RIGHT — touch nothing you keep; the new line is the whole body.
|
||||
insert after 2:
|
||||
+ extra = compute(name)
|
||||
|
||||
# WRONG — `insert after block N:` anchored on a closing delimiter / last visible line. RIGHT: plain `insert after M:`
|
||||
insert after block 3:
|
||||
+after()
|
||||
# RIGHT
|
||||
insert after 3:
|
||||
+after()
|
||||
</anti-patterns>
|
||||
|
||||
<critical>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
parsePatch,
|
||||
resolveBlockEdits,
|
||||
} from "@oh-my-pi/hashline";
|
||||
import promptText from "../src/prompt.md" with { type: "text" };
|
||||
|
||||
const PATH = "x.ts";
|
||||
|
||||
@@ -340,6 +341,33 @@ describe("insert after block", () => {
|
||||
expect(() => resolveBlockEdits(edits, "ignored", PATH, () => null)).toThrow("`insert after block 7:`");
|
||||
});
|
||||
|
||||
it("rejects a closing-delimiter line as an insert-after-block anchor", () => {
|
||||
const section = Patch.parseSingle(`[${PATH}#1A2B]\ninsert after block 3:\n+ done();`);
|
||||
const resolver: BlockResolver = ({ line }) => (line === 2 ? { start: 2, end: 3 } : null);
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
section.applyTo(text, resolver);
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err : new Error(String(err));
|
||||
}
|
||||
|
||||
expect(error?.message).toContain(
|
||||
"`insert after block 3:` could not resolve a syntactic block beginning on line 3",
|
||||
);
|
||||
expect(error?.message).toContain("Use `insert after M:` with the block's explicit last line instead");
|
||||
expect(error?.message).toContain("*3: }");
|
||||
});
|
||||
|
||||
it("documents the opener-only rule for insert-after-block anchors", () => {
|
||||
const entry = promptText.split("\n").find(line => line.startsWith("`insert after block N:`"));
|
||||
|
||||
expect(entry).toContain("OPENS");
|
||||
expect(entry).toContain("not the closing delimiter / last visible line");
|
||||
expect(promptText).toContain(
|
||||
"# WRONG — `insert after block N:` anchored on a closing delimiter / last visible line. RIGHT: plain `insert after M:`",
|
||||
);
|
||||
});
|
||||
|
||||
it("applyTo inserts the body after the resolved block's last line", () => {
|
||||
const section = Patch.parseSingle(`[${PATH}#1A2B]\ninsert after block 2:\n+ done();`);
|
||||
// stub span [2,3] → body lands after " }" (line 3), before the final "}".
|
||||
|
||||
Reference in New Issue
Block a user