@ripple-ts/language-server 0.2.188 → 0.2.189
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/package.json +3 -3
- package/src/autoInsertPlugin.js +5 -0
- package/src/completionPlugin.js +257 -11
- package/src/definitionPlugin.js +99 -6
- package/src/documentHighlightPlugin.js +2 -1
- package/src/hoverPlugin.js +12 -0
- package/src/utils.js +109 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ripple-ts/language-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.189",
|
|
4
4
|
"description": "Language Server Protocol implementation for Ripple",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
"volar-service-typescript": "0.0.65",
|
|
20
20
|
"vscode-languageserver-textdocument": "^1.0.12",
|
|
21
21
|
"vscode-uri": "^3.1.0",
|
|
22
|
-
"@ripple-ts/typescript-plugin": "0.2.
|
|
22
|
+
"@ripple-ts/typescript-plugin": "0.2.189"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"ripple": "0.2.
|
|
25
|
+
"ripple": "0.2.189"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"typescript": "^5.9.2"
|
package/src/autoInsertPlugin.js
CHANGED
|
@@ -66,6 +66,11 @@ function createAutoInsertPlugin() {
|
|
|
66
66
|
|
|
67
67
|
const [virtualCode] = getVirtualCode(document, context);
|
|
68
68
|
|
|
69
|
+
if (virtualCode.languageId !== 'ripple') {
|
|
70
|
+
log(`Skipping auto-insert processing in the '${virtualCode.languageId}' context`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
// Map position back to source
|
|
70
75
|
const offset = document.offsetAt(position);
|
|
71
76
|
const mapping = virtualCode.findMappingByGeneratedRange(lastChange.rangeOffset, offset);
|
package/src/completionPlugin.js
CHANGED
|
@@ -1,17 +1,209 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import {LanguageServicePlugin} from '@volar/language-server'
|
|
3
|
-
*/
|
|
1
|
+
/** @import { LanguageServicePlugin, TextEdit, CompletionItem } from '@volar/language-server'; */
|
|
4
2
|
|
|
5
3
|
const { CompletionItemKind, InsertTextFormat } = require('@volar/language-server');
|
|
6
|
-
const { getVirtualCode, createLogging } = require('./utils.js');
|
|
4
|
+
const { getVirtualCode, createLogging, isInsideImport, isInsideExport } = require('./utils.js');
|
|
7
5
|
|
|
8
6
|
const { log } = createLogging('[Ripple Completion Plugin]');
|
|
9
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Snippets that require auto-import from 'ripple'
|
|
10
|
+
* @type {Array<{label: string, filterText: string, detail: string, documentation: string, insertText: string, importName: string | null}>}
|
|
11
|
+
*/
|
|
12
|
+
const TRACKED_COLLECTION_SNIPPETS = [
|
|
13
|
+
{
|
|
14
|
+
label: '#Map',
|
|
15
|
+
filterText: '#Map',
|
|
16
|
+
detail: 'Create a Shorthand TrackedMap',
|
|
17
|
+
documentation: 'A reactive Map that triggers updates when modified',
|
|
18
|
+
insertText: 'new #Map(${1})',
|
|
19
|
+
importName: null,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: '#Set',
|
|
23
|
+
filterText: '#Set',
|
|
24
|
+
detail: 'Create a Shorthand TrackedSet',
|
|
25
|
+
documentation: 'A reactive Set that triggers updates when modified',
|
|
26
|
+
insertText: 'new #Set(${1})',
|
|
27
|
+
importName: null,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: 'TrackedMap',
|
|
31
|
+
filterText: 'TrackedMap',
|
|
32
|
+
detail: 'Create a TrackedMap',
|
|
33
|
+
documentation: 'A reactive Map that triggers updates when modified',
|
|
34
|
+
insertText: 'new TrackedMap(${1})',
|
|
35
|
+
importName: 'TrackedMap',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: 'TrackedSet',
|
|
39
|
+
filterText: 'TrackedSet',
|
|
40
|
+
detail: 'Create a TrackedSet',
|
|
41
|
+
documentation: 'A reactive Set that triggers updates when modified',
|
|
42
|
+
insertText: 'new TrackedSet(${1})',
|
|
43
|
+
importName: 'TrackedSet',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: 'TrackedArray',
|
|
47
|
+
filterText: 'TrackedArray',
|
|
48
|
+
detail: 'Create a TrackedArray',
|
|
49
|
+
documentation: 'A reactive Array that triggers updates when modified',
|
|
50
|
+
insertText: 'new TrackedArray(${1})',
|
|
51
|
+
importName: 'TrackedArray',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: 'TrackedArray.from',
|
|
55
|
+
filterText: 'TrackedArray.from',
|
|
56
|
+
detail: 'Create a TrackedArray.from',
|
|
57
|
+
documentation: 'A reactive Array that triggers when modified',
|
|
58
|
+
insertText: 'new TrackedArray.from(${1})',
|
|
59
|
+
importName: 'TrackedArray',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: 'TrackedObject',
|
|
63
|
+
filterText: 'TrackedObject',
|
|
64
|
+
detail: 'Create a TrackedObject',
|
|
65
|
+
documentation: 'A reactive Object that triggers updates when modified',
|
|
66
|
+
insertText: 'new TrackedObject(${1})',
|
|
67
|
+
importName: 'TrackedObject',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: 'TrackedDate',
|
|
71
|
+
filterText: 'TrackedDate',
|
|
72
|
+
detail: 'Create a TrackedDate',
|
|
73
|
+
documentation: 'A reactive Date that triggers updates when modified',
|
|
74
|
+
insertText: 'new TrackedDate(${1})',
|
|
75
|
+
importName: 'TrackedDate',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
label: 'TrackedURL',
|
|
79
|
+
filterText: 'TrackedURL',
|
|
80
|
+
detail: 'Create a TrackedURL',
|
|
81
|
+
documentation: 'A reactive URL that triggers updates when modified',
|
|
82
|
+
insertText: 'new TrackedURL(${1})',
|
|
83
|
+
importName: 'TrackedURL',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
label: 'TrackedURLSearchParams',
|
|
87
|
+
filterText: 'TrackedURLSearchParams',
|
|
88
|
+
detail: 'Create a TrackedURLSearchParams',
|
|
89
|
+
documentation: 'A reactive URLSearchParams that triggers updates when modified',
|
|
90
|
+
insertText: 'new TrackedURLSearchParams(${1})',
|
|
91
|
+
importName: 'TrackedURLSearchParams',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: 'MediaQuery',
|
|
95
|
+
filterText: 'MediaQuery',
|
|
96
|
+
detail: 'Create a MediaQuery',
|
|
97
|
+
documentation: 'A reactive media query that triggers updates when the query match changes',
|
|
98
|
+
insertText: 'new MediaQuery(${1})',
|
|
99
|
+
importName: 'MediaQuery',
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Find the ripple import statement in the document
|
|
105
|
+
* @param {string} text - Full document text
|
|
106
|
+
* @returns {{line: number, startChar: number, endChar: number, imports: string[], hasSemicolon: boolean, fullMatch: string} | null}
|
|
107
|
+
*/
|
|
108
|
+
function findRippleImport(text) {
|
|
109
|
+
const lines = text.split('\n');
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const line = lines[i];
|
|
112
|
+
// Match: import { x, y, z } from 'ripple'; (with optional semicolon and trailing whitespace)
|
|
113
|
+
const match = line.match(/^import\s*\{([^}]+)\}\s*from\s*['"]ripple['"](;?)(\s*)$/);
|
|
114
|
+
if (match) {
|
|
115
|
+
const imports = match[1]
|
|
116
|
+
.split(',')
|
|
117
|
+
.map((s) => s.trim())
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
return {
|
|
120
|
+
line: i,
|
|
121
|
+
startChar: 0,
|
|
122
|
+
endChar: line.length,
|
|
123
|
+
imports,
|
|
124
|
+
hasSemicolon: match[2] === ';',
|
|
125
|
+
fullMatch: line,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generate additionalTextEdits to add an import
|
|
134
|
+
* @param {string} documentText - Full document text
|
|
135
|
+
* @param {string} importName - Name to import (e.g., 'TrackedMap')
|
|
136
|
+
* @returns {TextEdit[]}
|
|
137
|
+
*/
|
|
138
|
+
function generateImportEdit(documentText, importName) {
|
|
139
|
+
const existing = findRippleImport(documentText);
|
|
140
|
+
|
|
141
|
+
if (existing) {
|
|
142
|
+
// Check if already imported
|
|
143
|
+
if (existing.imports.includes(importName)) {
|
|
144
|
+
return []; // Already imported, no edit needed
|
|
145
|
+
}
|
|
146
|
+
// Add to existing import, preserving semicolon status
|
|
147
|
+
const newImports = [...existing.imports, importName].sort().join(', ');
|
|
148
|
+
const semicolon = existing.hasSemicolon ? ';' : '';
|
|
149
|
+
const newLine = `import { ${newImports} } from 'ripple'${semicolon}`;
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
range: {
|
|
153
|
+
start: { line: existing.line, character: 0 },
|
|
154
|
+
end: { line: existing.line, character: existing.endChar },
|
|
155
|
+
},
|
|
156
|
+
newText: newLine,
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// No existing ripple import - add new one at the top
|
|
162
|
+
// Find the best insertion point (after other imports, or at line 0)
|
|
163
|
+
const lines = documentText.split('\n');
|
|
164
|
+
let insertLine = 0;
|
|
165
|
+
for (let i = 0; i < lines.length; i++) {
|
|
166
|
+
if (lines[i].match(/^import\s/)) {
|
|
167
|
+
insertLine = i + 1; // Insert after last import
|
|
168
|
+
} else if (insertLine > 0 && !lines[i].match(/^import\s/) && lines[i].trim() !== '') {
|
|
169
|
+
break; // Stop if we've passed the import block
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
range: {
|
|
176
|
+
start: { line: insertLine, character: 0 },
|
|
177
|
+
end: { line: insertLine, character: 0 },
|
|
178
|
+
},
|
|
179
|
+
newText: `import { ${importName} } from 'ripple';\n`,
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
10
184
|
/**
|
|
11
185
|
* Ripple-specific completion enhancements
|
|
12
186
|
* Adds custom completions for Ripple syntax patterns
|
|
13
187
|
*/
|
|
14
188
|
const RIPPLE_SNIPPETS = [
|
|
189
|
+
{
|
|
190
|
+
label: '#[]',
|
|
191
|
+
kind: CompletionItemKind.Snippet,
|
|
192
|
+
detail: 'Ripple Reactive Array Literal, shorthand for new TrackedArray',
|
|
193
|
+
documentation: 'Create a new Ripple Array Literal',
|
|
194
|
+
insertText: '#[${1}]',
|
|
195
|
+
insertTextFormat: InsertTextFormat.Snippet,
|
|
196
|
+
sortText: '0-#-array-literal',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
label: '#{}',
|
|
200
|
+
kind: CompletionItemKind.Snippet,
|
|
201
|
+
detail: 'Ripple Reactive Object Literal, shorthand for new TrackedObject',
|
|
202
|
+
documentation: 'Create a new Ripple Object Literal',
|
|
203
|
+
insertText: '#{${1}}',
|
|
204
|
+
insertTextFormat: InsertTextFormat.Snippet,
|
|
205
|
+
sortText: '0-#-object-literal',
|
|
206
|
+
},
|
|
15
207
|
{
|
|
16
208
|
label: 'component',
|
|
17
209
|
kind: CompletionItemKind.Snippet,
|
|
@@ -201,8 +393,9 @@ function createCompletionPlugin() {
|
|
|
201
393
|
completionProvider: {
|
|
202
394
|
// Trigger on Ripple-specific syntax:
|
|
203
395
|
// '<' - JSX/HTML tags
|
|
396
|
+
// '#' - TrackedMap/TrackedSet shortcuts
|
|
204
397
|
// Avoid '.' and ' ' to reduce noise - let manual trigger (Ctrl+Space) handle those
|
|
205
|
-
triggerCharacters: ['<'],
|
|
398
|
+
triggerCharacters: ['<', '#'],
|
|
206
399
|
resolveProvider: false,
|
|
207
400
|
},
|
|
208
401
|
},
|
|
@@ -219,10 +412,10 @@ function createCompletionPlugin() {
|
|
|
219
412
|
|
|
220
413
|
const [virtualCode] = getVirtualCode(document, context);
|
|
221
414
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
log(
|
|
415
|
+
if (virtualCode.languageId !== 'ripple') {
|
|
416
|
+
// Check if we're inside an embedded code (like CSS in <style> blocks)
|
|
417
|
+
// If so, don't provide Ripple snippets - let CSS completions take priority
|
|
418
|
+
log(`Skipping Ripple completions in the '${virtualCode.languageId}' context`);
|
|
226
419
|
return { items: [], isIncomplete: false };
|
|
227
420
|
}
|
|
228
421
|
|
|
@@ -231,6 +424,7 @@ function createCompletionPlugin() {
|
|
|
231
424
|
end: position,
|
|
232
425
|
});
|
|
233
426
|
|
|
427
|
+
/** @type {CompletionItem[]} */
|
|
234
428
|
const items = [];
|
|
235
429
|
|
|
236
430
|
// Debug: log trigger info with clear marker
|
|
@@ -250,9 +444,14 @@ function createCompletionPlugin() {
|
|
|
250
444
|
lineEnd: line.substring(Math.max(0, line.length - 30)),
|
|
251
445
|
});
|
|
252
446
|
|
|
253
|
-
|
|
254
|
-
|
|
447
|
+
const fullText = document.getText();
|
|
448
|
+
const cursorOffset = document.offsetAt(position);
|
|
449
|
+
|
|
450
|
+
if (isInsideImport(fullText, cursorOffset)) {
|
|
255
451
|
items.push(...RIPPLE_IMPORTS);
|
|
452
|
+
return { items, isIncomplete: false };
|
|
453
|
+
} else if (isInsideExport(fullText, cursorOffset)) {
|
|
454
|
+
return { items, isIncomplete: false };
|
|
256
455
|
}
|
|
257
456
|
|
|
258
457
|
// @ accessor hint when typing after @
|
|
@@ -265,6 +464,53 @@ function createCompletionPlugin() {
|
|
|
265
464
|
});
|
|
266
465
|
}
|
|
267
466
|
|
|
467
|
+
// TrackedMap/TrackedSet completions when typing T...
|
|
468
|
+
// Also detects if 'new' is already typed before it to avoid duplicating
|
|
469
|
+
const trackedMatch = line.match(/(new\s+)?[T,M,#]([\w\.]*)$/);
|
|
470
|
+
|
|
471
|
+
if (trackedMatch) {
|
|
472
|
+
const hasNew = !!trackedMatch[1];
|
|
473
|
+
const typed = trackedMatch[2].toLowerCase();
|
|
474
|
+
|
|
475
|
+
for (const snippet of TRACKED_COLLECTION_SNIPPETS) {
|
|
476
|
+
// Match if typing matches start of 'rackedMap', 'rackedSet' (after T)
|
|
477
|
+
const afterT = snippet.label.slice(1).toLowerCase(); // 'rackedmap' or 'rackedset'
|
|
478
|
+
if (typed === '' || afterT.startsWith(typed)) {
|
|
479
|
+
// Determine insert text - skip 'new ' if already present
|
|
480
|
+
const insertText = hasNew
|
|
481
|
+
? `${snippet.label}(\${1})`
|
|
482
|
+
: `new ${snippet.label}(\${1})`;
|
|
483
|
+
|
|
484
|
+
items.push({
|
|
485
|
+
label: snippet.label,
|
|
486
|
+
filterText: snippet.filterText,
|
|
487
|
+
kind: CompletionItemKind.Snippet,
|
|
488
|
+
detail: snippet.detail,
|
|
489
|
+
documentation: snippet.documentation,
|
|
490
|
+
insertText: insertText,
|
|
491
|
+
insertTextFormat: InsertTextFormat.Snippet,
|
|
492
|
+
sortText: '0-' + snippet.label.toLowerCase(),
|
|
493
|
+
// Replace 'T...' or 'new T...' depending on what was typed
|
|
494
|
+
textEdit: {
|
|
495
|
+
range: {
|
|
496
|
+
start: {
|
|
497
|
+
line: position.line,
|
|
498
|
+
character: position.character - trackedMatch[0].length,
|
|
499
|
+
},
|
|
500
|
+
end: position,
|
|
501
|
+
},
|
|
502
|
+
newText: insertText,
|
|
503
|
+
},
|
|
504
|
+
additionalTextEdits: snippet
|
|
505
|
+
? snippet.importName != null
|
|
506
|
+
? generateImportEdit(fullText, snippet.importName)
|
|
507
|
+
: undefined
|
|
508
|
+
: undefined,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
268
514
|
// Ripple keywords - extract the last word being typed
|
|
269
515
|
const wordMatch = line.match(/(\w+)$/);
|
|
270
516
|
const currentWord = wordMatch ? wordMatch[1] : '';
|
package/src/definitionPlugin.js
CHANGED
|
@@ -4,8 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
const { TextDocument } = require('vscode-languageserver-textdocument');
|
|
6
6
|
const { getVirtualCode, createLogging, getWordFromPosition } = require('./utils.js');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
normalizeFileNameOrUri,
|
|
12
|
+
getRippleDirForFile,
|
|
13
|
+
getCachedTypeDefinitionFile,
|
|
14
|
+
getCachedTypeMatches,
|
|
15
|
+
} = require('@ripple-ts/typescript-plugin/src/language.js');
|
|
7
16
|
|
|
8
17
|
const { log } = createLogging('[Ripple Definition Plugin]');
|
|
18
|
+
/** @type {string | undefined} */
|
|
19
|
+
let ripple_dir;
|
|
9
20
|
|
|
10
21
|
/**
|
|
11
22
|
* @returns {LanguageServicePlugin}
|
|
@@ -34,6 +45,12 @@ function createDefinitionPlugin() {
|
|
|
34
45
|
|
|
35
46
|
const [virtualCode, sourceUri] = getVirtualCode(document, context);
|
|
36
47
|
|
|
48
|
+
if (virtualCode.languageId !== 'ripple') {
|
|
49
|
+
// like embedded css
|
|
50
|
+
log(`Skipping definitions processing in the '${virtualCode.languageId}' context`);
|
|
51
|
+
return tsDefinitions;
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
// First check for custom definitions (e.g., CSS class selectors)
|
|
38
55
|
const offset = document.offsetAt(position);
|
|
39
56
|
const text = document.getText();
|
|
@@ -44,7 +61,78 @@ function createDefinitionPlugin() {
|
|
|
44
61
|
log(`Cursor position in generated code for word '${word}':`, position);
|
|
45
62
|
log(`Cursor offset in generated code for word '${word}':`, offset);
|
|
46
63
|
|
|
47
|
-
//
|
|
64
|
+
// Handle `typeReplace` definitions
|
|
65
|
+
if (
|
|
66
|
+
customMapping?.data.customData.definition !== false &&
|
|
67
|
+
customMapping?.data.customData.definition?.typeReplace
|
|
68
|
+
) {
|
|
69
|
+
const { name: typeName, path: typePath } =
|
|
70
|
+
customMapping.data.customData.definition.typeReplace;
|
|
71
|
+
|
|
72
|
+
log(`Found replace definition for ${typeName}`);
|
|
73
|
+
|
|
74
|
+
const filePath = sourceUri.fsPath || sourceUri.path;
|
|
75
|
+
ripple_dir = ripple_dir ?? getRippleDirForFile(normalizeFileNameOrUri(filePath));
|
|
76
|
+
|
|
77
|
+
if (!ripple_dir) {
|
|
78
|
+
log(`Could not determine Ripple source directory for file: ${filePath}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const typesFilePath = path.join(ripple_dir, ...typePath.split('/'));
|
|
83
|
+
|
|
84
|
+
const fileContent = getCachedTypeDefinitionFile(typesFilePath);
|
|
85
|
+
|
|
86
|
+
if (!fileContent) {
|
|
87
|
+
// the `getCachedTypeDefinitionFile` already logs the error
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const match = getCachedTypeMatches(typeName, fileContent);
|
|
92
|
+
|
|
93
|
+
if (match && match.index !== undefined) {
|
|
94
|
+
const classStart = match.index + match[0].indexOf(typeName);
|
|
95
|
+
const classEnd = classStart + typeName.length;
|
|
96
|
+
|
|
97
|
+
// Convert offset to line/column
|
|
98
|
+
const lines = fileContent.substring(0, classStart).split('\n');
|
|
99
|
+
const line = lines.length - 1;
|
|
100
|
+
const character = lines[lines.length - 1].length;
|
|
101
|
+
|
|
102
|
+
const endLines = fileContent.substring(0, classEnd).split('\n');
|
|
103
|
+
const endLine = endLines.length - 1;
|
|
104
|
+
const endCharacter = endLines[endLines.length - 1].length;
|
|
105
|
+
|
|
106
|
+
// Create the origin selection range for #Map/#Set
|
|
107
|
+
const generatedStart = customMapping.generatedOffsets[0];
|
|
108
|
+
const generatedEnd =
|
|
109
|
+
generatedStart + customMapping.data.customData.generatedLengths[0];
|
|
110
|
+
const originStart = document.positionAt(generatedStart);
|
|
111
|
+
const originEnd = document.positionAt(generatedEnd);
|
|
112
|
+
|
|
113
|
+
/** @type {LocationLink} */
|
|
114
|
+
const locationLink = {
|
|
115
|
+
targetUri: `file://${typesFilePath}`,
|
|
116
|
+
targetRange: {
|
|
117
|
+
start: { line, character },
|
|
118
|
+
end: { line: endLine, character: endCharacter },
|
|
119
|
+
},
|
|
120
|
+
targetSelectionRange: {
|
|
121
|
+
start: { line, character },
|
|
122
|
+
end: { line: endLine, character: endCharacter },
|
|
123
|
+
},
|
|
124
|
+
originSelectionRange: {
|
|
125
|
+
start: originStart,
|
|
126
|
+
end: originEnd,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
log(`Created definition link to ${typesFilePath}:${line}:${character}`);
|
|
131
|
+
return [locationLink];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Handle embedded code definition location, e.g. CSS class selectors
|
|
48
136
|
if (
|
|
49
137
|
customMapping?.data.customData.definition !== false &&
|
|
50
138
|
customMapping?.data.customData.definition?.location
|
|
@@ -115,8 +203,11 @@ function createDefinitionPlugin() {
|
|
|
115
203
|
}
|
|
116
204
|
}
|
|
117
205
|
|
|
118
|
-
// Below here we handle adjusting
|
|
119
|
-
//
|
|
206
|
+
// Below here we handle adjusting TS definition for transformed tokens
|
|
207
|
+
// `originSelectionRange` returned by TS needs its end character adjusted
|
|
208
|
+
// to account for the source length differing from generated length
|
|
209
|
+
// e.g. when "component" in Ripple maps to "function" in TS
|
|
210
|
+
// Or when "#Map" maps to "TrackedMap", etc.
|
|
120
211
|
|
|
121
212
|
// If no TypeScript definitions, nothing to modify
|
|
122
213
|
// Volar will let the next ts plugin handle it
|
|
@@ -144,13 +235,15 @@ function createDefinitionPlugin() {
|
|
|
144
235
|
|
|
145
236
|
log('Found mapping for definition at range', 'start: ', rangeStart, 'end: ', rangeEnd);
|
|
146
237
|
|
|
147
|
-
// Check if source length
|
|
238
|
+
// Check if source length differs from generated length
|
|
239
|
+
// e.g., "component" -> "function" (source > generated)
|
|
240
|
+
// e.g., "#Map" -> "TrackedMap" (source < generated)
|
|
148
241
|
const customData = mapping.data.customData;
|
|
149
242
|
const sourceLength = mapping.lengths[0];
|
|
150
243
|
const generatedLength = customData.generatedLengths[0];
|
|
151
244
|
|
|
152
|
-
// If
|
|
153
|
-
if (sourceLength
|
|
245
|
+
// If source and generated are same length, no transformation needed
|
|
246
|
+
if (sourceLength === generatedLength) {
|
|
154
247
|
return tsDefinitions;
|
|
155
248
|
}
|
|
156
249
|
|
|
@@ -58,7 +58,8 @@ function createDocumentHighlightPlugin() {
|
|
|
58
58
|
|
|
59
59
|
const [virtualCode] = getVirtualCode(document, context);
|
|
60
60
|
|
|
61
|
-
if (
|
|
61
|
+
if (virtualCode.languageId !== 'ripple') {
|
|
62
|
+
log(`Skipping highlight processing in the '${virtualCode.languageId}' context`);
|
|
62
63
|
return tsHighlights;
|
|
63
64
|
}
|
|
64
65
|
|
package/src/hoverPlugin.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
createLogging,
|
|
11
11
|
getWordFromPosition,
|
|
12
12
|
concatMarkdownContents,
|
|
13
|
+
deobfuscateImportDefinitions,
|
|
13
14
|
} = require('./utils.js');
|
|
14
15
|
|
|
15
16
|
const { log, logError } = createLogging('[Ripple Hover Plugin]');
|
|
@@ -53,6 +54,12 @@ function createHoverPlugin() {
|
|
|
53
54
|
tsHover = await originalProvideHover.call(originalInstance, document, position, token);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
if (tsHover && tsHover.contents) {
|
|
58
|
+
/** @type {MarkupContent} **/ (tsHover.contents).value = deobfuscateImportDefinitions(
|
|
59
|
+
/** @type {MarkupContent} **/ (tsHover.contents).value,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
const [virtualCode] = getVirtualCode(document, context);
|
|
57
64
|
|
|
58
65
|
if (!virtualCode) {
|
|
@@ -79,6 +86,11 @@ function createHoverPlugin() {
|
|
|
79
86
|
log(`Cursor offset in generated code for word '${word}':`, offset);
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
if (virtualCode.languageId !== 'ripple') {
|
|
90
|
+
log(`Skipping hover processing in the '${virtualCode.languageId}' context`);
|
|
91
|
+
return tsHover;
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
const mapping = virtualCode.findMappingByGeneratedRange(starOffset, endOffset);
|
|
83
95
|
|
|
84
96
|
if (!mapping) {
|
package/src/utils.js
CHANGED
|
@@ -1,10 +1,60 @@
|
|
|
1
1
|
/** @import { TextDocument } from 'vscode-languageserver-textdocument' */
|
|
2
2
|
/** @import { LanguageServiceContext } from '@volar/language-server' */
|
|
3
3
|
/** @import {RippleVirtualCode} from '@ripple-ts/typescript-plugin/src/language.js' */
|
|
4
|
+
// @ts-expect-error: ESM type import is fine
|
|
5
|
+
/** @import {is_imported_obfuscated, deobfuscate_imported, IMPORT_OBFUSCATION_PREFIX} from 'ripple/compiler/internal/import/utils' */
|
|
4
6
|
|
|
5
7
|
const { URI } = require('vscode-uri');
|
|
6
8
|
const { createLogging, DEBUG } = require('@ripple-ts/typescript-plugin/src/utils.js');
|
|
7
|
-
|
|
9
|
+
// Matches valid JS/CSS identifier characters: word chars, dashes (CSS), $, and # (Ripple shorthands)
|
|
10
|
+
const charAllowedWordRegex = /[\w\-$#]/;
|
|
11
|
+
const IMPORT_EXPORT_REGEX = {
|
|
12
|
+
import: {
|
|
13
|
+
findBefore: /import\s+(?:\{[^}]*|\*\s+as\s+\w*|\w*)$/s,
|
|
14
|
+
sameLine: /^import\s/,
|
|
15
|
+
},
|
|
16
|
+
export: {
|
|
17
|
+
findBefore: /export\s+(?:\{[^}]*|\*\s+as\s+\w*|\w*)$/s,
|
|
18
|
+
sameLine: /^export\s/,
|
|
19
|
+
},
|
|
20
|
+
from: /from\s*['"][^'"]*['"]\s*;?/,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** @type {is_imported_obfuscated} */
|
|
24
|
+
let is_imported_obfuscated;
|
|
25
|
+
/** @type {deobfuscate_imported} */
|
|
26
|
+
let deobfuscate_imported;
|
|
27
|
+
/** @type {IMPORT_OBFUSCATION_PREFIX} */
|
|
28
|
+
let IMPORT_OBFUSCATION_PREFIX;
|
|
29
|
+
/** @type {RegExp} */
|
|
30
|
+
let obfuscatedImportRegex;
|
|
31
|
+
|
|
32
|
+
import('ripple/compiler/internal/import/utils').then((imports) => {
|
|
33
|
+
is_imported_obfuscated = imports.is_imported_obfuscated;
|
|
34
|
+
deobfuscate_imported = imports.deobfuscate_imported;
|
|
35
|
+
IMPORT_OBFUSCATION_PREFIX = imports.IMPORT_OBFUSCATION_PREFIX;
|
|
36
|
+
obfuscatedImportRegex = new RegExp(
|
|
37
|
+
escapeRegExp(IMPORT_OBFUSCATION_PREFIX) + charAllowedWordRegex.source + '+',
|
|
38
|
+
'gm',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} source
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function escapeRegExp(source) {
|
|
47
|
+
// $& means the whole matched source
|
|
48
|
+
return source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} text
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function deobfuscateImportDefinitions(text) {
|
|
56
|
+
return text.replace(obfuscatedImportRegex, (match) => deobfuscate_imported(match));
|
|
57
|
+
}
|
|
8
58
|
|
|
9
59
|
/**
|
|
10
60
|
* @param {...string} contents
|
|
@@ -43,10 +93,10 @@ function getVirtualCode(document, context) {
|
|
|
43
93
|
function getWordFromPosition(text, start) {
|
|
44
94
|
let wordStart = start;
|
|
45
95
|
let wordEnd = start;
|
|
46
|
-
while (wordStart > 0 &&
|
|
96
|
+
while (wordStart > 0 && charAllowedWordRegex.test(text[wordStart - 1])) {
|
|
47
97
|
wordStart--;
|
|
48
98
|
}
|
|
49
|
-
while (wordEnd < text.length &&
|
|
99
|
+
while (wordEnd < text.length && charAllowedWordRegex.test(text[wordEnd])) {
|
|
50
100
|
wordEnd++;
|
|
51
101
|
}
|
|
52
102
|
|
|
@@ -59,10 +109,66 @@ function getWordFromPosition(text, start) {
|
|
|
59
109
|
};
|
|
60
110
|
}
|
|
61
111
|
|
|
112
|
+
/**
|
|
113
|
+
* @param {'import' | 'export'} type
|
|
114
|
+
* @param {string} text
|
|
115
|
+
* @param {number} start
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
function isInsideImportOrExport(type, text, start) {
|
|
119
|
+
const textBeforeCursor = text.slice(0, start);
|
|
120
|
+
|
|
121
|
+
// Find the last 'import' keyword before cursor
|
|
122
|
+
const lastImportMatch = textBeforeCursor.match(IMPORT_EXPORT_REGEX[type].findBefore);
|
|
123
|
+
if (!lastImportMatch) {
|
|
124
|
+
// Check if we're on a line that starts with import
|
|
125
|
+
const lineStart = textBeforeCursor.lastIndexOf('\n') + 1;
|
|
126
|
+
const lineBeforeCursor = textBeforeCursor.slice(lineStart);
|
|
127
|
+
return IMPORT_EXPORT_REGEX[type].sameLine.test(lineBeforeCursor.trim());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// We found an import - check if it's been closed with 'from'
|
|
131
|
+
const importStart = textBeforeCursor.lastIndexOf(type);
|
|
132
|
+
const textFromImport = text.slice(importStart);
|
|
133
|
+
|
|
134
|
+
// Find the end of this import statement (semicolon or newline after 'from "..."')
|
|
135
|
+
const fromMatch = textFromImport.match(IMPORT_EXPORT_REGEX.from);
|
|
136
|
+
if (!fromMatch || fromMatch.index === undefined) {
|
|
137
|
+
// No 'from' found yet - we're inside an incomplete import
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const importEndOffset = importStart + fromMatch.index + fromMatch[0].length;
|
|
142
|
+
|
|
143
|
+
// If cursor is before the import ends, we're inside it
|
|
144
|
+
return start < importEndOffset;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} text
|
|
149
|
+
* @param {number} start
|
|
150
|
+
* @returns {boolean}
|
|
151
|
+
*/
|
|
152
|
+
function isInsideImport(text, start) {
|
|
153
|
+
return isInsideImportOrExport('import', text, start);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} text
|
|
158
|
+
* @param {number} start
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
function isInsideExport(text, start) {
|
|
162
|
+
return isInsideImportOrExport('export', text, start);
|
|
163
|
+
}
|
|
164
|
+
|
|
62
165
|
module.exports = {
|
|
63
166
|
getVirtualCode,
|
|
64
167
|
getWordFromPosition,
|
|
168
|
+
isInsideImport,
|
|
169
|
+
isInsideExport,
|
|
65
170
|
createLogging,
|
|
66
171
|
concatMarkdownContents,
|
|
172
|
+
deobfuscateImportDefinitions,
|
|
67
173
|
DEBUG,
|
|
68
174
|
};
|