rip-lang 3.14.0 → 3.14.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.
@@ -1,521 +0,0 @@
1
- // Shared source-map position utilities used by both the CLI type-checker
2
- // (src/typecheck.js) and the VS Code language server (packages/vscode/src/lsp.js).
3
-
4
- // When a switch expression is the implicit return of a function, the compiler
5
- // wraps it in an IIFE: `return (() => { switch ... })()`. Non-exhaustive
6
- // switches produce TS2322 on the IIFE return, which source-maps to the `switch`
7
- // line. This helper detects that pattern and remaps the diagnostic to the
8
- // function's return-type annotation (matching TS behaviour on the raw .ts).
9
- // Returns { line, col, len } if remapped, or null if no adjustment needed.
10
- export function adjustSwitchDiagnostic(source, pos, code) {
11
- if (code !== 2322) return null;
12
- const srcLines = source.split('\n');
13
- const line = srcLines[pos.line] || '';
14
- if (!/^\s*switch\b/.test(line)) return null;
15
-
16
- const switchIndent = line.match(/^(\s*)/)[1].length;
17
- for (let i = pos.line - 1; i >= 0; i--) {
18
- const defLine = srcLines[i];
19
- const defMatch = defLine.match(/^(\s*)def\b/);
20
- if (defMatch && defMatch[1].length < switchIndent) {
21
- // Found enclosing function — look for return-type annotation "):: Type"
22
- const retMatch = defLine.match(/\)\s*::\s*(\w+)\s*$/);
23
- if (retMatch) {
24
- const typeStart = defLine.lastIndexOf(retMatch[1]);
25
- return { line: i, col: typeStart, len: retMatch[1].length };
26
- }
27
- return { line: i, col: defMatch[1].length, len: 3 }; // fallback: highlight `def`
28
- }
29
- // Stop if we leave the indentation context
30
- if (/\S/.test(defLine) && !defLine.match(/^(\s*)/)[1].length && !/^\s*#/.test(defLine)) break;
31
- }
32
- return null;
33
- }
34
-
35
- export function getLineText(text, lineNum) {
36
- let start = 0, line = 0;
37
- for (let i = 0; i <= text.length; i++) {
38
- if (i === text.length || text[i] === '\n') {
39
- if (line === lineNum) return text.slice(start, i);
40
- start = i + 1;
41
- line++;
42
- }
43
- }
44
- return '';
45
- }
46
-
47
- export function findNearestWord(text, word, approx) {
48
- let bestIdx = -1, bestDist = Infinity, idx = 0;
49
- while ((idx = text.indexOf(word, idx)) >= 0) {
50
- const before = idx === 0 || /\W/.test(text[idx - 1]);
51
- const after = idx + word.length >= text.length || /\W/.test(text[idx + word.length]);
52
- if (before && after) {
53
- const dist = Math.abs(idx - approx);
54
- if (dist < bestDist) { bestDist = dist; bestIdx = idx; }
55
- }
56
- idx++;
57
- }
58
- return bestIdx;
59
- }
60
-
61
- // Check whether an offset falls on an injected function overload signature line
62
- // (generated by compileForCheck, not from user code). These are body lines that
63
- // match `function NAME(...): TYPE;` and have no genToSrc entry.
64
- export function isInjectedOverload(entry, offset) {
65
- const tsLine = offsetToLine(entry.tsContent, offset);
66
- if (tsLine < entry.headerLines) return false;
67
- if (entry.genToSrc.get(tsLine) !== undefined) return false;
68
- const lineText = getLineText(entry.tsContent, tsLine);
69
- return /^(?:async\s+)?function\s+\w+\s*\(/.test(lineText) && lineText.trimEnd().endsWith(';');
70
- }
71
-
72
- export function offsetToLine(text, offset) {
73
- let line = 0;
74
- for (let i = 0; i < offset && i < text.length; i++) {
75
- if (text[i] === '\n') line++;
76
- }
77
- return line;
78
- }
79
-
80
- export function lineColToOffset(text, line, col) {
81
- let r = 0;
82
- for (let i = 0; i < text.length; i++) {
83
- if (r === line) return i + col;
84
- if (text[i] === '\n') r++;
85
- }
86
- return text.length;
87
- }
88
-
89
- export function offsetToLineCol(text, offset) {
90
- let line = 0, ls = 0;
91
- for (let i = 0; i < offset && i < text.length; i++) {
92
- if (text[i] === '\n') { line++; ls = i + 1; }
93
- }
94
- return { line, col: offset - ls };
95
- }
96
-
97
- // Map a TypeScript offset back to a Rip source { line, col } (0-based).
98
- // Returns null if the offset falls in the DTS header (and no match is found).
99
- //
100
- // `entry` must have: tsContent, headerLines, genToSrc, source, srcColToGen (optional)
101
- export function mapToSourcePos(entry, offset) {
102
- const tsLine = offsetToLine(entry.tsContent, offset);
103
- if (tsLine < entry.headerLines) {
104
- // DTS preamble — find the identifier at the offset and locate it in the source
105
- const genLineText = getLineText(entry.tsContent, tsLine);
106
-
107
- // Skip compiler-injected stdlib declarations (declare function warn, etc.)
108
- // — diagnostics on these lines are never user-authored and would incorrectly
109
- // match string literals or identifiers in the source.
110
- if (/^declare\s+function\s/.test(genLineText)) return null;
111
-
112
- // If genToSrc has a mapping for this header line (e.g. imports, declarations),
113
- // use it to target the correct source line for word matching.
114
- const mappedSrcLine = entry.genToSrc.get(tsLine);
115
-
116
- let lineStart = 0, curLine = 0;
117
- for (let i = 0; i < entry.tsContent.length; i++) {
118
- if (curLine === tsLine) { lineStart = i; break; }
119
- if (entry.tsContent[i] === '\n') curLine++;
120
- }
121
- const genCol = offset - lineStart;
122
- const wordMatch = genLineText.slice(genCol).match(/^\w+/);
123
- if (wordMatch && entry.source) {
124
- const word = wordMatch[0];
125
- const srcLines = entry.source.split('\n');
126
- const re = new RegExp('\\b' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
127
-
128
- // If we have a direct line mapping, try that source line first
129
- if (mappedSrcLine !== undefined) {
130
- const m = re.exec(srcLines[mappedSrcLine]);
131
- if (m) return { line: mappedSrcLine, col: m.index };
132
- return { line: mappedSrcLine, col: genCol };
133
- }
134
-
135
- // For let/var declarations, the error word may appear on many source lines
136
- // (e.g. `Status` referenced in multiple variable annotations). Narrow the
137
- // search to the source line that declares the same variable.
138
- const letMatch = genLineText.match(/^(?:export\s+)?(?:declare\s+)?(?:let|var)\s+(\w+)/);
139
- if (letMatch) {
140
- const varName = letMatch[1];
141
- const varRe = new RegExp('\\b' + varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*::');
142
- for (let s = 0; s < srcLines.length; s++) {
143
- if (varRe.test(srcLines[s])) {
144
- const m = re.exec(srcLines[s]);
145
- if (m) return { line: s, col: m.index };
146
- return { line: s, col: 0 };
147
- }
148
- }
149
- }
150
-
151
- // Find enclosing type/interface from DTS context to narrow search —
152
- // without this, duplicate member names (e.g. "host" in two types) always
153
- // resolve to the first occurrence in the source.
154
- let searchStart = 0;
155
- for (let t = tsLine; t >= 0; t--) {
156
- const tl = getLineText(entry.tsContent, t);
157
- const tm = tl.match(/^(?:type|interface)\s+(\w+)/);
158
- if (tm) {
159
- const typeRe = new RegExp('(?:type|interface)\\s+' + tm[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
160
- for (let s = 0; s < srcLines.length; s++) {
161
- if (typeRe.test(srcLines[s])) { searchStart = s; break; }
162
- }
163
- break;
164
- }
165
- if (/^\}/.test(tl.trim())) break; // exited a type block — not inside one
166
- }
167
-
168
- for (let s = searchStart; s < srcLines.length; s++) {
169
- const m = re.exec(srcLines[s]);
170
- if (m) return { line: s, col: m.index };
171
- }
172
- }
173
- return null;
174
- }
175
-
176
- // Hoisted multi-variable `let` declaration (e.g. `let a, b, items, ...;`) —
177
- // the compiler aggregates variable declarations into one line with no useful
178
- // per-variable source mapping. Detect the pattern (both top-level and inside
179
- // functions), extract the word at the offset, and find its assignment in
180
- // the Rip source. Use the genToSrc mapping of the preceding TS line (the
181
- // function declaration) to scope the search and avoid matching a same-named
182
- // variable in a different function.
183
- const hoistLine = getLineText(entry.tsContent, tsLine);
184
- if (/^\s*let\s+[$\w]+\s*,/.test(hoistLine) && entry.source) {
185
- let hl = 0;
186
- for (let i = 0; i < entry.tsContent.length; i++) {
187
- if (hl === tsLine) { hl = i; break; }
188
- if (entry.tsContent[i] === '\n') hl++;
189
- }
190
- const hCol = offset - hl;
191
- const hWord = hoistLine.slice(hCol).match(/^[$\w]+/);
192
- if (hWord) {
193
- const word = hWord[0];
194
- const srcLines = entry.source.split('\n');
195
- const assignRe = new RegExp('^' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*(?:::|=!|:=|~=|=)');
196
-
197
- // Scope the search: find the source line of the enclosing function by
198
- // checking genToSrc for the TS line just before the hoisted let.
199
- let searchStart = 0;
200
- if (entry.genToSrc) {
201
- for (let g = tsLine - 1; g >= 0; g--) {
202
- const s = entry.genToSrc.get(g);
203
- if (s !== undefined) { searchStart = s; break; }
204
- }
205
- }
206
-
207
- for (let s = searchStart; s < srcLines.length; s++) {
208
- if (assignRe.test(srcLines[s].trimStart())) {
209
- const col = srcLines[s].indexOf(word);
210
- if (col >= 0) return { line: s, col };
211
- }
212
- }
213
- // Variable is on a hoisted let but has no recognisable assignment in
214
- // the source (e.g. for-loop iterators, destructured names). Return
215
- // null so callers skip it rather than producing a garbage mapping.
216
- return null;
217
- }
218
- }
219
-
220
- // Injected function overload signatures (e.g. `function fetchUser(id: number): Promise<User>;`)
221
- // have no genToSrc entry. The backward-walk approximation below can map them to
222
- // wildly wrong source lines. Extract the function name and find its `def` in the source.
223
- const bodyLine = getLineText(entry.tsContent, tsLine);
224
- if (entry.genToSrc.get(tsLine) === undefined && entry.source) {
225
- const overloadMatch = bodyLine.match(/^(?:async\s+)?function\s+(\w+)\s*\(/);
226
- if (overloadMatch && bodyLine.trimEnd().endsWith(';')) {
227
- const fnName = overloadMatch[1];
228
- const srcLines = entry.source.split('\n');
229
- const defRe = new RegExp('\\bdef\\s+' + fnName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
230
- for (let s = 0; s < srcLines.length; s++) {
231
- if (defRe.test(srcLines[s])) {
232
- const col = srcLines[s].indexOf(fnName);
233
- return { line: s, col: col >= 0 ? col : 0 };
234
- }
235
- }
236
- }
237
- }
238
-
239
- // Resolve source line from genToSrc
240
- let srcLine = entry.genToSrc.get(tsLine);
241
- if (srcLine === undefined) {
242
- // Walk backward to find nearest mapped gen line
243
- let best = -1;
244
- for (const [g] of entry.genToSrc) if (g <= tsLine && g > best) best = g;
245
- if (best >= 0) {
246
- srcLine = entry.genToSrc.get(best) + (tsLine - best);
247
- } else {
248
- srcLine = tsLine - entry.headerLines;
249
- }
250
- }
251
-
252
- // Compute generated column
253
- let lineStart = 0, curLine = 0;
254
- for (let i = 0; i < entry.tsContent.length; i++) {
255
- if (curLine === tsLine) { lineStart = i; break; }
256
- if (entry.tsContent[i] === '\n') curLine++;
257
- }
258
- const genCol = offset - lineStart;
259
-
260
- // Remap column via text matching
261
- const genText = getLineText(entry.tsContent, tsLine);
262
- let srcCol = genCol;
263
- let approx = genCol; // default: assume same column
264
- // Scan ALL source lines for mappings to this gen line — a multi-line Rip
265
- // expression (e.g. object literal) may compile to a single gen line, so
266
- // multiple source lines can share one gen line. Pick the closest genCol.
267
- // On ties, prefer the source line that genToSrc already identified (e.g.
268
- // from an @rip-src annotation) so that stub render-block expressions land
269
- // on their correct source lines instead of a sibling attribute line.
270
- if (entry.srcColToGen) {
271
- const origSrcLine = srcLine;
272
- let bestDist = Infinity;
273
- for (const [sl, entries] of entry.srcColToGen) {
274
- for (const e of entries) {
275
- if (e.genLine === tsLine) {
276
- const dist = Math.abs(e.genCol - genCol);
277
- if (dist < bestDist || (dist === bestDist && sl === origSrcLine)) {
278
- bestDist = dist;
279
- srcLine = sl;
280
- approx = e.srcCol + (genCol - e.genCol);
281
- }
282
- }
283
- }
284
- }
285
- }
286
- const srcText = entry.source ? getLineText(entry.source, srcLine) : '';
287
- // Text-match: find the word at genCol in the gen line, then locate it in the source line
288
- if (srcText) {
289
- let wordAt = genText.slice(genCol).match(/^\w+/);
290
- // Quoted string literal (e.g. __ripEl('tag')) — peek inside the quotes
291
- if (!wordAt && (genText[genCol] === "'" || genText[genCol] === '"')) {
292
- wordAt = genText.slice(genCol + 1).match(/^\w+/);
293
- }
294
- if (wordAt) {
295
- let word = wordAt[0];
296
- let idx = findNearestWord(srcText, word, approx);
297
- // __bind_xxx__ → xxx: two-way binding props use mangled names in gen
298
- if (idx < 0 && word.startsWith('__bind_') && word.endsWith('__')) {
299
- word = word.slice(7, -2);
300
- idx = findNearestWord(srcText, word, approx);
301
- }
302
- if (idx >= 0) return { line: srcLine, col: idx };
303
- }
304
- if (genCol > 0 && (!wordAt || genCol >= genText.length)) {
305
- let wordBefore = genText.slice(0, genCol).match(/(\w+)$/);
306
- // Closing quote — peek inside to find the word (e.g. end of __ripEl('tag'))
307
- if (!wordBefore && (genText[genCol - 1] === "'" || genText[genCol - 1] === '"')) {
308
- wordBefore = genText.slice(0, genCol - 1).match(/(\w+)$/);
309
- }
310
- if (wordBefore) {
311
- let word = wordBefore[0];
312
- let idx = findNearestWord(srcText, word, approx - word.length);
313
- // __bind_xxx__ → xxx: two-way binding props use mangled names in gen
314
- if (idx < 0 && word.startsWith('__bind_') && word.endsWith('__')) {
315
- word = word.slice(7, -2);
316
- idx = findNearestWord(srcText, word, approx - word.length);
317
- }
318
- if (idx >= 0) return { line: srcLine, col: idx + word.length };
319
- // Injected property access (e.g. clicks.value from clicks :=) — map to end of object identifier
320
- const dotMatch = genText.slice(0, genCol - wordBefore[0].length).match(/(\w+)\.$/);
321
- if (dotMatch) {
322
- const objIdx = findNearestWord(srcText, dotMatch[1], approx - wordBefore[0].length - dotMatch[1].length - 1);
323
- if (objIdx >= 0) return { line: srcLine, col: objIdx + dotMatch[1].length };
324
- }
325
- }
326
- }
327
- srcCol = Math.max(0, approx);
328
- }
329
-
330
- // Word not found on mapped line (or line was empty) — search nearby lines
331
- // (handles cases where multiple source lines compress to one generated line,
332
- // e.g. constructor params, or srcLine is blank)
333
- {
334
- let wordFallback = genText.slice(genCol).match(/^\w+/);
335
- // Quoted string literal — peek inside the quotes (e.g. __RipProps<'inputz'>)
336
- if (!wordFallback && (genText[genCol] === "'" || genText[genCol] === '"')) {
337
- wordFallback = genText.slice(genCol + 1).match(/^\w+/);
338
- }
339
- if (wordFallback) {
340
- let word = wordFallback[0];
341
- if (word.startsWith('__bind_') && word.endsWith('__')) word = word.slice(7, -2);
342
- const srcLines = entry.source.split('\n');
343
- const re = new RegExp('\\b' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
344
- for (let delta = 0; delta <= 10; delta++) {
345
- for (const d of delta === 0 ? [srcLine] : [srcLine + delta, srcLine - delta]) {
346
- if (d >= 0 && d < srcLines.length) {
347
- const m = re.exec(srcLines[d]);
348
- if (m) return { line: d, col: m.index };
349
- }
350
- }
351
- }
352
- }
353
- }
354
-
355
- // When text matching failed entirely (generated identifier like _2 doesn't
356
- // exist in source), srcCol may land on whitespace or past EOL. Fall back to
357
- // the first word on the source line so the diagnostic highlights something
358
- // meaningful (e.g. the component name on a `Button` line).
359
- if (srcText) {
360
- if (srcCol >= srcText.length || /^\s*$/.test(srcText.slice(srcCol, srcCol + 1))) {
361
- const firstWord = srcText.match(/^\s*(\w+)/);
362
- if (firstWord) return { line: srcLine, col: firstWord.index + firstWord[0].length - firstWord[1].length };
363
- }
364
- }
365
- return { line: srcLine, col: srcCol };
366
- }
367
-
368
- // Map a Rip source (line, col) to a TypeScript virtual file byte offset.
369
- // This is the forward direction: source → generated (used for hover, definition, etc.)
370
- //
371
- // `entry` must have: tsContent, source, srcToGen, srcColToGen (optional)
372
- // Returns undefined if no mapping can be established.
373
- export function srcToOffset(entry, line, col) {
374
- if (!entry) return undefined;
375
- let genLine = entry.srcToGen.get(line);
376
- let genColHint = -1;
377
- let bestSrcCol = -1;
378
-
379
- // Column-aware lookup
380
- if (entry.srcColToGen) {
381
- const colEntries = entry.srcColToGen.get(line);
382
- if (colEntries && colEntries.length > 0) {
383
- let best = colEntries[0];
384
- for (const e of colEntries) {
385
- if (e.srcCol <= col && (best.srcCol > col || e.srcCol > best.srcCol)) best = e;
386
- }
387
- if (best.srcCol > col) {
388
- for (const e of colEntries) {
389
- if (Math.abs(e.srcCol - col) < Math.abs(best.srcCol - col)) best = e;
390
- }
391
- }
392
- genLine = best.genLine;
393
- genColHint = best.genCol;
394
- bestSrcCol = best.srcCol;
395
- }
396
- }
397
-
398
- if (genLine === undefined) {
399
- let best = -1;
400
- for (const [s] of entry.srcToGen) if (s <= line && s > best) best = s;
401
- if (best < 0) return undefined;
402
- genLine = entry.srcToGen.get(best);
403
- }
404
-
405
- const srcLines = entry.source.split('\n');
406
- const genLines = entry.tsContent.split('\n');
407
- const KEYWORDS = new Set(['interface', 'type', 'enum', 'class', 'export', 'declare', 'extends', 'implements', 'import', 'from', 'def', 'const', 'let', 'var']);
408
-
409
- if (srcLines[line] != null && genLines[genLine] != null) {
410
- const srcText = srcLines[line];
411
- const genText = genLines[genLine];
412
- const leftPart = srcText.substring(0, col).match(/\w*$/)?.[0] || '';
413
- const rightPart = srcText.substring(col).match(/^\w*/)?.[0] || '';
414
- let wordMatch = (leftPart + rightPart) ? [leftPart + rightPart] : null;
415
- if (wordMatch && KEYWORDS.has(wordMatch[0])) {
416
- const after = srcText.substring(col + wordMatch[0].length).match(/\s+(\w+)/);
417
- if (after) wordMatch = [after[1]];
418
- }
419
- if (wordMatch) {
420
- const word = wordMatch[0];
421
- const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
422
- const wordStart = col - leftPart.length;
423
- const useHint = genColHint >= 0 && bestSrcCol >= wordStart && bestSrcCol < wordStart + word.length;
424
-
425
- // Prefer the overload signature line (genLine-1) when it exists and
426
- // contains the same identifier — overloads carry typed parameters.
427
- let targetLine = genLine;
428
- let targetText = genText;
429
- if (genLine > 0) {
430
- const prevText = genLines[genLine - 1] || '';
431
- if (/^(?:export\s+)?function\s+\w+\(.*\).*;\s*$/.test(prevText)) {
432
- const re0 = new RegExp('\\b' + escaped + '\\b');
433
- if (re0.test(prevText)) { targetLine = genLine - 1; targetText = prevText; }
434
- }
435
- }
436
-
437
- const re = new RegExp('\\b' + escaped + '\\b', 'g');
438
- let m, bestCol = -1, bestDist = Infinity;
439
- // When the word doesn't fall exactly on a mapped srcCol, extrapolate
440
- // the expected gen column from the nearest mapping entry. This avoids
441
- // picking a same-named word inside a string literal that happens to be
442
- // closer to the raw source column.
443
- const expectedGenCol = useHint ? genColHint
444
- : genColHint >= 0 ? genColHint + (col - bestSrcCol) : col;
445
- while ((m = re.exec(targetText)) !== null) {
446
- const dist = Math.abs(m.index - expectedGenCol);
447
- if (dist < bestDist) { bestDist = dist; bestCol = m.index; }
448
- }
449
- if (bestCol >= 0) return lineColToOffset(entry.tsContent, targetLine, bestCol);
450
-
451
- // Fall back to original genLine if overload didn't match
452
- if (targetLine !== genLine) {
453
- const re1b = new RegExp('\\b' + escaped + '\\b', 'g');
454
- let m1b, bestCol1b = -1, bestDist1b = Infinity;
455
- while ((m1b = re1b.exec(genText)) !== null) {
456
- const dist = Math.abs(m1b.index - expectedGenCol);
457
- if (dist < bestDist1b) { bestDist1b = dist; bestCol1b = m1b.index; }
458
- }
459
- if (bestCol1b >= 0) return lineColToOffset(entry.tsContent, genLine, bestCol1b);
460
- }
461
-
462
- // Word not on mapped line — search nearby generated lines
463
- for (let delta = 1; delta <= 5; delta++) {
464
- for (const tryLine of [genLine + delta, genLine - delta]) {
465
- if (tryLine < 0 || tryLine >= genLines.length) continue;
466
- const tryText = genLines[tryLine];
467
- const re2 = new RegExp('\\b' + escaped + '\\b', 'g');
468
- let m2, best2 = -1, bestDist2 = Infinity;
469
- while ((m2 = re2.exec(tryText)) !== null) {
470
- const dist2 = Math.abs(m2.index - col);
471
- if (dist2 < bestDist2) { bestDist2 = dist2; best2 = m2.index; }
472
- }
473
- if (best2 >= 0) return lineColToOffset(entry.tsContent, tryLine, best2);
474
- }
475
- }
476
-
477
- // Neighbor-line fallback: when the word isn't on the mapped gen line or
478
- // nearby ±5 lines, check neighboring source lines for srcColToGen entries
479
- // that point to gen lines containing the word. Handles multi-line
480
- // expressions collapsed to one gen line, bodiless overload signatures
481
- // mapped to wrong gen lines, etc.
482
- if (entry.srcColToGen) {
483
- const candidateGenLines = new Set();
484
- for (let d = 0; d <= 10; d++) {
485
- for (const sl of d === 0 ? [line] : [line - d, line + d]) {
486
- if (sl < 0) continue;
487
- const ce = entry.srcColToGen.get(sl);
488
- if (ce) {
489
- for (const e of ce) candidateGenLines.add(e.genLine);
490
- }
491
- }
492
- }
493
- // Also try gen lines near those candidates (overload signatures are
494
- // typically on the line just before the function body)
495
- const expanded = new Set(candidateGenLines);
496
- for (const gl of candidateGenLines) {
497
- for (let d = 1; d <= 3; d++) {
498
- expanded.add(gl - d);
499
- expanded.add(gl + d);
500
- }
501
- }
502
- let bestAlt = -1, bestAltCol = -1, bestAltDist = Infinity;
503
- for (const gl of expanded) {
504
- if (gl < 0 || gl >= genLines.length) continue;
505
- const altText = genLines[gl] || '';
506
- const re3 = new RegExp('\\b' + escaped + '\\b', 'g');
507
- let m3;
508
- while ((m3 = re3.exec(altText)) !== null) {
509
- const dist3 = Math.abs(m3.index - expectedGenCol);
510
- if (dist3 < bestAltDist) { bestAltDist = dist3; bestAltCol = m3.index; bestAlt = gl; }
511
- }
512
- }
513
- if (bestAltCol >= 0) return lineColToOffset(entry.tsContent, bestAlt, bestAltCol);
514
- }
515
- }
516
- }
517
-
518
- const genText = entry.tsContent.split('\n')[genLine] || '';
519
- if (col < genText.length) return lineColToOffset(entry.tsContent, genLine, col);
520
- return lineColToOffset(entry.tsContent, genLine, 0);
521
- }