@ripple-ts/language-server 0.3.41 → 0.3.42
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/dist/language-server.js +9 -0
- package/dist/language-server.js.map +1 -0
- package/dist/package.json +1 -0
- package/dist/server.js +111 -0
- package/dist/server.js.map +1 -0
- package/dist/typescriptService-CWMxKghf.js +21866 -0
- package/dist/typescriptService-CWMxKghf.js.map +1 -0
- package/package.json +8 -5
- package/CHANGELOG.md +0 -513
- package/bin/language-server.js +0 -5
- package/src/autoInsertPlugin.js +0 -163
- package/src/compileErrorDiagnosticPlugin.js +0 -155
- package/src/completionPlugin.js +0 -508
- package/src/definitionPlugin.js +0 -208
- package/src/documentHighlightPlugin.js +0 -118
- package/src/hoverPlugin.js +0 -146
- package/src/server.js +0 -155
- package/src/typescriptDiagnosticPlugin.js +0 -139
- package/src/typescriptService.js +0 -49
- package/src/utils.js +0 -162
- package/tsconfig.json +0 -17
- package/tsconfig.typecheck.json +0 -7
- package/tsdown.config.js +0 -41
package/src/completionPlugin.js
DELETED
|
@@ -1,508 +0,0 @@
|
|
|
1
|
-
/** @import { LanguageServicePlugin, TextEdit, CompletionItem } from '@volar/language-server'; */
|
|
2
|
-
|
|
3
|
-
import { CompletionItemKind, InsertTextFormat } from '@volar/language-server';
|
|
4
|
-
import {
|
|
5
|
-
getVirtualCode,
|
|
6
|
-
createLogging,
|
|
7
|
-
isInsideImport,
|
|
8
|
-
isInsideExport,
|
|
9
|
-
is_ripple_document,
|
|
10
|
-
} from './utils.js';
|
|
11
|
-
|
|
12
|
-
const { log } = createLogging('[Ripple Completion Plugin]');
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Snippets that require auto-import from 'ripple'
|
|
16
|
-
* @type {Array<{label: string, filterText: string, detail: string, documentation: string, insertText: string, importName: string | null}>}
|
|
17
|
-
*/
|
|
18
|
-
const TRACKED_COLLECTION_SNIPPETS = [
|
|
19
|
-
{
|
|
20
|
-
label: 'RippleMap',
|
|
21
|
-
filterText: 'RippleMap',
|
|
22
|
-
detail: 'Create a RippleMap',
|
|
23
|
-
documentation: 'A reactive Map that triggers updates when modified',
|
|
24
|
-
insertText: 'new RippleMap(${1})',
|
|
25
|
-
importName: 'RippleMap',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
label: 'RippleSet',
|
|
29
|
-
filterText: 'RippleSet',
|
|
30
|
-
detail: 'Create a RippleSet',
|
|
31
|
-
documentation: 'A reactive Set that triggers updates when modified',
|
|
32
|
-
insertText: 'new RippleSet(${1})',
|
|
33
|
-
importName: 'RippleSet',
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
label: 'RippleArray',
|
|
37
|
-
filterText: 'RippleArray',
|
|
38
|
-
detail: 'Create a RippleArray',
|
|
39
|
-
documentation: 'A reactive Array that triggers updates when modified',
|
|
40
|
-
insertText: 'new RippleArray(${1})',
|
|
41
|
-
importName: 'RippleArray',
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
label: 'RippleArray.from',
|
|
45
|
-
filterText: 'RippleArray.from',
|
|
46
|
-
detail: 'Create a RippleArray.from',
|
|
47
|
-
documentation: 'A reactive Array that triggers when modified',
|
|
48
|
-
insertText: 'new RippleArray.from(${1})',
|
|
49
|
-
importName: 'RippleArray',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
label: 'RippleObject',
|
|
53
|
-
filterText: 'RippleObject',
|
|
54
|
-
detail: 'Create a RippleObject',
|
|
55
|
-
documentation: 'A reactive Object that triggers updates when modified',
|
|
56
|
-
insertText: 'new RippleObject(${1})',
|
|
57
|
-
importName: 'RippleObject',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
label: 'RippleDate',
|
|
61
|
-
filterText: 'RippleDate',
|
|
62
|
-
detail: 'Create a RippleDate',
|
|
63
|
-
documentation: 'A reactive Date that triggers updates when modified',
|
|
64
|
-
insertText: 'new RippleDate(${1})',
|
|
65
|
-
importName: 'RippleDate',
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
label: 'RippleURL',
|
|
69
|
-
filterText: 'RippleURL',
|
|
70
|
-
detail: 'Create a RippleURL',
|
|
71
|
-
documentation: 'A reactive URL that triggers updates when modified',
|
|
72
|
-
insertText: 'new RippleURL(${1})',
|
|
73
|
-
importName: 'RippleURL',
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
label: 'RippleURLSearchParams',
|
|
77
|
-
filterText: 'RippleURLSearchParams',
|
|
78
|
-
detail: 'Create a RippleURLSearchParams',
|
|
79
|
-
documentation: 'A reactive URLSearchParams that triggers updates when modified',
|
|
80
|
-
insertText: 'new RippleURLSearchParams(${1})',
|
|
81
|
-
importName: 'RippleURLSearchParams',
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
label: 'MediaQuery',
|
|
85
|
-
filterText: 'MediaQuery',
|
|
86
|
-
detail: 'Create a MediaQuery',
|
|
87
|
-
documentation: 'A reactive media query that triggers updates when the query match changes',
|
|
88
|
-
insertText: 'new MediaQuery(${1})',
|
|
89
|
-
importName: 'MediaQuery',
|
|
90
|
-
},
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Find the ripple import statement in the document
|
|
95
|
-
* @param {string} text - Full document text
|
|
96
|
-
* @returns {{line: number, startChar: number, endChar: number, imports: string[], hasSemicolon: boolean, fullMatch: string} | null}
|
|
97
|
-
*/
|
|
98
|
-
function findRippleImport(text) {
|
|
99
|
-
const lines = text.split('\n');
|
|
100
|
-
for (let i = 0; i < lines.length; i++) {
|
|
101
|
-
const line = lines[i];
|
|
102
|
-
// Match: import { x, y, z } from 'ripple'; (with optional semicolon and trailing whitespace)
|
|
103
|
-
const match = line.match(/^import\s*\{([^}]+)\}\s*from\s*['"]ripple['"](;?)(\s*)$/);
|
|
104
|
-
if (match) {
|
|
105
|
-
const imports = match[1]
|
|
106
|
-
.split(',')
|
|
107
|
-
.map((s) => s.trim())
|
|
108
|
-
.filter(Boolean);
|
|
109
|
-
return {
|
|
110
|
-
line: i,
|
|
111
|
-
startChar: 0,
|
|
112
|
-
endChar: line.length,
|
|
113
|
-
imports,
|
|
114
|
-
hasSemicolon: match[2] === ';',
|
|
115
|
-
fullMatch: line,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Generate additionalTextEdits to add an import
|
|
124
|
-
* @param {string} documentText - Full document text
|
|
125
|
-
* @param {string} importName - Name to import (e.g., 'RippleMap')
|
|
126
|
-
* @returns {TextEdit[]}
|
|
127
|
-
*/
|
|
128
|
-
function generateImportEdit(documentText, importName) {
|
|
129
|
-
const existing = findRippleImport(documentText);
|
|
130
|
-
|
|
131
|
-
if (existing) {
|
|
132
|
-
// Check if already imported
|
|
133
|
-
if (existing.imports.includes(importName)) {
|
|
134
|
-
return []; // Already imported, no edit needed
|
|
135
|
-
}
|
|
136
|
-
// Add to existing import, preserving semicolon status
|
|
137
|
-
const newImports = [...existing.imports, importName].sort().join(', ');
|
|
138
|
-
const semicolon = existing.hasSemicolon ? ';' : '';
|
|
139
|
-
const newLine = `import { ${newImports} } from 'ripple'${semicolon}`;
|
|
140
|
-
return [
|
|
141
|
-
{
|
|
142
|
-
range: {
|
|
143
|
-
start: { line: existing.line, character: 0 },
|
|
144
|
-
end: { line: existing.line, character: existing.endChar },
|
|
145
|
-
},
|
|
146
|
-
newText: newLine,
|
|
147
|
-
},
|
|
148
|
-
];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// No existing ripple import - add new one at the top
|
|
152
|
-
// Find the best insertion point (after other imports, or at line 0)
|
|
153
|
-
const lines = documentText.split('\n');
|
|
154
|
-
let insertLine = 0;
|
|
155
|
-
for (let i = 0; i < lines.length; i++) {
|
|
156
|
-
if (lines[i].match(/^import\s/)) {
|
|
157
|
-
insertLine = i + 1; // Insert after last import
|
|
158
|
-
} else if (insertLine > 0 && !lines[i].match(/^import\s/) && lines[i].trim() !== '') {
|
|
159
|
-
break; // Stop if we've passed the import block
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return [
|
|
164
|
-
{
|
|
165
|
-
range: {
|
|
166
|
-
start: { line: insertLine, character: 0 },
|
|
167
|
-
end: { line: insertLine, character: 0 },
|
|
168
|
-
},
|
|
169
|
-
newText: `import { ${importName} } from 'ripple';\n`,
|
|
170
|
-
},
|
|
171
|
-
];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Ripple-specific completion enhancements
|
|
176
|
-
* Adds custom completions for Ripple syntax patterns
|
|
177
|
-
*/
|
|
178
|
-
const RIPPLE_SNIPPETS = [
|
|
179
|
-
{
|
|
180
|
-
label: '#style',
|
|
181
|
-
kind: CompletionItemKind.Snippet,
|
|
182
|
-
detail: 'Scoped CSS class reference',
|
|
183
|
-
documentation:
|
|
184
|
-
'Produces a scoped CSS class string for passing to child components.\nThe class must be defined as a standalone selector in <style>.\n\nUsage: <Child cls={#style.highlight} />',
|
|
185
|
-
insertText: '#style.${1:className}',
|
|
186
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
187
|
-
sortText: '0-#-style',
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
label: '#server',
|
|
191
|
-
kind: CompletionItemKind.Snippet,
|
|
192
|
-
detail: 'Server-only code block (module level)',
|
|
193
|
-
documentation:
|
|
194
|
-
'Marks a block as server-only. Code inside is tree-shaken on the client.\nMust be at module top level.\n\nUsage:\n#server {\n export async function loadData() { ... }\n}',
|
|
195
|
-
insertText: '#server {\n\t$0\n}',
|
|
196
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
197
|
-
sortText: '0-#-server',
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
label: 'component',
|
|
201
|
-
kind: CompletionItemKind.Snippet,
|
|
202
|
-
detail: 'Ripple Component',
|
|
203
|
-
documentation: 'Create a new Ripple component',
|
|
204
|
-
insertText: 'component ${1:ComponentName}(${2:props}) {\n\t$0\n}',
|
|
205
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
206
|
-
sortText: '0-component',
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
label: 'track',
|
|
210
|
-
kind: CompletionItemKind.Snippet,
|
|
211
|
-
detail: 'Reactive state with track',
|
|
212
|
-
documentation: 'Create a reactive tracked value',
|
|
213
|
-
insertText: 'let ${1:name} = track(${2:initialValue});',
|
|
214
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
215
|
-
sortText: '0-track',
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
label: 'track-derived',
|
|
219
|
-
kind: CompletionItemKind.Snippet,
|
|
220
|
-
detail: 'Derived reactive value',
|
|
221
|
-
documentation: 'Create a derived reactive value',
|
|
222
|
-
insertText: 'let ${1:name} = track(() => ${2:@dependency});',
|
|
223
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
224
|
-
sortText: '0-track-derived',
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
label: 'track-getter-setter',
|
|
228
|
-
kind: CompletionItemKind.Snippet,
|
|
229
|
-
detail: 'track with get/set',
|
|
230
|
-
documentation: 'Create tracked value with custom getter/setter',
|
|
231
|
-
insertText:
|
|
232
|
-
'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);',
|
|
233
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
234
|
-
sortText: '0-track-getter-setter',
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
label: 'effect',
|
|
238
|
-
kind: CompletionItemKind.Snippet,
|
|
239
|
-
detail: 'Create an effect',
|
|
240
|
-
documentation: 'Run side effects when reactive dependencies change',
|
|
241
|
-
insertText: 'effect(() => {\n\t${1:console.log(@value);}\n});',
|
|
242
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
243
|
-
sortText: '0-effect',
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
label: 'for-of',
|
|
247
|
-
kind: CompletionItemKind.Snippet,
|
|
248
|
-
detail: 'for...of loop',
|
|
249
|
-
documentation: 'Iterate over items in Ripple template',
|
|
250
|
-
insertText: 'for (const ${1:item} of ${2:items}) {\n\t<${3:li}>{${1:item}}</${3:li}>\n}',
|
|
251
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
252
|
-
sortText: '0-for-of',
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
label: 'for-index',
|
|
256
|
-
kind: CompletionItemKind.Snippet,
|
|
257
|
-
detail: 'for...of loop with index',
|
|
258
|
-
documentation: 'Iterate with index',
|
|
259
|
-
insertText:
|
|
260
|
-
'for (const ${1:item} of ${2:items}; index ${3:i}) {\n\t<${4:li}>{${1:item}}{" at "}{${3:i}}</${4:li}>\n}',
|
|
261
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
262
|
-
sortText: '0-for-index',
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
label: 'for-key',
|
|
266
|
-
kind: CompletionItemKind.Snippet,
|
|
267
|
-
detail: 'for...of loop with key',
|
|
268
|
-
documentation: 'Iterate with key for identity',
|
|
269
|
-
insertText:
|
|
270
|
-
'for (const ${1:item} of ${2:items}; key ${1:item}.${3:id}) {\n\t<${4:li}>{${1:item}.${5:text}}</${4:li}>\n}',
|
|
271
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
272
|
-
sortText: '0-for-key',
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
label: 'for-index-key',
|
|
276
|
-
kind: CompletionItemKind.Snippet,
|
|
277
|
-
detail: 'for...of loop with key',
|
|
278
|
-
documentation: 'Iterate with key for identity',
|
|
279
|
-
insertText:
|
|
280
|
-
"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}",
|
|
281
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
282
|
-
sortText: '0-for-key-index',
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
label: 'if-else',
|
|
286
|
-
kind: CompletionItemKind.Snippet,
|
|
287
|
-
detail: 'if...else statement',
|
|
288
|
-
documentation: 'Conditional rendering',
|
|
289
|
-
insertText: 'if (${1:condition}) {\n\t$2\n} else {\n\t$3\n}',
|
|
290
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
291
|
-
sortText: '0-if-else',
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
label: 'switch-case',
|
|
295
|
-
kind: CompletionItemKind.Snippet,
|
|
296
|
-
detail: 'switch statement',
|
|
297
|
-
documentation: 'Switch-based conditional rendering',
|
|
298
|
-
insertText:
|
|
299
|
-
"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}",
|
|
300
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
301
|
-
sortText: '0-switch-case',
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
label: 'untrack',
|
|
305
|
-
kind: CompletionItemKind.Snippet,
|
|
306
|
-
detail: 'Untrack reactive value',
|
|
307
|
-
documentation: 'Read reactive value without creating dependency',
|
|
308
|
-
insertText: 'untrack(() => @${1:value})',
|
|
309
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
310
|
-
sortText: '0-untrack',
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
label: 'try-pending',
|
|
314
|
-
kind: CompletionItemKind.Snippet,
|
|
315
|
-
detail: 'try...pending block',
|
|
316
|
-
documentation: 'Handle async content with loading fallback',
|
|
317
|
-
insertText: "try {\n\t$1\n} pending {\n\t<div>{'Loading...'}</div>\n}",
|
|
318
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
319
|
-
sortText: '0-try-pending',
|
|
320
|
-
},
|
|
321
|
-
];
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Import suggestions for Ripple
|
|
325
|
-
*/
|
|
326
|
-
const RIPPLE_IMPORTS = [
|
|
327
|
-
{
|
|
328
|
-
label: 'import track',
|
|
329
|
-
kind: CompletionItemKind.Snippet,
|
|
330
|
-
detail: 'Import track from ripple',
|
|
331
|
-
insertText: "import { track } from 'ripple';",
|
|
332
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
333
|
-
sortText: '0-import-track',
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
label: 'import effect',
|
|
337
|
-
kind: CompletionItemKind.Snippet,
|
|
338
|
-
detail: 'Import effect from ripple',
|
|
339
|
-
insertText: "import { effect } from 'ripple';",
|
|
340
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
341
|
-
sortText: '0-import-effect',
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
label: 'import untrack',
|
|
345
|
-
kind: CompletionItemKind.Snippet,
|
|
346
|
-
detail: 'Import untrack from ripple',
|
|
347
|
-
insertText: "import { untrack } from 'ripple';",
|
|
348
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
349
|
-
sortText: '0-import-untrack',
|
|
350
|
-
},
|
|
351
|
-
// {
|
|
352
|
-
// label: 'import ripple-types',
|
|
353
|
-
// kind: CompletionItemKind.Snippet,
|
|
354
|
-
// detail: 'Import Ripple types',
|
|
355
|
-
// insertText: "import type { Tracked, PropsWithChildren, Component } from 'ripple';",
|
|
356
|
-
// insertTextFormat: InsertTextFormat.Snippet,
|
|
357
|
-
// sortText: '0-import-types',
|
|
358
|
-
// },
|
|
359
|
-
];
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* @returns {LanguageServicePlugin}
|
|
363
|
-
*/
|
|
364
|
-
export function createCompletionPlugin() {
|
|
365
|
-
return {
|
|
366
|
-
name: 'ripple-completion-enhancer',
|
|
367
|
-
capabilities: {
|
|
368
|
-
completionProvider: {
|
|
369
|
-
// Trigger on Ripple-specific syntax:
|
|
370
|
-
// '<' - JSX/HTML tags
|
|
371
|
-
// '#' - #style, #server keywords
|
|
372
|
-
triggerCharacters: ['<', '#'],
|
|
373
|
-
resolveProvider: false,
|
|
374
|
-
},
|
|
375
|
-
},
|
|
376
|
-
// leaving context for future use
|
|
377
|
-
create(context) {
|
|
378
|
-
return {
|
|
379
|
-
// Mark this as providing additional completions, not replacing existing ones
|
|
380
|
-
// This ensures TypeScript/JavaScript completions are still shown alongside Ripple snippets
|
|
381
|
-
isAdditionalCompletion: true,
|
|
382
|
-
async provideCompletionItems(document, position, completionContext, _token) {
|
|
383
|
-
if (!is_ripple_document(document.uri)) {
|
|
384
|
-
return { items: [], isIncomplete: false };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const { virtualCode } = getVirtualCode(document, context);
|
|
388
|
-
|
|
389
|
-
if (virtualCode && virtualCode.languageId !== 'ripple') {
|
|
390
|
-
// Check if we're inside an embedded code (like CSS in <style> blocks)
|
|
391
|
-
// If so, don't provide Ripple snippets - let CSS completions take priority
|
|
392
|
-
log(`Skipping Ripple completions in the '${virtualCode.languageId}' context`);
|
|
393
|
-
return { items: [], isIncomplete: false };
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const line = document.getText({
|
|
397
|
-
start: { line: position.line, character: 0 },
|
|
398
|
-
end: position,
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
/** @type {CompletionItem[]} */
|
|
402
|
-
const items = [];
|
|
403
|
-
|
|
404
|
-
// Debug: log trigger info with clear marker
|
|
405
|
-
// triggerKind: 1 = Invoked (Ctrl+Space), 2 = TriggerCharacter, 3 = TriggerForIncompleteCompletions
|
|
406
|
-
log('🔔 Completion triggered:', {
|
|
407
|
-
triggerKind: completionContext.triggerKind,
|
|
408
|
-
triggerKindName:
|
|
409
|
-
completionContext.triggerKind === 1
|
|
410
|
-
? 'Invoked'
|
|
411
|
-
: completionContext.triggerKind === 2
|
|
412
|
-
? 'TriggerCharacter'
|
|
413
|
-
: completionContext.triggerKind === 3
|
|
414
|
-
? 'Incomplete'
|
|
415
|
-
: 'Unknown',
|
|
416
|
-
triggerCharacter: completionContext.triggerCharacter || '(none)',
|
|
417
|
-
position: `${position.line}:${position.character}`,
|
|
418
|
-
lineEnd: line.substring(Math.max(0, line.length - 30)),
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
const fullText = document.getText();
|
|
422
|
-
const cursorOffset = document.offsetAt(position);
|
|
423
|
-
|
|
424
|
-
if (isInsideImport(fullText, cursorOffset)) {
|
|
425
|
-
items.push(...RIPPLE_IMPORTS);
|
|
426
|
-
return { items, isIncomplete: false };
|
|
427
|
-
} else if (isInsideExport(fullText, cursorOffset)) {
|
|
428
|
-
return { items, isIncomplete: false };
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// @ accessor hint when typing after @
|
|
432
|
-
if (/@\w*$/.test(line)) {
|
|
433
|
-
items.push({
|
|
434
|
-
label: '@value',
|
|
435
|
-
kind: CompletionItemKind.Variable,
|
|
436
|
-
detail: 'Access tracked value',
|
|
437
|
-
documentation: 'Use @ to read/write tracked values',
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// RippleMap/RippleSet completions when typing R, M...
|
|
442
|
-
// Also detects if 'new' is already typed before it to avoid duplicating
|
|
443
|
-
const trackedMatch = line.match(/(new\s+)?[R,M]([\w\.]*)$/);
|
|
444
|
-
|
|
445
|
-
if (trackedMatch) {
|
|
446
|
-
const hasNew = !!trackedMatch[1];
|
|
447
|
-
const typed = trackedMatch[2].toLowerCase();
|
|
448
|
-
|
|
449
|
-
for (const snippet of TRACKED_COLLECTION_SNIPPETS) {
|
|
450
|
-
// Match if typing matches start of 'rackedMap', 'rackedSet' (after T)
|
|
451
|
-
const afterT = snippet.label.slice(1).toLowerCase(); // 'rackedmap' or 'rackedset'
|
|
452
|
-
if (typed === '' || afterT.startsWith(typed)) {
|
|
453
|
-
// Determine insert text - skip 'new ' if already present
|
|
454
|
-
const insertText = hasNew
|
|
455
|
-
? `${snippet.label}(\${1})`
|
|
456
|
-
: `new ${snippet.label}(\${1})`;
|
|
457
|
-
|
|
458
|
-
items.push({
|
|
459
|
-
label: snippet.label,
|
|
460
|
-
filterText: snippet.filterText,
|
|
461
|
-
kind: CompletionItemKind.Snippet,
|
|
462
|
-
detail: snippet.detail,
|
|
463
|
-
documentation: snippet.documentation,
|
|
464
|
-
insertText: insertText,
|
|
465
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
466
|
-
sortText: '0-' + snippet.label.toLowerCase(),
|
|
467
|
-
// Replace 'T...' or 'new T...' depending on what was typed
|
|
468
|
-
textEdit: {
|
|
469
|
-
range: {
|
|
470
|
-
start: {
|
|
471
|
-
line: position.line,
|
|
472
|
-
character: position.character - trackedMatch[0].length,
|
|
473
|
-
},
|
|
474
|
-
end: position,
|
|
475
|
-
},
|
|
476
|
-
newText: insertText,
|
|
477
|
-
},
|
|
478
|
-
additionalTextEdits: snippet
|
|
479
|
-
? snippet.importName != null
|
|
480
|
-
? generateImportEdit(fullText, snippet.importName)
|
|
481
|
-
: undefined
|
|
482
|
-
: undefined,
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Ripple keywords - extract the last word being typed
|
|
489
|
-
const wordMatch = line.match(/(\w+)$/);
|
|
490
|
-
const currentWord = wordMatch ? wordMatch[1] : '';
|
|
491
|
-
|
|
492
|
-
// Debug: show what word we're matching
|
|
493
|
-
log('Current word:', currentWord, 'length:', currentWord.length);
|
|
494
|
-
|
|
495
|
-
// ALWAYS provide Ripple snippets and keywords
|
|
496
|
-
// Even with 1 character, we return items so that when combined with TypeScript completions,
|
|
497
|
-
// the merged result will include our items. VS Code's fuzzy matching will filter them.
|
|
498
|
-
items.push(...RIPPLE_SNIPPETS);
|
|
499
|
-
|
|
500
|
-
// Return isIncomplete=false and let VS Code handle filtering
|
|
501
|
-
// Since we're providing all items every time, VS Code can cache and filter client-side
|
|
502
|
-
// This works because our items have proper labels that match VS Code's fuzzy matching
|
|
503
|
-
return { items, isIncomplete: currentWord.length < 2 };
|
|
504
|
-
},
|
|
505
|
-
};
|
|
506
|
-
},
|
|
507
|
-
};
|
|
508
|
-
}
|