@ripple-ts/language-server 0.2.174 → 0.2.176

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.174",
3
+ "version": "0.2.176",
4
4
  "description": "Language Server Protocol implementation for Ripple",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -15,10 +15,11 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@volar/language-server": "~2.4.23",
18
+ "volar-service-css": "0.0.65",
18
19
  "volar-service-typescript": "0.0.65",
19
20
  "vscode-languageserver-textdocument": "^1.0.12",
20
21
  "vscode-uri": "^3.1.0",
21
- "@ripple-ts/typescript-plugin": "0.2.174"
22
+ "@ripple-ts/typescript-plugin": "0.2.176"
22
23
  },
23
24
  "peerDependencies": {
24
25
  "typescript": "^5.9.2"
@@ -0,0 +1,183 @@
1
+ /** @import { LanguageServicePlugin } from '@volar/language-server' */
2
+ /** @import { RippleVirtualCode } from '@ripple-ts/typescript-plugin/src/language.js') */
3
+
4
+ const { URI } = require('vscode-uri');
5
+
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
+ }
16
+
17
+ /**
18
+ * List of HTML void/self-closing elements that don't need closing tags
19
+ * https://developer.mozilla.org/en-US/docs/Glossary/Void_element
20
+ */
21
+ const VOID_ELEMENTS = new Set([
22
+ 'area',
23
+ 'base',
24
+ 'br',
25
+ 'col',
26
+ 'command',
27
+ 'embed',
28
+ 'hr',
29
+ 'img',
30
+ 'input',
31
+ 'keygen',
32
+ 'link',
33
+ 'meta',
34
+ 'param',
35
+ 'source',
36
+ 'track',
37
+ 'wbr',
38
+ ]);
39
+
40
+ /**
41
+ * Auto-insert plugin for Ripple
42
+ * Handles auto-closing tags when typing '>' after a tag name
43
+ * @returns {LanguageServicePlugin}
44
+ */
45
+ function createAutoInsertPlugin() {
46
+ return {
47
+ name: 'ripple-auto-insert',
48
+ capabilities: {
49
+ autoInsertionProvider: {
50
+ triggerCharacters: ['>'],
51
+ configurationSections: ['ripple.autoClosingTags'],
52
+ },
53
+ documentOnTypeFormattingProvider: {
54
+ triggerCharacters: ['>'],
55
+ },
56
+ },
57
+ // leaving context for future use
58
+ create(context) {
59
+ return {
60
+ /**
61
+ * @param {import('vscode-languageserver-textdocument').TextDocument} document
62
+ * @param {import('@volar/language-server').Position} position
63
+ * @param {{ rangeOffset: number; rangeLength: number; text: string }} lastChange
64
+ * @param {import('@volar/language-server').CancellationToken} _token
65
+ * @returns {Promise<string | null>}
66
+ */
67
+ async provideAutoInsertSnippet(document, position, lastChange, _token) {
68
+ if (!document.uri.endsWith('.ripple')) {
69
+ return null;
70
+ }
71
+
72
+ // Only checking for '>' insertions
73
+ if (!lastChange.text.endsWith('>')) {
74
+ return null;
75
+ }
76
+
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
+ );
89
+
90
+ // Map position back to source
91
+ const offset = document.offsetAt(position);
92
+ const mapping = virtualCode.findMappingByGeneratedRange(lastChange.rangeOffset, offset);
93
+
94
+ if (!mapping) {
95
+ return null;
96
+ }
97
+
98
+ const sourceOffset = mapping.sourceOffsets[0];
99
+
100
+ // search backwards from sourceOffset to find the line tag
101
+ const sourceCode = virtualCode.originalCode;
102
+ if (sourceCode[sourceOffset - 1] === '/') {
103
+ // self-closing tag '/>'
104
+ return null;
105
+ }
106
+
107
+ let attempts = 0;
108
+ let found = false;
109
+ let i = sourceOffset - 1;
110
+ for (; i >= 0; i--) {
111
+ const char = sourceCode[i];
112
+ if (char === '<') {
113
+ attempts++;
114
+ // Confirm that it's definitely the start of the tag
115
+ // We have `<` and `>` in source maps
116
+ if (virtualCode.findMappingBySourceRange(i, i + 1)) {
117
+ found = true;
118
+ break;
119
+ }
120
+ }
121
+
122
+ if (attempts === 3) {
123
+ break;
124
+ }
125
+ }
126
+
127
+ if (!found) {
128
+ // This shouldn't happen in reality
129
+ log(`No opening tag position found from source position ${sourceOffset}`);
130
+ return null;
131
+ }
132
+
133
+ const line = sourceCode.slice(i, sourceOffset + 1);
134
+
135
+ log('Auto-insert triggered at:', {
136
+ selection: `${position.line}:${position.character}`,
137
+ line,
138
+ change: lastChange,
139
+ sourceOffset,
140
+ });
141
+
142
+ // Check if we just typed '>' after a tag name
143
+ // Match patterns like: <div> or <Component> but not <div /> or <Component/>
144
+ const tagMatch = line.match(/<([@$\w][\w.-]*)[^>]*?(?<!\/)>$/);
145
+ if (!tagMatch) {
146
+ log('No tag match found');
147
+ return null;
148
+ }
149
+
150
+ const tagName = tagMatch[1];
151
+ log('Tag matched:', tagName);
152
+
153
+ // Don't auto-close void elements (self-closing HTML tags)
154
+ if (VOID_ELEMENTS.has(tagName.toLowerCase())) {
155
+ log('Void element, skipping auto-close:', tagName);
156
+ return null;
157
+ }
158
+
159
+ // Check if there's already a closing tag ahead
160
+ const restOfLine = document.getText({
161
+ start: position,
162
+ end: { line: position.line, character: position.character + 100 },
163
+ });
164
+ if (restOfLine.startsWith(`</${tagName}>`)) {
165
+ log('Closing tag already exists, skipping');
166
+ return null;
167
+ }
168
+
169
+ // Insert the closing tag
170
+ const closingTag = `</${tagName}>`;
171
+ log('Inserting closing tag:', closingTag);
172
+
173
+ // Return a snippet with $0 to place cursor between the tags
174
+ return `$0${closingTag}`;
175
+ },
176
+ };
177
+ },
178
+ };
179
+ }
180
+
181
+ module.exports = {
182
+ createAutoInsertPlugin,
183
+ };
@@ -0,0 +1,321 @@
1
+ /**
2
+ * @typedef {import('@volar/language-server').LanguageServicePlugin} LanguageServicePlugin
3
+ */
4
+
5
+ const { CompletionItemKind, InsertTextFormat } = require('@volar/language-server');
6
+ const { URI } = require('vscode-uri');
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
+ }
18
+
19
+ /**
20
+ * Ripple-specific completion enhancements
21
+ * Adds custom completions for Ripple syntax patterns
22
+ */
23
+ const RIPPLE_SNIPPETS = [
24
+ {
25
+ label: 'component',
26
+ kind: CompletionItemKind.Snippet,
27
+ detail: 'Ripple Component',
28
+ documentation: 'Create a new Ripple component',
29
+ insertText: 'component ${1:ComponentName}(${2:props}) {\n\t$0\n}',
30
+ insertTextFormat: InsertTextFormat.Snippet,
31
+ sortText: '0-component',
32
+ },
33
+ {
34
+ label: 'track',
35
+ kind: CompletionItemKind.Snippet,
36
+ detail: 'Reactive state with track',
37
+ documentation: 'Create a reactive tracked value',
38
+ insertText: 'let ${1:name} = track(${2:initialValue});',
39
+ insertTextFormat: InsertTextFormat.Snippet,
40
+ sortText: '0-track',
41
+ },
42
+ {
43
+ label: 'track-derived',
44
+ kind: CompletionItemKind.Snippet,
45
+ detail: 'Derived reactive value',
46
+ documentation: 'Create a derived reactive value',
47
+ insertText: 'let ${1:name} = track(() => ${2:@dependency});',
48
+ insertTextFormat: InsertTextFormat.Snippet,
49
+ sortText: '0-track-derived',
50
+ },
51
+ {
52
+ label: 'track-getter-setter',
53
+ kind: CompletionItemKind.Snippet,
54
+ detail: 'track with get/set',
55
+ documentation: 'Create tracked value with custom getter/setter',
56
+ insertText:
57
+ 'let ${1:name} = track(${2:0},\n\t(current) => {\n\t\t$3\n\t\treturn current;\n\t},\n\t(next, prev) => {\n\t\t$4\n\t\treturn next;\n\t}\n);',
58
+ insertTextFormat: InsertTextFormat.Snippet,
59
+ sortText: '0-track-getter-setter',
60
+ },
61
+ {
62
+ label: 'trackSplit',
63
+ kind: CompletionItemKind.Snippet,
64
+ detail: 'Split props with trackSplit',
65
+ documentation: 'Destructure props while preserving reactivity',
66
+ insertText: "const [${1:children}, ${2:rest}] = trackSplit(props, [${3:'children'}]);",
67
+ insertTextFormat: InsertTextFormat.Snippet,
68
+ sortText: '0-trackSplit',
69
+ },
70
+ {
71
+ label: 'effect',
72
+ kind: CompletionItemKind.Snippet,
73
+ detail: 'Create an effect',
74
+ documentation: 'Run side effects when reactive dependencies change',
75
+ insertText: 'effect(() => {\n\t${1:console.log(@value);}\n});',
76
+ insertTextFormat: InsertTextFormat.Snippet,
77
+ sortText: '0-effect',
78
+ },
79
+ {
80
+ label: 'for-of',
81
+ kind: CompletionItemKind.Snippet,
82
+ detail: 'for...of loop',
83
+ documentation: 'Iterate over items in Ripple template',
84
+ insertText: 'for (const ${1:item} of ${2:items}) {\n\t<${3:li}>{${1:item}}</${3:li}>\n}',
85
+ insertTextFormat: InsertTextFormat.Snippet,
86
+ sortText: '0-for-of',
87
+ },
88
+ {
89
+ label: 'for-index',
90
+ kind: CompletionItemKind.Snippet,
91
+ detail: 'for...of loop with index',
92
+ documentation: 'Iterate with index',
93
+ insertText:
94
+ 'for (const ${1:item} of ${2:items}; index ${3:i}) {\n\t<${4:li}>{${1:item}}{" at "}{${3:i}}</${4:li}>\n}',
95
+ insertTextFormat: InsertTextFormat.Snippet,
96
+ sortText: '0-for-index',
97
+ },
98
+ {
99
+ label: 'for-key',
100
+ kind: CompletionItemKind.Snippet,
101
+ detail: 'for...of loop with key',
102
+ documentation: 'Iterate with key for identity',
103
+ insertText:
104
+ 'for (const ${1:item} of ${2:items}; key ${1:item}.${3:id}) {\n\t<${4:li}>{${1:item}.${5:text}}</${4:li}>\n}',
105
+ insertTextFormat: InsertTextFormat.Snippet,
106
+ sortText: '0-for-key',
107
+ },
108
+ {
109
+ label: 'for-index-key',
110
+ kind: CompletionItemKind.Snippet,
111
+ detail: 'for...of loop with key',
112
+ documentation: 'Iterate with key for identity',
113
+ insertText:
114
+ "for (const ${1:item} of ${2:items}; index ${3:i}; key ${1:item}.${4:id}) {\n\t<${5:li}>{${1:item}.${6:text}}{' at index '}{${3}}</${5:li}>\n}",
115
+ insertTextFormat: InsertTextFormat.Snippet,
116
+ sortText: '0-for-key-index',
117
+ },
118
+ {
119
+ label: 'if-else',
120
+ kind: CompletionItemKind.Snippet,
121
+ detail: 'if...else statement',
122
+ documentation: 'Conditional rendering',
123
+ insertText: 'if (${1:condition}) {\n\t$2\n} else {\n\t$3\n}',
124
+ insertTextFormat: InsertTextFormat.Snippet,
125
+ sortText: '0-if-else',
126
+ },
127
+ {
128
+ label: 'switch-case',
129
+ kind: CompletionItemKind.Snippet,
130
+ detail: 'switch statement',
131
+ documentation: 'Switch-based conditional rendering',
132
+ insertText:
133
+ "switch (${1:value}) {\n\tcase ${2:'case1'}:\n\t\t$3\n\t\tbreak;\n\tcase ${4:'case2'}:\n\t\t$5\n\t\tbreak;\n\tdefault:\n\t\t$6\n}",
134
+ insertTextFormat: InsertTextFormat.Snippet,
135
+ sortText: '0-switch-case',
136
+ },
137
+ {
138
+ label: 'untrack',
139
+ kind: CompletionItemKind.Snippet,
140
+ detail: 'Untrack reactive value',
141
+ documentation: 'Read reactive value without creating dependency',
142
+ insertText: 'untrack(() => @${1:value})',
143
+ insertTextFormat: InsertTextFormat.Snippet,
144
+ sortText: '0-untrack',
145
+ },
146
+ ];
147
+
148
+ /**
149
+ * Import suggestions for Ripple
150
+ */
151
+ const RIPPLE_IMPORTS = [
152
+ {
153
+ label: 'import track',
154
+ kind: CompletionItemKind.Snippet,
155
+ detail: 'Import track from ripple',
156
+ insertText: "import { track } from 'ripple';",
157
+ insertTextFormat: InsertTextFormat.Snippet,
158
+ sortText: '0-import-track',
159
+ },
160
+ {
161
+ label: 'import effect',
162
+ kind: CompletionItemKind.Snippet,
163
+ detail: 'Import effect from ripple',
164
+ insertText: "import { effect } from 'ripple';",
165
+ insertTextFormat: InsertTextFormat.Snippet,
166
+ sortText: '0-import-effect',
167
+ },
168
+ {
169
+ label: 'import trackSplit',
170
+ kind: CompletionItemKind.Snippet,
171
+ detail: 'Import trackSplit from ripple',
172
+ insertText: "import { trackSplit } from 'ripple';",
173
+ insertTextFormat: InsertTextFormat.Snippet,
174
+ sortText: '0-import-trackSplit',
175
+ },
176
+ {
177
+ label: 'import untrack',
178
+ kind: CompletionItemKind.Snippet,
179
+ detail: 'Import untrack from ripple',
180
+ insertText: "import { untrack } from 'ripple';",
181
+ insertTextFormat: InsertTextFormat.Snippet,
182
+ sortText: '0-import-untrack',
183
+ },
184
+ // {
185
+ // label: 'import ripple-types',
186
+ // kind: CompletionItemKind.Snippet,
187
+ // detail: 'Import Ripple types',
188
+ // insertText: "import type { Tracked, PropsWithChildren, Component } from 'ripple';",
189
+ // insertTextFormat: InsertTextFormat.Snippet,
190
+ // sortText: '0-import-types',
191
+ // },
192
+ ];
193
+
194
+ /**
195
+ * @returns {LanguageServicePlugin}
196
+ */
197
+ function createCompletionPlugin() {
198
+ return {
199
+ name: 'ripple-completion-enhancer',
200
+ capabilities: {
201
+ completionProvider: {
202
+ // Trigger on Ripple-specific syntax:
203
+ // '<' - JSX/HTML tags
204
+ // Avoid '.' and ' ' to reduce noise - let manual trigger (Ctrl+Space) handle those
205
+ triggerCharacters: ['<'],
206
+ resolveProvider: false,
207
+ },
208
+ },
209
+ // leaving context for future use
210
+ create(context) {
211
+ return {
212
+ // Mark this as providing additional completions, not replacing existing ones
213
+ // This ensures TypeScript/JavaScript completions are still shown alongside Ripple snippets
214
+ isAdditionalCompletion: true,
215
+ async provideCompletionItems(document, position, completionContext, _token) {
216
+ if (!document.uri.endsWith('.ripple')) {
217
+ return { items: [], isIncomplete: false };
218
+ }
219
+
220
+ // Check if we're inside an embedded code (like CSS in <style> blocks)
221
+ // 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
+ }
236
+ }
237
+
238
+ const line = document.getText({
239
+ start: { line: position.line, character: 0 },
240
+ end: position,
241
+ });
242
+
243
+ const items = [];
244
+
245
+ // Debug: log trigger info with clear marker
246
+ // triggerKind: 1 = Invoked (Ctrl+Space), 2 = TriggerCharacter, 3 = TriggerForIncompleteCompletions
247
+ log('🔔 Completion triggered:', {
248
+ triggerKind: completionContext.triggerKind,
249
+ triggerKindName:
250
+ completionContext.triggerKind === 1
251
+ ? 'Invoked'
252
+ : completionContext.triggerKind === 2
253
+ ? 'TriggerCharacter'
254
+ : completionContext.triggerKind === 3
255
+ ? 'Incomplete'
256
+ : 'Unknown',
257
+ triggerCharacter: completionContext.triggerCharacter || '(none)',
258
+ position: `${position.line}:${position.character}`,
259
+ lineEnd: line.substring(Math.max(0, line.length - 30)),
260
+ });
261
+
262
+ // Import completions when line starts with 'import'
263
+ if (line.trim().startsWith('import')) {
264
+ items.push(...RIPPLE_IMPORTS);
265
+ }
266
+
267
+ // @ accessor hint when typing after @
268
+ if (/@\w*$/.test(line)) {
269
+ items.push({
270
+ label: '@value',
271
+ kind: CompletionItemKind.Variable,
272
+ detail: 'Access tracked value',
273
+ documentation: 'Use @ to read/write tracked values',
274
+ });
275
+ }
276
+
277
+ // Ripple keywords - extract the last word being typed
278
+ const wordMatch = line.match(/(\w+)$/);
279
+ const currentWord = wordMatch ? wordMatch[1] : '';
280
+
281
+ // Debug: show what word we're matching
282
+ log('Current word:', currentWord, 'length:', currentWord.length);
283
+
284
+ // ALWAYS provide Ripple snippets and keywords
285
+ // Even with 1 character, we return items so that when combined with TypeScript completions,
286
+ // the merged result will include our items. VS Code's fuzzy matching will filter them.
287
+ items.push(...RIPPLE_SNIPPETS);
288
+
289
+ // Return isIncomplete=false and let VS Code handle filtering
290
+ // Since we're providing all items every time, VS Code can cache and filter client-side
291
+ // This works because our items have proper labels that match VS Code's fuzzy matching
292
+ return { items, isIncomplete: currentWord.length < 2 };
293
+ },
294
+ };
295
+ },
296
+ };
297
+ }
298
+
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
+ module.exports = {
320
+ createCompletionPlugin,
321
+ };
@@ -1,10 +1,7 @@
1
- const { RippleVirtualCode } = require('@ripple-ts/typescript-plugin/src/language.js');
2
- const { URI } = require('vscode-uri');
1
+ /** @import { LanguageServicePlugin } from '@volar/language-server' */
2
+ /** @import { RippleVirtualCode } from '@ripple-ts/typescript-plugin/src/language.js') */
3
3
 
