@ripple-ts/language-server 0.2.153

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # ripple-language-server
2
+
3
+ Language Server Protocol (LSP) implementation for Ripple. This package provides language intelligence features for
4
+ Ripple files and can be integrated into any editor that supports LSP.
5
+
6
+ ## Features
7
+
8
+ - TypeScript integration via Volar
9
+ - Ripple syntax diagnostics
10
+ - IntelliSense and autocomplete
11
+ - Go to definition
12
+ - Find references
13
+ - Hover information
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install ripple-language-server -g
19
+ ```
20
+
21
+ ### Editor Integration
22
+
23
+ This language server can be integrated into any editor that supports LSP:
24
+
25
+ #### VS Code
26
+
27
+ Use the [official extension](https://marketplace.visualstudio.com/items?itemName=ripple-ts.vscode-plugin
28
+ It uses this language server internally.
29
+
30
+ #### WebStorm/IntelliJ
31
+
32
+ 1. Install the [LSP4IJ plugin](https://plugins.jetbrains.com/plugin/23257-lsp4ij).
33
+ 2. Add a new language server in it
34
+ 3. Specify `ripple-language-server --stdio` as the command in it.
35
+ 4. Go to `Mappings` —> `File name patterns` and add a new value with `File name patterns` set to `*.ripple` and `Language Id` set to `ripple.
36
+
37
+ #### Neovim
38
+
39
+ TODO Write instructions
40
+
41
+ #### Sublime Text
42
+
43
+ TODO Write instructions
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { createRippleLanguageServer } = require('../src/server.js');
4
+
5
+ createRippleLanguageServer();
6
+
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./src/server');
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@ripple-ts/language-server",
3
+ "version": "0.2.153",
4
+ "description": "Language Server Protocol implementation for Ripple",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "@ripple-ts/language-server": "bin/language-server.js"
8
+ },
9
+ "author": "Dominic Gannaway",
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/Ripple-TS/ripple.git",
14
+ "directory": "packages/language-server"
15
+ },
16
+ "dependencies": {
17
+ "@volar/language-server": "~2.4.23",
18
+ "volar-service-typescript": "0.0.65",
19
+ "vscode-languageserver-textdocument": "^1.0.12",
20
+ "vscode-uri": "^3.1.0",
21
+ "@ripple-ts/typescript-plugin": "0.2.153"
22
+ },
23
+ "peerDependencies": {
24
+ "typescript": "^5.9.2"
25
+ }
26
+ }
@@ -0,0 +1,240 @@
1
+ const { RippleVirtualCode } = require('@ripple-ts/typescript-plugin/src/language.js');
2
+ const { URI } = require('vscode-uri');
3
+
4
+ /**
5
+ * @typedef {import('@volar/language-server').LanguageServicePlugin} LanguageServicePlugin
6
+ * @typedef {import('@volar/language-server').LanguageServiceContext} LanguageServiceContext
7
+ * @typedef {import('@volar/language-server').Diagnostic} Diagnostic
8
+ * @typedef {import('@volar/language-server').DiagnosticSeverity} DiagnosticSeverity
9
+ * @typedef {import('@volar/language-server').Position} Position
10
+ * @typedef {import('vscode-languageserver-textdocument').TextDocument} TextDocument
11
+ */
12
+
13
+ const DEBUG = process.env.RIPPLE_DEBUG === 'true';
14
+
15
+ /**
16
+ * @param {...unknown} args
17
+ */
18
+ function log(...args) {
19
+ if (DEBUG) {
20
+ console.log('[Ripple Language]', ...args);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * @param {...unknown} args
26
+ */
27
+ function logError(...args) {
28
+ console.error('[Ripple Language ERROR]', ...args);
29
+ }
30
+
31
+ /**
32
+ * @returns {LanguageServicePlugin}
33
+ */
34
+ function createRippleDiagnosticPlugin() {
35
+ log('Creating Ripple diagnostic plugin...');
36
+
37
+ return {
38
+ name: 'ripple-diagnostics',
39
+ capabilities: {
40
+ diagnosticProvider: {
41
+ interFileDependencies: false,
42
+ workspaceDiagnostics: false,
43
+ },
44
+ },
45
+ create(/** @type {LanguageServiceContext} */ context) {
46
+ return {
47
+ /**
48
+ * @param {TextDocument} document
49
+ * @param {import('@volar/language-server').CancellationToken} _token
50
+ * @returns {import('@volar/language-server').NullableProviderResult<Diagnostic[]>}
51
+ */
52
+ provideDiagnostics(document, _token) {
53
+ try {
54
+ log('Providing Ripple diagnostics for:', document.uri);
55
+
56
+ const info = getEmbeddedInfo(context, document);
57
+
58
+ if (info && info.virtualCode.errors && info.virtualCode.errors.length > 0) {
59
+ const virtualCode = info.virtualCode;
60
+ const diagnostics = [];
61
+
62
+ log('Processing', virtualCode.errors.length, 'errors');
63
+
64
+ // Convert each stored error to a diagnostic
65
+ for (const error of virtualCode.errors) {
66
+ try {
67
+ // Use the actual snapshot text that Volar is working with
68
+ const snapshotText = virtualCode.snapshot.getText(
69
+ 0,
70
+ virtualCode.snapshot.getLength(),
71
+ );
72
+ const diagnostic = parseCompilationErrorWithDocument(
73
+ error,
74
+ virtualCode.fileName,
75
+ snapshotText,
76
+ document,
77
+ );
78
+ diagnostics.push(diagnostic);
79
+ } catch (parseError) {
80
+ logError('Failed to parse compilation error:', parseError);
81
+ }
82
+ }
83
+
84
+ log('Generated', diagnostics.length, 'diagnostics');
85
+ return diagnostics;
86
+ }
87
+
88
+ return [];
89
+ } catch (err) {
90
+ logError('Failed to provide diagnostics:', err);
91
+ return [];
92
+ }
93
+ },
94
+ };
95
+ },
96
+ };
97
+ }
98
+
99
+ // Helper function to parse compilation errors using document.positionAt (Glint style)
100
+ /**
101
+ * @param {unknown} error
102
+ * @param {string} fallbackFileName
103
+ * @param {string} sourceText
104
+ * @param {TextDocument} document
105
+ * @returns {Diagnostic}
106
+ */
107
+ function parseCompilationErrorWithDocument(error, fallbackFileName, sourceText, document) {
108
+ const errorObject = /** @type {{ message?: string }} */ (error);
109
+ const message = errorObject.message || String(error);
110
+
111
+ try {
112
+ // First check if there's a GitHub-style range in the error
113
+ // Format: filename#L39C24-L39C32
114
+ const githubRangeMatch = message.match(/\(([^#]+)#L(\d+)C(\d+)-L(\d+)C(\d+)\)/);
115
+
116
+ if (githubRangeMatch) {
117
+ // Use the GitHub range data directly
118
+ const startLine = parseInt(githubRangeMatch[2]);
119
+ const startColumn = parseInt(githubRangeMatch[3]);
120
+ const endLine = parseInt(githubRangeMatch[4]);
121
+ const endColumn = parseInt(githubRangeMatch[5]);
122
+
123
+ // Convert to zero-based
124
+ const zeroBasedStartLine = Math.max(0, startLine - 1);
125
+ const zeroBasedStartColumn = Math.max(0, startColumn);
126
+ const zeroBasedEndLine = Math.max(0, endLine - 1);
127
+ const zeroBasedEndColumn = Math.max(0, endColumn);
128
+
129
+ return {
130
+ severity: 1, // DiagnosticSeverity.Error
131
+ range: {
132
+ start: { line: zeroBasedStartLine, character: zeroBasedStartColumn },
133
+ end: { line: zeroBasedEndLine, character: zeroBasedEndColumn },
134
+ },
135
+ message: message.replace(/\s*\([^#]+#L\d+C\d+-L\d+C\d+\)/, '').trim(), // Remove the range part from message
136
+ source: 'Ripple',
137
+ code: 'ripple-compile-error',
138
+ };
139
+ }
140
+
141
+ // Fallback to old parsing method if no range found
142
+ // Try to parse location from error message
143
+ // Format: "Error message (filename:line:column)"
144
+ const locationMatch = message.match(/\(([^:]+):(\d+):(\d+)\)$/);
145
+
146
+ if (locationMatch) {
147
+ const [, fileName, lineStr, columnStr] = locationMatch;
148
+ const line = parseInt(lineStr, 10);
149
+ const column = parseInt(columnStr, 10);
150
+
151
+ // Extract the main error message (without location)
152
+ const cleanMessage = message.replace(/\s*\([^:]+:\d+:\d+\)$/, '');
153
+
154
+ // Convert 1-based line/column to 0-based for VS Code
155
+ const zeroBasedLine = Math.max(0, line - 1);
156
+ const actualColumn = Math.max(0, column - 1);
157
+
158
+ // Use the original error coordinates from the Ripple compiler
159
+ // Just use the compiler's position as-is, with a simple 1-character highlight
160
+ let length = Math.min(1, sourceText.split('\n')[zeroBasedLine]?.length - actualColumn || 1);
161
+
162
+ return {
163
+ severity: 1, // DiagnosticSeverity.Error
164
+ range: {
165
+ start: { line: zeroBasedLine, character: actualColumn },
166
+ end: { line: zeroBasedLine, character: actualColumn + length },
167
+ },
168
+ message: cleanMessage,
169
+ source: 'Ripple',
170
+ code: 'ripple-compile-error',
171
+ };
172
+ } else {
173
+ // Fallback for errors without location information
174
+ const startPosition = document.positionAt(0);
175
+ const endPosition = document.positionAt(Math.min(1, sourceText.length));
176
+
177
+ return {
178
+ severity: 1, // DiagnosticSeverity.Error
179
+ range: {
180
+ start: startPosition,
181
+ end: endPosition,
182
+ },
183
+ message: `Ripple compilation error: ${message}`,
184
+ source: 'Ripple',
185
+ code: 'ripple-compile-error',
186
+ };
187
+ }
188
+ } catch (parseError) {
189
+ logError('Error parsing compilation error:', parseError);
190
+
191
+ return {
192
+ severity: 1,
193
+ range: {
194
+ start: { line: 0, character: 0 },
195
+ end: { line: 0, character: 1 },
196
+ },
197
+ message: `Ripple compilation error: ${message}`,
198
+ source: 'Ripple',
199
+ code: 'ripple-parse-error',
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * @param {LanguageServiceContext} context
206
+ * @param {TextDocument} document
207
+ */
208
+ function getEmbeddedInfo(context, document) {
209
+ try {
210
+ const uri = URI.parse(document.uri);
211
+ const decoded = context.decodeEmbeddedDocumentUri(uri);
212
+ if (!decoded) {
213
+ return;
214
+ }
215
+
216
+ const [documentUri, embeddedCodeId] = decoded;
217
+
218
+ const sourceScript = context.language.scripts.get(documentUri);
219
+ if (!sourceScript?.generated) {
220
+ return;
221
+ }
222
+
223
+ const virtualCode = sourceScript.generated.embeddedCodes.get(embeddedCodeId);
224
+ if (!(virtualCode instanceof RippleVirtualCode)) {
225
+ return;
226
+ }
227
+
228
+ return {
229
+ sourceScript: sourceScript,
230
+ virtualCode,
231
+ };
232
+ } catch (err) {
233
+ logError('Failed to get embedded info:', err);
234
+ return null;
235
+ }
236
+ }
237
+
238
+ module.exports = {
239
+ createRippleDiagnosticPlugin,
240
+ };
package/src/server.js ADDED
@@ -0,0 +1,135 @@
1
+ const {
2
+ createConnection,
3
+ createServer,
4
+ createTypeScriptProject,
5
+ } = require('@volar/language-server/node');
6
+ const { createRippleDiagnosticPlugin } = require('./diagnosticPlugin.js');
7
+ const { getRippleLanguagePlugin, resolveConfig } = require('@ripple-ts/typescript-plugin/src/language.js');
8
+ const { create: createTypeScriptServices } = require('volar-service-typescript');
9
+
10
+ const DEBUG = process.env.RIPPLE_DEBUG === 'true';
11
+
12
+ /** @typedef {import('typescript').CompilerOptions} CompilerOptions */
13
+
14
+ /**
15
+ * @param {...unknown} args
16
+ */
17
+ function log(...args) {
18
+ if (DEBUG) {
19
+ console.log('[Ripple Server]', ...args);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * @param {...unknown} args
25
+ */
26
+ function logError(...args) {
27
+ console.error('[Ripple Server ERROR]', ...args);
28
+ }
29
+
30
+ function createRippleLanguageServer() {
31
+ const connection = createConnection();
32
+ const server = createServer(connection);
33
+
34
+ connection.listen();
35
+
36
+ // Create language plugin instance once and reuse it
37
+ // This prevents creating multiple instances if the callback is called multiple times
38
+ const rippleLanguagePlugin = getRippleLanguagePlugin();
39
+ log('Language plugin instance created');
40
+
41
+ /** @type {WeakSet<Function>} */
42
+ const wrappedFunctions = new WeakSet();
43
+
44
+ /**
45
+ * Ensure TypeScript hosts always see compiler options with Ripple defaults.
46
+ * @param {unknown} target
47
+ * @param {string} method
48
+ */
49
+ function wrapCompilerOptionsProvider(target, method) {
50
+ if (!target) {
51
+ return;
52
+ }
53
+
54
+ const host = /** @type {{ [key: string]: unknown }} */ (target);
55
+ const original = host[method];
56
+ if (typeof original !== 'function' || wrappedFunctions.has(original)) {
57
+ return;
58
+ }
59
+
60
+ /** @type {CompilerOptions | undefined} */
61
+ let cachedInput;
62
+ /** @type {CompilerOptions | undefined} */
63
+ let cachedOutput;
64
+
65
+ const wrapped = () => {
66
+ /** @type {CompilerOptions} */
67
+ const input = original.call(host);
68
+ if (cachedInput !== input) {
69
+ cachedInput = input;
70
+ cachedOutput = resolveConfig({ options: input }).options;
71
+ }
72
+ return cachedOutput;
73
+ };
74
+
75
+ wrappedFunctions.add(original);
76
+ wrappedFunctions.add(wrapped);
77
+ host[method] = wrapped;
78
+ }
79
+
80
+ connection.onInitialize(async (params) => {
81
+ try {
82
+ log('Initializing Ripple language server...');
83
+ log('Initialization options:', JSON.stringify(params.initializationOptions, null, 2));
84
+
85
+ const ts = require('typescript');
86
+
87
+ const initResult = server.initialize(
88
+ params,
89
+ createTypeScriptProject(
90
+ ts,
91
+ undefined,
92
+ ({ projectHost }) => {
93
+ wrapCompilerOptionsProvider(projectHost, 'getCompilationSettings');
94
+
95
+ return {
96
+ languagePlugins: [rippleLanguagePlugin],
97
+ setup({ project }) {
98
+ wrapCompilerOptionsProvider(project?.typescript?.languageServiceHost, 'getCompilationSettings');
99
+ },
100
+ };
101
+ },
102
+ ),
103
+ [
104
+ createRippleDiagnosticPlugin(),
105
+ ...createTypeScriptServices(ts),
106
+ ],
107
+ );
108
+
109
+ log('Server initialization complete');
110
+ return initResult;
111
+ } catch (initError) {
112
+ logError('Server initialization failed:', initError);
113
+ throw initError;
114
+ }
115
+ });
116
+
117
+ connection.onInitialized(() => {
118
+ log('Server initialized.');
119
+ server.initialized();
120
+ });
121
+
122
+ process.on('uncaughtException', (err) => {
123
+ logError('Uncaught exception:', err);
124
+ });
125
+
126
+ process.on('unhandledRejection', (reason, promise) => {
127
+ logError('Unhandled rejection at:', promise, 'reason:', reason);
128
+ });
129
+
130
+ return { connection, server };
131
+ }
132
+
133
+ module.exports = {
134
+ createRippleLanguageServer,
135
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "node16",
4
+ "moduleResolution": "node16",
5
+ "target": "es2020",
6
+ "allowJs": true,
7
+ "checkJs": true,
8
+ "noEmit": true,
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "resolveJsonModule": true,
12
+ "types": []
13
+ },
14
+ "include": ["src/**/*.js", "index.js", "bin/**/*.js"]
15
+ }