@ripple-ts/language-server 0.2.175 → 0.2.177
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 -2
- package/src/autoInsertPlugin.js +183 -0
- package/src/completionPlugin.js +321 -0
- package/src/definitionPlugin.js +7 -18
- package/src/hoverPlugin.js +130 -0
- package/src/server.js +16 -2
- package/src/typescriptService.js +46 -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.177",
|
|
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.
|
|
22
|
+
"@ripple-ts/typescript-plugin": "0.2.177"
|
|
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
|
+
};
|
package/src/definitionPlugin.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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(
|
|
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 =
|
|
67
|
-
|
|
68
|
-
|
|
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 {
|
|
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
|
-
[
|
|
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
|
+
};
|