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/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 { RequestManager, Client, WebSocketTransport } from '@open-rpc/client-js';
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.requestManager = new RequestManager([this.transport]);
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.sendChanges([
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.sendChanges(changeSetToEvents(startState.doc, changes));
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
- if (this.allowHTMLContent) {
306
- dom.innerHTML = await formatContents(contents);
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 = await formatContents(contents);
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
- view.dispatch(insertCompletionText(view.state, textEdit.newText, posToOffset(view.state.doc, textEdit.range.start), posToOffset(view.state.doc, textEdit.range.end)));
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
- view.dispatch(insertCompletionText(view.state, label, from, to));
374
- }
375
- if (!additionalTextEdits) {
376
- return;
546
+ changes.push({
547
+ from,
548
+ to,
549
+ insert: label,
550
+ });
377
551
  }
378
- additionalTextEdits
379
- .sort(({ range: { end: a } }, { range: { end: b } }) => {
380
- if (posToOffset(view.state.doc, a) <
381
- posToOffset(view.state.doc, b)) {
382
- return 1;
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
- else if (posToOffset(view.state.doc, a) >
385
- posToOffset(view.state.doc, b)) {
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
- let { documentation } = item;
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
- if (this.allowHTMLContent) {
415
- dom.innerHTML = await formatContents(documentation);
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 = await formatContents(documentation);
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
- value = await marked.parse(value);
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
- return contents.map((c) => formatContents(c) + '\n\n').join('');
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, '&amp;')
701
+ .replace(/</g, '&lt;')
702
+ .replace(/>/g, '&gt;');
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 (trigKind === CompletionTriggerKind.Invoked &&
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 };