@lewin671/lsp-client 0.1.0
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 +21 -0
- package/README.md +64 -0
- package/lib/common/Feature.d.ts +192 -0
- package/lib/common/Feature.js +159 -0
- package/lib/common/Feature.js.map +1 -0
- package/lib/common/LanguageClient.d.ts +230 -0
- package/lib/common/LanguageClient.js +512 -0
- package/lib/common/LanguageClient.js.map +1 -0
- package/lib/common/features/CompletionFeature.d.ts +23 -0
- package/lib/common/features/CompletionFeature.js +101 -0
- package/lib/common/features/CompletionFeature.js.map +1 -0
- package/lib/common/features/DefinitionFeature.d.ts +23 -0
- package/lib/common/features/DefinitionFeature.js +49 -0
- package/lib/common/features/DefinitionFeature.js.map +1 -0
- package/lib/common/features/DiagnosticFeature.d.ts +15 -0
- package/lib/common/features/DiagnosticFeature.js +39 -0
- package/lib/common/features/DiagnosticFeature.js.map +1 -0
- package/lib/common/features/DocumentSymbolFeature.d.ts +23 -0
- package/lib/common/features/DocumentSymbolFeature.js +87 -0
- package/lib/common/features/DocumentSymbolFeature.js.map +1 -0
- package/lib/common/features/FormattingFeature.d.ts +32 -0
- package/lib/common/features/FormattingFeature.js +72 -0
- package/lib/common/features/FormattingFeature.js.map +1 -0
- package/lib/common/features/HoverFeature.d.ts +23 -0
- package/lib/common/features/HoverFeature.js +49 -0
- package/lib/common/features/HoverFeature.js.map +1 -0
- package/lib/common/features/ReferencesFeature.d.ts +23 -0
- package/lib/common/features/ReferencesFeature.js +48 -0
- package/lib/common/features/ReferencesFeature.js.map +1 -0
- package/lib/common/features/RenameFeature.d.ts +37 -0
- package/lib/common/features/RenameFeature.js +72 -0
- package/lib/common/features/RenameFeature.js.map +1 -0
- package/lib/common/features/index.d.ts +8 -0
- package/lib/common/features/index.js +21 -0
- package/lib/common/features/index.js.map +1 -0
- package/lib/common/utils/index.d.ts +2 -0
- package/lib/common/utils/index.js +19 -0
- package/lib/common/utils/index.js.map +1 -0
- package/lib/common/utils/is.d.ts +15 -0
- package/lib/common/utils/is.js +56 -0
- package/lib/common/utils/is.js.map +1 -0
- package/lib/common/utils/uuid.d.ts +17 -0
- package/lib/common/utils/uuid.js +87 -0
- package/lib/common/utils/uuid.js.map +1 -0
- package/lib/demo/custom-lsp/client.d.ts +1 -0
- package/lib/demo/custom-lsp/client.js +86 -0
- package/lib/demo/custom-lsp/client.js.map +1 -0
- package/lib/demo/custom-lsp/server.d.ts +1 -0
- package/lib/demo/custom-lsp/server.js +64 -0
- package/lib/demo/custom-lsp/server.js.map +1 -0
- package/lib/demo/go-demo/client.d.ts +14 -0
- package/lib/demo/go-demo/client.js +697 -0
- package/lib/demo/go-demo/client.js.map +1 -0
- package/lib/demo/typescript-demo/client.d.ts +14 -0
- package/lib/demo/typescript-demo/client.js +680 -0
- package/lib/demo/typescript-demo/client.js.map +1 -0
- package/lib/demo/typescript-demo/sample-project/src/index.d.ts +9 -0
- package/lib/demo/typescript-demo/sample-project/src/index.js +65 -0
- package/lib/demo/typescript-demo/sample-project/src/index.js.map +1 -0
- package/lib/demo/typescript-demo/sample-project/src/math.d.ts +38 -0
- package/lib/demo/typescript-demo/sample-project/src/math.js +63 -0
- package/lib/demo/typescript-demo/sample-project/src/math.js.map +1 -0
- package/lib/demo/typescript-demo/sample-project/src/models/person.d.ts +41 -0
- package/lib/demo/typescript-demo/sample-project/src/models/person.js +53 -0
- package/lib/demo/typescript-demo/sample-project/src/models/person.js.map +1 -0
- package/lib/demo/typescript-demo/sample-project/src/services/calculator.d.ts +39 -0
- package/lib/demo/typescript-demo/sample-project/src/services/calculator.js +69 -0
- package/lib/demo/typescript-demo/sample-project/src/services/calculator.js.map +1 -0
- package/lib/demo/typescript-demo/sample-project/src/services/user-service.d.ts +40 -0
- package/lib/demo/typescript-demo/sample-project/src/services/user-service.js +88 -0
- package/lib/demo/typescript-demo/sample-project/src/services/user-service.js.map +1 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +32 -0
- package/lib/index.js.map +1 -0
- package/lib/interfaces/IHost.d.ts +19 -0
- package/lib/interfaces/IHost.js +3 -0
- package/lib/interfaces/IHost.js.map +1 -0
- package/lib/transports/ITransport.d.ts +8 -0
- package/lib/transports/ITransport.js +3 -0
- package/lib/transports/ITransport.js.map +1 -0
- package/lib/transports/StdioTransport.d.ts +13 -0
- package/lib/transports/StdioTransport.js +27 -0
- package/lib/transports/StdioTransport.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TypeScript LSP Demo Client
|
|
4
|
+
*
|
|
5
|
+
* This demo shows how to use the lsp-client library to interact with
|
|
6
|
+
* TypeScript Language Server (tsserver) and test various LSP features:
|
|
7
|
+
* - Hover
|
|
8
|
+
* - Completion
|
|
9
|
+
* - Go to Definition
|
|
10
|
+
* - Find References
|
|
11
|
+
* - Document Symbols
|
|
12
|
+
* - Rename
|
|
13
|
+
* - Diagnostics
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const LanguageClient_1 = require("../../common/LanguageClient");
|
|
19
|
+
const CompletionFeature_1 = require("../../common/features/CompletionFeature");
|
|
20
|
+
const HoverFeature_1 = require("../../common/features/HoverFeature");
|
|
21
|
+
const DefinitionFeature_1 = require("../../common/features/DefinitionFeature");
|
|
22
|
+
const ReferencesFeature_1 = require("../../common/features/ReferencesFeature");
|
|
23
|
+
const DocumentSymbolFeature_1 = require("../../common/features/DocumentSymbolFeature");
|
|
24
|
+
const RenameFeature_1 = require("../../common/features/RenameFeature");
|
|
25
|
+
const DiagnosticFeature_1 = require("../../common/features/DiagnosticFeature");
|
|
26
|
+
const StdioTransport_1 = require("../../transports/StdioTransport");
|
|
27
|
+
const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
|
|
28
|
+
// Color utilities for console output
|
|
29
|
+
const colors = {
|
|
30
|
+
reset: '\x1b[0m',
|
|
31
|
+
bright: '\x1b[1m',
|
|
32
|
+
green: '\x1b[32m',
|
|
33
|
+
yellow: '\x1b[33m',
|
|
34
|
+
blue: '\x1b[34m',
|
|
35
|
+
magenta: '\x1b[35m',
|
|
36
|
+
cyan: '\x1b[36m',
|
|
37
|
+
red: '\x1b[31m'
|
|
38
|
+
};
|
|
39
|
+
function log(color, prefix, message) {
|
|
40
|
+
console.log(`${color}[${prefix}]${colors.reset} ${message}`);
|
|
41
|
+
}
|
|
42
|
+
function logSection(title) {
|
|
43
|
+
console.log(`\n${colors.bright}${colors.cyan}${'='.repeat(60)}${colors.reset}`);
|
|
44
|
+
console.log(`${colors.bright}${colors.cyan} ${title}${colors.reset}`);
|
|
45
|
+
console.log(`${colors.bright}${colors.cyan}${'='.repeat(60)}${colors.reset}\n`);
|
|
46
|
+
}
|
|
47
|
+
function logSuccess(message) {
|
|
48
|
+
log(colors.green, '✓', message);
|
|
49
|
+
}
|
|
50
|
+
function logInfo(message) {
|
|
51
|
+
log(colors.blue, 'i', message);
|
|
52
|
+
}
|
|
53
|
+
function logWarning(message) {
|
|
54
|
+
log(colors.yellow, '!', message);
|
|
55
|
+
}
|
|
56
|
+
function logError(message) {
|
|
57
|
+
log(colors.red, '✗', message);
|
|
58
|
+
}
|
|
59
|
+
// Sample project paths - resolve from source directory, not lib
|
|
60
|
+
// When running from lib, we need to point to src/demo/typescript-demo/sample-project
|
|
61
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
62
|
+
const SAMPLE_PROJECT_DIR = path.join(PROJECT_ROOT, 'src', 'demo', 'typescript-demo', 'sample-project');
|
|
63
|
+
const SAMPLE_FILES = {
|
|
64
|
+
math: path.join(SAMPLE_PROJECT_DIR, 'src', 'math.ts'),
|
|
65
|
+
person: path.join(SAMPLE_PROJECT_DIR, 'src', 'models', 'person.ts'),
|
|
66
|
+
calculator: path.join(SAMPLE_PROJECT_DIR, 'src', 'services', 'calculator.ts'),
|
|
67
|
+
userService: path.join(SAMPLE_PROJECT_DIR, 'src', 'services', 'user-service.ts'),
|
|
68
|
+
index: path.join(SAMPLE_PROJECT_DIR, 'src', 'index.ts')
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Console-based Window implementation
|
|
72
|
+
*/
|
|
73
|
+
class ConsoleWindow {
|
|
74
|
+
constructor() {
|
|
75
|
+
this.diagnosticsMap = new Map();
|
|
76
|
+
}
|
|
77
|
+
showMessage(type, message) {
|
|
78
|
+
const typeStr = type === vscode_languageserver_protocol_1.MessageType.Error ? 'ERROR' :
|
|
79
|
+
type === vscode_languageserver_protocol_1.MessageType.Warning ? 'WARNING' : 'INFO';
|
|
80
|
+
log(colors.magenta, `Window.${typeStr}`, message);
|
|
81
|
+
}
|
|
82
|
+
async showMessageRequest(type, message, actions) {
|
|
83
|
+
log(colors.magenta, 'Window.Request', message);
|
|
84
|
+
if (actions && actions.length > 0) {
|
|
85
|
+
logInfo(`Available actions: ${actions.map(a => a.title).join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
logMessage(type, message) {
|
|
90
|
+
log(colors.magenta, 'Window.Log', message);
|
|
91
|
+
}
|
|
92
|
+
publishDiagnostics(uri, diagnostics) {
|
|
93
|
+
this.diagnosticsMap.set(uri, diagnostics);
|
|
94
|
+
if (diagnostics.length > 0) {
|
|
95
|
+
logWarning(`Diagnostics for ${path.basename(uri)}: ${diagnostics.length} issue(s)`);
|
|
96
|
+
diagnostics.forEach(d => {
|
|
97
|
+
const severity = d.severity === 1 ? 'Error' : d.severity === 2 ? 'Warning' : 'Info';
|
|
98
|
+
console.log(` - [${severity}] Line ${d.range.start.line + 1}: ${d.message}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
getDiagnostics(uri) {
|
|
103
|
+
return this.diagnosticsMap.get(uri) || [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Workspace implementation pointing to sample project
|
|
108
|
+
*/
|
|
109
|
+
class SampleWorkspace {
|
|
110
|
+
constructor() {
|
|
111
|
+
this.rootUri = `file://${SAMPLE_PROJECT_DIR}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Simple configuration
|
|
116
|
+
*/
|
|
117
|
+
class SimpleConfiguration {
|
|
118
|
+
get(section) {
|
|
119
|
+
// TypeScript specific settings
|
|
120
|
+
if (section === 'typescript') {
|
|
121
|
+
return {
|
|
122
|
+
suggest: {
|
|
123
|
+
autoImports: true,
|
|
124
|
+
completeFunctionCalls: true
|
|
125
|
+
},
|
|
126
|
+
format: {
|
|
127
|
+
enable: true
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Host implementation
|
|
136
|
+
*/
|
|
137
|
+
class TypeScriptHost {
|
|
138
|
+
constructor() {
|
|
139
|
+
this.window = new ConsoleWindow();
|
|
140
|
+
this.workspace = new SampleWorkspace();
|
|
141
|
+
this.configuration = new SimpleConfiguration();
|
|
142
|
+
}
|
|
143
|
+
dispose() { }
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Read file content
|
|
147
|
+
*/
|
|
148
|
+
function readFileContent(filePath) {
|
|
149
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Convert file path to URI
|
|
153
|
+
*/
|
|
154
|
+
function toUri(filePath) {
|
|
155
|
+
return `file://${filePath}`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Find line and character for a search string in file content
|
|
159
|
+
*/
|
|
160
|
+
function findPosition(content, searchStr) {
|
|
161
|
+
const index = content.indexOf(searchStr);
|
|
162
|
+
if (index === -1)
|
|
163
|
+
return null;
|
|
164
|
+
const lines = content.substring(0, index).split('\n');
|
|
165
|
+
return {
|
|
166
|
+
line: lines.length - 1,
|
|
167
|
+
character: lines[lines.length - 1].length
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Main demo function
|
|
172
|
+
*/
|
|
173
|
+
async function main() {
|
|
174
|
+
console.log(`${colors.bright}${colors.cyan}`);
|
|
175
|
+
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
176
|
+
console.log('║ TypeScript LSP Client Demo ║');
|
|
177
|
+
console.log('║ Testing LSP features with real tsserver ║');
|
|
178
|
+
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
179
|
+
console.log(colors.reset);
|
|
180
|
+
// Check if sample project exists
|
|
181
|
+
if (!fs.existsSync(SAMPLE_PROJECT_DIR)) {
|
|
182
|
+
logError(`Sample project not found at: ${SAMPLE_PROJECT_DIR}`);
|
|
183
|
+
logInfo('Please make sure the sample-project directory exists.');
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
// Find TypeScript Language Server
|
|
187
|
+
const tsserverPath = findTsServer();
|
|
188
|
+
if (!tsserverPath) {
|
|
189
|
+
logError('Could not find TypeScript Language Server (tsserver)');
|
|
190
|
+
logInfo('Please install TypeScript: npm install -g typescript');
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
logSuccess(`Found tsserver at: ${tsserverPath}`);
|
|
194
|
+
// Create transport using typescript-language-server or tsserver wrapper
|
|
195
|
+
// Using typescript-language-server as it provides proper LSP interface
|
|
196
|
+
let transport;
|
|
197
|
+
try {
|
|
198
|
+
// Try to use typescript-language-server (recommended for LSP)
|
|
199
|
+
transport = new StdioTransport_1.StdioTransport('npx', ['typescript-language-server', '--stdio']);
|
|
200
|
+
logInfo('Using typescript-language-server for LSP communication');
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
logWarning('typescript-language-server not available, falling back to tsserver');
|
|
204
|
+
transport = new StdioTransport_1.StdioTransport(tsserverPath, ['--stdio']);
|
|
205
|
+
}
|
|
206
|
+
const host = new TypeScriptHost();
|
|
207
|
+
const client = new LanguageClient_1.LanguageClient(host, transport, {
|
|
208
|
+
textDocument: {
|
|
209
|
+
hover: { dynamicRegistration: true, contentFormat: ['markdown', 'plaintext'] },
|
|
210
|
+
completion: {
|
|
211
|
+
dynamicRegistration: true,
|
|
212
|
+
completionItem: {
|
|
213
|
+
snippetSupport: true,
|
|
214
|
+
documentationFormat: ['markdown', 'plaintext']
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
definition: { dynamicRegistration: true, linkSupport: true },
|
|
218
|
+
references: { dynamicRegistration: true },
|
|
219
|
+
documentSymbol: {
|
|
220
|
+
dynamicRegistration: true,
|
|
221
|
+
hierarchicalDocumentSymbolSupport: true
|
|
222
|
+
},
|
|
223
|
+
rename: { dynamicRegistration: true, prepareSupport: true },
|
|
224
|
+
publishDiagnostics: { relatedInformation: true }
|
|
225
|
+
},
|
|
226
|
+
workspace: {
|
|
227
|
+
workspaceFolders: true
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
// Register all features
|
|
231
|
+
const hoverFeature = new HoverFeature_1.HoverFeature(client);
|
|
232
|
+
const completionFeature = new CompletionFeature_1.CompletionFeature(client);
|
|
233
|
+
const definitionFeature = new DefinitionFeature_1.DefinitionFeature(client);
|
|
234
|
+
const referencesFeature = new ReferencesFeature_1.ReferencesFeature(client);
|
|
235
|
+
const documentSymbolFeature = new DocumentSymbolFeature_1.DocumentSymbolFeature(client);
|
|
236
|
+
const renameFeature = new RenameFeature_1.RenameFeature(client);
|
|
237
|
+
const diagnosticFeature = new DiagnosticFeature_1.DiagnosticFeature(client);
|
|
238
|
+
client.registerFeature(hoverFeature);
|
|
239
|
+
client.registerFeature(completionFeature);
|
|
240
|
+
client.registerFeature(definitionFeature);
|
|
241
|
+
client.registerFeature(referencesFeature);
|
|
242
|
+
client.registerFeature(documentSymbolFeature);
|
|
243
|
+
client.registerFeature(renameFeature);
|
|
244
|
+
client.registerFeature(diagnosticFeature);
|
|
245
|
+
try {
|
|
246
|
+
logSection('Starting Language Client');
|
|
247
|
+
await client.start();
|
|
248
|
+
logSuccess('Language client started successfully!');
|
|
249
|
+
// Give server time to initialize
|
|
250
|
+
await sleep(2000);
|
|
251
|
+
// Open documents
|
|
252
|
+
logSection('Opening Documents');
|
|
253
|
+
await openDocuments(client);
|
|
254
|
+
// Wait for server to process documents
|
|
255
|
+
await sleep(2000);
|
|
256
|
+
// Test each feature
|
|
257
|
+
await testHover(client, hoverFeature);
|
|
258
|
+
await testCompletion(client, completionFeature);
|
|
259
|
+
await testDefinition(client, definitionFeature);
|
|
260
|
+
await testReferences(client, referencesFeature);
|
|
261
|
+
await testDocumentSymbols(client, documentSymbolFeature);
|
|
262
|
+
await testRename(client, renameFeature);
|
|
263
|
+
// Final summary
|
|
264
|
+
logSection('Demo Complete');
|
|
265
|
+
logSuccess('All LSP feature tests completed!');
|
|
266
|
+
// Keep alive briefly then stop
|
|
267
|
+
await sleep(1000);
|
|
268
|
+
logInfo('Stopping language client...');
|
|
269
|
+
await client.stop();
|
|
270
|
+
logSuccess('Client stopped gracefully');
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
logError(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
274
|
+
console.error(e);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Find tsserver executable
|
|
280
|
+
*/
|
|
281
|
+
function findTsServer() {
|
|
282
|
+
const possiblePaths = [
|
|
283
|
+
// Local node_modules
|
|
284
|
+
path.join(process.cwd(), 'node_modules', '.bin', 'tsserver'),
|
|
285
|
+
path.join(process.cwd(), 'node_modules', 'typescript', 'bin', 'tsserver'),
|
|
286
|
+
// Global installations (common locations)
|
|
287
|
+
'/usr/local/bin/tsserver',
|
|
288
|
+
'/usr/bin/tsserver',
|
|
289
|
+
];
|
|
290
|
+
// Try using which/where command
|
|
291
|
+
try {
|
|
292
|
+
const { execSync } = require('child_process');
|
|
293
|
+
const result = execSync('which tsserver', { encoding: 'utf-8' }).trim();
|
|
294
|
+
if (result && fs.existsSync(result)) {
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (e) {
|
|
299
|
+
// Ignore
|
|
300
|
+
}
|
|
301
|
+
for (const p of possiblePaths) {
|
|
302
|
+
if (fs.existsSync(p)) {
|
|
303
|
+
return p;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Return npx fallback
|
|
307
|
+
return 'npx';
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Sleep utility
|
|
311
|
+
*/
|
|
312
|
+
function sleep(ms) {
|
|
313
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Open sample documents
|
|
317
|
+
*/
|
|
318
|
+
async function openDocuments(client) {
|
|
319
|
+
const files = [
|
|
320
|
+
{ name: 'math.ts', path: SAMPLE_FILES.math },
|
|
321
|
+
{ name: 'person.ts', path: SAMPLE_FILES.person },
|
|
322
|
+
{ name: 'calculator.ts', path: SAMPLE_FILES.calculator },
|
|
323
|
+
{ name: 'index.ts', path: SAMPLE_FILES.index }
|
|
324
|
+
];
|
|
325
|
+
for (const file of files) {
|
|
326
|
+
if (fs.existsSync(file.path)) {
|
|
327
|
+
const content = readFileContent(file.path);
|
|
328
|
+
client.didOpen({
|
|
329
|
+
textDocument: {
|
|
330
|
+
uri: toUri(file.path),
|
|
331
|
+
languageId: 'typescript',
|
|
332
|
+
version: 1,
|
|
333
|
+
text: content
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
logSuccess(`Opened: ${file.name}`);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
logWarning(`File not found: ${file.path}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Test Hover feature
|
|
345
|
+
*/
|
|
346
|
+
async function testHover(client, feature) {
|
|
347
|
+
logSection('Testing Hover Feature');
|
|
348
|
+
if (!feature.isSupported) {
|
|
349
|
+
logWarning('Hover not supported by server');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const filePath = SAMPLE_FILES.math;
|
|
353
|
+
const content = readFileContent(filePath);
|
|
354
|
+
// Test hover on 'add' function
|
|
355
|
+
const addPos = findPosition(content, 'function add');
|
|
356
|
+
if (addPos) {
|
|
357
|
+
logInfo(`Testing hover at position: line ${addPos.line + 1}, char ${addPos.character + 9}`);
|
|
358
|
+
const hover = await feature.provideHover({
|
|
359
|
+
textDocument: { uri: toUri(filePath) },
|
|
360
|
+
position: { line: addPos.line, character: addPos.character + 9 } // position on 'add'
|
|
361
|
+
});
|
|
362
|
+
if (hover) {
|
|
363
|
+
logSuccess('Hover result received:');
|
|
364
|
+
printHoverContent(hover);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
logWarning('No hover information returned');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Test hover on 'number' type
|
|
371
|
+
const numberPos = findPosition(content, 'a: number');
|
|
372
|
+
if (numberPos) {
|
|
373
|
+
logInfo(`Testing hover on type annotation`);
|
|
374
|
+
const hover = await feature.provideHover({
|
|
375
|
+
textDocument: { uri: toUri(filePath) },
|
|
376
|
+
position: { line: numberPos.line, character: numberPos.character + 3 }
|
|
377
|
+
});
|
|
378
|
+
if (hover) {
|
|
379
|
+
logSuccess('Hover on type:');
|
|
380
|
+
printHoverContent(hover);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Print hover content
|
|
386
|
+
*/
|
|
387
|
+
function printHoverContent(hover) {
|
|
388
|
+
if (typeof hover.contents === 'string') {
|
|
389
|
+
console.log(` ${hover.contents}`);
|
|
390
|
+
}
|
|
391
|
+
else if (Array.isArray(hover.contents)) {
|
|
392
|
+
hover.contents.forEach(c => {
|
|
393
|
+
if (typeof c === 'string') {
|
|
394
|
+
console.log(` ${c}`);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
console.log(` [${c.language}] ${c.value}`);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
else if ('kind' in hover.contents) {
|
|
402
|
+
console.log(` ${hover.contents.value.substring(0, 200)}...`);
|
|
403
|
+
}
|
|
404
|
+
else if ('value' in hover.contents) {
|
|
405
|
+
console.log(` [${hover.contents.language}] ${hover.contents.value}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Test Completion feature
|
|
410
|
+
*/
|
|
411
|
+
async function testCompletion(client, feature) {
|
|
412
|
+
logSection('Testing Completion Feature');
|
|
413
|
+
if (!feature.isSupported) {
|
|
414
|
+
logWarning('Completion not supported by server');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const filePath = SAMPLE_FILES.calculator;
|
|
418
|
+
const content = readFileContent(filePath);
|
|
419
|
+
// Find a good position for completion (after 'this.')
|
|
420
|
+
const thisPos = findPosition(content, 'this.history');
|
|
421
|
+
if (thisPos) {
|
|
422
|
+
logInfo(`Testing completion after 'this.'`);
|
|
423
|
+
const items = await feature.provideCompletion({
|
|
424
|
+
textDocument: { uri: toUri(filePath) },
|
|
425
|
+
position: { line: thisPos.line, character: thisPos.character + 5 } // after 'this.'
|
|
426
|
+
});
|
|
427
|
+
if (items) {
|
|
428
|
+
const completionItems = Array.isArray(items) ? items : items.items;
|
|
429
|
+
logSuccess(`Received ${completionItems.length} completion items:`);
|
|
430
|
+
completionItems.slice(0, 10).forEach((item) => {
|
|
431
|
+
console.log(` - ${item.label} (${getCompletionKindName(item.kind || 0)})`);
|
|
432
|
+
});
|
|
433
|
+
if (completionItems.length > 10) {
|
|
434
|
+
console.log(` ... and ${completionItems.length - 10} more`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
logWarning('No completion items returned');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Test completion for imports
|
|
442
|
+
const importPos = findPosition(content, "from '../math'");
|
|
443
|
+
if (importPos) {
|
|
444
|
+
logInfo(`Testing completion in import statement`);
|
|
445
|
+
const items = await feature.provideCompletion({
|
|
446
|
+
textDocument: { uri: toUri(filePath) },
|
|
447
|
+
position: { line: importPos.line, character: 10 } // in the import list
|
|
448
|
+
});
|
|
449
|
+
if (items) {
|
|
450
|
+
const completionItems = Array.isArray(items) ? items : items.items;
|
|
451
|
+
logSuccess(`Import completion: ${completionItems.length} items`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get completion kind name
|
|
457
|
+
*/
|
|
458
|
+
function getCompletionKindName(kind) {
|
|
459
|
+
const kinds = {
|
|
460
|
+
1: 'Text', 2: 'Method', 3: 'Function', 4: 'Constructor',
|
|
461
|
+
5: 'Field', 6: 'Variable', 7: 'Class', 8: 'Interface',
|
|
462
|
+
9: 'Module', 10: 'Property', 11: 'Unit', 12: 'Value',
|
|
463
|
+
13: 'Enum', 14: 'Keyword', 15: 'Snippet', 16: 'Color',
|
|
464
|
+
17: 'File', 18: 'Reference', 19: 'Folder', 20: 'EnumMember',
|
|
465
|
+
21: 'Constant', 22: 'Struct', 23: 'Event', 24: 'Operator',
|
|
466
|
+
25: 'TypeParameter'
|
|
467
|
+
};
|
|
468
|
+
return kinds[kind] || 'Unknown';
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Test Definition feature
|
|
472
|
+
*/
|
|
473
|
+
async function testDefinition(client, feature) {
|
|
474
|
+
logSection('Testing Go to Definition');
|
|
475
|
+
if (!feature.isSupported) {
|
|
476
|
+
logWarning('Definition not supported by server');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const filePath = SAMPLE_FILES.calculator;
|
|
480
|
+
const content = readFileContent(filePath);
|
|
481
|
+
// Find usage of 'add' function
|
|
482
|
+
const addUsage = findPosition(content, 'result = add(');
|
|
483
|
+
if (addUsage) {
|
|
484
|
+
logInfo(`Testing go to definition on 'add' function call`);
|
|
485
|
+
const definition = await feature.provideDefinition({
|
|
486
|
+
textDocument: { uri: toUri(filePath) },
|
|
487
|
+
position: { line: addUsage.line, character: addUsage.character + 9 }
|
|
488
|
+
});
|
|
489
|
+
if (definition) {
|
|
490
|
+
if (Array.isArray(definition)) {
|
|
491
|
+
logSuccess(`Found ${definition.length} definition(s):`);
|
|
492
|
+
definition.forEach((loc) => {
|
|
493
|
+
const uri = 'targetUri' in loc ? loc.targetUri : loc.uri;
|
|
494
|
+
const range = 'targetRange' in loc ? loc.targetRange : loc.range;
|
|
495
|
+
console.log(` - ${path.basename(uri)} at line ${range.start.line + 1}`);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
const loc = definition;
|
|
500
|
+
logSuccess(`Definition found:`);
|
|
501
|
+
console.log(` - ${path.basename(loc.uri)} at line ${loc.range.start.line + 1}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
logWarning('No definition found');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Test definition on Person class
|
|
509
|
+
const filePath2 = SAMPLE_FILES.userService;
|
|
510
|
+
const content2 = readFileContent(filePath2);
|
|
511
|
+
const personUsage = findPosition(content2, 'new Person(');
|
|
512
|
+
if (personUsage) {
|
|
513
|
+
logInfo(`Testing go to definition on 'Person' class usage`);
|
|
514
|
+
const definition = await feature.provideDefinition({
|
|
515
|
+
textDocument: { uri: toUri(filePath2) },
|
|
516
|
+
position: { line: personUsage.line, character: personUsage.character + 4 }
|
|
517
|
+
});
|
|
518
|
+
if (definition) {
|
|
519
|
+
logSuccess('Person class definition found');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Test References feature
|
|
525
|
+
*/
|
|
526
|
+
async function testReferences(client, feature) {
|
|
527
|
+
logSection('Testing Find References');
|
|
528
|
+
if (!feature.isSupported) {
|
|
529
|
+
logWarning('References not supported by server');
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const filePath = SAMPLE_FILES.math;
|
|
533
|
+
const content = readFileContent(filePath);
|
|
534
|
+
// Find references to 'add' function
|
|
535
|
+
const addDef = findPosition(content, 'function add');
|
|
536
|
+
if (addDef) {
|
|
537
|
+
logInfo(`Finding all references to 'add' function`);
|
|
538
|
+
const references = await feature.findReferences({
|
|
539
|
+
textDocument: { uri: toUri(filePath) },
|
|
540
|
+
position: { line: addDef.line, character: addDef.character + 9 },
|
|
541
|
+
context: { includeDeclaration: true }
|
|
542
|
+
});
|
|
543
|
+
if (references && references.length > 0) {
|
|
544
|
+
logSuccess(`Found ${references.length} reference(s):`);
|
|
545
|
+
references.forEach((ref) => {
|
|
546
|
+
console.log(` - ${path.basename(ref.uri)} at line ${ref.range.start.line + 1}`);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
logWarning('No references found');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Test Document Symbols feature
|
|
556
|
+
*/
|
|
557
|
+
async function testDocumentSymbols(client, feature) {
|
|
558
|
+
logSection('Testing Document Symbols');
|
|
559
|
+
if (!feature.isSupported) {
|
|
560
|
+
logWarning('Document symbols not supported by server');
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const filePath = SAMPLE_FILES.person;
|
|
564
|
+
logInfo(`Getting symbols for person.ts`);
|
|
565
|
+
const symbols = await feature.getDocumentSymbols({
|
|
566
|
+
textDocument: { uri: toUri(filePath) }
|
|
567
|
+
});
|
|
568
|
+
if (symbols && symbols.length > 0) {
|
|
569
|
+
logSuccess(`Found ${symbols.length} symbol(s):`);
|
|
570
|
+
printSymbols(symbols, 0);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
logWarning('No symbols found');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Print symbols recursively
|
|
578
|
+
*/
|
|
579
|
+
function printSymbols(symbols, indent) {
|
|
580
|
+
const prefix = ' '.repeat(indent);
|
|
581
|
+
symbols.forEach(symbol => {
|
|
582
|
+
if ('children' in symbol) {
|
|
583
|
+
// DocumentSymbol with children
|
|
584
|
+
console.log(`${prefix}- ${symbol.name} (${getSymbolKindName(symbol.kind)})`);
|
|
585
|
+
if (symbol.children) {
|
|
586
|
+
printSymbols(symbol.children, indent + 1);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
// SymbolInformation
|
|
591
|
+
console.log(`${prefix}- ${symbol.name} (${getSymbolKindName(symbol.kind)})`);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Get symbol kind name
|
|
597
|
+
*/
|
|
598
|
+
function getSymbolKindName(kind) {
|
|
599
|
+
const kinds = {
|
|
600
|
+
1: 'File', 2: 'Module', 3: 'Namespace', 4: 'Package',
|
|
601
|
+
5: 'Class', 6: 'Method', 7: 'Property', 8: 'Field',
|
|
602
|
+
9: 'Constructor', 10: 'Enum', 11: 'Interface', 12: 'Function',
|
|
603
|
+
13: 'Variable', 14: 'Constant', 15: 'String', 16: 'Number',
|
|
604
|
+
17: 'Boolean', 18: 'Array', 19: 'Object', 20: 'Key',
|
|
605
|
+
21: 'Null', 22: 'EnumMember', 23: 'Struct', 24: 'Event',
|
|
606
|
+
25: 'Operator', 26: 'TypeParameter'
|
|
607
|
+
};
|
|
608
|
+
return kinds[kind] || 'Unknown';
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Test Rename feature
|
|
612
|
+
*/
|
|
613
|
+
async function testRename(client, feature) {
|
|
614
|
+
logSection('Testing Rename');
|
|
615
|
+
if (!feature.isSupported) {
|
|
616
|
+
logWarning('Rename not supported by server');
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const filePath = SAMPLE_FILES.math;
|
|
620
|
+
const content = readFileContent(filePath);
|
|
621
|
+
// Test prepare rename on 'add' function
|
|
622
|
+
const addDef = findPosition(content, 'function add');
|
|
623
|
+
if (addDef && feature.isPrepareSupported) {
|
|
624
|
+
logInfo(`Testing prepare rename on 'add' function`);
|
|
625
|
+
const prepareResult = await feature.prepareRename({
|
|
626
|
+
textDocument: { uri: toUri(filePath) },
|
|
627
|
+
position: { line: addDef.line, character: addDef.character + 9 }
|
|
628
|
+
});
|
|
629
|
+
if (prepareResult) {
|
|
630
|
+
logSuccess('Prepare rename result:');
|
|
631
|
+
if ('placeholder' in prepareResult) {
|
|
632
|
+
console.log(` Placeholder: ${prepareResult.placeholder}`);
|
|
633
|
+
}
|
|
634
|
+
else if ('defaultBehavior' in prepareResult) {
|
|
635
|
+
console.log(` Default behavior: ${prepareResult.defaultBehavior}`);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
console.log(` Range: line ${prepareResult.start.line + 1}, char ${prepareResult.start.character}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// Test actual rename (dry run - we won't apply the changes)
|
|
643
|
+
if (addDef) {
|
|
644
|
+
logInfo(`Testing rename 'add' -> 'addNumbers' (dry run)`);
|
|
645
|
+
const workspaceEdit = await feature.rename({
|
|
646
|
+
textDocument: { uri: toUri(filePath) },
|
|
647
|
+
position: { line: addDef.line, character: addDef.character + 9 },
|
|
648
|
+
newName: 'addNumbers'
|
|
649
|
+
});
|
|
650
|
+
if (workspaceEdit) {
|
|
651
|
+
logSuccess('Rename would affect:');
|
|
652
|
+
printWorkspaceEdit(workspaceEdit);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
logWarning('No rename edits returned');
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Print workspace edit
|
|
661
|
+
*/
|
|
662
|
+
function printWorkspaceEdit(edit) {
|
|
663
|
+
if (edit.changes) {
|
|
664
|
+
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
665
|
+
console.log(` ${path.basename(uri)}: ${edits.length} edit(s)`);
|
|
666
|
+
edits.slice(0, 3).forEach(e => {
|
|
667
|
+
console.log(` - Line ${e.range.start.line + 1}: "${e.newText}"`);
|
|
668
|
+
});
|
|
669
|
+
if (edits.length > 3) {
|
|
670
|
+
console.log(` ... and ${edits.length - 3} more`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (edit.documentChanges) {
|
|
675
|
+
console.log(` Document changes: ${edit.documentChanges.length} file(s)`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// Run the demo
|
|
679
|
+
main().catch(console.error);
|
|
680
|
+
//# sourceMappingURL=client.js.map
|