4
- /**
5
- * @typedef {import('@volar/language-server').LanguageServicePlugin} LanguageServicePlugin
6
- * @typedef {import('@volar/language-server').LanguageServiceContext} LanguageServiceContext
7
- */
4
+ const { URI } = require('vscode-uri');
8
5
 
9
6
  const DEBUG = process.env.RIPPLE_DEBUG === 'true';
10
7
 
@@ -26,14 +23,8 @@ function createDefinitionPlugin() {
26
23
  capabilities: {
27
24
  definitionProvider: true,
28
25
  },
29
- create(/** @type {LanguageServiceContext} */ context) {
26
+ create(context) {
30
27
  return {
31
- /**
32
- * Provide definition with component keyword support
33
- * @param {import('vscode-languageserver-textdocument').TextDocument} document
34
- * @param {import('@volar/language-server').Position} position
35
- * @param {import('@volar/language-server').CancellationToken} token
36
- */
37
28
  async provideDefinition(document, position, token) {
38
29
  const uri = URI.parse(document.uri);
39
30
  const decoded = context.decodeEmbeddedDocumentUri(uri);
@@ -63,11 +54,9 @@ function createDefinitionPlugin() {
63
54
 
64
55
  const [sourceUri, virtualCodeId] = decoded;
65
56
  const sourceScript = context.language.scripts.get(sourceUri);
66
- const virtualCode = sourceScript?.generated?.embeddedCodes.get(virtualCodeId);
67
-
68
- if (!(virtualCode instanceof RippleVirtualCode) || !virtualCode.mappings) {
69
- return tsDefinitions;
70
- }
57
+ const virtualCode = /** @type {RippleVirtualCode } */ (
58
+ sourceScript?.generated?.embeddedCodes.get(virtualCodeId)
59
+ );
71
60
 
72
61
  // Get the range from TypeScript's definition to find the exact token
73
62
  // This gives us the precise start and end of the token (e.g., "function")
@@ -0,0 +1,130 @@
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 */
4
+
5
+ const { URI } = require('vscode-uri');
6
+
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
+ }
24
+
25
+ /**
26
+ * @returns {LanguageServicePlugin}
27
+ */
28
+ function createHoverPlugin() {
29
+ return {
30
+ name: 'ripple-hover',
31
+ capabilities: {
32
+ hoverProvider: true,
33
+ },
34
+ create(context) {
35
+ /** @type {LanguageServicePluginInstance['provideHover']} */
36
+ let originalProvideHover;
37
+ /** @type {LanguageServicePluginInstance} */
38
+ let originalInstance;
39
+
40
+ // Disable typescript-semantic's provideHover so it doesn't merge with ours
41
+ for (const [plugin, instance] of context.plugins) {
42
+ if (plugin.name === 'typescript-semantic') {
43
+ originalInstance = instance;
44
+ originalProvideHover = instance.provideHover;
45
+ instance.provideHover = undefined;
46
+ break;
47
+ }
48
+ }
49
+
50
+ if (!originalProvideHover) {
51
+ logError(
52
+ "'typescript-semantic plugin' was not found or has no 'provideHover'. \
53
+ This plugin must be loaded after Volar's typescript-semantic plugin.",
54
+ );
55
+ }
56
+ return {
57
+ async provideHover(document, position, token) {
58
+ const uri = URI.parse(document.uri);
59
+ const decoded = context.decodeEmbeddedDocumentUri(uri);
60
+
61
+ // Get TypeScript hover from typescript-semantic service
62
+ let tsHover = null;
63
+ if (originalProvideHover) {
64
+ tsHover = await originalProvideHover.call(originalInstance, document, position, token);
65
+ }
66
+
67
+ // If no TypeScript hover, nothing to modify
68
+ if (!tsHover) {
69
+ return;
70
+ }
71
+
72
+ // If not in a Ripple embedded context, just return TypeScript results
73
+ if (!decoded) {
74
+ return tsHover;
75
+ }
76
+
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) {
85
+ return tsHover;
86
+ }
87
+
88
+ const range = tsHover.range;
89
+ const rangeStart = document.offsetAt(range.start);
90
+ const rangeEnd = document.offsetAt(range.end);
91
+
92
+ const mapping = virtualCode.findMappingByGeneratedRange(rangeStart, rangeEnd);
93
+
94
+ if (!mapping) {
95
+ return tsHover;
96
+ }
97
+
98
+ log('Found mapping for hover at range', 'start: ', rangeStart, 'end: ', rangeEnd);
99
+
100
+ // Check if source length is greater than generated length (component -> function)
101
+ const customData = mapping.data.customData;
102
+ const sourceLength = mapping.lengths[0];
103
+ const generatedLength = customData.generatedLengths[0];
104
+
105
+ // If no generatedLengths, or source and generated are same length, no transformation
106
+ if (sourceLength <= generatedLength) {
107
+ return tsHover;
108
+ }
109
+
110
+ const diffLength = sourceLength - generatedLength;
111
+
112
+ // Adjust the hover range to highlight the full "component" keyword
113
+ tsHover.range = {
114
+ start: range.start,
115
+ end: {
116
+ line: range.end.line,
117
+ character: range.end.character + diffLength,
118
+ },
119
+ };
120
+
121
+ return tsHover;
122
+ },
123
+ };
124
+ },
125
+ };
126
+ }
127
+
128
+ module.exports = {
129
+ createHoverPlugin,
130
+ };
package/src/server.js CHANGED
@@ -5,11 +5,15 @@
5
5
  } = require('@volar/language-server/node');
6
6
  const { createDiagnosticPlugin } = require('./diagnosticPlugin.js');
7
7
  const { createDefinitionPlugin } = require('./definitionPlugin.js');
8
+ const { createHoverPlugin } = require('./hoverPlugin.js');
9
+ const { createCompletionPlugin } = require('./completionPlugin.js');
10
+ const { createAutoInsertPlugin } = require('./autoInsertPlugin.js');
8
11
  const {
9
12
  getRippleLanguagePlugin,
10
13
  resolveConfig,
11
14
  } = require('@ripple-ts/typescript-plugin/src/language.js');
12
- const { create: createTypeScriptServices } = require('volar-service-typescript');
15
+ const { createTypeScriptServices } = require('./typescriptService.js');
16
+ const { create: createCssService } = require('volar-service-css');
13
17
 
14
18
  const DEBUG = process.env.RIPPLE_DEBUG === 'true';
15
19
 
@@ -103,7 +107,17 @@ function createRippleLanguageServer() {
103
107
  },
104
108
  };
105
109
  }),
