@ripple-ts/language-server 0.2.177 → 0.2.179
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 +2 -2
- package/src/autoInsertPlugin.js +3 -24
- package/src/{diagnosticPlugin.js → compileErrorDiagnosticPlugin.js} +40 -100
- package/src/completionPlugin.js +17 -46
- package/src/definitionPlugin.js +4 -26
- package/src/documentHighlightPlugin.js +121 -0
- package/src/hoverPlugin.js +26 -36
- package/src/server.js +14 -24
- package/src/typescriptDiagnosticPlugin.js +103 -0
- package/src/typescriptService.js +2 -2
- package/src/utils.js +58 -0
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.179",
|
|
4
4
|
"description": "Language Server Protocol implementation for Ripple",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
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.179"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"typescript": "^5.9.2"
|
package/src/autoInsertPlugin.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
/** @import { LanguageServicePlugin } from '@volar/language-server' */
|
|
2
|
-
/** @import { RippleVirtualCode } from '@ripple-ts/typescript-plugin/src/language.js') */
|
|
3
2
|
|
|
4
|
-
const {
|
|
3
|
+
const { getVirtualCode, createLogging } = require('./utils.js');
|
|
5
4
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {...unknown} args
|
|
10
|
-
*/
|
|
11
|
-
function log(...args) {
|
|
12
|
-
if (DEBUG) {
|
|
13
|
-
console.log('[Ripple Auto-Insert]', ...args);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
5
|
+
const { log } = createLogging('[Ripple Auto-Insert Plugin]');
|
|
16
6
|
|
|
17
7
|
/**
|
|
18
8
|
* List of HTML void/self-closing elements that don't need closing tags
|
|
@@ -74,18 +64,7 @@ function createAutoInsertPlugin() {
|
|
|
74
64
|
return null;
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
const
|
|
78
|
-
const decoded = context.decodeEmbeddedDocumentUri(uri);
|
|
79
|
-
|
|
80
|
-
if (!decoded) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const [sourceUri, virtualCodeId] = decoded;
|
|
85
|
-
const sourceScript = context.language.scripts.get(sourceUri);
|
|
86
|
-
const virtualCode = /** @type {RippleVirtualCode } */ (
|
|
87
|
-
sourceScript?.generated?.embeddedCodes.get(virtualCodeId)
|
|
88
|
-
);
|
|
67
|
+
const virtualCode = getVirtualCode(document, context);
|
|
89
68
|
|
|
90
69
|
// Map position back to source
|
|
91
70
|
const offset = document.offsetAt(position);
|
|
@@ -1,37 +1,17 @@
|
|
|
1
|
-
const { RippleVirtualCode } = require('@ripple-ts/typescript-plugin/src/language.js');
|
|
2
|
-
const { URI } = require('vscode-uri');
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
* @
|
|
6
|
-
* @
|
|
7
|
-
* @typedef {import('@volar/language-server').Diagnostic} Diagnostic
|
|
8
|
-
* @typedef {import('@volar/language-server').DiagnosticSeverity} DiagnosticSeverity
|
|
9
|
-
* @typedef {import('@volar/language-server').Position} Position
|
|
10
|
-
* @typedef {import('vscode-languageserver-textdocument').TextDocument} TextDocument
|
|
2
|
+
* @import {Diagnostic, LanguageServicePlugin, LanguageServiceContext} from '@volar/language-server'
|
|
3
|
+
* @import {TextDocument} from 'vscode-languageserver-textdocument'
|
|
11
4
|
*/
|
|
12
5
|
|
|
13
|
-
const
|
|
6
|
+
const { getVirtualCode, createLogging } = require('./utils.js');
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
function log(...args) {
|
|
19
|
-
if (DEBUG) {
|
|
20
|
-
console.log('[Ripple Language]', ...args);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {...unknown} args
|
|
26
|
-
*/
|
|
27
|
-
function logError(...args) {
|
|
28
|
-
console.error('[Ripple Language ERROR]', ...args);
|
|
29
|
-
}
|
|
8
|
+
const { log, logError } = createLogging('[Ripple Compile Error Diagnostic Plugin]');
|
|
9
|
+
const { DiagnosticSeverity } = require('@volar/language-server');
|
|
30
10
|
|
|
31
11
|
/**
|
|
32
12
|
* @returns {LanguageServicePlugin}
|
|
33
13
|
*/
|
|
34
|
-
function
|
|
14
|
+
function createCompileErrorDiagnosticPlugin() {
|
|
35
15
|
log('Creating Ripple diagnostic plugin...');
|
|
36
16
|
|
|
37
17
|
return {
|
|
@@ -44,48 +24,42 @@ function createDiagnosticPlugin() {
|
|
|
44
24
|
},
|
|
45
25
|
create(/** @type {LanguageServiceContext} */ context) {
|
|
46
26
|
return {
|
|
47
|
-
/**
|
|
48
|
-
* @param {TextDocument} document
|
|
49
|
-
* @param {import('@volar/language-server').CancellationToken} _token
|
|
50
|
-
* @returns {import('@volar/language-server').NullableProviderResult<Diagnostic[]>}
|
|
51
|
-
*/
|
|
52
27
|
provideDiagnostics(document, _token) {
|
|
53
28
|
try {
|
|
54
29
|
log('Providing Ripple diagnostics for:', document.uri);
|
|
55
30
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
if (info && info.virtualCode.errors && info.virtualCode.errors.length > 0) {
|
|
59
|
-
const virtualCode = info.virtualCode;
|
|
60
|
-
const diagnostics = [];
|
|
61
|
-
|
|
62
|
-
log('Processing', virtualCode.errors.length, 'errors');
|
|
63
|
-
|
|
64
|
-
// Convert each stored error to a diagnostic
|
|
65
|
-
for (const error of virtualCode.errors) {
|
|
66
|
-
try {
|
|
67
|
-
// Use the actual snapshot text that Volar is working with
|
|
68
|
-
const snapshotText = virtualCode.snapshot.getText(
|
|
69
|
-
0,
|
|
70
|
-
virtualCode.snapshot.getLength(),
|
|
71
|
-
);
|
|
72
|
-
const diagnostic = parseCompilationErrorWithDocument(
|
|
73
|
-
error,
|
|
74
|
-
virtualCode.fileName,
|
|
75
|
-
snapshotText,
|
|
76
|
-
document,
|
|
77
|
-
);
|
|
78
|
-
diagnostics.push(diagnostic);
|
|
79
|
-
} catch (parseError) {
|
|
80
|
-
logError('Failed to parse compilation error:', parseError);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
31
|
+
const virtualCode = getVirtualCode(document, context);
|
|
83
32
|
|
|
84
|
-
|
|
85
|
-
return
|
|
33
|
+
if (!virtualCode || !virtualCode.errors || virtualCode.errors.length === 0) {
|
|
34
|
+
return [];
|
|
86
35
|
}
|
|
87
36
|
|
|
88
|
-
|
|
37
|
+
const diagnostics = [];
|
|
38
|
+
|
|
39
|
+
log('Processing', virtualCode.errors.length, 'errors');
|
|
40
|
+
|
|
41
|
+
// Convert each stored error to a diagnostic
|
|
42
|
+
for (const error of virtualCode.errors) {
|
|
43
|
+
try {
|
|
44
|
+
// Use the actual snapshot text that Volar is working with
|
|
45
|
+
const snapshotText = virtualCode.snapshot.getText(
|
|
46
|
+
0,
|
|
47
|
+
virtualCode.snapshot.getLength(),
|
|
48
|
+
);
|
|
49
|
+
const diagnostic = parseCompilationErrorWithDocument(
|
|
50
|
+
error,
|
|
51
|
+
virtualCode.fileName,
|
|
52
|
+
snapshotText,
|
|
53
|
+
document,
|
|
54
|
+
);
|
|
55
|
+
diagnostics.push(diagnostic);
|
|
56
|
+
} catch (parseError) {
|
|
57
|
+
logError('Failed to parse compilation error:', parseError);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
log('Generated', diagnostics.length, 'diagnostics');
|
|
62
|
+
return diagnostics;
|
|
89
63
|
} catch (err) {
|
|
90
64
|
logError('Failed to provide diagnostics:', err);
|
|
91
65
|
return [];
|
|
@@ -127,7 +101,7 @@ function parseCompilationErrorWithDocument(error, fallbackFileName, sourceText,
|
|
|
127
101
|
const zeroBasedEndColumn = Math.max(0, endColumn);
|
|
128
102
|
|
|
129
103
|
return {
|
|
130
|
-
severity:
|
|
104
|
+
severity: DiagnosticSeverity.Error,
|
|
131
105
|
range: {
|
|
132
106
|
start: { line: zeroBasedStartLine, character: zeroBasedStartColumn },
|
|
133
107
|
end: { line: zeroBasedEndLine, character: zeroBasedEndColumn },
|
|
@@ -160,7 +134,7 @@ function parseCompilationErrorWithDocument(error, fallbackFileName, sourceText,
|
|
|
160
134
|
let length = Math.min(1, sourceText.split('\n')[zeroBasedLine]?.length - actualColumn || 1);
|
|
161
135
|
|
|
162
136
|
return {
|
|
163
|
-
severity:
|
|
137
|
+
severity: DiagnosticSeverity.Error,
|
|
164
138
|
range: {
|
|
165
139
|
start: { line: zeroBasedLine, character: actualColumn },
|
|
166
140
|
end: { line: zeroBasedLine, character: actualColumn + length },
|
|
@@ -175,7 +149,7 @@ function parseCompilationErrorWithDocument(error, fallbackFileName, sourceText,
|
|
|
175
149
|
const endPosition = document.positionAt(Math.min(1, sourceText.length));
|
|
176
150
|
|
|
177
151
|
return {
|
|
178
|
-
severity:
|
|
152
|
+
severity: DiagnosticSeverity.Error,
|
|
179
153
|
range: {
|
|
180
154
|
start: startPosition,
|
|
181
155
|
end: endPosition,
|
|
@@ -189,7 +163,7 @@ function parseCompilationErrorWithDocument(error, fallbackFileName, sourceText,
|
|
|
189
163
|
logError('Error parsing compilation error:', parseError);
|
|
190
164
|
|
|
191
165
|
return {
|
|
192
|
-
severity:
|
|
166
|
+
severity: DiagnosticSeverity.Error,
|
|
193
167
|
range: {
|
|
194
168
|
start: { line: 0, character: 0 },
|
|
195
169
|
end: { line: 0, character: 1 },
|
|
@@ -201,40 +175,6 @@ function parseCompilationErrorWithDocument(error, fallbackFileName, sourceText,
|
|
|
201
175
|
}
|
|
202
176
|
}
|
|
203
177
|
|
|
204
|
-
/**
|
|
205
|
-
* @param {LanguageServiceContext} context
|
|
206
|
-
* @param {TextDocument} document
|
|
207
|
-
*/
|
|
208
|
-
function getEmbeddedInfo(context, document) {
|
|
209
|
-
try {
|
|
210
|
-
const uri = URI.parse(document.uri);
|
|
211
|
-
const decoded = context.decodeEmbeddedDocumentUri(uri);
|
|
212
|
-
if (!decoded) {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const [documentUri, embeddedCodeId] = decoded;
|
|
217
|
-
|
|
218
|
-
const sourceScript = context.language.scripts.get(documentUri);
|
|
219
|
-
if (!sourceScript?.generated) {
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const virtualCode = sourceScript.generated.embeddedCodes.get(embeddedCodeId);
|
|
224
|
-
if (!(virtualCode instanceof RippleVirtualCode)) {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return {
|
|
229
|
-
sourceScript: sourceScript,
|
|
230
|
-
virtualCode,
|
|
231
|
-
};
|
|
232
|
-
} catch (err) {
|
|
233
|
-
logError('Failed to get embedded info:', err);
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
178
|
module.exports = {
|
|
239
|
-
|
|
179
|
+
createCompileErrorDiagnosticPlugin,
|
|
240
180
|
};
|
package/src/completionPlugin.js
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @import {LanguageServicePlugin} from '@volar/language-server'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { CompletionItemKind, InsertTextFormat } = require('@volar/language-server');
|
|
6
|
-
const {
|
|
6
|
+
const { getVirtualCode, createLogging } = require('./utils.js');
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @param {...unknown} args
|
|
12
|
-
*/
|
|
13
|
-
function log(...args) {
|
|
14
|
-
if (DEBUG) {
|
|
15
|
-
console.log('[Ripple Completion]', ...args);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
8
|
+
const { log } = createLogging('[Ripple Completion Plugin]');
|
|
18
9
|
|
|
19
10
|
/**
|
|
20
11
|
* Ripple-specific completion enhancements
|
|
@@ -143,6 +134,15 @@ const RIPPLE_SNIPPETS = [
|
|
|
143
134
|
insertTextFormat: InsertTextFormat.Snippet,
|
|
144
135
|
sortText: '0-untrack',
|
|
145
136
|
},
|
|
137
|
+
{
|
|
138
|
+
label: 'try-pending',
|
|
139
|
+
kind: CompletionItemKind.Snippet,
|
|
140
|
+
detail: 'try...pending block',
|
|
141
|
+
documentation: 'Handle async content with loading fallback',
|
|
142
|
+
insertText: "try {\n\t$1\n} pending {\n\t<div>{'Loading...'}</div>\n}",
|
|
143
|
+
insertTextFormat: InsertTextFormat.Snippet,
|
|
144
|
+
sortText: '0-try-pending',
|
|
145
|
+
},
|
|
146
146
|
];
|
|
147
147
|
|
|
148
148
|
/**
|
|
@@ -217,22 +217,13 @@ function createCompletionPlugin() {
|
|
|
217
217
|
return { items: [], isIncomplete: false };
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
const virtualCode = getVirtualCode(document, context);
|
|
221
|
+
|
|
220
222
|
// Check if we're inside an embedded code (like CSS in <style> blocks)
|
|
221
223
|
// If so, don't provide Ripple snippets - let CSS completions take priority
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const [documentUri, embeddedCodeId] = decoded;
|
|
226
|
-
const sourceScript = context.language.scripts.get(documentUri);
|
|
227
|
-
|
|
228
|
-
if (sourceScript?.generated) {
|
|
229
|
-
const virtualCode = sourceScript.generated.embeddedCodes.get(embeddedCodeId);
|
|
230
|
-
// If we're in a CSS embedded code (from <style> blocks), skip Ripple completions
|
|
231
|
-
if (virtualCode && virtualCode.languageId === 'css') {
|
|
232
|
-
log('Skipping Ripple completions in CSS context');
|
|
233
|
-
return { items: [], isIncomplete: false };
|
|
234
|
-
}
|
|
235
|
-
}
|
|
224
|
+
if (virtualCode && virtualCode.languageId === 'css') {
|
|
225
|
+
log('Skipping Ripple completions in CSS context');
|
|
226
|
+
return { items: [], isIncomplete: false };
|
|
236
227
|
}
|
|
237
228
|
|
|
238
229
|
const line = document.getText({
|
|
@@ -296,26 +287,6 @@ function createCompletionPlugin() {
|
|
|
296
287
|
};
|
|
297
288
|
}
|
|
298
289
|
|
|
299
|
-
/**
|
|
300
|
-
* Heuristic to detect if we're inside a component template
|
|
301
|
-
* @param {string} text
|
|
302
|
-
* @returns {boolean}
|
|
303
|
-
*/
|
|
304
|
-
function isLikelyInTemplate(text) {
|
|
305
|
-
// Simple heuristic: inside component body after opening brace
|
|
306
|
-
const componentMatch = text.match(/component\s+\w+\([^)]*\)\s*\{/);
|
|
307
|
-
if (!componentMatch || componentMatch.index === undefined) return false;
|
|
308
|
-
|
|
309
|
-
// Count braces to see if we're inside
|
|
310
|
-
let braceCount = 0;
|
|
311
|
-
for (let i = componentMatch.index + componentMatch[0].length; i < text.length; i++) {
|
|
312
|
-
if (text[i] === '{') braceCount++;
|
|
313
|
-
if (text[i] === '}') braceCount--;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return braceCount > 0;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
290
|
module.exports = {
|
|
320
291
|
createCompletionPlugin,
|
|
321
292
|
};
|
package/src/definitionPlugin.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
/** @import { LanguageServicePlugin } from '@volar/language-server' */
|
|
2
|
-
/** @import { RippleVirtualCode } from '@ripple-ts/typescript-plugin/src/language.js') */
|
|
3
2
|
|
|
4
|
-
const {
|
|
3
|
+
const { getVirtualCode, createLogging } = require('./utils.js');
|
|
5
4
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {...unknown} args
|
|
10
|
-
*/
|
|
11
|
-
function log(...args) {
|
|
12
|
-
if (DEBUG) {
|
|
13
|
-
console.log('[Ripple Definition]', ...args);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
5
|
+
const { log } = createLogging('[Ripple Definition Plugin]');
|
|
16
6
|
|
|
17
7
|
/**
|
|
18
8
|
* @returns {LanguageServicePlugin}
|
|
@@ -26,9 +16,6 @@ function createDefinitionPlugin() {
|
|
|
26
16
|
create(context) {
|
|
27
17
|
return {
|
|
28
18
|
async provideDefinition(document, position, token) {
|
|
29
|
-
const uri = URI.parse(document.uri);
|
|
30
|
-
const decoded = context.decodeEmbeddedDocumentUri(uri);
|
|
31
|
-
|
|
32
19
|
// Get TypeScript definition from typescript-semantic service
|
|
33
20
|
let tsDefinitions = [];
|
|
34
21
|
for (const [plugin, instance] of context.plugins) {
|
|
@@ -47,17 +34,6 @@ function createDefinitionPlugin() {
|
|
|
47
34
|
return;
|
|
48
35
|
}
|
|
49
36
|
|
|
50
|
-
// If not in a Ripple embedded context, just return TypeScript results
|
|
51
|
-
if (!decoded) {
|
|
52
|
-
return tsDefinitions;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const [sourceUri, virtualCodeId] = decoded;
|
|
56
|
-
const sourceScript = context.language.scripts.get(sourceUri);
|
|
57
|
-
const virtualCode = /** @type {RippleVirtualCode } */ (
|
|
58
|
-
sourceScript?.generated?.embeddedCodes.get(virtualCodeId)
|
|
59
|
-
);
|
|
60
|
-
|
|
61
37
|
// Get the range from TypeScript's definition to find the exact token
|
|
62
38
|
// This gives us the precise start and end of the token (e.g., "function")
|
|
63
39
|
const firstDefinition = tsDefinitions[0];
|
|
@@ -69,6 +45,8 @@ function createDefinitionPlugin() {
|
|
|
69
45
|
const rangeStart = document.offsetAt(range.start);
|
|
70
46
|
const rangeEnd = document.offsetAt(range.end);
|
|
71
47
|
|
|
48
|
+
const virtualCode = getVirtualCode(document, context);
|
|
49
|
+
|
|
72
50
|
// Find the mapping using the exact token range for O(1) lookup
|
|
73
51
|
const mapping = virtualCode.findMappingByGeneratedRange(rangeStart, rangeEnd);
|
|
74
52
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** @import { LanguageServicePlugin } from '@volar/language-server' */
|
|
2
|
+
/** @import { LanguageServicePluginInstance } from '@volar/language-server' */
|
|
3
|
+
|
|
4
|
+
const { getVirtualCode, getWordFromPosition, createLogging } = require('./utils.js');
|
|
5
|
+
const { log } = createLogging('[Ripple Document Highlight Plugin]');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Document Highlight plugin for Ripple
|
|
9
|
+
* Provides word highlighting (grey background) for custom Ripple keywords like 'pending'
|
|
10
|
+
* @returns {LanguageServicePlugin}
|
|
11
|
+
*/
|
|
12
|
+
function createDocumentHighlightPlugin() {
|
|
13
|
+
return {
|
|
14
|
+
name: 'ripple-document-highlight',
|
|
15
|
+
capabilities: {
|
|
16
|
+
documentHighlightProvider: true,
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
/** @type {LanguageServicePluginInstance['provideDocumentHighlights']} */
|
|
20
|
+
let originalProvideDocumentHighlights;
|
|
21
|
+
/** @type {LanguageServicePluginInstance} */
|
|
22
|
+
let originalInstance;
|
|
23
|
+
|
|
24
|
+
// Get TypeScript's document highlights provider
|
|
25
|
+
for (const [plugin, instance] of context.plugins) {
|
|
26
|
+
if (plugin.name === 'typescript-semantic' && instance.provideDocumentHighlights) {
|
|
27
|
+
originalInstance = instance;
|
|
28
|
+
originalProvideDocumentHighlights = instance.provideDocumentHighlights;
|
|
29
|
+
instance.provideDocumentHighlights = undefined;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!originalProvideDocumentHighlights) {
|
|
35
|
+
log(
|
|
36
|
+
"'typescript-semantic plugin' was not found or has no 'provideDocumentHighlights'. \
|
|
37
|
+
Document highlights will be limited to custom Ripple keywords only.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
async provideDocumentHighlights(document, position, token) {
|
|
43
|
+
if (!originalProvideDocumentHighlights) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let tsHighlights = await originalProvideDocumentHighlights.call(
|
|
48
|
+
originalInstance,
|
|
49
|
+
document,
|
|
50
|
+
position,
|
|
51
|
+
token,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (!tsHighlights || tsHighlights.length > 0) {
|
|
55
|
+
// If TypeScript recognized tokens and provided highlights, return them
|
|
56
|
+
return tsHighlights;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const virtualCode = getVirtualCode(document, context);
|
|
60
|
+
|
|
61
|
+
if (!virtualCode) {
|
|
62
|
+
return tsHighlights;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if we're on a custom Ripple keyword
|
|
66
|
+
const offset = document.offsetAt(position);
|
|
67
|
+
const text = document.getText();
|
|
68
|
+
|
|
69
|
+
// Find word boundaries
|
|
70
|
+
const { word } = getWordFromPosition(text, offset);
|
|
71
|
+
|
|
72
|
+
// If the word is a Ripple keyword, find all occurrences in the document
|
|
73
|
+
|
|
74
|
+
const regex = new RegExp(`\\b${word}\\b`, 'g');
|
|
75
|
+
let match;
|
|
76
|
+
|
|
77
|
+
while ((match = regex.exec(text)) !== null) {
|
|
78
|
+
const start = match.index;
|
|
79
|
+
const end = match.index + word.length;
|
|
80
|
+
const mapping = virtualCode.findMappingByGeneratedRange(start, end);
|
|
81
|
+
|
|
82
|
+
if (!mapping) {
|
|
83
|
+
// If no mapping, skip all others as well
|
|
84
|
+
// This shouldn't happen as TS handles only mapped ranges
|
|
85
|
+
return tsHighlights;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!mapping.data.customData?.wordHighlight?.kind) {
|
|
89
|
+
// Skip if we didn't define word highlighting in segments
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!tsHighlights) {
|
|
94
|
+
tsHighlights = [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
tsHighlights.push({
|
|
98
|
+
range: {
|
|
99
|
+
start: document.positionAt(start),
|
|
100
|
+
end: document.positionAt(end),
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
kind: mapping.data.customData.wordHighlight.kind,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (tsHighlights.length > 0) {
|
|
108
|
+
log(`Found ${tsHighlights.length} occurrences of '${word}'`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Return TypeScript highlights if no custom keyword was found
|
|
112
|
+
return [...tsHighlights];
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
createDocumentHighlightPlugin,
|
|
121
|
+
};
|
package/src/hoverPlugin.js
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
/** @import { LanguageServicePlugin } from '@volar/language-server' */
|
|
2
|
-
/** @import {
|
|
3
|
-
/** @typedef {import('@volar/language-server').LanguageServicePluginInstance} LanguageServicePluginInstance */
|
|
2
|
+
/** @import { LanguageServicePluginInstance } from '@volar/language-server' */
|
|
4
3
|
|
|
5
|
-
const {
|
|
4
|
+
const { getVirtualCode, createLogging } = require('./utils.js');
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @param {...unknown} args
|
|
11
|
-
*/
|
|
12
|
-
function log(...args) {
|
|
13
|
-
if (DEBUG) {
|
|
14
|
-
console.log('[Ripple Hover]', ...args);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {...unknown} args
|
|
20
|
-
*/
|
|
21
|
-
function logError(...args) {
|
|
22
|
-
console.error('[Ripple Hover]', ...args);
|
|
23
|
-
}
|
|
6
|
+
const { log, logError } = createLogging('[Ripple Hover Plugin]');
|
|
24
7
|
|
|
25
8
|
/**
|
|
26
9
|
* @returns {LanguageServicePlugin}
|
|
@@ -55,9 +38,6 @@ function createHoverPlugin() {
|
|
|
55
38
|
}
|
|
56
39
|
return {
|
|
57
40
|
async provideHover(document, position, token) {
|
|
58
|
-
const uri = URI.parse(document.uri);
|
|
59
|
-
const decoded = context.decodeEmbeddedDocumentUri(uri);
|
|
60
|
-
|
|
61
41
|
// Get TypeScript hover from typescript-semantic service
|
|
62
42
|
let tsHover = null;
|
|
63
43
|
if (originalProvideHover) {
|
|
@@ -65,23 +45,13 @@ function createHoverPlugin() {
|
|
|
65
45
|
}
|
|
66
46
|
|
|
67
47
|
// If no TypeScript hover, nothing to modify
|
|
68
|
-
if (!tsHover) {
|
|
48
|
+
if (!tsHover || !tsHover.range) {
|
|
69
49
|
return;
|
|
70
50
|
}
|
|
71
51
|
|
|
72
|
-
|
|
73
|
-
if (!decoded) {
|
|
74
|
-
return tsHover;
|
|
75
|
-
}
|
|
52
|
+
const virtualCode = getVirtualCode(document, context);
|
|
76
53
|
|
|
77
|
-
|
|
78
|
-
const sourceScript = context.language.scripts.get(sourceUri);
|
|
79
|
-
const virtualCode = /** @type {RippleVirtualCode } */ (
|
|
80
|
-
sourceScript?.generated?.embeddedCodes.get(virtualCodeId)
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
// If there's no range to adjust, return as-is
|
|
84
|
-
if (!tsHover.range) {
|
|
54
|
+
if (!virtualCode) {
|
|
85
55
|
return tsHover;
|
|
86
56
|
}
|
|
87
57
|
|
|
@@ -95,6 +65,26 @@ function createHoverPlugin() {
|
|
|
95
65
|
return tsHover;
|
|
96
66
|
}
|
|
97
67
|
|
|
68
|
+
const customHover = mapping?.data?.customData?.hover;
|
|
69
|
+
if (customHover) {
|
|
70
|
+
log('Found custom hover data in mapping');
|
|
71
|
+
return {
|
|
72
|
+
contents: {
|
|
73
|
+
kind: 'markdown',
|
|
74
|
+
value: customHover.contents,
|
|
75
|
+
},
|
|
76
|
+
range: {
|
|
77
|
+
start: position,
|
|
78
|
+
end: position,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
} else if (customHover === false) {
|
|
82
|
+
log(
|
|
83
|
+
`Hover explicitly suppressed in mapping at range start: ${rangeStart}, end: ${rangeEnd}`,
|
|
84
|
+
);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
98
88
|
log('Found mapping for hover at range', 'start: ', rangeStart, 'end: ', rangeEnd);
|
|
99
89
|
|
|
100
90
|
// Check if source length is greater than generated length (component -> function)
|
package/src/server.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/** @import {CompilerOptions} from 'typescript' */
|
|
2
|
+
|
|
3
|
+
const { createLogging } = require('./utils.js');
|
|
4
|
+
const {
|
|
2
5
|
createConnection,
|
|
3
6
|
createServer,
|
|
4
7
|
createTypeScriptProject,
|
|
5
8
|
} = require('@volar/language-server/node');
|
|
6
|
-
const {
|
|
9
|
+
const { createCompileErrorDiagnosticPlugin } = require('./compileErrorDiagnosticPlugin.js');
|
|
7
10
|
const { createDefinitionPlugin } = require('./definitionPlugin.js');
|
|
8
11
|
const { createHoverPlugin } = require('./hoverPlugin.js');
|
|
9
12
|
const { createCompletionPlugin } = require('./completionPlugin.js');
|
|
10
13
|
const { createAutoInsertPlugin } = require('./autoInsertPlugin.js');
|
|
14
|
+
const { createTypeScriptDiagnosticFilterPlugin } = require('./typescriptDiagnosticPlugin.js');
|
|
15
|
+
const { createDocumentHighlightPlugin } = require('./documentHighlightPlugin.js');
|
|
11
16
|
const {
|
|
12
17
|
getRippleLanguagePlugin,
|
|
13
18
|
resolveConfig,
|
|
@@ -15,25 +20,7 @@ const {
|
|
|
15
20
|
const { createTypeScriptServices } = require('./typescriptService.js');
|
|
16
21
|
const { create: createCssService } = require('volar-service-css');
|
|
17
22
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
/** @typedef {import('typescript').CompilerOptions} CompilerOptions */
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {...unknown} args
|
|
24
|
-
*/
|
|
25
|
-
function log(...args) {
|
|
26
|
-
if (DEBUG) {
|
|
27
|
-
console.log('[Ripple Server]', ...args);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @param {...unknown} args
|
|
33
|
-
*/
|
|
34
|
-
function logError(...args) {
|
|
35
|
-
console.error('[Ripple Server ERROR]', ...args);
|
|
36
|
-
}
|
|
23
|
+
const { log, logError } = createLogging('[Ripple Language Server]');
|
|
37
24
|
|
|
38
25
|
function createRippleLanguageServer() {
|
|
39
26
|
const connection = createConnection();
|
|
@@ -110,13 +97,16 @@ function createRippleLanguageServer() {
|
|
|
110
97
|
[
|
|
111
98
|
createAutoInsertPlugin(),
|
|
112
99
|
createCompletionPlugin(),
|
|
113
|
-
|
|
100
|
+
createCompileErrorDiagnosticPlugin(),
|
|
114
101
|
createDefinitionPlugin(),
|
|
115
102
|
createCssService(),
|
|
116
103
|
...createTypeScriptServices(ts),
|
|
117
|
-
// !IMPORTANT
|
|
118
|
-
//
|
|
104
|
+
// !IMPORTANT 'createTypeScriptDiagnosticFilterPlugin', 'createHoverPlugin',
|
|
105
|
+
// and 'createDocumentHighlightPlugin' must come after TypeScript services
|
|
106
|
+
// to intercept volar's and vscode default providers
|
|
107
|
+
createTypeScriptDiagnosticFilterPlugin(),
|
|
119
108
|
createHoverPlugin(),
|
|
109
|
+
createDocumentHighlightPlugin(),
|
|
120
110
|
],
|
|
121
111
|
);
|
|
122
112
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** @import { LanguageServicePlugin } from '@volar/language-server' */
|
|
2
|
+
/** @import { LanguageServicePluginInstance } from '@volar/language-server' */
|
|
3
|
+
|
|
4
|
+
const { getVirtualCode, createLogging } = require('./utils.js');
|
|
5
|
+
|
|
6
|
+
const { log, logError } = createLogging('[Ripple TypeScript Diagnostic Plugin]');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @returns {LanguageServicePlugin}
|
|
10
|
+
*/
|
|
11
|
+
function createTypeScriptDiagnosticFilterPlugin() {
|
|
12
|
+
log('Creating TypeScript diagnostic filter plugin...');
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
name: 'ripple-typescript-diagnostic-filter',
|
|
16
|
+
capabilities: {
|
|
17
|
+
diagnosticProvider: {
|
|
18
|
+
interFileDependencies: false,
|
|
19
|
+
workspaceDiagnostics: false,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
/** @type {LanguageServicePluginInstance['provideDiagnostics']} */
|
|
24
|
+
let originalProvideDiagnostics;
|
|
25
|
+
/** @type {LanguageServicePluginInstance} */
|
|
26
|
+
let originalInstance;
|
|
27
|
+
|
|
28
|
+
// Disable typescript-semantic's provideDiagnostics so it doesn't merge with ours
|
|
29
|
+
for (const [plugin, instance] of context.plugins) {
|
|
30
|
+
if (plugin.name === 'typescript-semantic') {
|
|
31
|
+
originalInstance = instance;
|
|
32
|
+
originalProvideDiagnostics = instance.provideDiagnostics;
|
|
33
|
+
instance.provideDiagnostics = undefined;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!originalProvideDiagnostics) {
|
|
39
|
+
logError(
|
|
40
|
+
"'typescript-semantic plugin' was not found or has no 'provideDiagnostics'. \
|
|
41
|
+
This plugin must be loaded after Volar's typescript-semantic plugin.",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
async provideDiagnostics(document, token) {
|
|
47
|
+
let diagnostics;
|
|
48
|
+
|
|
49
|
+
if (originalProvideDiagnostics) {
|
|
50
|
+
diagnostics = await originalProvideDiagnostics.call(originalInstance, document, token);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!diagnostics || diagnostics.length === 0) {
|
|
54
|
+
return diagnostics;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
log(`Filtering ${diagnostics.length} TypeScript diagnostics for ${document.uri}`);
|
|
58
|
+
|
|
59
|
+
const virtualCode = getVirtualCode(document, context);
|
|
60
|
+
|
|
61
|
+
const filtered = diagnostics.filter((diagnostic) => {
|
|
62
|
+
const range = diagnostic.range;
|
|
63
|
+
const rangeStart = document.offsetAt(range.start);
|
|
64
|
+
const rangeEnd = document.offsetAt(range.end);
|
|
65
|
+
// Get the mapping at this diagnostic position
|
|
66
|
+
const mapping = virtualCode.findMappingByGeneratedRange(rangeStart, rangeEnd);
|
|
67
|
+
|
|
68
|
+
if (!mapping) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const suppressedCodes = mapping.data.customData?.suppressedDiagnostics;
|
|
73
|
+
|
|
74
|
+
if (!suppressedCodes || suppressedCodes.length === 0) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const diagnosticCode =
|
|
79
|
+
typeof diagnostic.code === 'number'
|
|
80
|
+
? diagnostic.code
|
|
81
|
+
: typeof diagnostic.code === 'string'
|
|
82
|
+
? parseInt(diagnostic.code)
|
|
83
|
+
: null;
|
|
84
|
+
|
|
85
|
+
if (diagnosticCode && suppressedCodes.includes(diagnosticCode)) {
|
|
86
|
+
log(`Suppressing diagnostic ${diagnosticCode}: ${diagnostic.message}`);
|
|
87
|
+
return false; // Filter out this diagnostic
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true; // Keep this diagnostic
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
log(`Filtered from ${diagnostics.length} to ${filtered.length} diagnostics`);
|
|
94
|
+
return filtered;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
createTypeScriptDiagnosticFilterPlugin,
|
|
103
|
+
};
|
package/src/typescriptService.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
3
|
-
* @
|
|
2
|
+
* @import {LanguageServiceContext} from '@volar/language-server'
|
|
3
|
+
* @import {TextDocument} from 'vscode-languageserver-textdocument'
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Monkey-patch getUserPreferences before requiring the main module
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** @import { TextDocument } from 'vscode-languageserver-textdocument' */
|
|
2
|
+
/** @import { LanguageServiceContext } from '@volar/language-server' */
|
|
3
|
+
/** @import {RippleVirtualCode} from '@ripple-ts/typescript-plugin/src/language.js' */
|
|
4
|
+
|
|
5
|
+
const { URI } = require('vscode-uri');
|
|
6
|
+
const { createLogging, DEBUG } = require('@ripple-ts/typescript-plugin/src/utils.js');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get virtual code from the encoded document URI
|
|
10
|
+
* @param {LanguageServiceContext} context
|
|
11
|
+
* @param {TextDocument} document
|
|
12
|
+
* @returns {RippleVirtualCode}
|
|
13
|
+
*/
|
|
14
|
+
function getVirtualCode(document, context) {
|
|
15
|
+
const uri = URI.parse(document.uri);
|
|
16
|
+
const decoded = /** @type {[documentUri: URI, embeddedCodeId: string]} */ (
|
|
17
|
+
context.decodeEmbeddedDocumentUri(uri)
|
|
18
|
+
);
|
|
19
|
+
const [sourceUri, virtualCodeId] = decoded;
|
|
20
|
+
const sourceScript = context.language.scripts.get(sourceUri);
|
|
21
|
+
const virtualCode = /** @type {RippleVirtualCode} */ (
|
|
22
|
+
sourceScript?.generated?.embeddedCodes.get(virtualCodeId)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return virtualCode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the word at a specific position in the text
|
|
30
|
+
* @param {string} text
|
|
31
|
+
* @param {number} start
|
|
32
|
+
* @returns {{word: string, start: number, end: number}}
|
|
33
|
+
*/
|
|
34
|
+
function getWordFromPosition(text, start) {
|
|
35
|
+
let wordStart = start;
|
|
36
|
+
let wordEnd = start;
|
|
37
|
+
while (wordStart > 0 && /\w/.test(text[wordStart - 1])) {
|
|
38
|
+
wordStart--;
|
|
39
|
+
}
|
|
40
|
+
while (wordEnd < text.length && /\w/.test(text[wordEnd])) {
|
|
41
|
+
wordEnd++;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const word = text.substring(wordStart, wordEnd);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
word,
|
|
48
|
+
start: wordStart,
|
|
49
|
+
end: wordEnd,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
getVirtualCode,
|
|
55
|
+
getWordFromPosition,
|
|
56
|
+
createLogging,
|
|
57
|
+
DEBUG,
|
|
58
|
+
};
|