fix(hashline): prevented exposing terminal newline as an addressable row

- Added `splitAddressableFileLines` to strip terminal newlines from line addressability without removing genuine blank lines.
- Updated coding-agent read tool context parsing to use addressable file lines.
This commit is contained in:
can1357
2026-08-12 03:20:29 +02:00
parent 154c6222f6
commit 21a7693dd7
7 changed files with 76 additions and 22 deletions
+1 -3
View File
@@ -2,9 +2,6 @@
## [Unreleased]
### Fixed
- Stopped `formatNumberedLines` from exposing a terminal newline as an editable blank line; deleting that synthetic row previously produced a misleading no-op.
### Added
@@ -13,6 +10,7 @@
### Fixed
- Fixed Rust lifetimes blinding the delimiter-balance scanner. `'` entered string state to end-of-line, so `&'static str {` hid its opening brace; a replacement range swallowing such a signature line looked balance-neutral and the mid-block advisory never fired, silently deleting the signature. Single-quote lexing is now language-aware: on `.rs` targets, `'` opens a literal only when it lexes as a real char literal (`'a'`, `'\n'`, `'\u{7FFF}'`), and lifetimes stay ordinary characters — apostrophes are never paired across lifetimes, which would swallow the delimiters between them (`<'a>(x: &'a str)`).
- Fixed hashline reads exposing a terminal newline as an editable blank row. `splitAddressableFileLines` now removes that sentinel before consumers build line anchors while retaining genuine blank lines.
## [17.2.12] - 2026-08-08
+12 -5
View File
@@ -140,12 +140,19 @@ export function formatNumberedLine(lineNumber: number, line: string): string {
}
/**
* Format file text with hashline-mode line-number prefixes for display.
* A terminal newline terminates the preceding line; it is not an addressable
* blank line.
* Split LF-delimited file text into lines hashline anchors can address.
* A terminal newline terminates the preceding line; it is not content.
*/
export function formatNumberedLines(text: string, startLine = 1): string {
export function splitAddressableFileLines(text: string): string[] {
const lines = text.split("\n");
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
return lines.map((line, i) => formatNumberedLine(startLine + i, line)).join("\n");
return lines;
}
/** Format file text with hashline-mode line-number prefixes for display. */
export function formatNumberedLines(text: string, startLine = 1): string {
return text
.split("\n")
.map((line, i) => formatNumberedLine(startLine + i, line))
.join("\n");
}
+9 -3
View File
@@ -5,6 +5,7 @@ import {
parseLid,
parsePatch,
parsePatchStreaming,
splitAddressableFileLines,
Tokenizer,
} from "@oh-my-pi/hashline";
@@ -105,9 +106,14 @@ describe("hashline format v4", () => {
expect(applyEdits("a\nb\n", edits).text).toBe("a\nb\n");
});
it("does not expose the terminal newline as an editable blank line", () => {
expect(formatNumberedLines("a\nb\n")).toBe("1:a\n2:b");
expect(formatNumberedLines("a\nb\n\n")).toBe("1:a\n2:b\n3:");
it("separates terminal newline sentinels from addressable file lines", () => {
expect(splitAddressableFileLines("a\nb\n")).toEqual(["a", "b"]);
expect(splitAddressableFileLines("a\nb\n\n")).toEqual(["a", "b", ""]);
});
it("keeps a selected terminal blank line when formatting", () => {
const selected = splitAddressableFileLines("a\n\nb\n").slice(0, 2).join("\n");
expect(formatNumberedLines(selected)).toBe("1:a\n2:");
});
it("treats a cut range ending at the trailing sentinel as ending at the last real line", () => {