codemirror-languageserver 1.18.1 → 1.20.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/README.md +201 -26
- package/dist/definition.d.ts +12 -0
- package/dist/formatting.d.ts +14 -0
- package/dist/highlight.d.ts +8 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +601 -66
- package/dist/jsonrpc.d.ts +42 -0
- package/dist/plugin.d.ts +59 -6
- package/dist/rename.d.ts +10 -0
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,10 +1,121 @@
|
|
|
1
|
-
import { insertCompletionText, autocompletion as autocompletion$1 } from '@codemirror/autocomplete';
|
|
2
1
|
import { setDiagnostics } from '@codemirror/lint';
|
|
3
|
-
import { Facet } from '@codemirror/state';
|
|
4
|
-
import { ViewPlugin, hoverTooltip as hoverTooltip$1, EditorView, keymap } from '@codemirror/view';
|
|
5
|
-
import {
|
|
6
|
-
import { CompletionItemKind, DiagnosticSeverity, CompletionTriggerKind } from 'vscode-languageserver-protocol';
|
|
2
|
+
import { Facet, StateEffect, StateField } from '@codemirror/state';
|
|
3
|
+
import { ViewPlugin, showPanel, hoverTooltip as hoverTooltip$1, EditorView, Decoration, keymap } from '@codemirror/view';
|
|
4
|
+
import { CompletionItemKind, DiagnosticSeverity, CompletionTriggerKind, DocumentHighlightKind } from 'vscode-languageserver-protocol';
|
|
7
5
|
import { marked } from 'marked';
|
|
6
|
+
import { autocompletion as autocompletion$1 } from '@codemirror/autocomplete';
|
|
7
|
+
|
|
8
|
+
/** A {@link Transport} that communicates over a browser-native WebSocket. */
|
|
9
|
+
class WebSocketTransport {
|
|
10
|
+
constructor(uri) {
|
|
11
|
+
this.connection = new WebSocket(uri);
|
|
12
|
+
}
|
|
13
|
+
send(message) {
|
|
14
|
+
this.connection.send(message);
|
|
15
|
+
}
|
|
16
|
+
onMessage(callback) {
|
|
17
|
+
this.connection.addEventListener('message', (event) => {
|
|
18
|
+
callback(typeof event.data === 'string' ? event.data : '');
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
onClose(callback) {
|
|
22
|
+
this.connection.addEventListener('close', callback);
|
|
23
|
+
}
|
|
24
|
+
onError(callback) {
|
|
25
|
+
this.connection.addEventListener('error', () => {
|
|
26
|
+
callback(new Error('WebSocket error'));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
close() {
|
|
30
|
+
this.connection.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
class JSONRPCClient {
|
|
34
|
+
constructor(transport) {
|
|
35
|
+
this.nextId = 0;
|
|
36
|
+
this.pending = new Map();
|
|
37
|
+
this.notificationHandlers = [];
|
|
38
|
+
this.transport = transport;
|
|
39
|
+
this.ready = new Promise((resolve, reject) => {
|
|
40
|
+
if (transport instanceof WebSocketTransport) {
|
|
41
|
+
const ws = transport.connection;
|
|
42
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
43
|
+
resolve();
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
ws.addEventListener('open', () => resolve());
|
|
47
|
+
ws.addEventListener('error', () => reject(new Error('WebSocket connection failed')));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
resolve();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
transport.onMessage((raw) => {
|
|
55
|
+
let msg;
|
|
56
|
+
try {
|
|
57
|
+
msg = JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if ('id' in msg && msg.id != null && this.pending.has(msg.id)) {
|
|
63
|
+
const pending = this.pending.get(msg.id);
|
|
64
|
+
this.pending.delete(msg.id);
|
|
65
|
+
clearTimeout(pending.timer);
|
|
66
|
+
if (msg.error) {
|
|
67
|
+
pending.reject(new Error(msg.error.message || 'JSON-RPC error'));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
pending.resolve(msg.result);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if ('method' in msg) {
|
|
74
|
+
for (const handler of this.notificationHandlers) {
|
|
75
|
+
handler(msg);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async request(req, timeout) {
|
|
81
|
+
await this.ready;
|
|
82
|
+
const id = this.nextId++;
|
|
83
|
+
const message = {
|
|
84
|
+
jsonrpc: '2.0',
|
|
85
|
+
id,
|
|
86
|
+
method: req.method,
|
|
87
|
+
params: req.params,
|
|
88
|
+
};
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const timer = setTimeout(() => {
|
|
91
|
+
this.pending.delete(id);
|
|
92
|
+
reject(new Error(`Request ${req.method} timed out`));
|
|
93
|
+
}, timeout);
|
|
94
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
95
|
+
this.transport.send(JSON.stringify(message));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async notify(notification) {
|
|
99
|
+
await this.ready;
|
|
100
|
+
const message = {
|
|
101
|
+
jsonrpc: '2.0',
|
|
102
|
+
method: notification.method,
|
|
103
|
+
params: notification.params,
|
|
104
|
+
};
|
|
105
|
+
this.transport.send(JSON.stringify(message));
|
|
106
|
+
}
|
|
107
|
+
onNotification(handler) {
|
|
108
|
+
this.notificationHandlers.push(handler);
|
|
109
|
+
}
|
|
110
|
+
close() {
|
|
111
|
+
for (const [id, pending] of this.pending) {
|
|
112
|
+
clearTimeout(pending.timer);
|
|
113
|
+
pending.reject(new Error('Client closed'));
|
|
114
|
+
}
|
|
115
|
+
this.pending.clear();
|
|
116
|
+
this.transport.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
8
119
|
|
|
9
120
|
function posToOffset(doc, pos) {
|
|
10
121
|
if (pos.line >= doc.lines) {
|
|
@@ -46,6 +157,7 @@ function changeSetToEvents(doc, changes) {
|
|
|
46
157
|
}
|
|
47
158
|
|
|
48
159
|
const timeout = 10000;
|
|
160
|
+
const changesDelay = 500;
|
|
49
161
|
const CompletionItemKindMap = Object.fromEntries(Object.entries(CompletionItemKind).map(([key, value]) => [value, key]));
|
|
50
162
|
const useLast = (values) => values.reduce((_, v) => v, '');
|
|
51
163
|
Facet.define({
|
|
@@ -53,6 +165,12 @@ Facet.define({
|
|
|
53
165
|
});
|
|
54
166
|
Facet.define({ combine: useLast });
|
|
55
167
|
Facet.define({ combine: useLast });
|
|
168
|
+
/**
|
|
169
|
+
* Manages the JSON-RPC connection to a language server.
|
|
170
|
+
*
|
|
171
|
+
* Handles initialization, request/response routing, and notification dispatch.
|
|
172
|
+
* Can be shared across multiple editor instances via the `client` option.
|
|
173
|
+
*/
|
|
56
174
|
class LanguageServerClient {
|
|
57
175
|
constructor(options) {
|
|
58
176
|
this.options = options;
|
|
@@ -61,26 +179,19 @@ class LanguageServerClient {
|
|
|
61
179
|
this.autoClose = options.autoClose;
|
|
62
180
|
this.plugins = [];
|
|
63
181
|
this.transport = options.transport;
|
|
64
|
-
this.
|
|
65
|
-
this.client = new Client(this.requestManager);
|
|
182
|
+
this.client = new JSONRPCClient(this.transport);
|
|
66
183
|
this.client.onNotification((data) => {
|
|
184
|
+
if (data.method && data.id) {
|
|
185
|
+
// Server-to-client request: respond with null result.
|
|
186
|
+
// https://github.com/FurqanSoftware/codemirror-languageserver/issues/9
|
|
187
|
+
this.transport.send(JSON.stringify({
|
|
188
|
+
jsonrpc: '2.0',
|
|
189
|
+
id: data.id,
|
|
190
|
+
result: null,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
67
193
|
this.processNotification(data);
|
|
68
194
|
});
|
|
69
|
-
const webSocketTransport = this.transport;
|
|
70
|
-
if (webSocketTransport && webSocketTransport.connection) {
|
|
71
|
-
// XXX(hjr265): Need a better way to do this. Relevant issue:
|
|
72
|
-
// https://github.com/FurqanSoftware/codemirror-languageserver/issues/9
|
|
73
|
-
webSocketTransport.connection.addEventListener('message', (message) => {
|
|
74
|
-
const data = JSON.parse(message.data);
|
|
75
|
-
if (data.method && data.id) {
|
|
76
|
-
webSocketTransport.connection.send(JSON.stringify({
|
|
77
|
-
jsonrpc: '2.0',
|
|
78
|
-
id: data.id,
|
|
79
|
-
result: null,
|
|
80
|
-
}));
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
195
|
this.initializePromise = this.initialize();
|
|
85
196
|
}
|
|
86
197
|
getInitializeParams() {
|
|
@@ -135,6 +246,19 @@ class LanguageServerClient {
|
|
|
135
246
|
dynamicRegistration: true,
|
|
136
247
|
linkSupport: true,
|
|
137
248
|
},
|
|
249
|
+
documentHighlight: {
|
|
250
|
+
dynamicRegistration: true,
|
|
251
|
+
},
|
|
252
|
+
formatting: {
|
|
253
|
+
dynamicRegistration: true,
|
|
254
|
+
},
|
|
255
|
+
rangeFormatting: {
|
|
256
|
+
dynamicRegistration: true,
|
|
257
|
+
},
|
|
258
|
+
rename: {
|
|
259
|
+
dynamicRegistration: true,
|
|
260
|
+
prepareSupport: true,
|
|
261
|
+
},
|
|
138
262
|
},
|
|
139
263
|
workspace: {
|
|
140
264
|
didChangeConfiguration: {
|
|
@@ -143,16 +267,19 @@ class LanguageServerClient {
|
|
|
143
267
|
},
|
|
144
268
|
},
|
|
145
269
|
initializationOptions: (_a = this.options.initializationOptions) !== null && _a !== void 0 ? _a : null,
|
|
270
|
+
locale: this.options.locale,
|
|
146
271
|
processId: null,
|
|
147
272
|
rootUri: this.rootUri,
|
|
148
273
|
workspaceFolders: this.workspaceFolders,
|
|
149
274
|
};
|
|
150
275
|
}
|
|
151
276
|
async initialize() {
|
|
277
|
+
var _a, _b;
|
|
152
278
|
const { capabilities } = await this.request('initialize', this.getInitializeParams(), timeout * 3);
|
|
153
279
|
this.capabilities = capabilities;
|
|
154
280
|
this.notify('initialized', {});
|
|
155
281
|
this.ready = true;
|
|
282
|
+
(_b = (_a = this.options).onCapabilities) === null || _b === void 0 ? void 0 : _b.call(_a, capabilities);
|
|
156
283
|
}
|
|
157
284
|
close() {
|
|
158
285
|
this.client.close();
|
|
@@ -178,6 +305,21 @@ class LanguageServerClient {
|
|
|
178
305
|
async textDocumentTypeDefinition(params) {
|
|
179
306
|
return await this.request('textDocument/typeDefinition', params, timeout);
|
|
180
307
|
}
|
|
308
|
+
async textDocumentDocumentHighlight(params) {
|
|
309
|
+
return await this.request('textDocument/documentHighlight', params, timeout);
|
|
310
|
+
}
|
|
311
|
+
async textDocumentFormatting(params) {
|
|
312
|
+
return await this.request('textDocument/formatting', params, timeout);
|
|
313
|
+
}
|
|
314
|
+
async textDocumentRangeFormatting(params) {
|
|
315
|
+
return await this.request('textDocument/rangeFormatting', params, timeout);
|
|
316
|
+
}
|
|
317
|
+
async textDocumentPrepareRename(params) {
|
|
318
|
+
return await this.request('textDocument/prepareRename', params, timeout);
|
|
319
|
+
}
|
|
320
|
+
async textDocumentRename(params) {
|
|
321
|
+
return await this.request('textDocument/rename', params, timeout);
|
|
322
|
+
}
|
|
181
323
|
async completionItemResolve(params) {
|
|
182
324
|
return await this.request('completionItem/resolve', params, timeout);
|
|
183
325
|
}
|
|
@@ -206,14 +348,19 @@ class LanguageServerClient {
|
|
|
206
348
|
}
|
|
207
349
|
}
|
|
208
350
|
}
|
|
351
|
+
/** Controls how document changes are sent to the language server. */
|
|
209
352
|
var SynchronizationMethod;
|
|
210
353
|
(function (SynchronizationMethod) {
|
|
354
|
+
/** Send the full document text on every change. */
|
|
211
355
|
SynchronizationMethod["Full"] = "full";
|
|
356
|
+
/** Send only the changed ranges. More efficient for large documents. */
|
|
212
357
|
SynchronizationMethod["Incremental"] = "incremental";
|
|
213
358
|
})(SynchronizationMethod || (SynchronizationMethod = {}));
|
|
214
359
|
class LanguageServerPlugin {
|
|
215
360
|
constructor(view, { client, documentUri, languageId, allowHTMLContent, synchronizationMethod, }) {
|
|
216
361
|
this.view = view;
|
|
362
|
+
this.changesTimeout = null;
|
|
363
|
+
this.pendingChanges = null;
|
|
217
364
|
this.client = client;
|
|
218
365
|
this.documentUri = documentUri;
|
|
219
366
|
this.languageId = languageId;
|
|
@@ -230,20 +377,40 @@ class LanguageServerPlugin {
|
|
|
230
377
|
if (!docChanged) {
|
|
231
378
|
return;
|
|
232
379
|
}
|
|
380
|
+
if (this.changesTimeout) {
|
|
381
|
+
clearTimeout(this.changesTimeout);
|
|
382
|
+
}
|
|
233
383
|
switch (this.synchronizationMethod) {
|
|
234
384
|
case SynchronizationMethod.Full:
|
|
235
|
-
this.
|
|
385
|
+
this.pendingChanges = [
|
|
236
386
|
{
|
|
237
387
|
text: this.view.state.doc.toString(),
|
|
238
388
|
},
|
|
239
|
-
]
|
|
389
|
+
];
|
|
240
390
|
break;
|
|
241
391
|
case SynchronizationMethod.Incremental:
|
|
242
|
-
this.
|
|
392
|
+
if (!this.pendingChanges) {
|
|
393
|
+
this.pendingChanges = [];
|
|
394
|
+
}
|
|
395
|
+
this.pendingChanges.push(...changeSetToEvents(startState.doc, changes));
|
|
243
396
|
break;
|
|
244
397
|
}
|
|
398
|
+
this.changesTimeout = setTimeout(() => {
|
|
399
|
+
this.changesTimeout = null;
|
|
400
|
+
if (this.pendingChanges) {
|
|
401
|
+
this.sendChanges(this.pendingChanges);
|
|
402
|
+
this.pendingChanges = null;
|
|
403
|
+
}
|
|
404
|
+
}, changesDelay);
|
|
245
405
|
}
|
|
246
406
|
destroy() {
|
|
407
|
+
if (this.changesTimeout) {
|
|
408
|
+
clearTimeout(this.changesTimeout);
|
|
409
|
+
if (this.pendingChanges) {
|
|
410
|
+
this.sendChanges(this.pendingChanges);
|
|
411
|
+
this.pendingChanges = null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
247
414
|
this.client.detachPlugin(this);
|
|
248
415
|
}
|
|
249
416
|
async initialize({ documentText }) {
|
|
@@ -302,11 +469,12 @@ class LanguageServerPlugin {
|
|
|
302
469
|
}
|
|
303
470
|
const dom = document.createElement('div');
|
|
304
471
|
dom.classList.add('documentation');
|
|
305
|
-
|
|
306
|
-
|
|
472
|
+
const formatted = await formatContents(contents);
|
|
473
|
+
if (formatted.isHTML || this.allowHTMLContent) {
|
|
474
|
+
dom.innerHTML = formatted.value;
|
|
307
475
|
}
|
|
308
476
|
else {
|
|
309
|
-
dom.textContent =
|
|
477
|
+
dom.textContent = formatted.value;
|
|
310
478
|
}
|
|
311
479
|
return {
|
|
312
480
|
pos,
|
|
@@ -366,56 +534,52 @@ class LanguageServerPlugin {
|
|
|
366
534
|
label,
|
|
367
535
|
detail,
|
|
368
536
|
apply(view, completion, from, to) {
|
|
537
|
+
const changes = [];
|
|
369
538
|
if (isLSPTextEdit(textEdit)) {
|
|
370
|
-
|
|
539
|
+
changes.push({
|
|
540
|
+
from: posToOffset(view.state.doc, textEdit.range.start),
|
|
541
|
+
to: Math.max(posToOffset(view.state.doc, textEdit.range.end), to),
|
|
542
|
+
insert: textEdit.newText,
|
|
543
|
+
});
|
|
371
544
|
}
|
|
372
545
|
else {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
546
|
+
changes.push({
|
|
547
|
+
from,
|
|
548
|
+
to,
|
|
549
|
+
insert: label,
|
|
550
|
+
});
|
|
377
551
|
}
|
|
378
|
-
additionalTextEdits
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
552
|
+
if (additionalTextEdits) {
|
|
553
|
+
for (const edit of additionalTextEdits) {
|
|
554
|
+
changes.push({
|
|
555
|
+
from: posToOffset(view.state.doc, edit.range.start),
|
|
556
|
+
to: posToOffset(view.state.doc, edit.range.end),
|
|
557
|
+
insert: edit.newText,
|
|
558
|
+
});
|
|
383
559
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return -1;
|
|
387
|
-
}
|
|
388
|
-
return 0;
|
|
389
|
-
})
|
|
390
|
-
.forEach((textEdit) => {
|
|
391
|
-
view.dispatch(view.state.update({
|
|
392
|
-
changes: {
|
|
393
|
-
from: posToOffset(view.state.doc, textEdit.range.start),
|
|
394
|
-
to: posToOffset(view.state.doc, textEdit.range.end),
|
|
395
|
-
insert: textEdit.newText,
|
|
396
|
-
},
|
|
397
|
-
}));
|
|
398
|
-
});
|
|
560
|
+
}
|
|
561
|
+
view.dispatch(view.state.update({ changes }));
|
|
399
562
|
},
|
|
400
563
|
type: kind && CompletionItemKindMap[kind].toLowerCase(),
|
|
401
564
|
};
|
|
402
565
|
completion.info = async () => {
|
|
403
566
|
var _a;
|
|
404
|
-
|
|
405
|
-
if (!documentation &&
|
|
567
|
+
if (!item.documentation &&
|
|
406
568
|
((_a = this.client.capabilities.completionProvider) === null || _a === void 0 ? void 0 : _a.resolveProvider)) {
|
|
407
569
|
const resolvedItem = await this.client.completionItemResolve(item);
|
|
408
|
-
documentation = resolvedItem.documentation;
|
|
570
|
+
item.documentation = resolvedItem.documentation;
|
|
409
571
|
}
|
|
572
|
+
const { documentation } = item;
|
|
410
573
|
if (!documentation)
|
|
411
574
|
return null;
|
|
412
575
|
const dom = document.createElement('div');
|
|
413
576
|
dom.classList.add('documentation');
|
|
414
|
-
|
|
415
|
-
|
|
577
|
+
const formatted = await formatContents(documentation);
|
|
578
|
+
if (formatted.isHTML || this.allowHTMLContent) {
|
|
579
|
+
dom.innerHTML = formatted.value;
|
|
416
580
|
}
|
|
417
581
|
else {
|
|
418
|
-
dom.textContent =
|
|
582
|
+
dom.textContent = formatted.value;
|
|
419
583
|
}
|
|
420
584
|
return dom;
|
|
421
585
|
};
|
|
@@ -511,19 +675,32 @@ class LanguageServerPlugin {
|
|
|
511
675
|
const languageServerPlugin = ViewPlugin.fromClass(LanguageServerPlugin);
|
|
512
676
|
async function formatContents(contents) {
|
|
513
677
|
if (isLSPMarkupContent(contents)) {
|
|
514
|
-
let value = contents.value;
|
|
515
678
|
if (contents.kind === 'markdown') {
|
|
516
|
-
|
|
679
|
+
return {
|
|
680
|
+
value: await marked.parse(contents.value),
|
|
681
|
+
isHTML: true,
|
|
682
|
+
};
|
|
517
683
|
}
|
|
518
|
-
return value;
|
|
684
|
+
return { value: contents.value, isHTML: false };
|
|
519
685
|
}
|
|
520
686
|
else if (Array.isArray(contents)) {
|
|
521
|
-
|
|
687
|
+
const parts = await Promise.all(contents.map((c) => formatContents(c)));
|
|
688
|
+
const isHTML = parts.some((p) => p.isHTML);
|
|
689
|
+
const value = parts
|
|
690
|
+
.map((p) => (isHTML && !p.isHTML ? escapeHTML(p.value) : p.value))
|
|
691
|
+
.join(isHTML ? '<br><br>' : '\n\n');
|
|
692
|
+
return { value, isHTML };
|
|
522
693
|
}
|
|
523
694
|
else if (typeof contents === 'string') {
|
|
524
|
-
return contents;
|
|
695
|
+
return { value: contents, isHTML: false };
|
|
525
696
|
}
|
|
526
697
|
}
|
|
698
|
+
function escapeHTML(text) {
|
|
699
|
+
return text
|
|
700
|
+
.replace(/&/g, '&')
|
|
701
|
+
.replace(/</g, '<')
|
|
702
|
+
.replace(/>/g, '>');
|
|
703
|
+
}
|
|
527
704
|
function toSet(chars) {
|
|
528
705
|
let preamble = '';
|
|
529
706
|
let flat = Array.from(chars).join('');
|
|
@@ -570,15 +747,256 @@ function requestTypeDefinition(view, pos) {
|
|
|
570
747
|
plugin.requestTypeDefinition(view, offsetToPos(view.state.doc, pos));
|
|
571
748
|
return true;
|
|
572
749
|
}
|
|
750
|
+
/** Command: jump to the definition of the symbol at the cursor. */
|
|
573
751
|
const jumpToDefinition = (view) => requestDefinition(view, view.state.selection.main.head);
|
|
752
|
+
/** Returns a command that jumps to the definition of the symbol at `pos`. */
|
|
574
753
|
const jumpToDefinitionPos = (pos) => (view) => requestDefinition(view, pos);
|
|
754
|
+
/** Command: jump to the type definition of the symbol at the cursor. */
|
|
575
755
|
const jumpToTypeDefinition = (view) => requestTypeDefinition(view, view.state.selection.main.head);
|
|
756
|
+
/** Returns a command that jumps to the type definition of the symbol at `pos`. */
|
|
576
757
|
const jumpToTypeDefinitionPos = (pos) => (view) => requestTypeDefinition(view, pos);
|
|
758
|
+
/**
|
|
759
|
+
* Default keymap for go-to-definition commands.
|
|
760
|
+
*
|
|
761
|
+
* - `F12` — jump to definition
|
|
762
|
+
* - `Mod-F12` — jump to type definition
|
|
763
|
+
*/
|
|
577
764
|
const jumpToDefinitionKeymap = [
|
|
578
765
|
{ key: 'F12', run: jumpToDefinition, preventDefault: true },
|
|
579
766
|
{ key: 'Mod-F12', run: jumpToTypeDefinition, preventDefault: true },
|
|
580
767
|
];
|
|
581
768
|
|
|
769
|
+
/**
|
|
770
|
+
* Facet for configuring LSP formatting options (tab size, spaces vs tabs, etc.).
|
|
771
|
+
*
|
|
772
|
+
* Defaults to `{ tabSize: 4, insertSpaces: true }`. The `tabSize` falls back
|
|
773
|
+
* to the editor state's `tabSize` if not explicitly set.
|
|
774
|
+
*/
|
|
775
|
+
const formattingOptions = Facet.define({
|
|
776
|
+
combine(values) {
|
|
777
|
+
return Object.assign({ tabSize: 4, insertSpaces: true }, ...values);
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
function getFormattingOptions(view) {
|
|
781
|
+
var _a;
|
|
782
|
+
const opts = view.state.facet(formattingOptions);
|
|
783
|
+
return {
|
|
784
|
+
...opts,
|
|
785
|
+
tabSize: (_a = opts.tabSize) !== null && _a !== void 0 ? _a : view.state.tabSize,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function applyTextEdits(view, edits) {
|
|
789
|
+
const changes = edits.map((edit) => ({
|
|
790
|
+
from: posToOffset(view.state.doc, edit.range.start),
|
|
791
|
+
to: posToOffset(view.state.doc, edit.range.end),
|
|
792
|
+
insert: edit.newText,
|
|
793
|
+
}));
|
|
794
|
+
view.dispatch(view.state.update({ changes }));
|
|
795
|
+
}
|
|
796
|
+
/** Command: format the entire document using the language server. */
|
|
797
|
+
function formatDocument(view) {
|
|
798
|
+
var _a;
|
|
799
|
+
const plugin = view.plugin(languageServerPlugin);
|
|
800
|
+
if (!plugin)
|
|
801
|
+
return false;
|
|
802
|
+
if (!plugin.client.ready ||
|
|
803
|
+
!((_a = plugin.client.capabilities) === null || _a === void 0 ? void 0 : _a.documentFormattingProvider)) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
plugin.client
|
|
807
|
+
.textDocumentFormatting({
|
|
808
|
+
textDocument: { uri: plugin.documentUri },
|
|
809
|
+
options: getFormattingOptions(view),
|
|
810
|
+
})
|
|
811
|
+
.then((edits) => {
|
|
812
|
+
if (edits && edits.length > 0) {
|
|
813
|
+
applyTextEdits(view, edits);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
/** Command: format the current selection using the language server. */
|
|
819
|
+
function formatSelection(view) {
|
|
820
|
+
var _a;
|
|
821
|
+
const plugin = view.plugin(languageServerPlugin);
|
|
822
|
+
if (!plugin)
|
|
823
|
+
return false;
|
|
824
|
+
if (!plugin.client.ready ||
|
|
825
|
+
!((_a = plugin.client.capabilities) === null || _a === void 0 ? void 0 : _a.documentRangeFormattingProvider)) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
const { from, to } = view.state.selection.main;
|
|
829
|
+
plugin.client
|
|
830
|
+
.textDocumentRangeFormatting({
|
|
831
|
+
textDocument: { uri: plugin.documentUri },
|
|
832
|
+
range: {
|
|
833
|
+
start: offsetToPos(view.state.doc, from),
|
|
834
|
+
end: offsetToPos(view.state.doc, to),
|
|
835
|
+
},
|
|
836
|
+
options: getFormattingOptions(view),
|
|
837
|
+
})
|
|
838
|
+
.then((edits) => {
|
|
839
|
+
if (edits && edits.length > 0) {
|
|
840
|
+
applyTextEdits(view, edits);
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const openRenamePanel = StateEffect.define();
|
|
847
|
+
const closeRenamePanel = StateEffect.define();
|
|
848
|
+
const renamePanelState = StateField.define({
|
|
849
|
+
create() {
|
|
850
|
+
return null;
|
|
851
|
+
},
|
|
852
|
+
update(value, tr) {
|
|
853
|
+
for (const effect of tr.effects) {
|
|
854
|
+
if (effect.is(openRenamePanel))
|
|
855
|
+
return effect.value;
|
|
856
|
+
if (effect.is(closeRenamePanel))
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
return value;
|
|
860
|
+
},
|
|
861
|
+
provide: (field) => showPanel.from(field, (value) => value != null ? (view) => createRenamePanel(view, value) : null),
|
|
862
|
+
});
|
|
863
|
+
function applyWorkspaceEdit(view, edit) {
|
|
864
|
+
const plugin = view.plugin(languageServerPlugin);
|
|
865
|
+
if (!plugin)
|
|
866
|
+
return;
|
|
867
|
+
const changes = [];
|
|
868
|
+
if (edit.changes) {
|
|
869
|
+
const edits = edit.changes[plugin.documentUri];
|
|
870
|
+
if (edits) {
|
|
871
|
+
for (const e of edits) {
|
|
872
|
+
changes.push({
|
|
873
|
+
from: posToOffset(view.state.doc, e.range.start),
|
|
874
|
+
to: posToOffset(view.state.doc, e.range.end),
|
|
875
|
+
insert: e.newText,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (edit.documentChanges) {
|
|
881
|
+
for (const change of edit.documentChanges) {
|
|
882
|
+
if ('textDocument' in change) {
|
|
883
|
+
if (change.textDocument.uri !== plugin.documentUri)
|
|
884
|
+
continue;
|
|
885
|
+
for (const e of change.edits) {
|
|
886
|
+
if ('range' in e) {
|
|
887
|
+
changes.push({
|
|
888
|
+
from: posToOffset(view.state.doc, e.range.start),
|
|
889
|
+
to: posToOffset(view.state.doc, e.range.end),
|
|
890
|
+
insert: e.newText,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (changes.length > 0) {
|
|
898
|
+
view.dispatch(view.state.update({ changes }));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function createRenamePanel(view, placeholder) {
|
|
902
|
+
const dom = document.createElement('div');
|
|
903
|
+
dom.className = 'cm-lsp-rename-panel';
|
|
904
|
+
const input = document.createElement('input');
|
|
905
|
+
input.className = 'cm-lsp-rename-input';
|
|
906
|
+
input.value = placeholder;
|
|
907
|
+
input.setAttribute('aria-label', 'New name');
|
|
908
|
+
const submit = () => {
|
|
909
|
+
const newName = input.value.trim();
|
|
910
|
+
if (!newName || newName === placeholder) {
|
|
911
|
+
view.dispatch({ effects: closeRenamePanel.of(null) });
|
|
912
|
+
view.focus();
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
performRename(view, newName);
|
|
916
|
+
};
|
|
917
|
+
input.addEventListener('keydown', (e) => {
|
|
918
|
+
if (e.key === 'Enter') {
|
|
919
|
+
e.preventDefault();
|
|
920
|
+
submit();
|
|
921
|
+
}
|
|
922
|
+
else if (e.key === 'Escape') {
|
|
923
|
+
e.preventDefault();
|
|
924
|
+
view.dispatch({ effects: closeRenamePanel.of(null) });
|
|
925
|
+
view.focus();
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
dom.appendChild(input);
|
|
929
|
+
return {
|
|
930
|
+
dom,
|
|
931
|
+
top: true,
|
|
932
|
+
mount() {
|
|
933
|
+
input.select();
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
async function performRename(view, newName) {
|
|
938
|
+
const plugin = view.plugin(languageServerPlugin);
|
|
939
|
+
if (!plugin)
|
|
940
|
+
return;
|
|
941
|
+
const pos = view.state.selection.main.head;
|
|
942
|
+
view.dispatch({ effects: closeRenamePanel.of(null) });
|
|
943
|
+
view.focus();
|
|
944
|
+
const result = await plugin.client.textDocumentRename({
|
|
945
|
+
textDocument: { uri: plugin.documentUri },
|
|
946
|
+
position: offsetToPos(view.state.doc, pos),
|
|
947
|
+
newName,
|
|
948
|
+
});
|
|
949
|
+
if (result) {
|
|
950
|
+
applyWorkspaceEdit(view, result);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Command: open a rename prompt for the symbol at the cursor.
|
|
955
|
+
*
|
|
956
|
+
* Uses `textDocument/prepareRename` to pre-fill the current name if supported
|
|
957
|
+
* by the server. Press Enter to confirm or Escape to cancel.
|
|
958
|
+
*/
|
|
959
|
+
function renameSymbol(view) {
|
|
960
|
+
var _a;
|
|
961
|
+
const plugin = view.plugin(languageServerPlugin);
|
|
962
|
+
if (!plugin)
|
|
963
|
+
return false;
|
|
964
|
+
if (!plugin.client.ready ||
|
|
965
|
+
!((_a = plugin.client.capabilities) === null || _a === void 0 ? void 0 : _a.renameProvider)) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
const pos = view.state.selection.main.head;
|
|
969
|
+
const capabilities = plugin.client.capabilities;
|
|
970
|
+
const renameProvider = capabilities.renameProvider;
|
|
971
|
+
const supportsPrepare = typeof renameProvider === 'object' && renameProvider.prepareProvider;
|
|
972
|
+
if (supportsPrepare) {
|
|
973
|
+
plugin.client
|
|
974
|
+
.textDocumentPrepareRename({
|
|
975
|
+
textDocument: { uri: plugin.documentUri },
|
|
976
|
+
position: offsetToPos(view.state.doc, pos),
|
|
977
|
+
})
|
|
978
|
+
.then((result) => {
|
|
979
|
+
if (!result)
|
|
980
|
+
return;
|
|
981
|
+
const placeholder = 'placeholder' in result
|
|
982
|
+
? result.placeholder
|
|
983
|
+
: view.state.doc.sliceString(posToOffset(view.state.doc, result.start), posToOffset(view.state.doc, result.end));
|
|
984
|
+
view.dispatch({
|
|
985
|
+
effects: openRenamePanel.of(placeholder),
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
const word = view.state.wordAt(pos);
|
|
991
|
+
const placeholder = word
|
|
992
|
+
? view.state.doc.sliceString(word.from, word.to)
|
|
993
|
+
: '';
|
|
994
|
+
view.dispatch({ effects: openRenamePanel.of(placeholder) });
|
|
995
|
+
}
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
const renameExtension = () => renamePanelState;
|
|
999
|
+
|
|
582
1000
|
const hoverTooltip = () => hoverTooltip$1((view, pos) => {
|
|
583
1001
|
var _a;
|
|
584
1002
|
const plugin = view.plugin(languageServerPlugin);
|
|
@@ -601,7 +1019,8 @@ const autocompletion = () => autocompletion$1({
|
|
|
601
1019
|
trigKind = CompletionTriggerKind.TriggerCharacter;
|
|
602
1020
|
trigChar = line.text[pos - line.from - 1];
|
|
603
1021
|
}
|
|
604
|
-
if (
|
|
1022
|
+
if (!explicit &&
|
|
1023
|
+
trigKind === CompletionTriggerKind.Invoked &&
|
|
605
1024
|
!context.matchBefore(/\w+$/)) {
|
|
606
1025
|
return null;
|
|
607
1026
|
}
|
|
@@ -613,6 +1032,109 @@ const autocompletion = () => autocompletion$1({
|
|
|
613
1032
|
],
|
|
614
1033
|
});
|
|
615
1034
|
|
|
1035
|
+
const highlightField = StateField.define({
|
|
1036
|
+
create() {
|
|
1037
|
+
return Decoration.none;
|
|
1038
|
+
},
|
|
1039
|
+
update(decorations, tr) {
|
|
1040
|
+
for (const effect of tr.effects) {
|
|
1041
|
+
if (effect.is(setHighlightsEffect)) {
|
|
1042
|
+
return effect.value;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (tr.docChanged) {
|
|
1046
|
+
return Decoration.none;
|
|
1047
|
+
}
|
|
1048
|
+
return decorations;
|
|
1049
|
+
},
|
|
1050
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
1051
|
+
});
|
|
1052
|
+
const setHighlightsEffect = StateEffect.define();
|
|
1053
|
+
const textDecoration = Decoration.mark({
|
|
1054
|
+
class: 'cm-lsp-highlight-text',
|
|
1055
|
+
});
|
|
1056
|
+
const readDecoration = Decoration.mark({
|
|
1057
|
+
class: 'cm-lsp-highlight-read',
|
|
1058
|
+
});
|
|
1059
|
+
const writeDecoration = Decoration.mark({
|
|
1060
|
+
class: 'cm-lsp-highlight-write',
|
|
1061
|
+
});
|
|
1062
|
+
function decorationForKind(kind) {
|
|
1063
|
+
switch (kind) {
|
|
1064
|
+
case DocumentHighlightKind.Read:
|
|
1065
|
+
return readDecoration;
|
|
1066
|
+
case DocumentHighlightKind.Write:
|
|
1067
|
+
return writeDecoration;
|
|
1068
|
+
default:
|
|
1069
|
+
return textDecoration;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const highlightPlugin = ViewPlugin.fromClass(class {
|
|
1073
|
+
constructor() {
|
|
1074
|
+
this.pending = -1;
|
|
1075
|
+
}
|
|
1076
|
+
update(update) {
|
|
1077
|
+
if (!update.selectionSet && !update.docChanged) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (update.docChanged) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
clearTimeout(this.pending);
|
|
1084
|
+
this.pending = setTimeout(() => this.requestHighlights(update.view), 100);
|
|
1085
|
+
}
|
|
1086
|
+
async requestHighlights(view) {
|
|
1087
|
+
var _a;
|
|
1088
|
+
const plugin = view.plugin(languageServerPlugin);
|
|
1089
|
+
if (!plugin)
|
|
1090
|
+
return;
|
|
1091
|
+
const { state } = view;
|
|
1092
|
+
const pos = state.selection.main.head;
|
|
1093
|
+
if (!plugin.client.ready ||
|
|
1094
|
+
!((_a = plugin.client.capabilities) === null || _a === void 0 ? void 0 : _a.documentHighlightProvider)) {
|
|
1095
|
+
view.dispatch({ effects: setHighlightsEffect.of(Decoration.none) });
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const result = await plugin.client.textDocumentDocumentHighlight({
|
|
1099
|
+
textDocument: { uri: plugin.documentUri },
|
|
1100
|
+
position: offsetToPos(state.doc, pos),
|
|
1101
|
+
});
|
|
1102
|
+
if (view.state.selection.main.head !== pos) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (!result || result.length === 0) {
|
|
1106
|
+
view.dispatch({ effects: setHighlightsEffect.of(Decoration.none) });
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const decorations = result
|
|
1110
|
+
.map((highlight) => {
|
|
1111
|
+
const from = posToOffset(view.state.doc, highlight.range.start);
|
|
1112
|
+
const to = posToOffset(view.state.doc, highlight.range.end);
|
|
1113
|
+
if (from == null || to == null)
|
|
1114
|
+
return null;
|
|
1115
|
+
return decorationForKind(highlight.kind).range(from, to);
|
|
1116
|
+
})
|
|
1117
|
+
.filter((d) => d != null)
|
|
1118
|
+
.sort((a, b) => a.from - b.from);
|
|
1119
|
+
view.dispatch({
|
|
1120
|
+
effects: setHighlightsEffect.of(Decoration.set(decorations)),
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
destroy() {
|
|
1124
|
+
clearTimeout(this.pending);
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
/**
|
|
1128
|
+
* Extension that highlights all occurrences of the symbol under the cursor.
|
|
1129
|
+
*
|
|
1130
|
+
* Applies CSS classes `cm-lsp-highlight-text`, `cm-lsp-highlight-read`, and
|
|
1131
|
+
* `cm-lsp-highlight-write` based on the highlight kind returned by the server.
|
|
1132
|
+
*/
|
|
1133
|
+
const documentHighlight = () => [
|
|
1134
|
+
highlightField,
|
|
1135
|
+
highlightPlugin,
|
|
1136
|
+
];
|
|
1137
|
+
|
|
616
1138
|
const mouseHandler = () => EditorView.domEventHandlers({
|
|
617
1139
|
mousedown: (event, view) => {
|
|
618
1140
|
if (!event.ctrlKey && !event.metaKey)
|
|
@@ -629,6 +1151,12 @@ const mouseHandler = () => EditorView.domEventHandlers({
|
|
|
629
1151
|
},
|
|
630
1152
|
});
|
|
631
1153
|
|
|
1154
|
+
/**
|
|
1155
|
+
* Creates a set of CodeMirror extensions that connect to a language server over WebSocket.
|
|
1156
|
+
*
|
|
1157
|
+
* Includes completion, hover, diagnostics, go-to-definition, document highlight,
|
|
1158
|
+
* and rename support. This is the main entry point for most use cases.
|
|
1159
|
+
*/
|
|
632
1160
|
function languageServer(options) {
|
|
633
1161
|
const serverUri = options.serverUri;
|
|
634
1162
|
const { serverUri: _, ...optionsWithoutServerUri } = options;
|
|
@@ -637,6 +1165,11 @@ function languageServer(options) {
|
|
|
637
1165
|
transport: new WebSocketTransport(serverUri),
|
|
638
1166
|
});
|
|
639
1167
|
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Like {@link languageServer}, but accepts a custom {@link Transport} instead of a WebSocket URI.
|
|
1170
|
+
*
|
|
1171
|
+
* Use this when communicating with the language server over a non-WebSocket channel.
|
|
1172
|
+
*/
|
|
640
1173
|
function languageServerWithTransport(options) {
|
|
641
1174
|
return [
|
|
642
1175
|
languageServerPlugin.of({
|
|
@@ -652,9 +1185,11 @@ function languageServerWithTransport(options) {
|
|
|
652
1185
|
}),
|
|
653
1186
|
hoverTooltip(),
|
|
654
1187
|
autocompletion(),
|
|
1188
|
+
documentHighlight(),
|
|
1189
|
+
renameExtension(),
|
|
655
1190
|
keymap.of([...jumpToDefinitionKeymap]),
|
|
656
1191
|
mouseHandler(),
|
|
657
1192
|
];
|
|
658
1193
|
}
|
|
659
1194
|
|
|
660
|
-
export { LanguageServerClient, SynchronizationMethod, jumpToDefinition, jumpToDefinitionKeymap, jumpToDefinitionPos, languageServer, languageServerPlugin, languageServerWithTransport };
|
|
1195
|
+
export { LanguageServerClient, SynchronizationMethod, WebSocketTransport, formatDocument, formatSelection, formattingOptions, jumpToDefinition, jumpToDefinitionKeymap, jumpToDefinitionPos, languageServer, languageServerPlugin, languageServerWithTransport, renameSymbol };
|