@oh-my-pi/hashline 17.2.0 → 17.2.2

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/src/messages.ts CHANGED
@@ -33,16 +33,34 @@ export function formatAnchoredContext(anchorLines: readonly number[], fileLines:
33
33
  /** Concrete range operation rejected because its absolute end precedes its start. */
34
34
  export type AbsoluteRangeOp = "replace" | "cut";
35
35
 
36
- /** Header forms per concrete-range op, used to compose retry suggestions. */
37
- const RANGE_OP_FORMS: Record<AbsoluteRangeOp, { keyword: string; colon: string; blockKeyword: string }> = {
38
- replace: { keyword: "SWAP", colon: ":", blockKeyword: "SWAP.BLK" },
39
- cut: { keyword: "CUT", colon: "", blockKeyword: "CUT.BLK" },
36
+ /** Format a register suffix (` @name` or empty). */
37
+ function regSuffix(register?: string): string {
38
+ return register ? ` @${register}` : "";
39
+ }
40
+
41
+ /** Header composers per concrete-range op, used in retry suggestions. */
42
+ const RANGE_OP_FORMS: Record<
43
+ AbsoluteRangeOp,
44
+ { single: (line: number, reg?: string) => string; range: (start: number, end: number, reg?: string) => string }
45
+ > = {
46
+ replace: {
47
+ single: (line, reg) => (reg ? `PUT ${line}${regSuffix(reg)}` : `PUT ${line}:`),
48
+ range: (start, end, reg) =>
49
+ reg ? `PUT ${start}${HL_RANGE_SEP}${end}${regSuffix(reg)}` : `PUT ${start}${HL_RANGE_SEP}${end}:`,
50
+ },
51
+ cut: {
52
+ single: (line, reg) => `CUT ${line}${regSuffix(reg)}`,
53
+ range: (start, end, reg) => `CUT ${start}${HL_RANGE_SEP}${end}${regSuffix(reg)}`,
54
+ },
40
55
  };
41
56
 
42
- /** `OP.BLK N`-style header for a concrete-range op (`SWAP.BLK 5:` / `CUT.BLK 5`). */
43
- function blockFormAt(op: AbsoluteRangeOp, line: number): string {
44
- const forms = RANGE_OP_FORMS[op];
45
- return `${forms.blockKeyword} ${line}${forms.colon}`;
57
+ /** Block-locator header for a concrete-range op (`PUT 5*:` / `CUT 5*`). */
58
+ function blockFormAt(op: AbsoluteRangeOp, line: number, reg?: string): string {
59
+ return op === "replace"
60
+ ? reg
61
+ ? `PUT ${line}*${regSuffix(reg)}`
62
+ : `PUT ${line}*:`
63
+ : `CUT ${line}*${regSuffix(reg)}`;
46
64
  }
47
65
 
48
66
  /** Explain absolute range endpoints and provide safe, non-applying retry forms. */
@@ -52,15 +70,14 @@ export function invalidAbsoluteRangeMessage(
52
70
  end: number,
53
71
  op: AbsoluteRangeOp,
54
72
  block?: BlockSpan,
73
+ register?: string,
55
74
  ): string {
56
75
  const forms = RANGE_OP_FORMS[op];
57
- const single = op === "replace" ? `SWAP ${start}${HL_RANGE_SEP}${start}:` : `${forms.keyword} ${start}`;
76
+ const single = forms.single(start, register);
58
77
  const countedEnd = start + end - 1;
59
78
  const counted =
60
- Number.isSafeInteger(countedEnd) && countedEnd >= start
61
- ? `${forms.keyword} ${start}${HL_RANGE_SEP}${countedEnd}${forms.colon}`
62
- : null;
63
- const blockForm = blockFormAt(op, start);
79
+ Number.isSafeInteger(countedEnd) && countedEnd >= start ? forms.range(start, countedEnd, register) : null;
80
+ const blockForm = blockFormAt(op, start, register);
64
81
  let message =
65
82
  `line ${patchLine}: Invalid absolute range: start ${start}, end ${end}. ` +
66
83
  `The value after \`${HL_RANGE_SEP}\` is an absolute source line, not a line count or replacement length. ` +
@@ -87,34 +104,49 @@ export const END_PATCH_MARKER = "*** End Patch";
87
104
  */
88
105
  export const ABORT_MARKER = "*** Abort";
89
106
 
90
- /** Two consecutive hunks targeted the exact same concrete range. */
91
- export const REPLACE_PAIR_COALESCED_WARNING = `Two hunks targeted the same range; kept only the second. One \`SWAP N${HL_RANGE_SEP}M:\` hunk per range — the body is the final content, never old+new.`;
107
+ /** Exact-range duplicate hunks were normalized to the final hunk. */
108
+ export const REPLACE_PAIR_COALESCED_WARNING =
109
+ "Multiple hunks targeted the same exact range; kept only the last. Issue one `PUT` or `CUT` hunk per range.";
92
110
 
93
- /** Bare bodyless hunk followed by an overlapping concrete hunk. */
94
- `Dropped a bare hunk overlapped by the concrete hunk after it. One \`SWAP N${HL_RANGE_SEP}M:\` hunk per range — the body is the final content, never old+new.`;
111
+ /** Replacement body indentation was aligned from unchanged structural rows. */
112
+ export const REPLACEMENT_INDENT_AUTO_SHIFT_WARNING =
113
+ "Auto-indented a replacement body to match unchanged structural rows in its source range.";
95
114
 
96
115
  /** Bare body rows auto-converted to literal `+` rows. */
97
116
  export const BARE_BODY_AUTO_PIPED_WARNING =
98
117
  "Auto-prefixed bare body row(s) with `+`. Body rows must be `+TEXT` literal lines.";
99
118
 
119
+ /** Top-level read-output rows recovered as single-line replacements. */
120
+ export const SNAPSHOT_ROWS_AUTO_PUT_WARNING = `Recovered top-level \`N:TEXT\` snapshot row(s) as single-line \`PUT N${HL_RANGE_SEP}N:\` replacements. Use explicit \`PUT\` headers for reliable edits.`;
121
+ /** Bare range header recovered as an implicit replacement hunk. */
122
+ export const BARE_RANGE_AUTO_PUT_WARNING = `Recovered a bare \`N${HL_RANGE_SEP}M:\` header as \`PUT N${HL_RANGE_SEP}M:\`. Prefix replacement ranges with \`PUT\`.`;
123
+
124
+ /** Copied read-output elision rows were ignored rather than written as source. */
125
+ export const READ_METADATA_IGNORED_WARNING =
126
+ "Ignored copied read-output elision row(s). Re-read elided ranges before editing them.";
127
+
128
+ /** Empty span/block PUT recovered as a delete-only edit. */
129
+ export const EMPTY_PUT_AUTO_CUT_WARNING = `Interpreted an empty \`PUT\` body as deletion. Use \`CUT N${HL_RANGE_SEP}M\` or \`CUT N*\` for bodyless deletes.`;
130
+
131
+ /** A bodyless CUT carried a harmless trailing colon. */
132
+ export const CUT_COLON_IGNORED_WARNING = `Ignored a trailing \`:\` on bodyless \`CUT\`. Prefer \`CUT N${HL_RANGE_SEP}M\` / \`CUT N*\` without a colon.`;
133
+
100
134
  /**
101
135
  * Bare `-` body rows accepted as literal Markdown bullets. Only emitted when
102
136
  * the hunk is unambiguously a bullet list: every `-` row is bullet-shaped
137
+
103
138
  * (`- item`) and the body has no unified-diff `+new` counterpart rows.
104
139
  */
105
140
  export const MINUS_BULLET_AUTO_PIPED_WARNING =
106
141
  "Auto-prefixed bare `- ` bullet row(s) as literal content. `-` rows never remove lines — the range does that; always prefix literal body rows with `+`: `+- item`.";
142
+ /** Unified-diff old rows were discarded; explicit `+` rows are final content. */
143
+ export const DIFF_OLD_ROWS_IGNORED_WARNING =
144
+ "Ignored unified-diff `-old` row(s); the range already removes old content, so only `+new` rows were kept.";
107
145
 
108
146
  /** Unified-diff-style `-` row in a hunk body. */
109
147
  export const MINUS_ROW_REJECTED =
110
148
  "`-` 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`.";
111
149
 
112
- /** Replace hunk with no body. */
113
- export const EMPTY_REPLACE = `\`SWAP N${HL_RANGE_SEP}M:\` needs at least one \`+TEXT\` body row. To delete lines, use \`CUT N${HL_RANGE_SEP}M\`.`;
114
-
115
- /** `SWAP.BLK N:` hunk with no body. */
116
- export const EMPTY_BLOCK = "`SWAP.BLK N:` needs at least one `+TEXT` body row. To delete a block, use `CUT.BLK N`.";
117
-
118
150
  /** Optional source-aware suggestions appended to block-anchor diagnostics. */
119
151
  export interface BlockDiagnosticSuggestions {
120
152
  /** Closest following multi-line block that begins after the authored anchor. */
@@ -126,22 +158,29 @@ export interface BlockDiagnosticSuggestions {
126
158
  /**
127
159
  * A block-anchored replace/cut could not resolve to a syntactic block.
128
160
  * Appends a {@link formatAnchoredContext} preview when `fileLines` is given.
129
- * `INS.BLK.POST N:` never reaches this path; it lowers to `INS.POST N:`.
161
+ * `PUT >N*` never reaches this path; it lowers to `PUT >N`.
130
162
  */
131
163
  export function blockUnresolvedMessage(
132
164
  line: number,
133
165
  op: AbsoluteRangeOp = "replace",
134
166
  fileLines?: readonly string[],
135
167
  suggestions: BlockDiagnosticSuggestions = {},
168
+ register?: string,
136
169
  ): string {
137
- const forms = RANGE_OP_FORMS[op];
138
- const phrase = blockFormAt(op, line);
139
- const fallback = `${forms.keyword} ${line}${HL_RANGE_SEP}M${forms.colon}`;
170
+ const phrase = blockFormAt(op, line, register);
171
+ const fallback =
172
+ op === "replace"
173
+ ? register
174
+ ? `PUT ${line}${HL_RANGE_SEP}M @${register}`
175
+ : `PUT ${line}${HL_RANGE_SEP}M:`
176
+ : register
177
+ ? `CUT ${line}${HL_RANGE_SEP}M @${register}`
178
+ : `CUT ${line}${HL_RANGE_SEP}M`;
140
179
  const anchorText = fileLines?.[line - 1];
141
180
  const nextBlock = suggestions.nextBlock;
142
181
  let message: string;
143
182
  if (anchorText !== undefined && anchorText.trim().length === 0 && nextBlock) {
144
- const retry = blockFormAt(op, nextBlock.start);
183
+ const retry = blockFormAt(op, nextBlock.start, register);
145
184
  message =
146
185
  `Line ${line} is blank; no syntactic block can begin there. ` +
147
186
  `The next multi-line block begins at line ${nextBlock.start} and ends at line ${nextBlock.end}. ` +
@@ -153,7 +192,7 @@ export function blockUnresolvedMessage(
153
192
  }
154
193
  const enclosingBlock = suggestions.enclosingBlock;
155
194
  if (enclosingBlock) {
156
- const retry = blockFormAt(op, enclosingBlock.start);
195
+ const retry = blockFormAt(op, enclosingBlock.start, register);
157
196
  message +=
158
197
  ` The nearest enclosing multi-line block begins at line ${enclosingBlock.start} ` +
159
198
  `and ends at line ${enclosingBlock.end}; use \`${retry}\` to target it.`;
@@ -167,7 +206,7 @@ export function blockUnresolvedMessage(
167
206
 
168
207
  /** Block-anchored edit reached a path with no {@link BlockResolver} wired in. */
169
208
  export const BLOCK_RESOLVER_UNAVAILABLE =
170
- "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.";
209
+ "Block locators (`N*` in `PUT N*:`, `PUT >N*`, `CUT N*`) are not available here (no block resolver configured). Use a concrete line range.";
171
210
 
172
211
  /**
173
212
  * An after-block op anchored on a closing-delimiter line, lowered to its
@@ -187,24 +226,24 @@ function unresolvedLoweredWarning(blockForm: string, line: number, plainForm: st
187
226
  return `\`${blockForm}\` could not resolve a syntactic block on line ${line}, so it was applied as plain \`${plainForm}\`. Verify the landing line; anchor on a line that OPENS a construct.`;
188
227
  }
189
228
 
190
- /** `INS.BLK.POST N:` anchored on a closing-delimiter line; applied as `INS.POST N:`. */
229
+ /** `PUT >N*:` anchored on a closing-delimiter line; applied as `PUT >N:`. */
191
230
  export function insertAfterBlockCloserLoweredWarning(line: number): string {
192
- return closerLoweredWarning(`INS.BLK.POST ${line}:`, `INS.POST ${line}:`);
231
+ return closerLoweredWarning(`PUT >${line}*:`, `PUT >${line}:`);
193
232
  }
194
233
 
195
- /** `INS.BLK.POST N:` anchor unresolvable; applied as `INS.POST N:`. */
234
+ /** `PUT >N*:` anchor unresolvable; applied as `PUT >N:`. */
196
235
  export function insertAfterBlockUnresolvedLoweredWarning(line: number): string {
197
- return unresolvedLoweredWarning(`INS.BLK.POST ${line}:`, line, `INS.POST ${line}:`);
236
+ return unresolvedLoweredWarning(`PUT >${line}*:`, line, `PUT >${line}:`);
198
237
  }
199
238
 
200
- /** `PASTE.BLK.POST N` anchored on a closing-delimiter line; applied as `PASTE.POST N`. */
239
+ /** Register `PUT >N*` anchored on a closing-delimiter line; applied as `PUT >N`. */
201
240
  export function pasteAfterBlockCloserLoweredWarning(line: number): string {
202
- return closerLoweredWarning(`PASTE.BLK.POST ${line}`, `PASTE.POST ${line}`);
241
+ return closerLoweredWarning(`PUT >${line}*`, `PUT >${line}`);
203
242
  }
204
243
 
205
- /** `PASTE.BLK.POST N` anchor unresolvable; applied as `PASTE.POST N`. */
244
+ /** Register `PUT >N*` anchor unresolvable; applied as `PUT >N`. */
206
245
  export function pasteAfterBlockUnresolvedLoweredWarning(line: number): string {
207
- return unresolvedLoweredWarning(`PASTE.BLK.POST ${line}`, line, `PASTE.POST ${line}`);
246
+ return unresolvedLoweredWarning(`PUT >${line}*`, line, `PUT >${line}`);
208
247
  }
209
248
  /**
210
249
  * A one-sided boundary echo whose payload is too short to be the widened
@@ -224,7 +263,7 @@ export function ambiguousBoundaryEchoMessage(
224
263
  ? `opens by restating the ${count} line(s) just above the range`
225
264
  : `ends by restating the ${count} line(s) just below the range`;
226
265
  return (
227
- `\`SWAP ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: the body ${where}, ` +
266
+ `\`PUT ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: the body ${where}, ` +
228
267
  `but is too short to be the full final content of the widened range — applying it as-is or ` +
229
268
  `auto-repairing would delete range line(s) the body never restates. ` +
230
269
  `Re-issue with the range covering exactly the lines that change and the body as their complete ` +
@@ -248,11 +287,11 @@ export function ambiguousCloserSpareMessage(
248
287
  ): string {
249
288
  const closers = count === 1 ? `line ${closerLine}` : `lines ${closerLine}-${closerLine + count - 1}`;
250
289
  return (
251
- `\`SWAP ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: the range deletes the closing-delimiter ` +
290
+ `\`PUT ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: the range deletes the closing-delimiter ` +
252
291
  `${closers} but the body never restates it, and the body claims no position inside that block ` +
253
292
  `(no unmatched opener, indentation not deeper than the closer) — whether the new content belongs ` +
254
293
  `before or after the closer is ambiguous. Restate the closer in the body at the intended position, ` +
255
- `or use \`INS.PRE ${closerLine}:\` / \`INS.POST ${closerLine}:\` instead.`
294
+ `or use \`PUT <${closerLine}:\` / \`PUT >${closerLine}:\` instead.`
256
295
  );
257
296
  }
258
297
 
@@ -271,15 +310,40 @@ export const REM_TAKES_NO_BODY =
271
310
  export const MOVE_TAKES_NO_BODY =
272
311
  "`MV DEST` does not take body rows. Put line edits above the `MV` row; the destination path follows `MV` on the same line.";
273
312
 
274
- /** `CUT N.=M` hunk received a body row. */
275
- export const CUT_TAKES_NO_BODY = `\`CUT N${HL_RANGE_SEP}M\` captures + deletes lines and takes no body rows. To replace lines with new content, use \`SWAP N${HL_RANGE_SEP}M:\`.`;
313
+ /** `CUT` hunk received a body row. */
314
+ export const CUT_TAKES_NO_BODY = `\`CUT\` deletes (and captures) the named lines and takes no body rows. To write new content, use \`PUT N${HL_RANGE_SEP}M:\` with \`+TEXT\` rows.`;
315
+
316
+ /** Register `PUT` header carried a `:`. */
317
+ export const COLON_ON_REGISTER_PUT =
318
+ "`PUT … @name` pastes the register and never takes `:` — the colon promises body rows. Drop the colon (`PUT >40 @name`), or drop `@name` and write `+TEXT` body rows.";
319
+
320
+ /** Register `PUT` hunk received a body row. */
321
+ export const REGISTER_PUT_TAKES_NO_BODY =
322
+ "A register `PUT` pastes captured lines and takes no `+` body rows. To write literal text, drop the `@name` and use `PUT …:` with body rows.";
323
+
324
+ /** Colonless `PUT` hunk received a body row. */
325
+ export const COLONLESS_PUT_TAKES_NO_BODY =
326
+ "`PUT` without `:` is clipboard-backed and takes no body rows. Add `:` after the locator to write literal content (`PUT >40:` then `+TEXT` rows).";
327
+
328
+ /** Colonless anonymous `PUT` on a span target. */
329
+ export const COLONLESS_SPAN_PUT = `Colonless \`PUT\` is clipboard-backed, and span targets need a named register (\`PUT 5${HL_RANGE_SEP}9 @name\`); the anonymous register pastes only at gaps (\`PUT >40\`). To write literal content, add \`:\` and \`+TEXT\` body rows.`;
276
330
 
277
- /** `PASTE` hunk received a body row. */
278
- export const PASTE_TAKES_NO_BODY =
279
- "`PASTE` inserts the clipboard content and takes no `+` body rows. To insert literal text, use `INS`.";
331
+ /** Anonymous paste ran with an empty anonymous register. */
332
+ export const EMPTY_PASTE = `Nothing to paste: no unlabeled \`CUT\` precedes this \`PUT\` in this call, and the anonymous register never carries across calls. Put \`CUT N${HL_RANGE_SEP}M\` / \`CUT N*\` above it, or use named registers (\`CUT … @name\` → \`PUT … @name\`) for cross-call moves.`;
280
333
 
281
- /** `PASTE` ran with an empty clipboard register. */
282
- export const EMPTY_PASTE = `\`PASTE\` found nothing in the clipboard. Ops run top-to-bottom across the whole patch (sections included): put \`CUT N${HL_RANGE_SEP}M\` or \`CUT.BLK N\` above the \`PASTE\`.`;
334
+ /** Named paste read a register that holds nothing. */
335
+ export function unknownRegisterMessage(name: string, known: readonly string[]): string {
336
+ const base = `\`@${name}\` is empty: no \`CUT … @${name}\` precedes this op in this call and no persisted register has that name.`;
337
+ return known.length === 0 ? base : `${base} Available registers: ${known.map(k => `\`@${k}\``).join(", ")}.`;
338
+ }
339
+
340
+ /** Unlabeled paste with two or more unlabeled cuts pending. */
341
+ export function ambiguousAnonymousPasteMessage(pending: readonly string[]): string {
342
+ return (
343
+ `${pending.length} unlabeled \`CUT\`s are pending (${pending.join(", ")}) — an unlabeled paste cannot tell which one you meant. ` +
344
+ "Label the moves (`CUT … @name` → `PUT … @name`), or keep at most one unlabeled `CUT` before each unlabeled paste."
345
+ );
346
+ }
283
347
 
284
348
  /**
285
349
  * Clipboard ops inside a same-path section that was merged across another
@@ -287,10 +351,11 @@ export const EMPTY_PASTE = `\`PASTE\` found nothing in the clipboard. Ops run to
287
351
  * an interleaved layout would silently reorder the register sequence.
288
352
  */
289
353
  export const CLIPBOARD_INTERLEAVED_SECTIONS =
290
- "`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.";
354
+ "`CUT`/register-`PUT` ops 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 register sequence. Keep each file's ops under ONE `[path#TAG]` header.";
291
355
 
292
- /** Insert hunk with no body. */
293
- export const EMPTY_INSERT = "`INS` needs at least one `+TEXT` body row.";
356
+ /** Gap `PUT` with `:` but no body. */
357
+ export const EMPTY_INSERT =
358
+ "`PUT <N:` / `PUT >N:` promises body rows and got none. Write `+TEXT` rows, or drop the `:` to paste a register (`PUT >N` = anonymous, `PUT >N @name` = named).";
294
359
 
295
360
  /**
296
361
  * `insert after` body indented shallower than the anchor: the landing slid
@@ -298,16 +363,16 @@ export const EMPTY_INSERT = "`INS` needs at least one `+TEXT` body row.";
298
363
  * I read instead of after the block" mistake.
299
364
  */
300
365
  export function afterInsertLandingShiftWarning(anchorLine: number, landingLine: number, crossed: number): string {
301
- return `INS.POST ${anchorLine}: body indented shallower than the anchor, so the landing moved past ${crossed} closing line${crossed === 1 ? "" : "s"} to after line ${landingLine}. For the deeper position inside the block, re-issue with the body indented to match.`;
366
+ return `PUT >${anchorLine}: body indented shallower than the anchor, so the landing moved past ${crossed} closing line${crossed === 1 ? "" : "s"} to after line ${landingLine}. For the deeper position inside the block, re-issue with the body indented to match.`;
302
367
  }
303
368
 
304
369
  /**
305
- * `insert_after_block N:` body indented deeper than the block's closer: the
306
- * landing was pulled inside the block — a deeper body almost always means
307
- * "append inside the block's body".
370
+ * `PUT >N*:` body indented deeper than the block's closer: the landing was
371
+ * pulled inside the block — a deeper body almost always means "append inside
372
+ * the block's body".
308
373
  */
309
374
  export function blockInsertLandingShiftWarning(blockStart: number, closerLine: number, landingLine: number): string {
310
- return `INS.BLK.POST ${blockStart}: body indented deeper than closing line ${closerLine}, so it was placed inside the block, after line ${landingLine}. \`INS.BLK.POST\` lands AFTER the block at sibling depth — if inside was intended, use plain \`INS.POST ${closerLine}:\`.`;
375
+ return `PUT >${blockStart}*: body indented deeper than closing line ${closerLine}, so it was placed inside the block, after line ${landingLine}. \`PUT >N*\` lands AFTER the block at sibling depth — if inside was intended, use plain \`PUT >${closerLine}:\`.`;
311
376
  }
312
377
 
313
378
  /** `Recovery`: an external write matched a cached snapshot. */
@@ -328,7 +393,7 @@ export const RECOVERY_LINE_REMAP_WARNING =
328
393
  * onto live content and warn instead of hard-failing.
329
394
  */
330
395
  export const HEADTAIL_DRIFT_WARNING =
331
- "Applied the `INS.HEAD:`/`INS.TAIL:` edit despite a stale snapshot tag (file changed since your read) — head/tail position is content-independent. Re-read if the drift was unexpected.";
396
+ "Applied the `PUT <1:`/`PUT >$:` edit despite a stale snapshot tag (file changed since your read) — head/tail position is content-independent. Re-read if the drift was unexpected.";
332
397
 
333
398
  /**
334
399
  * The `Filesystem` reported that what actually landed on disk differs from
@@ -457,11 +522,11 @@ export function unseenLinesMessage(
457
522
  export type BlockOp = "replace" | "insert_after" | "cut" | "paste_after";
458
523
 
459
524
  /** Display forms per deferred-block op: block keyword, trailing colon, and single-line plain form. */
460
- const BLOCK_OP_FORMS: Record<BlockOp, { keyword: string; colon: string; plain: (line: number) => string }> = {
461
- replace: { keyword: "SWAP.BLK", colon: ":", plain: line => `SWAP ${line}${HL_RANGE_SEP}${line}:` },
462
- insert_after: { keyword: "INS.BLK.POST", colon: ":", plain: line => `INS.POST ${line}:` },
463
- cut: { keyword: "CUT.BLK", colon: "", plain: line => `CUT ${line}` },
464
- paste_after: { keyword: "PASTE.BLK.POST", colon: "", plain: line => `PASTE.POST ${line}` },
525
+ const BLOCK_OP_FORMS: Record<BlockOp, { form: (line: number) => string; plain: (line: number) => string }> = {
526
+ replace: { form: line => `PUT ${line}*:`, plain: line => `PUT ${line}:` },
527
+ insert_after: { form: line => `PUT >${line}*:`, plain: line => `PUT >${line}:` },
528
+ cut: { form: line => `CUT ${line}*`, plain: line => `CUT ${line}` },
529
+ paste_after: { form: line => `PUT >${line}*`, plain: line => `PUT >${line}` },
465
530
  };
466
531
 
467
532
  /**
@@ -473,13 +538,12 @@ export function blockSingleLineMessage(line: number, op: BlockOp, enclosingBlock
473
538
  const forms = BLOCK_OP_FORMS[op];
474
539
  const plainForm = forms.plain(line);
475
540
  let message =
476
- `\`${forms.keyword} ${line}\` resolved a single-line block — line ${line} is a bare statement, not the opening line ` +
541
+ `\`${forms.form(line)}\` resolved a single-line block — line ${line} is a bare statement, not the opening line ` +
477
542
  `of a multi-line construct. For only this statement use \`${plainForm}\`.`;
478
543
  if (enclosingBlock) {
479
- const enclosingForm = `${forms.keyword} ${enclosingBlock.start}${forms.colon}`;
480
544
  message +=
481
545
  ` The nearest enclosing multi-line block begins at line ${enclosingBlock.start} ` +
482
- `and ends at line ${enclosingBlock.end}; use \`${enclosingForm}\` to target it.`;
546
+ `and ends at line ${enclosingBlock.end}; use \`${forms.form(enclosingBlock.start)}\` to target it.`;
483
547
  }
484
548
  return message;
485
549
  }