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.
- package/README.md +1 -1
- package/docs/dist/rip.js +33 -11
- package/docs/dist/rip.min.js +129 -108
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +3 -2
- package/src/schema.js +38 -9
- package/src/typecheck.js +532 -3
- package/src/sourcemap-utils.js +0 -521
package/src/sourcemap-utils.js
DELETED
|
@@ -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
|
-
}
|