From 1865afb7c4a53d2891f3953347b851ad7bd0716d Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 7 Aug 2026 12:20:45 +0000 Subject: [PATCH] fix(hashline): recovered pipe-numbered read rows Accepted pipe separators in snapshot-row recovery and bare-body prefix stripping, matching non-hashline read and search output. Fixes #7905 --- packages/hashline/CHANGELOG.md | 4 ++++ packages/hashline/src/parser.ts | 12 ++++++------ packages/hashline/src/prefixes.ts | 20 ++++++++++---------- packages/hashline/test/leniency.test.ts | 18 +++++++++++------- packages/utils/src/frontmatter.ts | 10 +++++++++- 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/packages/hashline/CHANGELOG.md b/packages/hashline/CHANGELOG.md index 58ea50d80..04ee44ecb 100644 --- a/packages/hashline/CHANGELOG.md +++ b/packages/hashline/CHANGELOG.md @@ -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 diff --git a/packages/hashline/src/parser.ts b/packages/hashline/src/parser.ts index 6e88f5f60..bf1fd0289 100644 --- a/packages/hashline/src/parser.ts +++ b/packages/hashline/src/parser.ts @@ -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; diff --git a/packages/hashline/src/prefixes.ts b/packages/hashline/src/prefixes.ts index cde86b2b3..d38f7af7e 100644 --- a/packages/hashline/src/prefixes.ts +++ b/packages/hashline/src/prefixes.ts @@ -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 `+:` form when present * diff --git a/packages/hashline/test/leniency.test.ts b/packages/hashline/test/leniency.test.ts index 193947a60..aca3b92a4 100644 --- a/packages/hashline/test/leniency.test.ts +++ b/packages/hashline/test/leniency.test.ts @@ -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"); diff --git a/packages/utils/src/frontmatter.ts b/packages/utils/src/frontmatter.ts index 7e8ad8e67..47a8f6c1f 100644 --- a/packages/utils/src/frontmatter.ts +++ b/packages/utils/src/frontmatter.ts @@ -126,7 +126,15 @@ export function parseFrontmatter( content: string, options?: FrontmatterOptions, ): { frontmatter: Record; body: string } { - const { location, source, fallback, normalize = true, level = "warn", repair = true, rawKeys = false } = options ?? {}; + const { + location, + source, + fallback, + normalize = true, + level = "warn", + repair = true, + rawKeys = false, + } = options ?? {}; const finalizeKeys = (fm: Record): Record => rawKeys ? fm : normalizeFrontmatterKeys(fm); const loc = location ?? source;