@oh-my-pi/hashline 17.1.7 → 17.2.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 +21 -0
- package/README.md +2 -1
- package/dist/types/apply.d.ts +22 -2
- package/dist/types/block.d.ts +4 -7
- package/dist/types/clipboard.d.ts +22 -0
- package/dist/types/format.d.ts +10 -6
- package/dist/types/index.d.ts +1 -0
- package/dist/types/input.d.ts +15 -4
- package/dist/types/messages.d.ts +44 -36
- package/dist/types/patcher.d.ts +21 -6
- package/dist/types/recovery.d.ts +3 -1
- package/dist/types/tokenizer.d.ts +12 -6
- package/dist/types/types.d.ts +57 -30
- package/package.json +2 -2
- package/src/apply.ts +26 -5
- package/src/block.ts +52 -24
- package/src/clipboard.ts +110 -0
- package/src/format.ts +11 -7
- package/src/grammar.lark +12 -15
- package/src/index.ts +1 -0
- package/src/input.ts +44 -7
- package/src/messages.ts +113 -59
- package/src/parser.ts +96 -34
- package/src/patcher.ts +89 -15
- package/src/prompt.md +51 -89
- package/src/recovery.ts +28 -4
- package/src/tokenizer.ts +85 -38
- package/src/types.ts +60 -30
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.2.0] - 2026-07-30
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Removed `DEL`, `DEL.BLK`, `COPY`, and `COPY.BLK` from the patch language. Use `CUT` / `CUT.BLK` for deletion; a cut does not require a following `PASTE` and leaves the removed content available to later pastes.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added clipboard ops: `CUT N.=M` captures lines into a register (and deletes them), `CUT.BLK N` captures tree-sitter blocks, and `PASTE.PRE|POST N` / `PASTE.HEAD|TAIL` / `PASTE.BLK.POST N` insert the captured lines without retyping. The register flows top-to-bottom across sections, so content moves between files in one patch; `PASTE` does not consume it and the last capture wins.
|
|
14
|
+
- Added `PatcherOptions.clipboard` for a host-owned register that persists across `Patcher.apply` batches. Batches work on a fork (`forkClipboard`) published per landed section (`commitClipboard`), so failed batches never poison the register and a mid-batch write failure still preserves content already cut from disk.
|
|
15
|
+
- Added clipboard safety guards: a `PASTE` with an empty register, a capture overwriting un-pasted `CUT` content, and clipboard ops in same-path sections interleaved across another file's section are all rejected with targeted diagnostics. `CUT` ranges participate in overlap validation, the seen-lines guard, and drift recovery (every captured line must remap).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Simplified `grammar.lark` around shared target and position shapes, collapsing the concrete and block `CUT` forms plus the `INS` / `PASTE` position variants into their common grammar rules.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Prevented CPU and memory exhaustion in streaming previews by rejecting line anchors above Number.MAX_SAFE_INTEGER and ranges spanning more than 100,000 lines.
|
|
24
|
+
- Fixed an issue where recorded snapshot tags desynced from disk when the filesystem transformed content on write (e.g., auto-formatting on save), which previously caused subsequent edits to incorrectly reformat unrelated parts of the file. `Patcher.commit` now correctly keys the returned file hash and snapshot on the actual content written to disk and issues a warning when a drift is detected.
|
|
25
|
+
|
|
5
26
|
## [17.1.5] - 2026-07-27
|
|
6
27
|
|
|
7
28
|
### Changed
|
package/README.md
CHANGED
|
@@ -49,9 +49,10 @@ session-aware recovery on mismatch.
|
|
|
49
49
|
Inside a section:
|
|
50
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
|
-
- `
|
|
52
|
+
- `CUT A.=B` / `CUT.BLK A` — delete concrete lines or a resolved block and capture them for `PASTE`.
|
|
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
|
+
- `PASTE.PRE A` / `PASTE.POST A` / `PASTE.HEAD` / `PASTE.TAIL` / `PASTE.BLK.POST A` — insert the clipboard; no body rows. The register flows top-to-bottom across sections (cross-file moves) and can be host-persisted across batches via `PatcherOptions.clipboard`.
|
|
55
56
|
- `REM` — delete the whole file named by the section header.
|
|
56
57
|
- `MV DEST` — move/rename the section file to `DEST` (optionally after line edits).
|
|
57
58
|
- `+TEXT` — literal body row (use `+` alone for a blank line).
|
package/dist/types/apply.d.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Apply a parsed list of {@link Edit}s to a text body and return the
|
|
3
|
+
* post-edit lines plus any diagnostic warnings. Pure function: no FS, no
|
|
4
|
+
* mutation of the input.
|
|
5
|
+
*
|
|
6
|
+
* Replacement groups are first normalized by {@link repairReplacementBoundaries},
|
|
7
|
+
* which absorbs common model mistakes where a payload restates unchanged range
|
|
8
|
+
* boundaries or duplicates/drops structural closers.
|
|
9
|
+
*/
|
|
10
|
+
import type { ApplyResult, Clipboard, Edit } from "./types.js";
|
|
2
11
|
/** A line that is nothing but closing delimiters: `}`, `)`, `];`, `})`, `},`. */
|
|
3
12
|
export declare const STRUCTURAL_CLOSER_RE: RegExp;
|
|
13
|
+
/** Optional knobs for {@link applyEdits}. */
|
|
14
|
+
export interface ApplyEditsOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Clipboard register filled by `cut` edits and read by `paste` edits.
|
|
17
|
+
* Thread one register through every section of a batch to move content
|
|
18
|
+
* across files; omitted, the call gets a private register.
|
|
19
|
+
*/
|
|
20
|
+
clipboard?: Clipboard;
|
|
21
|
+
/** `PASTE` with an empty register: `throw` (default) or `drop` (streaming previews). */
|
|
22
|
+
onEmptyPaste?: "throw" | "drop";
|
|
23
|
+
}
|
|
4
24
|
/**
|
|
5
25
|
* Apply a parsed list of edits to a text body. Pure function — no I/O.
|
|
6
26
|
*
|
|
7
27
|
* Returns the post-edit text and the first changed line number (1-indexed).
|
|
8
28
|
* Throws if an anchor is out of bounds.
|
|
9
29
|
*/
|
|
10
|
-
export declare function applyEdits(text: string, edits: readonly Edit[]): ApplyResult;
|
|
30
|
+
export declare function applyEdits(text: string, edits: readonly Edit[], options?: ApplyEditsOptions): ApplyResult;
|
package/dist/types/block.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { BlockResolution, BlockResolver, Edit } from "./types.js";
|
|
2
|
+
/** Optional knobs for {@link resolveBlockEdits}. */
|
|
2
3
|
export interface ResolveBlockEditsOptions {
|
|
3
4
|
/**
|
|
4
|
-
* How to handle a replace/
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
7
|
-
* preview paths. `"drop"` silently skips the edit — used by the streaming
|
|
8
|
-
* preview, where a half-written file or transient parse error must not
|
|
9
|
-
* throw. Unresolvable `insert_after_block N:` edits never reach this: they
|
|
10
|
-
* are lowered to plain `insert after N:` with a warning.
|
|
5
|
+
* How to handle a replace/cut block edit that cannot be resolved. `"throw"`
|
|
6
|
+
* (default) raises a block error; `"drop"` skips it for streaming previews.
|
|
7
|
+
* Unresolvable after-block edits lower to their plain after-line form.
|
|
11
8
|
*/
|
|
12
9
|
onUnresolved?: "throw" | "drop";
|
|
13
10
|
/**
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Clipboard, Edit } from "./types.js";
|
|
2
|
+
/** True when at least one edit reads or writes the clipboard register. */
|
|
3
|
+
export declare function hasClipboardEdit(edits: readonly Edit[]): boolean;
|
|
4
|
+
/** Optional knobs for {@link resolveClipboardEdits}. */
|
|
5
|
+
export interface ResolveClipboardEditsOptions {
|
|
6
|
+
/** `PASTE` with an empty register: `throw` (default) or `drop` (streaming previews). */
|
|
7
|
+
onEmptyPaste?: "throw" | "drop";
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve clipboard edits against the original file lines in authored order.
|
|
11
|
+
* Cuts fill the register and emit nothing; pastes become plain inserts.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveClipboardEdits(edits: readonly Edit[], fileLines: readonly string[], clipboard: Clipboard, options?: ResolveClipboardEditsOptions): readonly Edit[];
|
|
14
|
+
/** Create a transactional working copy of a clipboard register. */
|
|
15
|
+
export declare function forkClipboard(source?: Clipboard): Clipboard;
|
|
16
|
+
/** Publish a clipboard fork back to its source register. */
|
|
17
|
+
export declare function commitClipboard(fork: Clipboard, target: Clipboard): void;
|
|
18
|
+
/**
|
|
19
|
+
* Validate that every paste has a preceding or persisted capture without
|
|
20
|
+
* mutating the register or reading file content.
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateClipboardSequence(edits: readonly Edit[], clipboard: Clipboard): void;
|
package/dist/types/format.d.ts
CHANGED
|
@@ -11,8 +11,6 @@ export declare const HL_FILE_SUFFIX = "]";
|
|
|
11
11
|
export declare const HL_PAYLOAD_REPLACE = "+";
|
|
12
12
|
/** Hunk-header keyword for concrete line replacement. */
|
|
13
13
|
export declare const HL_REPLACE_KEYWORD = "SWAP";
|
|
14
|
-
/** Hunk-header keyword for concrete line deletion. */
|
|
15
|
-
export declare const HL_DELETE_KEYWORD = "DEL";
|
|
16
14
|
/** Hunk-header keyword for insertion operations. */
|
|
17
15
|
export declare const HL_INSERT_KEYWORD = "INS";
|
|
18
16
|
/** Insert position keyword for inserting before a concrete line. */
|
|
@@ -25,10 +23,16 @@ export declare const HL_INSERT_HEAD = "HEAD";
|
|
|
25
23
|
export declare const HL_INSERT_TAIL = "TAIL";
|
|
26
24
|
/** Hunk-header keyword: `SWAP.BLK N:` resolves N to a tree-sitter block range and replaces its span. */
|
|
27
25
|
export declare const HL_REPLACE_BLOCK_KEYWORD = "SWAP.BLK";
|
|
28
|
-
/** Hunk-header keyword: `DEL.BLK N` resolves N to a tree-sitter block range and deletes its span. */
|
|
29
|
-
export declare const HL_DELETE_BLOCK_KEYWORD = "DEL.BLK";
|
|
30
26
|
/** Hunk-header keyword: `INS.BLK.POST N:` inserts after the last line of the tree-sitter block at N. */
|
|
31
27
|
export declare const HL_INSERT_AFTER_BLOCK_KEYWORD = "INS.BLK.POST";
|
|
28
|
+
/** Hunk-header keyword: `CUT N.=M` captures lines into the clipboard register and deletes them. */
|
|
29
|
+
export declare const HL_CUT_KEYWORD = "CUT";
|
|
30
|
+
/** Hunk-header keyword: `CUT.BLK N` captures the tree-sitter block at N and deletes its span. */
|
|
31
|
+
export declare const HL_CUT_BLOCK_KEYWORD = "CUT.BLK";
|
|
32
|
+
/** Hunk-header keyword prefix: `PASTE.PRE|POST N` / `PASTE.HEAD|TAIL` inserts the clipboard. */
|
|
33
|
+
export declare const HL_PASTE_KEYWORD = "PASTE";
|
|
34
|
+
/** Hunk-header keyword: `PASTE.BLK.POST N` inserts the clipboard after the tree-sitter block at N. */
|
|
35
|
+
export declare const HL_PASTE_AFTER_BLOCK_KEYWORD = "PASTE.BLK.POST";
|
|
32
36
|
/** File-level keyword: `REM` deletes the whole file named by the section header. */
|
|
33
37
|
export declare const HL_REM_KEYWORD = "REM";
|
|
34
38
|
/** File-level keyword: `MV DEST` renames/moves the section file to `DEST`. */
|
|
@@ -46,8 +50,8 @@ export declare const HL_LINE_RE_RAW = "[1-9]\\d*";
|
|
|
46
50
|
export declare const HL_LINE_CAPTURE_RE_RAW = "([1-9]\\d*)";
|
|
47
51
|
/** Format a concrete replacement hunk header. */
|
|
48
52
|
export declare function formatReplaceHeader(start: number, end: number): string;
|
|
49
|
-
/** Format a concrete
|
|
50
|
-
export declare function
|
|
53
|
+
/** Format a concrete cut hunk header. */
|
|
54
|
+
export declare function formatCutHeader(start: number, end?: number): string;
|
|
51
55
|
/** Format an insertion hunk header for a cursor position. */
|
|
52
56
|
export declare function formatInsertHeader(cursor: Cursor): string;
|
|
53
57
|
/** Number of hex characters in a content-derived file-hash tag. */
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/input.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import type { ApplyResult, BlockResolver, Edit, FileOp, SplitOptions } from "./types.js";
|
|
1
|
+
import type { ApplyResult, BlockResolver, Clipboard, Edit, FileOp, SplitOptions } from "./types.js";
|
|
2
2
|
interface RawSection {
|
|
3
3
|
path: string;
|
|
4
4
|
fileHash?: string;
|
|
5
5
|
diff: string;
|
|
6
|
+
/**
|
|
7
|
+
* True when this section coalesced same-path sections that were NOT
|
|
8
|
+
* adjacent in the authored input (another file's section sat between
|
|
9
|
+
* them). Merging moves the later ops up to the first occurrence, which
|
|
10
|
+
* would silently reorder the clipboard register sequence — so clipboard
|
|
11
|
+
* ops are rejected in such sections.
|
|
12
|
+
*/
|
|
13
|
+
interleaved?: boolean;
|
|
6
14
|
}
|
|
7
15
|
/**
|
|
8
16
|
* Returns true when the input contains at least one line that the tokenizer
|
|
@@ -55,8 +63,11 @@ export declare class PatchSection {
|
|
|
55
63
|
*
|
|
56
64
|
* `blockResolver` resolves any `replace_block N:` edits against `text`; an
|
|
57
65
|
* unresolvable block throws (this is the final, authoritative preview path).
|
|
66
|
+
*
|
|
67
|
+
* `clipboard` is the register shared by `CUT`/`PASTE` ops. Pass one when
|
|
68
|
+
* applying several sections so content can move across files.
|
|
58
69
|
*/
|
|
59
|
-
applyTo(text: string, blockResolver?: BlockResolver): ApplyResult;
|
|
70
|
+
applyTo(text: string, blockResolver?: BlockResolver, clipboard?: Clipboard): ApplyResult;
|
|
60
71
|
/**
|
|
61
72
|
* Streaming-tolerant counterpart to {@link applyTo}. Uses
|
|
62
73
|
* {@link parsePatchStreaming} so a trailing in-flight op (no payload yet,
|
|
@@ -66,9 +77,9 @@ export declare class PatchSection {
|
|
|
66
77
|
*
|
|
67
78
|
* `blockResolver` resolves any `replace_block N:` edits against `text`; an
|
|
68
79
|
* unresolvable block is silently dropped so a half-written file does not
|
|
69
|
-
* throw mid-stream.
|
|
80
|
+
* throw mid-stream. A `PASTE` with an empty register is dropped too.
|
|
70
81
|
*/
|
|
71
|
-
applyPartialTo(text: string, blockResolver?: BlockResolver): ApplyResult;
|
|
82
|
+
applyPartialTo(text: string, blockResolver?: BlockResolver, clipboard?: Clipboard): ApplyResult;
|
|
72
83
|
/**
|
|
73
84
|
* A copy of this section rebound to a different target `path`, preserving
|
|
74
85
|
* the snapshot tag, diff body, and any cached parse result. Used by the
|
package/dist/types/messages.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare const MISMATCH_CONTEXT = 2;
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function formatAnchoredContext(anchorLines: readonly number[], fileLines: readonly string[]): string[];
|
|
11
11
|
/** Concrete range operation rejected because its absolute end precedes its start. */
|
|
12
|
-
export type AbsoluteRangeOp = "replace" | "
|
|
12
|
+
export type AbsoluteRangeOp = "replace" | "cut";
|
|
13
13
|
/** Explain absolute range endpoints and provide safe, non-applying retry forms. */
|
|
14
14
|
export declare function invalidAbsoluteRangeMessage(patchLine: number, start: number, end: number, op: AbsoluteRangeOp, block?: BlockSpan): string;
|
|
15
15
|
/** Optional patch envelope start marker; silently consumed. */
|
|
@@ -34,9 +34,9 @@ export declare const MINUS_BULLET_AUTO_PIPED_WARNING = "Auto-prefixed bare `- `
|
|
|
34
34
|
/** Unified-diff-style `-` row in a hunk body. */
|
|
35
35
|
export declare const MINUS_ROW_REJECTED = "`-` rows are not valid; the range already names the lines being changed. For Markdown bullets or other literal `-` lines, prefix the literal row with `+`: `+- item`.";
|
|
36
36
|
/** Replace hunk with no body. */
|
|
37
|
-
export declare const EMPTY_REPLACE = "`SWAP N.=M:` needs at least one `+TEXT` body row. To delete lines, use `
|
|
38
|
-
/** `
|
|
39
|
-
export declare const EMPTY_BLOCK = "`SWAP.BLK N:` needs at least one `+TEXT` body row. To delete a block, use `
|
|
37
|
+
export declare const EMPTY_REPLACE = "`SWAP N.=M:` needs at least one `+TEXT` body row. To delete lines, use `CUT N.=M`.";
|
|
38
|
+
/** `SWAP.BLK N:` hunk with no body. */
|
|
39
|
+
export declare const EMPTY_BLOCK = "`SWAP.BLK N:` needs at least one `+TEXT` body row. To delete a block, use `CUT.BLK N`.";
|
|
40
40
|
/** Optional source-aware suggestions appended to block-anchor diagnostics. */
|
|
41
41
|
export interface BlockDiagnosticSuggestions {
|
|
42
42
|
/** Closest following multi-line block that begins after the authored anchor. */
|
|
@@ -45,28 +45,21 @@ export interface BlockDiagnosticSuggestions {
|
|
|
45
45
|
enclosingBlock?: BlockSpan;
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* `fileLines` is given. `insert_after_block N:` never reaches this — it is
|
|
52
|
-
* lowered to plain `insert after N:` instead (see
|
|
53
|
-
* {@link insertAfterBlockUnresolvedLoweredWarning}).
|
|
54
|
-
*/
|
|
55
|
-
export declare function blockUnresolvedMessage(line: number, op?: "replace" | "delete", fileLines?: readonly string[], suggestions?: BlockDiagnosticSuggestions): string;
|
|
56
|
-
/** Block-anchored edit reached a path with no {@link BlockResolver} wired in — a host-configuration bug. */
|
|
57
|
-
export declare const BLOCK_RESOLVER_UNAVAILABLE = "`SWAP.BLK`/`DEL.BLK`/`INS.BLK.POST` are not available here (no block resolver configured). Use a concrete line range.";
|
|
58
|
-
/**
|
|
59
|
-
* `insert_after_block N:` anchored on a closing-delimiter line, lowered to
|
|
60
|
-
* plain `insert after N:` — the closer ends a block, and inserting after it
|
|
61
|
-
* is exactly what the plain form does.
|
|
48
|
+
* A block-anchored replace/cut could not resolve to a syntactic block.
|
|
49
|
+
* Appends a {@link formatAnchoredContext} preview when `fileLines` is given.
|
|
50
|
+
* `INS.BLK.POST N:` never reaches this path; it lowers to `INS.POST N:`.
|
|
62
51
|
*/
|
|
52
|
+
export declare function blockUnresolvedMessage(line: number, op?: AbsoluteRangeOp, fileLines?: readonly string[], suggestions?: BlockDiagnosticSuggestions): string;
|
|
53
|
+
/** Block-anchored edit reached a path with no {@link BlockResolver} wired in. */
|
|
54
|
+
export declare const BLOCK_RESOLVER_UNAVAILABLE = "Block ops (`SWAP.BLK`, `INS.BLK.POST`, `CUT.BLK`, `PASTE.BLK.POST`) are not available here (no block resolver configured). Use a concrete line range.";
|
|
55
|
+
/** `INS.BLK.POST N:` anchored on a closing-delimiter line; applied as `INS.POST N:`. */
|
|
63
56
|
export declare function insertAfterBlockCloserLoweredWarning(line: number): string;
|
|
64
|
-
/**
|
|
65
|
-
* `insert_after_block N:` anchor unresolvable (unsupported language, blank
|
|
66
|
-
* line, parse error, or no resolver), lowered to plain `insert after N:` —
|
|
67
|
-
* applying with a warning beats failing the patch.
|
|
68
|
-
*/
|
|
57
|
+
/** `INS.BLK.POST N:` anchor unresolvable; applied as `INS.POST N:`. */
|
|
69
58
|
export declare function insertAfterBlockUnresolvedLoweredWarning(line: number): string;
|
|
59
|
+
/** `PASTE.BLK.POST N` anchored on a closing-delimiter line; applied as `PASTE.POST N`. */
|
|
60
|
+
export declare function pasteAfterBlockCloserLoweredWarning(line: number): string;
|
|
61
|
+
/** `PASTE.BLK.POST N` anchor unresolvable; applied as `PASTE.POST N`. */
|
|
62
|
+
export declare function pasteAfterBlockUnresolvedLoweredWarning(line: number): string;
|
|
70
63
|
/**
|
|
71
64
|
* A one-sided boundary echo whose payload is too short to be the widened
|
|
72
65
|
* range's full content: dropping the echo deletes range line(s) the payload
|
|
@@ -85,18 +78,26 @@ export declare function ambiguousBoundaryEchoMessage(startLine: number, endLine:
|
|
|
85
78
|
*/
|
|
86
79
|
export declare function ambiguousCloserSpareMessage(startLine: number, endLine: number, closerLine: number, count: number): string;
|
|
87
80
|
/**
|
|
88
|
-
* Internal invariant: `applyEdits` received an unresolved
|
|
89
|
-
*
|
|
81
|
+
* Internal invariant: `applyEdits` received an unresolved block edit;
|
|
82
|
+
* `resolveBlockEdits` must run first.
|
|
90
83
|
*/
|
|
91
|
-
export declare const UNRESOLVED_BLOCK_INTERNAL = "internal error: unresolved
|
|
92
|
-
/** Delete hunk received a body row. */
|
|
93
|
-
export declare const DELETE_TAKES_NO_BODY = "`DEL N.=M` does not take body rows. Remove the body, or use `SWAP N.=M:`.";
|
|
84
|
+
export declare const UNRESOLVED_BLOCK_INTERNAL = "internal error: unresolved block edit reached the applier (resolveBlockEdits was not run).";
|
|
94
85
|
/** `REM` received a body row or coexists with line edits. */
|
|
95
86
|
export declare const REM_TAKES_NO_BODY = "`REM` deletes the whole file and takes no body rows or line ops. Issue it alone under the header.";
|
|
96
87
|
/** `MV` received a body row. */
|
|
97
88
|
export declare const MOVE_TAKES_NO_BODY = "`MV DEST` does not take body rows. Put line edits above the `MV` row; the destination path follows `MV` on the same line.";
|
|
98
|
-
/** `
|
|
99
|
-
export declare const
|
|
89
|
+
/** `CUT N.=M` hunk received a body row. */
|
|
90
|
+
export declare const CUT_TAKES_NO_BODY = "`CUT N.=M` captures + deletes lines and takes no body rows. To replace lines with new content, use `SWAP N.=M:`.";
|
|
91
|
+
/** `PASTE` hunk received a body row. */
|
|
92
|
+
export declare const PASTE_TAKES_NO_BODY = "`PASTE` inserts the clipboard content and takes no `+` body rows. To insert literal text, use `INS`.";
|
|
93
|
+
/** `PASTE` ran with an empty clipboard register. */
|
|
94
|
+
export declare const EMPTY_PASTE = "`PASTE` found nothing in the clipboard. Ops run top-to-bottom across the whole patch (sections included): put `CUT N.=M` or `CUT.BLK N` above the `PASTE`.";
|
|
95
|
+
/**
|
|
96
|
+
* Clipboard ops inside a same-path section that was merged across another
|
|
97
|
+
* file's section. Same-path sections coalesce into their first occurrence, so
|
|
98
|
+
* an interleaved layout would silently reorder the register sequence.
|
|
99
|
+
*/
|
|
100
|
+
export declare const CLIPBOARD_INTERLEAVED_SECTIONS = "`CUT`/`PASTE` cannot be used in a file whose sections are interleaved with another file's: same-path sections merge into the first occurrence, which would reorder the clipboard sequence. Keep each file's ops under ONE `[path#TAG]` header.";
|
|
100
101
|
/** Insert hunk with no body. */
|
|
101
102
|
export declare const EMPTY_INSERT = "`INS` needs at least one `+TEXT` body row.";
|
|
102
103
|
/**
|
|
@@ -123,6 +124,15 @@ export declare const RECOVERY_LINE_REMAP_WARNING = "Recovered by remapping stale
|
|
|
123
124
|
* onto live content and warn instead of hard-failing.
|
|
124
125
|
*/
|
|
125
126
|
export declare const HEADTAIL_DRIFT_WARNING = "Applied the `INS.HEAD:`/`INS.TAIL:` edit despite a stale snapshot tag (file changed since your read) \u2014 head/tail position is content-independent. Re-read if the drift was unexpected.";
|
|
127
|
+
/**
|
|
128
|
+
* The `Filesystem` reported that what actually landed on disk differs from
|
|
129
|
+
* what was written (see `WriteResult.text`) — most commonly an ACP-connected
|
|
130
|
+
* editor reformatting the buffer on save (e.g. `format_on_save` with tab/space
|
|
131
|
+
* settings that don't match the file). The recorded snapshot is re-keyed on
|
|
132
|
+
* the real, post-write content so the next edit's tag validation matches
|
|
133
|
+
* reality instead of silently drifting.
|
|
134
|
+
*/
|
|
135
|
+
export declare function writeDriftWarning(path: string): string;
|
|
126
136
|
/**
|
|
127
137
|
* Section omitted the mandatory snapshot tag. Shared by the apply
|
|
128
138
|
* ({@link Patcher.prepare}) and preview/diff paths so both stay in lockstep.
|
|
@@ -164,12 +174,10 @@ export interface UnseenLinesReveal {
|
|
|
164
174
|
*/
|
|
165
175
|
export declare function unseenLinesMessage(sectionPath: string, unseenLines: readonly number[], tag: string, reveal?: UnseenLinesReveal): string;
|
|
166
176
|
/** Op kind of a deferred block edit, for {@link blockSingleLineMessage}. */
|
|
167
|
-
export type BlockOp = "replace" | "
|
|
177
|
+
export type BlockOp = "replace" | "insert_after" | "cut" | "paste_after";
|
|
168
178
|
/**
|
|
169
|
-
* A
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* form only earns its keep when it spares counting a closing line you cannot
|
|
173
|
-
* see. Reject and point at both fixes.
|
|
179
|
+
* A block-op anchor resolved to a single line: line N is a bare statement,
|
|
180
|
+
* not the opening line of a multi-line construct. The plain op is exact for
|
|
181
|
+
* one line, so reject and point at it.
|
|
174
182
|
*/
|
|
175
183
|
export declare function blockSingleLineMessage(line: number, op: BlockOp, enclosingBlock?: BlockSpan): string;
|
package/dist/types/patcher.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Patch, PatchSection } from "./input.js";
|
|
|
3
3
|
import { type LineEnding } from "./normalize.js";
|
|
4
4
|
import { Recovery } from "./recovery.js";
|
|
5
5
|
import type { SnapshotStore } from "./snapshots.js";
|
|
6
|
-
import type { ApplyResult, BlockResolution, BlockResolver, FileOp } from "./types.js";
|
|
6
|
+
import type { ApplyResult, BlockResolution, BlockResolver, Clipboard, FileOp } from "./types.js";
|
|
7
7
|
export interface PatcherOptions {
|
|
8
8
|
/** Storage backend used for all reads and writes. */
|
|
9
9
|
fs: Filesystem;
|
|
@@ -21,6 +21,12 @@ export interface PatcherOptions {
|
|
|
21
21
|
* validate on content hash alone and any anchor into the tagged content applies.
|
|
22
22
|
*/
|
|
23
23
|
enforceSeenLines?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Host-owned clipboard register shared across batches, so `CUT` content
|
|
26
|
+
* can be `PASTE`d by a later {@link Patcher.apply} call. Each batch works
|
|
27
|
+
* on a fork and publishes it back only after writes land.
|
|
28
|
+
*/
|
|
29
|
+
clipboard?: Clipboard;
|
|
24
30
|
}
|
|
25
31
|
/** Per-section result returned by {@link Patcher.apply} / {@link Patcher.commit}. */
|
|
26
32
|
export interface PatchSectionResult {
|
|
@@ -38,7 +44,13 @@ export interface PatchSectionResult {
|
|
|
38
44
|
persisted: string;
|
|
39
45
|
/** Final text that the {@link Filesystem} actually wrote (may differ if the FS transformed it). */
|
|
40
46
|
written: string;
|
|
41
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* 4-hex content-hash tag. Hashes the content the {@link Filesystem}
|
|
49
|
+
* reports actually landed on disk (see `written`), which normally equals
|
|
50
|
+
* `after` but can diverge when the write path transforms content (e.g. an
|
|
51
|
+
* ACP-bridge write reformatted by the client's format-on-save). Use to
|
|
52
|
+
* anchor follow-up edits.
|
|
53
|
+
*/
|
|
42
54
|
fileHash: string;
|
|
43
55
|
/** Hashline section header (`[path#tag]`) of the post-edit content. */
|
|
44
56
|
header: string;
|
|
@@ -49,9 +61,8 @@ export interface PatchSectionResult {
|
|
|
49
61
|
/** Destination path when this section includes `MV DEST`. */
|
|
50
62
|
moveDest?: string;
|
|
51
63
|
/**
|
|
52
|
-
* Resolved spans for
|
|
53
|
-
*
|
|
54
|
-
* (and for resolutions routed through drift recovery, where numbers shift).
|
|
64
|
+
* Resolved spans for block ops, present when the apply matched the tagged
|
|
65
|
+
* content. Undefined for patches with no block ops and for drift recovery.
|
|
55
66
|
*/
|
|
56
67
|
blockResolutions?: BlockResolution[];
|
|
57
68
|
}
|
|
@@ -91,6 +102,7 @@ export declare class Patcher {
|
|
|
91
102
|
readonly snapshots: SnapshotStore;
|
|
92
103
|
readonly recovery: Recovery;
|
|
93
104
|
readonly blockResolver: BlockResolver | undefined;
|
|
105
|
+
readonly clipboard: Clipboard | undefined;
|
|
94
106
|
constructor(options: PatcherOptions);
|
|
95
107
|
/**
|
|
96
108
|
* Apply every section in `patch`. `prepare` runs the full apply for each
|
|
@@ -110,10 +122,13 @@ export declare class Patcher {
|
|
|
110
122
|
* {@link PreparedSection} which can be fed to {@link commit} to land
|
|
111
123
|
* the result on the filesystem.
|
|
112
124
|
*
|
|
125
|
+
* `clipboard` is the register shared by `CUT`/`PASTE` ops. Pass the batch
|
|
126
|
+
* register when preparing several sections so content can move across files.
|
|
127
|
+
*
|
|
113
128
|
* Throws on parse error, missing-file-for-anchored-edit, or unrecovered
|
|
114
129
|
* tag mismatch ({@link MismatchError}).
|
|
115
130
|
*/
|
|
116
|
-
prepare(section: PatchSection): Promise<PreparedSection>;
|
|
131
|
+
prepare(section: PatchSection, clipboard?: Clipboard): Promise<PreparedSection>;
|
|
117
132
|
/**
|
|
118
133
|
* Commit a previously {@link prepare}d section to the filesystem.
|
|
119
134
|
* Restores line endings and BOM, writes via the {@link Filesystem}, and
|
package/dist/types/recovery.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { SnapshotStore } from "./snapshots.js";
|
|
2
|
-
import type { Edit } from "./types.js";
|
|
2
|
+
import type { Clipboard, Edit } from "./types.js";
|
|
3
3
|
export interface RecoveryArgs {
|
|
4
4
|
path: string;
|
|
5
5
|
currentText: string;
|
|
6
6
|
fileHash: string;
|
|
7
7
|
edits: readonly Edit[];
|
|
8
|
+
/** Shared clipboard register for `cut`/`paste` edits, threaded into the replay apply. */
|
|
9
|
+
clipboard?: Clipboard;
|
|
8
10
|
}
|
|
9
11
|
export interface RecoveryResult {
|
|
10
12
|
/** Post-recovery text. */
|
|
@@ -9,12 +9,6 @@ export type BlockTarget = {
|
|
|
9
9
|
} | {
|
|
10
10
|
kind: "block";
|
|
11
11
|
anchor: Anchor;
|
|
12
|
-
} | {
|
|
13
|
-
kind: "delete";
|
|
14
|
-
range: ParsedRange;
|
|
15
|
-
} | {
|
|
16
|
-
kind: "delete_block";
|
|
17
|
-
anchor: Anchor;
|
|
18
12
|
} | {
|
|
19
13
|
kind: "insert_before";
|
|
20
14
|
anchor: Anchor;
|
|
@@ -24,6 +18,18 @@ export type BlockTarget = {
|
|
|
24
18
|
} | {
|
|
25
19
|
kind: "insert_after_block";
|
|
26
20
|
anchor: Anchor;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "cut";
|
|
23
|
+
range: ParsedRange;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "cut_block";
|
|
26
|
+
anchor: Anchor;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "paste";
|
|
29
|
+
cursor: Cursor;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "paste_after_block";
|
|
32
|
+
anchor: Anchor;
|
|
27
33
|
} | {
|
|
28
34
|
kind: "rem";
|
|
29
35
|
} | {
|
package/dist/types/types.d.ts
CHANGED
|
@@ -48,21 +48,44 @@ export type Edit = {
|
|
|
48
48
|
oldAssertion?: string;
|
|
49
49
|
} | {
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
* Clipboard cut (`CUT N.=M`, or the resolved form of `CUT.BLK N`).
|
|
52
|
+
* Captures the range's current lines into the {@link Clipboard}
|
|
53
|
+
* register during the applier's clipboard pre-pass and lowers to one
|
|
54
|
+
* `delete` per range line at parse/resolve time.
|
|
55
|
+
*/
|
|
56
|
+
kind: "cut";
|
|
57
|
+
range: ParsedRange;
|
|
58
|
+
lineNum: number;
|
|
59
|
+
index: number;
|
|
60
|
+
} | {
|
|
61
|
+
/**
|
|
62
|
+
* Clipboard insertion (`PASTE.PRE N` / `PASTE.POST N` / `PASTE.HEAD` /
|
|
63
|
+
* `PASTE.TAIL`, or the resolved form of `PASTE.BLK.POST N`). Expanded
|
|
64
|
+
* by the clipboard pre-pass into one plain insert per captured line.
|
|
65
|
+
* `blockStart` mirrors the insert variant's field for block-lowered
|
|
66
|
+
* pastes so landing correction can slide the body across trailing
|
|
67
|
+
* closer lines.
|
|
68
|
+
*/
|
|
69
|
+
kind: "paste";
|
|
70
|
+
cursor: Cursor;
|
|
71
|
+
lineNum: number;
|
|
72
|
+
index: number;
|
|
73
|
+
blockStart?: number;
|
|
74
|
+
} | {
|
|
75
|
+
/**
|
|
76
|
+
* Deferred block edit (`SWAP.BLK N:`, `INS.BLK.POST N:`,
|
|
77
|
+
* `CUT.BLK N`, or `PASTE.BLK.POST N`). The exact line span is
|
|
78
|
+
* unknown at parse time; {@link resolveBlockEdits} computes it once
|
|
79
|
+
* file text and language are available, then expands it into
|
|
80
|
+
* concrete edits. `mode: "insert_after"` becomes plain
|
|
81
|
+
* `after_anchor` inserts, `"cut"` becomes a clipboard cut plus
|
|
82
|
+
* per-line deletes, and `"paste_after"` becomes a paste after the
|
|
83
|
+
* resolved block. No mode denotes a block replacement.
|
|
61
84
|
*/
|
|
62
85
|
kind: "block";
|
|
63
86
|
anchor: Anchor;
|
|
64
87
|
payloads: string[];
|
|
65
|
-
mode?: "insert_after";
|
|
88
|
+
mode?: "insert_after" | "cut" | "paste_after";
|
|
66
89
|
lineNum: number;
|
|
67
90
|
index: number;
|
|
68
91
|
};
|
|
@@ -82,10 +105,9 @@ export interface ApplyResult {
|
|
|
82
105
|
/** Diagnostic warnings collected by the parser, patcher, or recovery. */
|
|
83
106
|
warnings?: string[];
|
|
84
107
|
/**
|
|
85
|
-
* Resolved spans for each
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* caller read. Absent when there were no block ops.
|
|
108
|
+
* Resolved spans for each block op in this apply, in patch order. Present
|
|
109
|
+
* only when the apply matched the tagged content, so the line numbers match
|
|
110
|
+
* what the caller read. Absent when there were no block ops.
|
|
89
111
|
*/
|
|
90
112
|
blockResolutions?: BlockResolution[];
|
|
91
113
|
}
|
|
@@ -127,9 +149,7 @@ export interface CompactDiffOptions {
|
|
|
127
149
|
/** Back-compat alias for {@link maxAddedRunContext}. */
|
|
128
150
|
maxUnchangedRun?: number;
|
|
129
151
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Resolved 1-indexed inclusive line span of a `replace_block N:` target.
|
|
132
|
-
*/
|
|
152
|
+
/** Resolved 1-indexed inclusive line span of a block target. */
|
|
133
153
|
export interface BlockSpan {
|
|
134
154
|
/** First line of the block (1-indexed, inclusive). */
|
|
135
155
|
start: number;
|
|
@@ -137,11 +157,9 @@ export interface BlockSpan {
|
|
|
137
157
|
end: number;
|
|
138
158
|
}
|
|
139
159
|
/**
|
|
140
|
-
* One
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* opener — e.g. a decorator or doc-comment that sits in a separate node
|
|
144
|
-
* outside the resolved block.
|
|
160
|
+
* One block-op anchor resolved to its concrete line span. Surfaced on
|
|
161
|
+
* {@link ApplyResult} so the host can echo the resolved range and let the
|
|
162
|
+
* model catch an anchor on the wrong opener.
|
|
145
163
|
*/
|
|
146
164
|
export interface BlockResolution {
|
|
147
165
|
/** The 1-indexed line the block op was anchored on (the `N`). */
|
|
@@ -151,9 +169,9 @@ export interface BlockResolution {
|
|
|
151
169
|
/** Last line of the resolved span (1-indexed, inclusive). */
|
|
152
170
|
end: number;
|
|
153
171
|
/** Which block op produced this resolution. */
|
|
154
|
-
op: "replace" | "
|
|
172
|
+
op: "replace" | "insert_after" | "cut" | "paste_after";
|
|
155
173
|
}
|
|
156
|
-
/** Request handed to a {@link BlockResolver} to resolve one
|
|
174
|
+
/** Request handed to a {@link BlockResolver} to resolve one block-op anchor. */
|
|
157
175
|
export interface BlockResolverRequest {
|
|
158
176
|
/** Target file path (used to infer language by extension). */
|
|
159
177
|
path: string;
|
|
@@ -163,10 +181,19 @@ export interface BlockResolverRequest {
|
|
|
163
181
|
line: number;
|
|
164
182
|
}
|
|
165
183
|
/**
|
|
166
|
-
* Resolves a
|
|
167
|
-
* that
|
|
168
|
-
*
|
|
169
|
-
* resolved subtree has a syntax error). Pure seam: the hashline core declares
|
|
170
|
-
* the contract; the host injects a tree-sitter-backed implementation.
|
|
184
|
+
* Resolves a block-op anchor to the line span of the syntactic block beginning
|
|
185
|
+
* on that line. Returns `null` for an unsupported language, invalid line, absent
|
|
186
|
+
* opener, or syntax error. The host injects the tree-sitter-backed implementation.
|
|
171
187
|
*/
|
|
172
188
|
export type BlockResolver = (request: BlockResolverRequest) => BlockSpan | null;
|
|
189
|
+
/**
|
|
190
|
+
* Mutable clipboard register threaded through one patch application. Filled
|
|
191
|
+
* by `CUT` edits and read by `PASTE` edits in patch source order, across
|
|
192
|
+
* sections, so content can move between files. Create one per batch (`{}`)
|
|
193
|
+
* and hand it to every {@link Patcher.prepare} / `applyTo` call in that batch;
|
|
194
|
+
* callers that omit it get a private per-call register.
|
|
195
|
+
*/
|
|
196
|
+
export interface Clipboard {
|
|
197
|
+
/** Lines captured by the most recent `CUT`, or unset. */
|
|
198
|
+
lines?: readonly string[];
|
|
199
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/hashline",
|
|
4
|
-
"version": "17.
|
|
4
|
+
"version": "17.2.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",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"fmt": "biome format --write ."
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@oh-my-pi/pi-natives": "17.
|
|
36
|
+
"@oh-my-pi/pi-natives": "17.2.0",
|
|
37
37
|
"lru-cache": "11.5.2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|