Merge PR #7906: fix(hashline): recover pipe-numbered read rows (@roboomp)

This commit is contained in:
can1357
2026-08-07 14:52:57 +02:00
4 changed files with 31 additions and 23 deletions
+4
View File
@@ -6,6 +6,10 @@
- Pasting an empty named register (`PUT … @name` with no matching capture) no longer errors — it pastes nothing (a span target is still removed) and surfaces a warning naming the available registers
### Fixed
- Recovered pipe-numbered `read`/`search` rows copied into top-level and bare-body patch payloads ([#7905](https://github.com/can1357/oh-my-pi/issues/7905))
## [17.2.10] - 2026-08-06
### Changed
+6 -6
View File
@@ -112,7 +112,7 @@ function bodylessTargetMessage(target: BlockTarget, hadColon: boolean): string |
*/
const BARE_LITERAL_VALUE_RE = /^\s*(?:"[^"]*"|'[^']*'|[-+]?\d+(?:\.\d+)?)\s*,?\s*$/;
const TOP_LEVEL_SNAPSHOT_ROW_RE = /^\s*([1-9]\d*):(.*)$/;
const TOP_LEVEL_SNAPSHOT_ROW_RE = /^\s*([1-9]\d*)[:|](.*)$/;
function parseTopLevelSnapshotRow(text: string): { line: number; text: string } | null {
const match = TOP_LEVEL_SNAPSHOT_ROW_RE.exec(text);
@@ -595,11 +595,11 @@ export class Executor {
}
/**
* Strip a single read-output line-number prefix (`N:`) from every bare body
* row, but only when *all* bare rows carry one. A uniform set of prefixes is
* the signature of content pasted straight from `read`/`search` output; a
* mixed set means the `N:` is genuine payload content and must stay. Rows
* authored with an explicit `+` are not bare and are never touched.
* Strip a single read-output line-number prefix (`N:` or `N|`) from every
* bare body row, but only when *all* bare rows carry one. A uniform set of
* prefixes is the signature of content pasted straight from `read`/`search`
* output; a mixed set means the prefix is genuine payload content and must
* stay. Rows authored with an explicit `+` are not bare and are never touched.
*/
#stripBarePrefixesIfUniform(payloads: PayloadRow[]): void {
let sawBare = false;
+10 -10
View File
@@ -1,8 +1,8 @@
/**
* When a hashline payload is authored against `read`/`search` output, each
* line is prefixed with either a hashline-mode line number (`123:`) or, for
* diff-style echoes, a leading `+`. These helpers detect that and recover
* the raw text. Two strip modes are exposed:
* When a payload is authored against `read`/`search` output, each line is
* prefixed with either a line number (`123:` in hashline mode or `123|`
* otherwise) or, for diff-style echoes, a leading `+`. These helpers detect
* that and recover the raw text. Two strip modes are exposed:
*
* - {@link stripNewLinePrefixes} — opportunistic: strips when the input
* clearly carries hashline or diff prefixes, leaves it alone otherwise.
@@ -16,7 +16,7 @@
import { HL_FILE_HASH_LENGTH } from "./format";
const HL_PREFIX_RE = /^\s*(?:>>>|>>)?\s*(?:[+*-]\s*)?\d+:/;
const HL_PREFIX_RE = /^\s*(?:>>>|>>)?\s*(?:[+*-]\s*)?\d+[:|]/;
const HL_PREFIX_PLUS_RE = /^\s*(?:>>>|>>)?\s*\+\s*\d+:/;
const HL_HEADER_RE = new RegExp(`^\\s*\\[[^#\\r\\n]+#[0-9a-fA-F]{${HL_FILE_HASH_LENGTH}}\\]\\s*$`);
const DIFF_PLUS_RE = /^[+](?![+])/;
@@ -41,10 +41,10 @@ function stripLeadingHashlinePrefixes(line: string): string {
}
/**
* Single-pass variant of {@link stripLeadingHashlinePrefixes} that strips at
* most one leading hashline prefix (`N:`, `>>>N:`, `+N:` etc.) and does NOT
* loop. Use this when the input carries at most one snapshot prefix (e.g. a
* bare body row paste from `read` output) — recursive stripping would corrupt
* content whose own text starts with `digits:`.
* most one leading line-number prefix (`N:`, `N|`, `>>>N:`, `+N:` etc.) and
* does NOT loop. Use this when the input carries at most one snapshot prefix
* (e.g. a bare body row paste from `read` output) — recursive stripping would
* corrupt content whose own text starts with a line-number prefix.
*/
export function stripOneLeadingHashlinePrefix(line: string): string {
return line.replace(HL_PREFIX_RE, "");
@@ -90,7 +90,7 @@ function collectLinePrefixStats(lines: string[]): LinePrefixStats {
/**
* Strip whichever prefix scheme the lines appear to be carrying:
* - hashline line-number prefixes (`123:`) when every content line has one
* - line-number prefixes (`123:` or `123|`) when every content line has one
* - leading `+` (diff style) when at least half the lines have one
* - mixed `+<n>:` form when present
*
+11 -7
View File
@@ -83,9 +83,11 @@ describe("hashline core — verb header forms", () => {
});
it("recovers top-level numbered snapshot rows as single-line replacements", () => {
const result = parsePatch("2:B\n4:D");
expect(applyEdits(FILE, result.edits).text).toBe("a\nB\nc\nD\ne");
expect(result.warnings.some(w => /snapshot row.*single-line `PUT N\.=N:`/i.test(w))).toBe(true);
for (const separator of [":", "|"]) {
const result = parsePatch(`2${separator}B\n4${separator}D`);
expect(applyEdits(FILE, result.edits).text).toBe("a\nB\nc\nD\ne");
expect(result.warnings.some(w => /snapshot row.*single-line `PUT N\.=N:`/i.test(w))).toBe(true);
}
});
it("recovers a bare range header as an implicit PUT", () => {
@@ -133,10 +135,12 @@ describe("hashline body contracts", () => {
expect(result.warnings.some(w => /Auto-prefixed bare body row/.test(w))).toBe(true);
});
it("strips read-output line number prefix from auto-piped bare body rows", () => {
const result = parsePatch("PUT 2-2:\n2:hello");
expect(applyEdits(FILE, result.edits).text).toBe("a\nhello\nc\nd\ne");
expect(result.warnings.some(w => /Auto-prefixed bare body row/.test(w))).toBe(true);
it("strips read-output line number prefixes from auto-piped bare body rows", () => {
for (const separator of [":", "|"]) {
const result = parsePatch(`PUT 2-2:\n2${separator}hello`);
expect(applyEdits(FILE, result.edits).text).toBe("a\nhello\nc\nd\ne");
expect(result.warnings.some(w => /Auto-prefixed bare body row/.test(w))).toBe(true);
}
});
it("preserves `+N:` literal payloads without stripping", () => {
const result = parsePatch("PUT 2-2:\n+3:keep");