@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.
@@ -1,208 +0,0 @@
1
- /** @import { LanguageServicePlugin, LocationLink } from '@volar/language-server'; */
2
- /** @import { DefinitionLocation } from '@tsrx/ripple'; */
3
-
4
- import { TextDocument } from 'vscode-languageserver-textdocument';
5
- import { getVirtualCode, createLogging, getWordFromPosition } from './utils.js';
6
- import path from 'path';
7
- import {
8
- normalizeFileNameOrUri,
9
- getRippleDirForFile,
10
- getCachedTypeDefinitionFile,
11
- getCachedTypeMatches,
12
- } from '@tsrx/typescript-plugin/src/language.js';
13
-
14
- const { log } = createLogging('[Ripple Definition Plugin]');
15
- /** @type {string | undefined} */
16
- let ripple_dir;
17
-
18
- /**
19
- * @returns {LanguageServicePlugin}
20
- */
21
- export function createDefinitionPlugin() {
22
- return {
23
- name: 'ripple-definition',
24
- capabilities: {
25
- definitionProvider: true,
26
- },
27
- create(context) {
28
- return {
29
- async provideDefinition(document, position, token) {
30
- // Get TypeScript definition from typescript-semantic service
31
- /** @type {LocationLink[]} */
32
- let tsDefinitions = [];
33
- for (const [plugin, instance] of context.plugins) {
34
- if (plugin.name === 'typescript-semantic' && instance.provideDefinition) {
35
- const result = await instance.provideDefinition(document, position, token);
36
- if (result) {
37
- tsDefinitions.push(...(Array.isArray(result) ? result : [result]));
38
- }
39
- break;
40
- }
41
- }
42
-
43
- const { virtualCode, sourceUri } = getVirtualCode(document, context);
44
-
45
- if (virtualCode.languageId !== 'ripple') {
46
- // like embedded css
47
- log(`Skipping definitions processing in the '${virtualCode.languageId}' context`);
48
- return tsDefinitions;
49
- }
50
-
51
- // First check for custom definitions (e.g., CSS class selectors)
52
- const offset = document.offsetAt(position);
53
- const text = document.getText();
54
- // Find word boundaries
55
- const { word, start, end } = getWordFromPosition(text, offset);
56
- const customMapping = virtualCode.findMappingByGeneratedRange(start, end);
57
-
58
- log(`Cursor position in generated code for word '${word}':`, position);
59
- log(`Cursor offset in generated code for word '${word}':`, offset);
60
-
61
- // Handle `typeReplace` definitions
62
- if (
63
- customMapping?.data.customData.definition !== false &&
64
- customMapping?.data.customData.definition?.typeReplace
65
- ) {
66
- const { name: typeName, path: typePath } =
67
- customMapping.data.customData.definition.typeReplace;
68
-
69
- log(`Found replace definition for ${typeName}`);
70
-
71
- const filePath = sourceUri.fsPath || sourceUri.path;
72
- ripple_dir = ripple_dir ?? getRippleDirForFile(normalizeFileNameOrUri(filePath));
73
-
74
- if (!ripple_dir) {
75
- log(`Could not determine Ripple source directory for file: ${filePath}`);
76
- return;
77
- }
78
-
79
- const typesFilePath = path.join(ripple_dir, ...typePath.split('/'));
80
-
81
- const fileContent = getCachedTypeDefinitionFile(typesFilePath);
82
-
83
- if (!fileContent) {
84
- // the `getCachedTypeDefinitionFile` already logs the error
85
- return;
86
- }
87
-
88
- const match = getCachedTypeMatches(typeName, fileContent);
89
-
90
- if (match && match.index !== undefined) {
91
- const classStart = match.index + match[0].indexOf(typeName);
92
- const classEnd = classStart + typeName.length;
93
-
94
- // Convert offset to line/column
95
- const lines = fileContent.substring(0, classStart).split('\n');
96
- const line = lines.length - 1;
97
- const character = lines[lines.length - 1].length;
98
-
99
- const endLines = fileContent.substring(0, classEnd).split('\n');
100
- const endLine = endLines.length - 1;
101
- const endCharacter = endLines[endLines.length - 1].length;
102
-
103
- // Create the origin selection range for #Map/#Set
104
- const generatedStart = customMapping.generatedOffsets[0];
105
- const generatedEnd = generatedStart + customMapping.generatedLengths[0];
106
- const originStart = document.positionAt(generatedStart);
107
- const originEnd = document.positionAt(generatedEnd);
108
-
109
- /** @type {LocationLink} */
110
- const locationLink = {
111
- targetUri: `file://${typesFilePath}`,
112
- targetRange: {
113
- start: { line, character },
114
- end: { line: endLine, character: endCharacter },
115
- },
116
- targetSelectionRange: {
117
- start: { line, character },
118
- end: { line: endLine, character: endCharacter },
119
- },
120
- originSelectionRange: {
121
- start: originStart,
122
- end: originEnd,
123
- },
124
- };
125
-
126
- log(`Created definition link to ${typesFilePath}:${line}:${character}`);
127
- return [locationLink];
128
- }
129
- }
130
-
131
- // Handle embedded code definition location, e.g. CSS class selectors
132
- if (
133
- customMapping?.data.customData.definition !== false &&
134
- customMapping?.data.customData.definition?.location
135
- ) {
136
- const def = customMapping.data.customData.definition;
137
- const loc = /** @type {DefinitionLocation} */ (def.location);
138
-
139
- const embeddedCode = loc.embeddedId
140
- ? virtualCode.embeddedCodes?.find(
141
- (/** @type {{ id: string }} */ { id }) => id === loc.embeddedId,
142
- )
143
- : undefined;
144
-
145
- if (embeddedCode) {
146
- const embedMapping = embeddedCode.mappings[0];
147
-
148
- // Calculate the position in the source document
149
- // CSS offset relative to embedded code start + source offset of CSS region
150
- const sourceStartOffset = embedMapping.sourceOffsets[0] + loc.start;
151
- const sourceEndOffset = embedMapping.sourceOffsets[0] + loc.end;
152
-
153
- log(
154
- 'Source document offsets - start for matching css:',
155
- sourceStartOffset,
156
- 'end:',
157
- sourceEndOffset,
158
- );
159
-
160
- // Calculate line/column positions using the source document's proper encoding
161
- // Create a TextDocument from the source code for proper position calculations
162
- const sourceDocument = TextDocument.create(
163
- sourceUri.toString(),
164
- 'ripple',
165
- 0,
166
- virtualCode.originalCode,
167
- );
168
- const targetStart = sourceDocument.positionAt(sourceStartOffset);
169
- const targetEnd = sourceDocument.positionAt(sourceEndOffset);
170
-
171
- log('Target positions in source - start:', targetStart, 'end:', targetEnd);
172
-
173
- // The origin selection range should be in the virtual document
174
- // not in the source document!
175
- const generatedStart = customMapping.generatedOffsets[0];
176
- const generatedEnd = generatedStart + customMapping.generatedLengths[0];
177
- const originStart = document.positionAt(generatedStart);
178
- const originEnd = document.positionAt(generatedEnd);
179
-
180
- log('Origin positions - start:', originStart, 'end:', originEnd);
181
-
182
- /** @type {LocationLink} */
183
- tsDefinitions.push({
184
- targetUri: sourceUri.toString(), // Use the actual source file URI
185
- targetRange: {
186
- start: targetStart,
187
- end: targetEnd,
188
- },
189
- targetSelectionRange: {
190
- start: targetStart,
191
- end: targetEnd,
192
- },
193
- originSelectionRange: {
194
- start: originStart,
195
- end: originEnd,
196
- },
197
- });
198
-
199
- return tsDefinitions;
200
- }
201
- }
202
-
203
- return tsDefinitions;
204
- },
205
- };
206
- },
207
- };
208
- }
@@ -1,118 +0,0 @@
1
- /** @import { LanguageServicePlugin } from '@volar/language-server' */
2
- /** @import { LanguageServicePluginInstance } from '@volar/language-server' */
3
-
4
- import { getVirtualCode, getWordFromPosition, createLogging } from './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
- export 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.languageId !== 'ripple') {
62
- log(`Skipping highlight processing in the '${virtualCode.languageId}' context`);
63
- return tsHighlights;
64
- }
65
-
66
- // Check if we're on a custom Ripple keyword
67
- const offset = document.offsetAt(position);
68
- const text = document.getText();
69
-
70
- // Find word boundaries
71
- const { word } = getWordFromPosition(text, offset);
72
-
73
- // If the word is a Ripple keyword, find all occurrences in the document
74
-
75
- const regex = new RegExp(`\\b${word}\\b`, 'g');
76
- let match;
77
-
78
- while ((match = regex.exec(text)) !== null) {
79
- const start = match.index;
80
- const end = match.index + word.length;
81
- const mapping = virtualCode.findMappingByGeneratedRange(start, end);
82
-
83
- if (!mapping) {
84
- // If no mapping, skip all others as well
85
- // This shouldn't happen as TS handles only mapped ranges
86
- return tsHighlights;
87
- }
88
-
89
- if (!mapping.data.customData?.wordHighlight?.kind) {
90
- // Skip if we didn't define word highlighting in segments
91
- continue;
92
- }
93
-
94
- if (!tsHighlights) {
95
- tsHighlights = [];
96
- }
97
-
98
- tsHighlights.push({
99
- range: {
100
- start: document.positionAt(start),
101
- end: document.positionAt(end),
102
- },
103
-
104
- kind: mapping.data.customData.wordHighlight.kind,
105
- });
106
- }
107
-
108
- if (tsHighlights.length > 0) {
109
- log(`Found ${tsHighlights.length} occurrences of '${word}'`);
110
- }
111
-
112
- // Return TypeScript highlights if no custom keyword was found
113
- return [...tsHighlights];
114
- },
115
- };
116
- },
117
- };
118
- }
@@ -1,146 +0,0 @@
1
- /**
2
- @import {
3
- LanguageServicePlugin,
4
- LanguageServicePluginInstance,
5
- MarkupContent,
6
- } from '@volar/language-server'; */
7
-
8
- import {
9
- getVirtualCode,
10
- createLogging,
11
- getWordFromPosition,
12
- concatMarkdownContents,
13
- deobfuscateIdentifiers,
14
- } from './utils.js';
15
-
16
- const { log, logError } = createLogging('[Ripple Hover Plugin]');
17
-
18
- /**
19
- * @returns {LanguageServicePlugin}
20
- */
21
- export function createHoverPlugin() {
22
- return {
23
- name: 'ripple-hover',
24
- capabilities: {
25
- hoverProvider: true,
26
- },
27
- create(context) {
28
- /** @type {LanguageServicePluginInstance['provideHover']} */
29
- let originalProvideHover;
30
- /** @type {LanguageServicePluginInstance} */
31
- let originalInstance;
32
-
33
- // Disable typescript-semantic's provideHover so it doesn't merge with ours
34
- for (const [plugin, instance] of context.plugins) {
35
- if (plugin.name === 'typescript-semantic') {
36
- originalInstance = instance;
37
- originalProvideHover = instance.provideHover;
38
- instance.provideHover = undefined;
39
- break;
40
- }
41
- }
42
-
43
- if (!originalProvideHover) {
44
- logError(
45
- "'typescript-semantic plugin' was not found or has no 'provideHover'. \
46
- This plugin must be loaded after Volar's typescript-semantic plugin.",
47
- );
48
- }
49
- return {
50
- async provideHover(document, position, token) {
51
- // Get TypeScript hover from typescript-semantic service
52
- let tsHover = null;
53
- if (originalProvideHover) {
54
- tsHover = await originalProvideHover.call(originalInstance, document, position, token);
55
- }
56
-
57
- if (tsHover && tsHover.contents) {
58
- /** @type {MarkupContent} **/ (tsHover.contents).value = deobfuscateIdentifiers(
59
- /** @type {MarkupContent} **/ (tsHover.contents).value,
60
- );
61
- }
62
-
63
- const { virtualCode } = getVirtualCode(document, context);
64
-
65
- if (!virtualCode) {
66
- return tsHover;
67
- }
68
-
69
- /** @type {number} */
70
- let starOffset;
71
- /** @type {number} */
72
- let endOffset;
73
-
74
- if (tsHover && tsHover.range) {
75
- starOffset = document.offsetAt(tsHover.range.start);
76
- endOffset = document.offsetAt(tsHover.range.end);
77
- } else {
78
- const offset = document.offsetAt(position);
79
- const text = document.getText();
80
- // Find word boundaries
81
- const { word, start, end } = getWordFromPosition(text, offset);
82
- starOffset = start;
83
- endOffset = end;
84
-
85
- log(`Cursor position in generated code for word '${word}':`, position);
86
- log(`Cursor offset in generated code for word '${word}':`, offset);
87
- }
88
-
89
- if (virtualCode.languageId !== 'ripple') {
90
- log(`Skipping hover processing in the '${virtualCode.languageId}' context`);
91
- return tsHover;
92
- }
93
-
94
- const mapping = virtualCode.findMappingByGeneratedRange(starOffset, endOffset);
95
-
96
- if (!mapping) {
97
- return tsHover;
98
- }
99
-
100
- const customHover = mapping?.data?.customData?.hover;
101
-
102
- if (customHover === undefined) {
103
- return tsHover;
104
- }
105
-
106
- if (typeof customHover === 'function') {
107
- if (tsHover) {
108
- /** @type {MarkupContent} **/ (tsHover.contents).value = customHover(
109
- /** @type {MarkupContent} **/ (tsHover.contents).value,
110
- );
111
- log('Modified hover contents using custom hover function');
112
- }
113
- return tsHover;
114
- } else if (typeof customHover === 'string') {
115
- const contents = tsHover
116
- ? concatMarkdownContents(
117
- /** @type {MarkupContent} **/ (tsHover.contents).value,
118
- customHover,
119
- )
120
- : customHover;
121
- log('Found custom hover data in mapping');
122
- return {
123
- contents: {
124
- kind: 'markdown',
125
- value: contents,
126
- },
127
- range: {
128
- start: position,
129
- end: position,
130
- },
131
- };
132
- } else if (customHover === false) {
133
- log(
134
- `Hover explicitly suppressed in mapping at range start: ${starOffset}, end: ${endOffset}`,
135
- );
136
- return null;
137
- }
138
-
139
- log('Found mapping for hover at range', 'start: ', starOffset, 'end: ', endOffset);
140
-
141
- return tsHover;
142
- },
143
- };
144
- },
145
- };
146
- }
package/src/server.js DELETED
@@ -1,155 +0,0 @@
1
- /** @import {CompilerOptions} from 'typescript' */
2
-
3
- import { createLogging } from './utils.js';
4
- import {
5
- createConnection,
6
- createServer,
7
- createTypeScriptProject,
8
- } from '@volar/language-server/node';
9
- import { createCompileErrorDiagnosticPlugin } from './compileErrorDiagnosticPlugin.js';
10
- import { createDefinitionPlugin } from './definitionPlugin.js';
11
- import { createHoverPlugin } from './hoverPlugin.js';
12
- import { createCompletionPlugin } from './completionPlugin.js';
13
- import { createAutoInsertPlugin } from './autoInsertPlugin.js';
14
- import { createTypeScriptDiagnosticFilterPlugin } from './typescriptDiagnosticPlugin.js';
15
- import { createDocumentHighlightPlugin } from './documentHighlightPlugin.js';
16
- import { getRippleLanguagePlugin, resolveConfig } from '@tsrx/typescript-plugin/src/language.js';
17
- import { createTypeScriptServices } from './typescriptService.js';
18
- import { create as createCssService } from 'volar-service-css';
19
-
20
- const { log, logError } = createLogging('[Ripple Language Server]');
21
-
22
- export function createRippleLanguageServer() {
23
- const connection = createConnection();
24
- const server = createServer(connection);
25
-
26
- connection.listen();
27
-
28
- // Create language plugin instance once and reuse it
29
- // This prevents creating multiple instances if the callback is called multiple times
30
- const rippleLanguagePlugin = getRippleLanguagePlugin();
31
- log('Language plugin instance created');
32
-
33
- /** @type {WeakSet<Function>} */
34
- const wrappedFunctions = new WeakSet();
35
-
36
- /**
37
- * Ensure TypeScript hosts always see compiler options with Ripple defaults.
38
- * @param {unknown} target
39
- * @param {string} method
40
- */
41
- function wrapCompilerOptionsProvider(target, method) {
42
- if (!target) {
43
- return;
44
- }
45
-
46
- const host = /** @type {{ [key: string]: unknown }} */ (target);
47
- const original = host[method];
48
- if (typeof original !== 'function' || wrappedFunctions.has(original)) {
49
- return;
50
- }
51
-
52
- /** @type {CompilerOptions | undefined} */
53
- let cachedInput;
54
- /** @type {CompilerOptions | undefined} */
55
- let cachedOutput;
56
-
57
- const wrapped = () => {
58
- /** @type {CompilerOptions} */
59
- const input = original.call(host);
60
- if (cachedInput !== input) {
61
- cachedInput = input;
62
- cachedOutput = resolveConfig({ options: input }).options;
63
- }
64
- return cachedOutput;
65
- };
66
-
67
- wrappedFunctions.add(original);
68
- wrappedFunctions.add(wrapped);
69
- host[method] = wrapped;
70
- }
71
-
72
- connection.onInitialize(async (params) => {
73
- try {
74
- log('Initializing Ripple language server...');
75
- log('Initialization options:', JSON.stringify(params.initializationOptions, null, 2));
76
-
77
- const ts = require('typescript');
78
-
79
- const initResult = server.initialize(
80
- params,
81
- createTypeScriptProject(ts, undefined, ({ projectHost }) => {
82
- wrapCompilerOptionsProvider(projectHost, 'getCompilationSettings');
83
-
84
- return {
85
- languagePlugins: [rippleLanguagePlugin],
86
- setup({ project }) {
87
- wrapCompilerOptionsProvider(
88
- project?.typescript?.languageServiceHost,
89
- 'getCompilationSettings',
90
- );
91
- },
92
- };
93
- }),
94
- [
95
- createAutoInsertPlugin(),
96
- createCompletionPlugin(),
97
- createCompileErrorDiagnosticPlugin(),
98
- createDefinitionPlugin(),
99
- createCssService(),
100
- ...createTypeScriptServices(ts),
101
- // !IMPORTANT 'createTypeScriptDiagnosticFilterPlugin', 'createHoverPlugin',
102
- // and 'createDocumentHighlightPlugin' must come after TypeScript services
103
- // to intercept volar's and vscode default providers
104
- createTypeScriptDiagnosticFilterPlugin(),
105
- createHoverPlugin(),
106
- createDocumentHighlightPlugin(),
107
- ],
108
- );
109
-
110
- log('Server initialization complete');
111
- return initResult;
112
- } catch (initError) {
113
- logError('Server initialization failed:', initError);
114
- throw initError;
115
- }
116
- });
117
-
118
- connection.onInitialized(async () => {
119
- log('Server initialized.');
120
- server.initialized();
121
-
122
- // Register file watchers for TypeScript/JavaScript files so the language
123
- // server is notified when they change on disk. Without this, changes to
124
- // .ts files that are imported by .tsrx files are not detected, causing
125
- // stale diagnostics until the server is restarted.
126
- try {
127
- await server.fileWatcher.watchFiles([
128
- '**/*.ts',
129
- '**/*.tsx',
130
- '**/*.cts',
131
- '**/*.mts',
132
- '**/*.js',
133
- '**/*.jsx',
134
- '**/*.cjs',
135
- '**/*.mjs',
136
- '**/*.d.ts',
137
- '**/tsconfig.json',
138
- '**/jsconfig.json',
139
- ]);
140
- log('File watchers registered for TypeScript/JavaScript files.');
141
- } catch (err) {
142
- logError('Failed to register file watchers:', err);
143
- }
144
- });
145
-
146
- process.on('uncaughtException', (err) => {
147
- logError('Uncaught exception:', err);
148
- });
149
-
150
- process.on('unhandledRejection', (reason, promise) => {
151
- logError('Unhandled rejection at:', promise, 'reason:', reason);
152
- });
153
-
154
- return { connection, server };
155
- }