pi-hashline-edit-pro 4.4.4 → 4.5.1

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/paths.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { homedir } from "node:os";
2
- import { isAbsolute, relative, resolve as resolvePath, join, dirname } from "node:path";
2
+ import { isAbsolute, relative, resolve as resolvePath, join, dirname, parse } from "node:path";
3
3
 
4
4
 
5
5
  function homeBase(): string {
@@ -55,6 +55,22 @@ export function toCwd(filePath: string, cwd: string): string {
55
55
  return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
56
56
  }
57
57
 
58
+ function pathDepth(path: string): number {
59
+ const root = parse(path).root;
60
+ return path.slice(root.length).split(/[\\/]+/).filter((part) => part.length > 0).length;
61
+ }
62
+
63
+ function climbsToRoot(cwd: string, relativePath: string): boolean {
64
+ let climbs = 0;
65
+ for (const part of relativePath.split("/")) {
66
+ if (part !== "..") break;
67
+ climbs += 1;
68
+ }
69
+ return climbs > 0 && climbs >= pathDepth(resolvePath(cwd));
70
+ }
71
+
58
72
  export function toDisplayPath(cwd: string, absolutePath: string, fallback?: string): string {
59
- return relative(cwd, absolutePath).replace(/\\/g, "/") || (fallback ?? absolutePath);
73
+ const rel = relative(cwd, absolutePath).replace(/\\/g, "/");
74
+ if (rel.length === 0 || climbsToRoot(cwd, rel)) return fallback ?? absolutePath;
75
+ return rel;
60
76
  }
@@ -4,7 +4,7 @@ import { isRec, normalizeRequest, rejectUnknownFields, assertNoNul } from "./uti
4
4
  const replacementLinesSchema = Type.Array(
5
5
  Type.String({
6
6
  description:
7
- "One replacement line; never embed \\n inside an element.",
7
+ "One replacement line. A real line break (\\n, \\r\\n, or \\r) splits it into lines and sets their endings.",
8
8
  }),
9
9
  {
10
10
  description:
@@ -131,3 +131,31 @@ export function assertInsertReq(request: unknown): asserts request is InsertReq
131
131
  }
132
132
  assertNoNul(request.lines);
133
133
  }
134
+
135
+ const TRANSFER_KEYS = new Set(["path", "source_from", "source_to", "insert_after"]);
136
+
137
+ export interface TransferReq {
138
+ path?: string;
139
+ source_from: string;
140
+ source_to: string;
141
+ insert_after: string;
142
+ }
143
+
144
+ export function assertTransferReq(request: unknown): asserts request is TransferReq {
145
+ if (!isRec(request)) {
146
+ throw new Error("[E_BAD_SHAPE] Copy/move request must be an object.");
147
+ }
148
+ rejectUnknownFields(request, TRANSFER_KEYS, "Copy/move request");
149
+ if (request.path !== undefined && typeof request.path !== "string") {
150
+ throw new Error('[E_BAD_SHAPE] Copy/move request field "path" must be a string when provided.');
151
+ }
152
+ if (typeof request.source_from !== "string" || request.source_from.length === 0) {
153
+ throw new Error('[E_BAD_SHAPE] Copy/move request requires a "source_from" string (4-char anchor from read output).');
154
+ }
155
+ if (typeof request.source_to !== "string" || request.source_to.length === 0) {
156
+ throw new Error('[E_BAD_SHAPE] Copy/move request requires a "source_to" string (4-char anchor from read output).');
157
+ }
158
+ if (typeof request.insert_after !== "string" || request.insert_after.length === 0) {
159
+ throw new Error('[E_BAD_SHAPE] Copy/move request requires an "insert_after" string (4-char anchor from read output).');
160
+ }
161
+ }
@@ -81,7 +81,7 @@ export function highlightBatchRefs(text: string, theme: FgT): string {
81
81
  }
82
82
 
83
83
  export function fmtCall(
84
- args: { path?: string; remove_from?: string; remove_to?: string; anchor?: string } | undefined,
84
+ args: { path?: string; remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string; insert_after?: string } | undefined,
85
85
  state: RRState,
86
86
  expanded: boolean,
87
87
  theme: CallT,
@@ -89,7 +89,7 @@ export function fmtCall(
89
89
  ): string {
90
90
  const previewPath = state.preview && "path" in state.preview ? state.preview.path : undefined;
91
91
  const path = args?.path ?? state.resolvedPath ?? previewPath;
92
- const anchorFallback = typeof args?.remove_from === "string" && typeof args?.remove_to === "string" ? `${args.remove_from}→${args.remove_to}` : typeof args?.anchor === "string" ? args.anchor : undefined;
92
+ const anchorFallback = typeof args?.remove_from === "string" && typeof args?.remove_to === "string" ? `${args.remove_from}→${args.remove_to}` : typeof args?.anchor === "string" ? args.anchor : typeof args?.source_from === "string" && typeof args?.insert_after === "string" ? `${args.source_from}→${args.insert_after}` : undefined;
93
93
  const pathDisplay =
94
94
  typeof path === "string" && path.length > 0
95
95
  ? theme.fg("accent", path)
@@ -126,7 +126,13 @@ export function getResultText(result: {
126
126
  export function extractWarnings(
127
127
  text: string | undefined,
128
128
  ): string | undefined {
129
- return text?.match(/(?:^|\n)Warnings:\n[\s\S]*$/)?.[0]?.trimStart();
129
+ return text?.match(/(?:^|\n)Warnings:\n[\s\S]*?(?=\n\nHints:\n|$)/)?.[0]?.trimStart();
130
+ }
131
+
132
+ export function extractHints(
133
+ text: string | undefined,
134
+ ): string | undefined {
135
+ return text?.match(/(?:^|\n)Hints:\n[\s\S]*$/)?.[0]?.trimStart();
130
136
  }
131
137
 
132
138
  export function isApplied(
@@ -153,8 +159,8 @@ export function expandHint(): string {
153
159
  function extractSummary(text: string | undefined): string | undefined {
154
160
  if (!text) return undefined;
155
161
  if (text.includes("│")) return undefined;
156
- const warningsIdx = text.indexOf("\n\nWarnings:");
157
- const summary = warningsIdx >= 0 ? text.slice(0, warningsIdx) : text;
162
+ const noticesIdx = text.search(/\n\n(?:Warnings|Hints):\n/);
163
+ const summary = noticesIdx >= 0 ? text.slice(0, noticesIdx) : text;
158
164
  return summary.length > 0 ? summary : undefined;
159
165
  }
160
166
 
@@ -190,6 +196,8 @@ export function buildAppliedText(
190
196
  }
191
197
  const warnings = details?.warnings?.length ? `Warnings:\n${details.warnings.join("\n")}` : extractWarnings(text);
192
198
  if (warnings) sections.push(warnings);
199
+ const hints = details?.hints?.length ? `Hints:\n${details.hints.join("\n")}` : extractHints(text);
200
+ if (hints) sections.push(hints);
193
201
  return sections.length > 0 ? sections.join("\n\n") : undefined;
194
202
  }
195
203
 
@@ -264,9 +272,9 @@ export function reuseMarkdown(context: any, content: string, theme: any): Markdo
264
272
  export function makeRenderCall(
265
273
  preview: (args: unknown, cwd: string, signal?: AbortSignal) => Promise<RPreview>,
266
274
  options: {
267
- getInput?: (args: unknown) => { path?: string; remove_from?: string; remove_to?: string; anchor?: string } | null;
275
+ getInput?: (args: unknown) => { path?: string; remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string; insert_after?: string } | null;
268
276
  toolName?: string;
269
- resolveTarget?: (input: { remove_from?: string; remove_to?: string; anchor?: string }) => string | undefined;
277
+ resolveTarget?: (input: { remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string; insert_after?: string }) => string | undefined;
270
278
  } = {},
271
279
  ) {
272
280
  const getInput = options.getInput ?? getPreviewInput;
@@ -85,6 +85,17 @@ function warnBlock(warnings: string[] | undefined): string {
85
85
  return warnings?.length ? `\n\nWarnings:\n${warnings.join("\n")}` : "";
86
86
  }
87
87
 
88
+ function hintBlock(hints: string[] | undefined): string {
89
+ return hints?.length ? `\n\nHints:\n${hints.join("\n")}` : "";
90
+ }
91
+
92
+ function splitNotices(notices: string[] | undefined): { warnings: string[]; hints: string[] } {
93
+ const warnings: string[] = [];
94
+ const hints: string[] = [];
95
+ for (const notice of notices ?? []) (notice.startsWith("[H_") ? hints : warnings).push(notice);
96
+ return { warnings, hints };
97
+ }
98
+
88
99
  export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
89
100
  const {
90
101
  path,
@@ -97,13 +108,14 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
97
108
  const noopDetailsText = noopEdit
98
109
  ? `${noopNoun} for ${noopEdit.loc} is identical to current content:\n ${noopEdit.loc}: ${clipLine(noopEdit.currentContent)}`
99
110
  : "The edit produced identical content.";
100
- const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}${warnBlock(warnings)}`;
111
+ const { warnings: noticeWarnings, hints } = splitNotices(warnings);
112
+ const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}${warnBlock(noticeWarnings)}${hintBlock(hints)}`;
101
113
 
102
114
  const metrics = buildMetrics({
103
115
  classification: "noop",
104
116
  editsAttempted: editMeta.editsAttempted,
105
117
  noopEditsCount: editMeta.noopEditsCount,
106
- warningsCount: warnings?.length ?? 0,
118
+ warningsCount: noticeWarnings.length,
107
119
  });
108
120
 
109
121
  return {
@@ -115,7 +127,8 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
115
127
  snapshotId,
116
128
  classification: "noop" as const,
117
129
  metrics,
118
- ...(warnings?.length ? { warnings: [...warnings] } : {}),
130
+ ...(noticeWarnings.length ? { warnings: [...noticeWarnings] } : {}),
131
+ ...(hints.length ? { hints: [...hints] } : {}),
119
132
  },
120
133
  };
121
134
  }
@@ -126,22 +139,23 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
126
139
  const diffResult = genDiff(originalNormalized, result, diffContextLines, resultHashes, originalHashes, undefined, spans);
127
140
  const addedLines = editMeta.addedLines;
128
141
  const removedLines = editMeta.removedLines;
129
- const warningsBlock = warnBlock(warnings);
142
+ const { warnings: noticeWarnings, hints } = splitNotices(warnings);
143
+ const noticesBlock = `${warnBlock(noticeWarnings)}${hintBlock(hints)}`;
130
144
  const successPrefix = `Successfully ${verb} in ${path}.`;
131
145
  const lineSummary = addedLines > 0 || removedLines > 0
132
146
  ? ` Added ${addedLines} line(s), removed ${removedLines} line(s).`
133
147
  : "";
134
148
  const text = resultLines.length === 0
135
149
  ? "File is empty. Use replace to insert content."
136
- : warningsBlock
137
- ? `${successPrefix}${lineSummary}${warningsBlock}`
150
+ : noticesBlock
151
+ ? `${successPrefix}${lineSummary}${noticesBlock}`
138
152
  : `${successPrefix}${lineSummary}`;
139
153
 
140
154
  const metrics = buildMetrics({
141
155
  classification: "applied",
142
156
  editsAttempted: editMeta.editsAttempted,
143
157
  noopEditsCount: editMeta.noopEditsCount,
144
- warningsCount: warnings?.length ?? 0,
158
+ warningsCount: noticeWarnings.length,
145
159
  firstChangedLine: editMeta.firstChangedLine,
146
160
  lastChangedLine: editMeta.lastChangedLine,
147
161
  addedLines,
@@ -160,7 +174,8 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
160
174
  snapshotId,
161
175
  metrics,
162
176
  diffLineNumbers: diffResult.lineNumbers.map((line) => line ?? null),
163
- ...(warnings?.length ? { warnings: [...warnings] } : {}),
177
+ ...(noticeWarnings.length ? { warnings: [...noticeWarnings] } : {}),
178
+ ...(hints.length ? { hints: [...hints] } : {}),
164
179
  },
165
180
  };
166
181
  }
@@ -9,6 +9,7 @@ import { lineChecksum } from "./hashline";
9
9
  import { freeAnchors, adoptAnchors, withAnchorSession, formatAnchorReclaimNotice, takeReclaimedPaths } from "./anchor-registry";
10
10
  import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
11
11
  import { toLF, stripBOM, restoreEndings, type LineEnding } from "./normalize";
12
+ import { joinSeparators } from "./line-endings";
12
13
  import { genDiff, genPatch, spansFromHashes } from "./replace-diff";
13
14
  import { getDiffContextLines } from "./config";
14
15
  import { cntDiff, errCode, makePrepareArguments, splitLines } from "./utils";
@@ -22,8 +23,10 @@ export interface UndoEntry {
22
23
  content: string;
23
24
  bom: string;
24
25
  originalEnding: LineEnding;
26
+ separators?: LineEnding[];
25
27
  hashes: string[];
26
28
  resultContent: string;
29
+ resultSeparators?: LineEnding[];
27
30
  mode?: number;
28
31
  }
29
32
 
@@ -44,8 +47,10 @@ export async function saveUndo(
44
47
  content: entry.content,
45
48
  bom: entry.bom,
46
49
  ending: entry.originalEnding,
50
+ ...(entry.separators !== undefined ? { separators: entry.separators } : {}),
47
51
  hashes: entry.hashes,
48
52
  resultContent: entry.resultContent,
53
+ ...(entry.resultSeparators !== undefined ? { resultSeparators: entry.resultSeparators } : {}),
49
54
  ...(mode !== undefined ? { mode } : {}),
50
55
  });
51
56
  } catch (error) {
@@ -80,8 +85,10 @@ export async function getUndo(path: string): Promise<UndoEntry | undefined> {
80
85
  content: record.content,
81
86
  bom: record.bom,
82
87
  originalEnding,
88
+ ...(record.separators !== undefined ? { separators: record.separators as LineEnding[] } : {}),
83
89
  hashes: record.hashes,
84
90
  resultContent: record.resultContent,
91
+ ...(record.resultSeparators !== undefined ? { resultSeparators: record.resultSeparators as LineEnding[] } : {}),
85
92
  ...(record.mode !== undefined ? { mode: record.mode } : {}),
86
93
  };
87
94
  } catch (error) {
@@ -171,7 +178,7 @@ export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
171
178
 
172
179
  if (
173
180
  currentRaw !== undefined &&
174
- currentRaw !== undo.bom + restoreEndings(undo.resultContent, undo.originalEnding)
181
+ currentRaw !== undo.bom + (undo.resultSeparators !== undefined ? joinSeparators(undo.resultContent, undo.resultSeparators) : restoreEndings(undo.resultContent, undo.originalEnding))
175
182
  ) {
176
183
  return {
177
184
  content: [
@@ -187,7 +194,7 @@ export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
187
194
 
188
195
  await writeAtomic(
189
196
  mutationTargetPath,
190
- undo.bom + restoreEndings(undo.content, undo.originalEnding),
197
+ undo.bom + (undo.separators !== undefined ? joinSeparators(undo.content, undo.separators) : restoreEndings(undo.content, undo.originalEnding)),
191
198
  currentIdentity,
192
199
  undo.mode ?? fallbackFileMode(),
193
200
  );
package/src/replace.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  } from "./replace-diff";
11
11
  import { readNormFile, type NormFile } from "./file-reader";
12
12
  import { editToolSchema, buildEditToolSchema, type ReqParams, assertReq, normReq } from "./payload-contract";
13
- import { splitLines } from "./utils";
13
+ import { literalEscapeHint, splitLines } from "./utils";
14
14
  import { loadP, loadGuide } from "./prompts";
15
15
  import { type FileIdentity } from "./fs-write";
16
16
  import { applyEdit,
@@ -21,6 +21,7 @@ import { applyEdit,
21
21
  AnchorMismatchError,
22
22
  type HEdit,
23
23
  type NEdit,
24
+ type StripWarningLocation,
24
25
  } from "./hashline";
25
26
  import { type RMetrics } from "./replace-response";
26
27
  import {
@@ -47,6 +48,7 @@ export type ReplaceDetails = {
47
48
  metrics?: RMetrics;
48
49
  diffLineNumbers?: (number | null)[];
49
50
  warnings?: string[];
51
+ hints?: string[];
50
52
  batch?: { id: number; size: number; last: boolean; total: number; aborted?: boolean; abortMessage?: string };
51
53
  };
52
54
 
@@ -56,6 +58,7 @@ export interface PipelineResult {
56
58
  result: string;
57
59
  bom: string;
58
60
  originalEnding: LineEnding;
61
+ originalSeparators: LineEnding[];
59
62
  hadUtf8DecodeErrors: boolean;
60
63
  warnings: string[];
61
64
  noopEdit?: NEdit;
@@ -67,6 +70,7 @@ export interface PipelineResult {
67
70
  totalRemovedLines: number;
68
71
  identity: FileIdentity;
69
72
  spans?: DiffSpan[];
73
+ contentSeparators?: (LineEnding | undefined)[];
70
74
  }
71
75
 
72
76
 
@@ -76,6 +80,9 @@ export interface ExecPipelineOptions {
76
80
  store?: HashStore;
77
81
  noPersist?: boolean;
78
82
  preloadedNorm?: NormFile;
83
+ served?: ReadonlyMap<string, string>;
84
+ allowEmpty?: boolean;
85
+ stripWarning?: StripWarningLocation;
79
86
  }
80
87
 
81
88
  export function hashSpan(hashes: string[], from: string, to: string): [number, number] | undefined {
@@ -91,7 +98,7 @@ export function spanForEdit(originalHashes: string[], from: string, to: string,
91
98
  const replacementCount = splitLines(resultContent).length - (originalHashes.length - (span[1] - span[0] + 1));
92
99
  return { start: span[0], end: span[1], replacementCount };
93
100
  }
94
- async function noteAnchorError(absolutePath: string, error: unknown, noPersist?: boolean): Promise<void> {
101
+ export async function noteAnchorError(absolutePath: string, error: unknown, noPersist?: boolean): Promise<void> {
95
102
  if (noPersist === true) return;
96
103
  if (error instanceof RangeStaleError) {
97
104
  adoptAnchors(absolutePath, error.rangeServedMap);
@@ -137,8 +144,8 @@ export async function execPipeline(
137
144
  const { edit, warnings: editWarnings } = buildReplaceHEdit(params);
138
145
  const hashStore = options?.store ?? await loadHashStore();
139
146
  const preResolvedPath = await resolveTarget(toCwd(targetPath, cwd));
140
- const served = servedForPath(preResolvedPath);
141
- const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath, identity } = await readNormFile(
147
+ const served = options?.served ?? servedForPath(preResolvedPath);
148
+ const { normalized: originalNormalized, bom, originalEnding, endingSeparators: originalSeparators, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath, identity } = await readNormFile(
142
149
  targetPath, cwd, { signal: options?.signal, accessMode: options?.accessMode, maxLines: MAX_HASH_LINES, store: hashStore, noPersist: options?.noPersist, allocation: options?.noPersist ? "shadow" : "real", preloadedNorm: options?.preloadedNorm },
143
150
  );
144
151
  const displayPath = toDisplayPath(cwd, absolutePath, targetPath);
@@ -152,6 +159,8 @@ export async function execPipeline(
152
159
  originalHashes,
153
160
  displayPath,
154
161
  served,
162
+ options?.allowEmpty,
163
+ options?.stripWarning,
155
164
  );
156
165
  } catch (error) {
157
166
  await noteAnchorError(absolutePath, error, options?.noPersist);
@@ -181,6 +190,7 @@ export async function execPipeline(
181
190
  result,
182
191
  bom,
183
192
  originalEnding,
193
+ originalSeparators,
184
194
  hadUtf8DecodeErrors,
185
195
  warnings,
186
196
  noopEdit: anchorResult.noopEdit,
@@ -192,6 +202,7 @@ export async function execPipeline(
192
202
  totalRemovedLines,
193
203
  identity,
194
204
  ...(pipeSpans ? { spans: pipeSpans } : {}),
205
+ ...(edit.content_separators !== undefined ? { contentSeparators: edit.content_separators } : {}),
195
206
  };
196
207
  }
197
208
 
@@ -263,6 +274,7 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
263
274
  const canonical = normReq(params);
264
275
  assertReq(canonical);
265
276
  const normalizedParams = canonical;
277
+ const literalEscape = literalEscapeHint(normalizedParams.replacement_lines, "replacement_lines");
266
278
  const targetPath = await resolveEditTargetWithRequirement({
267
279
  removeFrom: normalizedParams.remove_from,
268
280
  removeTo: normalizedParams.remove_to,
@@ -287,6 +299,7 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
287
299
  absolutePath,
288
300
  mutationTargetPath,
289
301
  editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
302
+ prefixWarnings: literalEscape !== undefined ? [literalEscape] : [],
290
303
  signal,
291
304
  });
292
305
  }
@@ -305,7 +318,7 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
305
318
  cwd: ctx.cwd,
306
319
  signal,
307
320
  hedit: built.edit,
308
- extraWarnings: built.warnings,
321
+ extraWarnings: [...(literalEscape !== undefined ? [literalEscape] : []), ...built.warnings],
309
322
  });
310
323
  });
311
324
  });
package/src/utils.ts CHANGED
@@ -342,10 +342,6 @@ export function decodeStringArray(value: unknown, warnings?: string[], label = "
342
342
  return undefined;
343
343
  }
344
344
 
345
- function splitEditLines(text: string): string[] {
346
- return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
347
- }
348
-
349
345
  function normalizeLineFieldValue(value: unknown): string[] | undefined {
350
346
  const candidate = typeof value === "string"
351
347
  ? value
@@ -356,7 +352,7 @@ function normalizeLineFieldValue(value: unknown): string[] | undefined {
356
352
  const decoded = decodeStringArray(candidate);
357
353
  if (decoded !== undefined) return decoded;
358
354
  if (/^\[\s*\]$/.test(stripTrailingMemberCall(stripCodeFence(candidate)))) return [];
359
- return splitEditLines(candidate);
355
+ return [candidate];
360
356
  }
361
357
 
362
358
  function normalizeEditLines(record: Record<string, unknown>): void {
@@ -366,3 +362,42 @@ function normalizeEditLines(record: Record<string, unknown>): void {
366
362
  if (lines !== undefined) record[key] = lines;
367
363
  }
368
364
  }
365
+
366
+ const LITERAL_ESCAPE_RE = /\\(?:u([0-9a-fA-F]{4})|([ntr"]))/g;
367
+
368
+ const REAL_LINE_BREAK_RE = /[\n\r]/;
369
+
370
+ function isSurrogateEscapePair(line: string, index: number, hex: string): boolean {
371
+ const code = Number.parseInt(hex, 16);
372
+ if (code >= 0xd800 && code <= 0xdbff) {
373
+ const next = /^\\u([0-9a-fA-F]{4})/.exec(line.slice(index + 6))?.[1];
374
+ if (next === undefined) return false;
375
+ const nextCode = Number.parseInt(next, 16);
376
+ return nextCode >= 0xdc00 && nextCode <= 0xdfff;
377
+ }
378
+ if (code >= 0xdc00 && code <= 0xdfff) {
379
+ const previous = /\\u([0-9a-fA-F]{4})$/.exec(line.slice(0, index))?.[1];
380
+ if (previous === undefined) return false;
381
+ const previousCode = Number.parseInt(previous, 16);
382
+ return previousCode >= 0xd800 && previousCode <= 0xdbff;
383
+ }
384
+ return false;
385
+ }
386
+
387
+ export function literalEscapeHint(lines: string[], label: string): string | undefined {
388
+ for (const line of lines) {
389
+ if (!line.includes("\\")) continue;
390
+ const hasRealBreak = REAL_LINE_BREAK_RE.test(line);
391
+ for (const match of line.matchAll(LITERAL_ESCAPE_RE)) {
392
+ const hex = match[1];
393
+ const simple = match[2];
394
+ if (simple !== undefined && hasRealBreak) continue;
395
+ if (hex !== undefined) {
396
+ if (hex.toLowerCase() === "dddd") continue;
397
+ if (isSurrogateEscapePair(line, match.index, hex)) continue;
398
+ }
399
+ return `[H_LITERAL_ESCAPE] "${label}" contains the literal escaped text "${match[0]}"`;
400
+ }
401
+ }
402
+ return undefined;
403
+ }