@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/CHANGELOG.md +20 -0
- package/README.md +7 -7
- package/dist/types/clipboard.d.ts +6 -4
- package/dist/types/format.d.ts +21 -27
- package/dist/types/messages.d.ts +48 -27
- package/dist/types/parser.d.ts +2 -1
- package/dist/types/prefixes.d.ts +2 -0
- package/dist/types/tokenizer.d.ts +12 -8
- package/dist/types/types.d.ts +53 -25
- package/package.json +2 -2
- package/src/apply.ts +72 -1
- package/src/block.ts +27 -5
- package/src/clipboard.ts +141 -42
- package/src/format.ts +35 -34
- package/src/grammar.lark +12 -11
- package/src/input.ts +14 -10
- package/src/messages.ts +129 -65
- package/src/parser.ts +307 -103
- package/src/patcher.ts +8 -8
- package/src/prefixes.ts +12 -4
- package/src/prompt.md +37 -38
- package/src/recovery.ts +56 -15
- package/src/tokenizer.ts +160 -191
- package/src/types.ts +48 -25
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
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
/**
|
|
43
|
-
function blockFormAt(op: AbsoluteRangeOp, line: number): string {
|
|
44
|
-
|
|
45
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
/**
|
|
91
|
-
export const REPLACE_PAIR_COALESCED_WARNING =
|
|
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
|
-
/**
|
|
94
|
-
|
|
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
|
-
* `
|
|
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
|
|
138
|
-
const
|
|
139
|
-
|
|
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
|
|
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
|
-
/** `
|
|
229
|
+
/** `PUT >N*:` anchored on a closing-delimiter line; applied as `PUT >N:`. */
|
|
191
230
|
export function insertAfterBlockCloserLoweredWarning(line: number): string {
|
|
192
|
-
return closerLoweredWarning(`
|
|
231
|
+
return closerLoweredWarning(`PUT >${line}*:`, `PUT >${line}:`);
|
|
193
232
|
}
|
|
194
233
|
|
|
195
|
-
/** `
|
|
234
|
+
/** `PUT >N*:` anchor unresolvable; applied as `PUT >N:`. */
|
|
196
235
|
export function insertAfterBlockUnresolvedLoweredWarning(line: number): string {
|
|
197
|
-
return unresolvedLoweredWarning(`
|
|
236
|
+
return unresolvedLoweredWarning(`PUT >${line}*:`, line, `PUT >${line}:`);
|
|
198
237
|
}
|
|
199
238
|
|
|
200
|
-
/** `
|
|
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(`
|
|
241
|
+
return closerLoweredWarning(`PUT >${line}*`, `PUT >${line}`);
|
|
203
242
|
}
|
|
204
243
|
|
|
205
|
-
/** `
|
|
244
|
+
/** Register `PUT >N*` anchor unresolvable; applied as `PUT >N`. */
|
|
206
245
|
export function pasteAfterBlockUnresolvedLoweredWarning(line: number): string {
|
|
207
|
-
return unresolvedLoweredWarning(`
|
|
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
|
-
`\`
|
|
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
|
-
`\`
|
|
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 \`
|
|
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
|
|
275
|
-
export const CUT_TAKES_NO_BODY = `\`CUT
|
|
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
|
-
/**
|
|
278
|
-
export const
|
|
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
|
-
/**
|
|
282
|
-
export
|
|
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
|
|
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
|
-
/**
|
|
293
|
-
export const EMPTY_INSERT =
|
|
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 `
|
|
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
|
-
* `
|
|
306
|
-
*
|
|
307
|
-
*
|
|
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 `
|
|
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 `
|
|
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, {
|
|
461
|
-
replace: {
|
|
462
|
-
insert_after: {
|
|
463
|
-
cut: {
|
|
464
|
-
paste_after: {
|
|
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.
|
|
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 \`${
|
|
546
|
+
`and ends at line ${enclosingBlock.end}; use \`${forms.form(enclosingBlock.start)}\` to target it.`;
|
|
483
547
|
}
|
|
484
548
|
return message;
|
|
485
549
|
}
|