106
- [createDiagnosticPlugin(), createDefinitionPlugin(), ...createTypeScriptServices(ts)],
110
+ [
111
+ createAutoInsertPlugin(),
112
+ createCompletionPlugin(),
113
+ createDiagnosticPlugin(),
114
+ createDefinitionPlugin(),
115
+ createCssService(),
116
+ ...createTypeScriptServices(ts),
117
+ // !IMPORTANT createHoverPlugin has to be loaded after Volar's ts plugins
118
+ // to overwrite `typescript-semantic` plugin's `provideHover`
119
+ createHoverPlugin(),
120
+ ],
107
121
  );
108
122
 
109
123
  log('Server initialization complete');
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @typedef {import('@volar/language-server').LanguageServiceContext} LanguageServiceContext
3
+ * @typedef {import('vscode-languageserver-textdocument').TextDocument} TextDocument
4
+ */
5
+
6
+ // Monkey-patch getUserPreferences before requiring the main module
7
+ const getUserPreferencesModule = require('volar-service-typescript/lib/configs/getUserPreferences');
8
+ const originalGetUserPreferences = getUserPreferencesModule.getUserPreferences;
9
+
10
+ /**
11
+ * Enhanced getUserPreferences to add all ts and ripple preferences
12
+ * Specifically makes preferTypeOnlyAutoImports true if not set
13
+ * @param {LanguageServiceContext} context
14
+ * @param {TextDocument} document
15
+ */
16
+ getUserPreferencesModule.getUserPreferences = async function (context, document) {
17
+ const origPreferences = await originalGetUserPreferences.call(this, context, document);
18
+
19
+ const [tsConfig, rippleConfig] = await Promise.all([
20
+ context.env.getConfiguration?.('typescript'),
21
+ context.env.getConfiguration?.('ripple'),
22
+ ]);
23
+
24
+ return {
25
+ preferTypeOnlyAutoImports: true,
26
+ ...origPreferences,
27
+ ...tsConfig?.preferences,
28
+ ...rippleConfig?.preferences,
29
+ };
30
+ };
31
+
32
+ // Now require the main module which will use our patched getUserPreferences
33
+ const { create } = require('volar-service-typescript');
34
+
35
+ /**
36
+ * Create TypeScript services with Ripple-specific enhancements.
37
+ * @param {typeof import('typescript')} ts
38
+ * @returns {ReturnType<typeof create>}
39
+ */
40
+ function createTypeScriptServices(ts) {
41
+ return create(ts);
42
+ }
43
+
44
+ module.exports = {
45
+ createTypeScriptServices,
46
+ };