@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ripple-ts/language-server",
3
- "version": "0.2.177",
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.177"
22
+ "@ripple-ts/typescript-plugin": "0.2.179"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "typescript": "^5.9.2"
@@ -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 { URI } = require('vscode-uri');
3
+ const { getVirtualCode, createLogging } = require('./utils.js');
5
4
 
6
- const DEBUG = process.env.RIPPLE_DEBUG === 'true';
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 uri = URI.parse(document.uri);
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
- * @typedef {import('@volar/language-server').LanguageServicePlugin} LanguageServicePlugin
6
- * @typedef {import('@volar/language-server').LanguageServiceContext} LanguageServiceContext
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 DEBUG = process.env.RIPPLE_DEBUG === 'true';
6
+ const { getVirtualCode, createLogging } = require('./utils.js');
14
7
 
15
- /**
16
- * @param {...unknown} args
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 createDiagnosticPlugin() {
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 info = getEmbeddedInfo(context, document);
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
- log('Generated', diagnostics.length, 'diagnostics');
85
- return diagnostics;
33
+ if (!virtualCode || !virtualCode.errors || virtualCode.errors.length === 0) {
34
+ return [];
86
35
  }
87
36
 
88
- return [];
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: 1, // DiagnosticSeverity.Error
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: 1, // DiagnosticSeverity.Error
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: 1, // DiagnosticSeverity.Error
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: 1,
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
- createDiagnosticPlugin,
179
+ createCompileErrorDiagnosticPlugin,
240
180
  };
@@ -1,20 +1,11 @@
1
1
  /**
2
- * @typedef {import('@volar/language-server').LanguageServicePlugin} LanguageServicePlugin
2
+ * @import {LanguageServicePlugin} from '@volar/language-server'
3
3
  */
4
4
 
5
5
  const { CompletionItemKind, InsertTextFormat } = require('@volar/language-server');
6
- const { URI } = require('vscode-uri');
6
+ const { getVirtualCode, createLogging } = require('./utils.js');
7
7
 
8
- const DEBUG = process.env.RIPPLE_DEBUG === 'true';
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
- const uri = URI.parse(document.uri);
223
- const decoded = context.decodeEmbeddedDocumentUri(uri);
224
- if (decoded) {
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
  };
@@ -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 { URI } = require('vscode-uri');
3
+ const { getVirtualCode, createLogging } = require('./utils.js');
5
4
 
6
- const DEBUG = process.env.RIPPLE_DEBUG === 'true';
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
+ };
@@ -1,26 +1,9 @@
1
1
  /** @import { LanguageServicePlugin } from '@volar/language-server' */
2
- /** @import { RippleVirtualCode } from '@ripple-ts/typescript-plugin/src/language.js') */
3
- /** @typedef {import('@volar/language-server').LanguageServicePluginInstance} LanguageServicePluginInstance */
2
+ /** @import { LanguageServicePluginInstance } from '@volar/language-server' */
4
3
 
5
- const { URI } = require('vscode-uri');
4
+ const { getVirtualCode, createLogging } = require('./utils.js');
6
5
 
7
- const DEBUG = process.env.RIPPLE_DEBUG === 'true';
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
- // If not in a Ripple embedded context, just return TypeScript results
73
- if (!decoded) {
74
- return tsHover;
75
- }
52
+ const virtualCode = getVirtualCode(document, context);
76
53
 
77
- const [sourceUri, virtualCodeId] = decoded;
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
- const {
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 { createDiagnosticPlugin } = require('./diagnosticPlugin.js');
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 DEBUG = process.env.RIPPLE_DEBUG === 'true';
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
- createDiagnosticPlugin(),
100
+ createCompileErrorDiagnosticPlugin(),
114
101
  createDefinitionPlugin(),
115
102
  createCssService(),
116
103
  ...createTypeScriptServices(ts),
117
- // !IMPORTANT createHoverPlugin has to be loaded after Volar's ts plugins
118
- // to overwrite `typescript-semantic` plugin's `provideHover`
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
+ };
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @typedef {import('@volar/language-server').LanguageServiceContext} LanguageServiceContext
3
- * @typedef {import('vscode-languageserver-textdocument').TextDocument} TextDocument
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
+ };