@lspeasy/client 1.0.1
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 +589 -0
- package/dist/capability-guard.d.ts +62 -0
- package/dist/capability-guard.d.ts.map +1 -0
- package/dist/capability-guard.js +230 -0
- package/dist/capability-guard.js.map +1 -0
- package/dist/capability-proxy.d.ts +16 -0
- package/dist/capability-proxy.d.ts.map +1 -0
- package/dist/capability-proxy.js +69 -0
- package/dist/capability-proxy.js.map +1 -0
- package/dist/client.d.ts +184 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +692 -0
- package/dist/client.js.map +1 -0
- package/dist/connection/health.d.ts +14 -0
- package/dist/connection/health.d.ts.map +1 -0
- package/dist/connection/health.js +77 -0
- package/dist/connection/health.js.map +1 -0
- package/dist/connection/heartbeat.d.ts +18 -0
- package/dist/connection/heartbeat.d.ts.map +1 -0
- package/dist/connection/heartbeat.js +57 -0
- package/dist/connection/heartbeat.js.map +1 -0
- package/dist/connection/index.d.ts +5 -0
- package/dist/connection/index.d.ts.map +1 -0
- package/dist/connection/index.js +4 -0
- package/dist/connection/index.js.map +1 -0
- package/dist/connection/types.d.ts +32 -0
- package/dist/connection/types.d.ts.map +1 -0
- package/dist/connection/types.js +9 -0
- package/dist/connection/types.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications/index.d.ts +3 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +2 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/wait.d.ts +19 -0
- package/dist/notifications/wait.d.ts.map +1 -0
- package/dist/notifications/wait.js +46 -0
- package/dist/notifications/wait.js.map +1 -0
- package/dist/progress.d.ts +54 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +52 -0
- package/dist/progress.js.map +1 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +43 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +56 -0
- package/dist/validation.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Pradeep Mouli
|
|
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,589 @@
|
|
|
1
|
+
# @lspeasy/client
|
|
2
|
+
|
|
3
|
+
Connect to Language Server Protocol servers with a simple, type-safe client API.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`@lspeasy/client` provides a high-level LSP client with:
|
|
8
|
+
|
|
9
|
+
- **LSPClient**: Complete LSP client implementation with lifecycle management
|
|
10
|
+
- **High-Level API**: Strongly-typed `textDocument.*` and `workspace.*` methods
|
|
11
|
+
- **Request/Notification**: Low-level access to send any LSP request or notification
|
|
12
|
+
- **Cancellation**: Built-in cancellation support for long-running requests
|
|
13
|
+
- **Event Subscriptions**: Subscribe to server notifications and events
|
|
14
|
+
- **Server Requests**: Handle requests from server to client
|
|
15
|
+
- **Notification Waiting**: Promise-based one-shot waiting with timeout and filters
|
|
16
|
+
- **Connection Health**: State transition and message activity monitoring
|
|
17
|
+
- **Type Safety**: Full TypeScript types from LSP protocol definitions
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @lspeasy/client @lspeasy/core vscode-languageserver-protocol
|
|
23
|
+
# or
|
|
24
|
+
pnpm add @lspeasy/client @lspeasy/core vscode-languageserver-protocol
|
|
25
|
+
# or
|
|
26
|
+
yarn add @lspeasy/client @lspeasy/core vscode-languageserver-protocol
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### Basic Client
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { LSPClient } from '@lspeasy/client';
|
|
35
|
+
import { StdioTransport } from '@lspeasy/core';
|
|
36
|
+
import { spawn } from 'child_process';
|
|
37
|
+
|
|
38
|
+
// Spawn language server
|
|
39
|
+
const serverProcess = spawn('typescript-language-server', ['--stdio']);
|
|
40
|
+
|
|
41
|
+
// Create transport
|
|
42
|
+
const transport = new StdioTransport({
|
|
43
|
+
input: serverProcess.stdout,
|
|
44
|
+
output: serverProcess.stdin
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Create client
|
|
48
|
+
const client = new LSPClient({
|
|
49
|
+
name: 'My Client',
|
|
50
|
+
version: '1.0.0',
|
|
51
|
+
transport
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Connect to server (sends initialize + initialized)
|
|
55
|
+
await client.connect(transport);
|
|
56
|
+
|
|
57
|
+
// Use high-level API
|
|
58
|
+
const hover = await client.textDocument.hover({
|
|
59
|
+
textDocument: { uri: 'file:///path/to/file.ts' },
|
|
60
|
+
position: { line: 10, character: 5 }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log('Hover:', hover?.contents);
|
|
64
|
+
|
|
65
|
+
// Disconnect
|
|
66
|
+
await client.disconnect();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### With Capabilities
|
|
70
|
+
|
|
71
|
+
Declare client capabilities:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const client = new LSPClient({
|
|
75
|
+
name: 'Advanced Client',
|
|
76
|
+
version: '1.0.0',
|
|
77
|
+
transport,
|
|
78
|
+
capabilities: {
|
|
79
|
+
textDocument: {
|
|
80
|
+
hover: {
|
|
81
|
+
contentFormat: ['markdown', 'plaintext']
|
|
82
|
+
},
|
|
83
|
+
completion: {
|
|
84
|
+
completionItem: {
|
|
85
|
+
snippetSupport: true,
|
|
86
|
+
commitCharactersSupport: true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
workspace: {
|
|
91
|
+
applyEdit: true,
|
|
92
|
+
workspaceEdit: {
|
|
93
|
+
documentChanges: true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## High-Level API
|
|
101
|
+
|
|
102
|
+
### Text Document Methods
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Hover
|
|
106
|
+
const hover = await client.textDocument.hover({
|
|
107
|
+
textDocument: { uri: 'file:///test.ts' },
|
|
108
|
+
position: { line: 0, character: 0 }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Completion
|
|
112
|
+
const completion = await client.textDocument.completion({
|
|
113
|
+
textDocument: { uri: 'file:///test.ts' },
|
|
114
|
+
position: { line: 5, character: 10 }
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Go to Definition
|
|
118
|
+
const definition = await client.textDocument.definition({
|
|
119
|
+
textDocument: { uri: 'file:///test.ts' },
|
|
120
|
+
position: { line: 10, character: 15 }
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Find References
|
|
124
|
+
const references = await client.textDocument.references({
|
|
125
|
+
textDocument: { uri: 'file:///test.ts' },
|
|
126
|
+
position: { line: 20, character: 5 },
|
|
127
|
+
context: { includeDeclaration: false }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Document Symbols
|
|
131
|
+
const symbols = await client.textDocument.documentSymbol({
|
|
132
|
+
textDocument: { uri: 'file:///test.ts' }
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Document Synchronization
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Open document
|
|
140
|
+
await client.textDocument.didOpen({
|
|
141
|
+
textDocument: {
|
|
142
|
+
uri: 'file:///test.ts',
|
|
143
|
+
languageId: 'typescript',
|
|
144
|
+
version: 1,
|
|
145
|
+
text: 'console.log("Hello");'
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Change document
|
|
150
|
+
await client.textDocument.didChange({
|
|
151
|
+
textDocument: {
|
|
152
|
+
uri: 'file:///test.ts',
|
|
153
|
+
version: 2
|
|
154
|
+
},
|
|
155
|
+
contentChanges: [
|
|
156
|
+
{
|
|
157
|
+
text: 'console.log("Hello, World!");'
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Save document
|
|
163
|
+
await client.textDocument.didSave({
|
|
164
|
+
textDocument: { uri: 'file:///test.ts' },
|
|
165
|
+
text: 'console.log("Hello, World!");'
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Close document
|
|
169
|
+
await client.textDocument.didClose({
|
|
170
|
+
textDocument: { uri: 'file:///test.ts' }
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Workspace Methods
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// Workspace symbols
|
|
178
|
+
const symbols = await client.workspace.symbol({
|
|
179
|
+
query: 'MyClass'
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Configuration (if server requests it)
|
|
183
|
+
// Server will call this via client.onRequest('workspace/configuration')
|
|
184
|
+
|
|
185
|
+
// Workspace folders
|
|
186
|
+
await client.workspace.didChangeWorkspaceFolders({
|
|
187
|
+
event: {
|
|
188
|
+
added: [{ uri: 'file:///new/folder', name: 'New Folder' }],
|
|
189
|
+
removed: []
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// File watching
|
|
194
|
+
await client.workspace.didChangeWatchedFiles({
|
|
195
|
+
changes: [
|
|
196
|
+
{
|
|
197
|
+
uri: 'file:///test.ts',
|
|
198
|
+
type: 2 // Changed
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Low-Level API
|
|
205
|
+
|
|
206
|
+
### Send Requests
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// Send any request
|
|
210
|
+
const result = await client.sendRequest<ParamsType, ResultType>(
|
|
211
|
+
'custom/method',
|
|
212
|
+
{ /* params */ }
|
|
213
|
+
);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Send Notifications
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// Send any notification
|
|
220
|
+
await client.sendNotification<ParamsType>(
|
|
221
|
+
'custom/notification',
|
|
222
|
+
{ /* params */ }
|
|
223
|
+
);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Cancellable Requests
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { CancellationTokenSource } from '@lspeasy/core';
|
|
230
|
+
|
|
231
|
+
const source = new CancellationTokenSource();
|
|
232
|
+
|
|
233
|
+
// Send cancellable request
|
|
234
|
+
const { promise, cancel } = client.sendRequestCancellable(
|
|
235
|
+
'textDocument/hover',
|
|
236
|
+
params,
|
|
237
|
+
source.token
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Cancel after 5 seconds
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
source.cancel();
|
|
243
|
+
// or use the returned cancel function
|
|
244
|
+
// cancel();
|
|
245
|
+
}, 5000);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const result = await promise;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error.message.includes('cancelled')) {
|
|
251
|
+
console.log('Request was cancelled');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Event Subscriptions
|
|
257
|
+
|
|
258
|
+
### Connection Events
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// Connected to server
|
|
262
|
+
client.onConnected(() => {
|
|
263
|
+
console.log('Connected to language server');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Disconnected from server
|
|
267
|
+
client.onDisconnected(() => {
|
|
268
|
+
console.log('Disconnected from language server');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Connection errors
|
|
272
|
+
client.onError((error) => {
|
|
273
|
+
console.error('Client error:', error);
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## waitForNotification
|
|
278
|
+
|
|
279
|
+
Use `waitForNotification` when you need the next matching server notification as a Promise.
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const diagnostics = await client.waitForNotification('textDocument/publishDiagnostics', {
|
|
283
|
+
timeout: 5000,
|
|
284
|
+
filter: (params) => params.uri === 'file:///example.ts'
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
console.log(diagnostics.diagnostics);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Notes:
|
|
291
|
+
- `timeout` is required.
|
|
292
|
+
- Waiters are cleaned up automatically on resolve, timeout, or disconnect.
|
|
293
|
+
- Multiple concurrent waiters for the same method are supported.
|
|
294
|
+
|
|
295
|
+
## Connection Health Monitoring
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
const client = new LSPClient({
|
|
299
|
+
name: 'health-aware-client',
|
|
300
|
+
version: '1.0.0',
|
|
301
|
+
heartbeat: {
|
|
302
|
+
enabled: true,
|
|
303
|
+
interval: 30000,
|
|
304
|
+
timeout: 10000
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const stateSubscription = client.onConnectionStateChange((event) => {
|
|
309
|
+
console.log('state', event.previous, '->', event.current, event.reason);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const healthSubscription = client.onConnectionHealthChange((health) => {
|
|
313
|
+
console.log('last sent', health.lastMessageSent);
|
|
314
|
+
console.log('last received', health.lastMessageReceived);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const health = client.getConnectionHealth();
|
|
318
|
+
console.log(health.state);
|
|
319
|
+
|
|
320
|
+
stateSubscription.dispose();
|
|
321
|
+
healthSubscription.dispose();
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Server Notifications
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// Diagnostics from server
|
|
328
|
+
client.onNotification('textDocument/publishDiagnostics', (params) => {
|
|
329
|
+
console.log(`Diagnostics for ${params.uri}:`, params.diagnostics);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Show message from server
|
|
333
|
+
client.onNotification('window/showMessage', (params) => {
|
|
334
|
+
console.log(`Server message (${params.type}): ${params.message}`);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Log message from server
|
|
338
|
+
client.onNotification('window/logMessage', (params) => {
|
|
339
|
+
console.log(`Server log (${params.type}): ${params.message}`);
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Server Requests
|
|
344
|
+
|
|
345
|
+
Handle requests from server to client:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// Configuration request
|
|
349
|
+
client.onRequest('workspace/configuration', async (params) => {
|
|
350
|
+
return [
|
|
351
|
+
{ enable: true },
|
|
352
|
+
{ maxProblems: 100 }
|
|
353
|
+
];
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Apply workspace edit
|
|
357
|
+
client.onRequest('workspace/applyEdit', async (params) => {
|
|
358
|
+
// Apply the edit
|
|
359
|
+
applyWorkspaceEdit(params.edit);
|
|
360
|
+
return { applied: true };
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Show message request (with actions)
|
|
364
|
+
client.onRequest('window/showMessageRequest', async (params) => {
|
|
365
|
+
// Show dialog to user
|
|
366
|
+
const choice = await showDialog(params.message, params.actions);
|
|
367
|
+
return choice;
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
When handling server-to-client requests:
|
|
372
|
+
- The handler parameter and return value are inferred from the method.
|
|
373
|
+
- If no handler exists, client replies with JSON-RPC `-32601` (method not found).
|
|
374
|
+
- If handler throws, client replies with JSON-RPC `-32603` (internal error).
|
|
375
|
+
|
|
376
|
+
## WebSocket Client
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { LSPClient } from '@lspeasy/client';
|
|
380
|
+
import { WebSocketTransport } from '@lspeasy/core';
|
|
381
|
+
|
|
382
|
+
// Connect over WebSocket with automatic reconnection
|
|
383
|
+
const transport = new WebSocketTransport({
|
|
384
|
+
url: 'ws://localhost:3000',
|
|
385
|
+
enableReconnect: true,
|
|
386
|
+
maxReconnectAttempts: 5,
|
|
387
|
+
reconnectDelay: 1000,
|
|
388
|
+
maxReconnectDelay: 30000,
|
|
389
|
+
reconnectBackoffMultiplier: 2
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const client = new LSPClient({
|
|
393
|
+
name: 'WebSocket Client',
|
|
394
|
+
version: '1.0.0',
|
|
395
|
+
transport
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Handle reconnection
|
|
399
|
+
transport.onClose(() => {
|
|
400
|
+
console.log('Connection lost, attempting to reconnect...');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Connect
|
|
404
|
+
await client.connect();
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Document Tracking
|
|
408
|
+
|
|
409
|
+
Implement a simple document tracker:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
class DocumentTracker {
|
|
413
|
+
private documents = new Map<string, { version: number; content: string }>();
|
|
414
|
+
|
|
415
|
+
async open(client: LSPClient, uri: string, languageId: string, content: string): Promise<void> {
|
|
416
|
+
this.documents.set(uri, { version: 1, content });
|
|
417
|
+
|
|
418
|
+
await client.textDocument.didOpen({
|
|
419
|
+
textDocument: {
|
|
420
|
+
uri,
|
|
421
|
+
languageId,
|
|
422
|
+
version: 1,
|
|
423
|
+
text: content
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async change(client: LSPClient, uri: string, newContent: string): Promise<void> {
|
|
429
|
+
const doc = this.documents.get(uri);
|
|
430
|
+
if (!doc) return;
|
|
431
|
+
|
|
432
|
+
const newVersion = doc.version + 1;
|
|
433
|
+
this.documents.set(uri, { version: newVersion, content: newContent });
|
|
434
|
+
|
|
435
|
+
await client.textDocument.didChange({
|
|
436
|
+
textDocument: { uri, version: newVersion },
|
|
437
|
+
contentChanges: [{ text: newContent }]
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async close(client: LSPClient, uri: string): Promise<void> {
|
|
442
|
+
this.documents.delete(uri);
|
|
443
|
+
|
|
444
|
+
await client.textDocument.didClose({
|
|
445
|
+
textDocument: { uri }
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
get(uri: string): string | undefined {
|
|
450
|
+
return this.documents.get(uri)?.content;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Usage
|
|
455
|
+
const tracker = new DocumentTracker();
|
|
456
|
+
await tracker.open(client, 'file:///test.ts', 'typescript', 'console.log();');
|
|
457
|
+
await tracker.change(client, 'file:///test.ts', 'console.log("Hello");');
|
|
458
|
+
await tracker.close(client, 'file:///test.ts');
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Diagnostic Handling
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
const diagnostics = new Map<string, Diagnostic[]>();
|
|
465
|
+
|
|
466
|
+
client.onNotification('textDocument/publishDiagnostics', (params) => {
|
|
467
|
+
diagnostics.set(params.uri, params.diagnostics);
|
|
468
|
+
|
|
469
|
+
// Display diagnostics
|
|
470
|
+
for (const diagnostic of params.diagnostics) {
|
|
471
|
+
console.log(`${params.uri}:${diagnostic.range.start.line + 1}: ${diagnostic.message}`);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Get diagnostics for a file
|
|
476
|
+
function getDiagnostics(uri: string): Diagnostic[] {
|
|
477
|
+
return diagnostics.get(uri) || [];
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## Testing
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
import { LSPClient } from '@lspeasy/client';
|
|
485
|
+
import { MockTransport } from '@lspeasy/core/test/utils';
|
|
486
|
+
|
|
487
|
+
describe('LSP Client', () => {
|
|
488
|
+
it('should send hover request', async () => {
|
|
489
|
+
const transport = new MockTransport();
|
|
490
|
+
const client = new LSPClient({
|
|
491
|
+
name: 'Test Client',
|
|
492
|
+
version: '1.0.0',
|
|
493
|
+
transport
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
await client.connect();
|
|
497
|
+
|
|
498
|
+
// Send hover request
|
|
499
|
+
const hoverPromise = client.textDocument.hover({
|
|
500
|
+
textDocument: { uri: 'file:///test.ts' },
|
|
501
|
+
position: { line: 0, character: 0 }
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Simulate server response
|
|
505
|
+
const request = transport.sentMessages.find(m => m.method === 'textDocument/hover');
|
|
506
|
+
transport.simulateMessage({
|
|
507
|
+
jsonrpc: '2.0',
|
|
508
|
+
id: request.id,
|
|
509
|
+
result: {
|
|
510
|
+
contents: 'Test hover'
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const hover = await hoverPromise;
|
|
515
|
+
expect(hover?.contents).toBe('Test hover');
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
## Best Practices
|
|
521
|
+
|
|
522
|
+
### Always Call connect()
|
|
523
|
+
|
|
524
|
+
Ensure the client is initialized before sending requests:
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
await client.connect();
|
|
528
|
+
// Now safe to send requests
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Handle Disconnections
|
|
532
|
+
|
|
533
|
+
Subscribe to disconnection events and handle gracefully:
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
client.onDisconnected(() => {
|
|
537
|
+
console.log('Server disconnected');
|
|
538
|
+
// Attempt to reconnect or notify user
|
|
539
|
+
});
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Use High-Level API
|
|
543
|
+
|
|
544
|
+
Prefer high-level methods over low-level sendRequest:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
// Good
|
|
548
|
+
const hover = await client.textDocument.hover(params);
|
|
549
|
+
|
|
550
|
+
// Less type-safe
|
|
551
|
+
const hover = await client.sendRequest('textDocument/hover', params);
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Clean Up Resources
|
|
555
|
+
|
|
556
|
+
Always disconnect when done:
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
try {
|
|
560
|
+
await client.connect();
|
|
561
|
+
// Use client
|
|
562
|
+
} finally {
|
|
563
|
+
await client.disconnect();
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Handle Cancellation
|
|
568
|
+
|
|
569
|
+
Use cancellation tokens for long-running operations:
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
const source = new CancellationTokenSource();
|
|
573
|
+
const { promise } = client.sendRequestCancellable(method, params, source.token);
|
|
574
|
+
|
|
575
|
+
// Cancel if needed
|
|
576
|
+
setTimeout(() => source.cancel(), 5000);
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
## API Reference
|
|
580
|
+
|
|
581
|
+
See [API.md](../../docs/API.md) for complete API documentation.
|
|
582
|
+
|
|
583
|
+
## Architecture
|
|
584
|
+
|
|
585
|
+
See [ARCHITECTURE.md](../../docs/ARCHITECTURE.md) for system architecture details.
|
|
586
|
+
|
|
587
|
+
## License
|
|
588
|
+
|
|
589
|
+
MIT
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability validation for client requests
|
|
3
|
+
*
|
|
4
|
+
* Ensures clients only send requests/notifications that the server supports
|
|
5
|
+
*/
|
|
6
|
+
import type { ClientCapabilities, ServerCapabilities } from '@lspeasy/core';
|
|
7
|
+
import type { Logger } from '@lspeasy/core';
|
|
8
|
+
/**
|
|
9
|
+
* Validates that a request/notification can be sent based on
|
|
10
|
+
* declared server capabilities
|
|
11
|
+
*/
|
|
12
|
+
export declare class CapabilityGuard {
|
|
13
|
+
private readonly capabilities;
|
|
14
|
+
private readonly logger;
|
|
15
|
+
private readonly strict;
|
|
16
|
+
constructor(capabilities: Partial<ServerCapabilities>, logger: Logger, strict?: boolean);
|
|
17
|
+
/**
|
|
18
|
+
* Check if a request can be sent to the server
|
|
19
|
+
*
|
|
20
|
+
* @param method - LSP method name
|
|
21
|
+
* @returns true if allowed, false otherwise
|
|
22
|
+
* @throws Error if strict mode enabled and server capability not declared
|
|
23
|
+
*/
|
|
24
|
+
canSendRequest(method: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a notification can be sent to the server
|
|
27
|
+
*
|
|
28
|
+
* @param method - LSP method name
|
|
29
|
+
* @returns true if allowed, false otherwise
|
|
30
|
+
* @throws Error if strict mode enabled and server capability not declared
|
|
31
|
+
*/
|
|
32
|
+
canSendNotification(method: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Get list of capabilities the server declared
|
|
35
|
+
*/
|
|
36
|
+
getServerCapabilities(): Partial<ServerCapabilities>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validates that handlers can be registered based on
|
|
40
|
+
* declared client capabilities
|
|
41
|
+
*/
|
|
42
|
+
export declare class ClientCapabilityGuard {
|
|
43
|
+
private readonly capabilities;
|
|
44
|
+
private readonly logger;
|
|
45
|
+
private readonly strict;
|
|
46
|
+
constructor(capabilities: Partial<ClientCapabilities>, logger: Logger, strict?: boolean);
|
|
47
|
+
/**
|
|
48
|
+
* Check if handler registration is allowed for this method
|
|
49
|
+
*
|
|
50
|
+
* @param method - LSP method name
|
|
51
|
+
* @returns true if allowed, false otherwise
|
|
52
|
+
* @throws Error if strict mode enabled and client capability not declared
|
|
53
|
+
*/
|
|
54
|
+
canRegisterHandler(method: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Get list of capabilities the client declared
|
|
57
|
+
*/
|
|
58
|
+
getClientCapabilities(): Partial<ClientCapabilities>;
|
|
59
|
+
private isRequestAllowed;
|
|
60
|
+
private isNotificationAllowed;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=capability-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-guard.d.ts","sourceRoot":"","sources":["../src/capability-guard.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AA+C5C;;;GAGG;AACH,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHzB,YACmB,YAAY,EAAE,OAAO,CAAC,kBAAkB,CAAC,EACzC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAe,EACtC;IAEJ;;;;;;OAMG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAgDtC;IAED;;;;;;OAMG;IACH,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAgD3C;IAED;;OAEG;IACH,qBAAqB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAEnD;CACF;AAED;;;GAGG;AACH,qBAAa,qBAAqB;IAE9B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHzB,YACmB,YAAY,EAAE,OAAO,CAAC,kBAAkB,CAAC,EACzC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAe,EACtC;IAEJ;;;;;;OAMG;IACH,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAiC1C;IAED;;OAEG;IACH,qBAAqB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAEnD;IAED,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,qBAAqB;CAiB9B"}
|