codemirror-languageserver 1.18.1 → 1.19.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 CHANGED
@@ -14,6 +14,9 @@ https://user-images.githubusercontent.com/348107/120141150-c6bb9180-c1fd-11eb-8a
14
14
  - 📚 Hover Documentation
15
15
  - 🩺 Diagnostics
16
16
  - 🔍 Go to Definition, Declaration, and Type Definition
17
+ - 🔦 Document Highlight
18
+ - 🎨 Document Formatting and Range Formatting
19
+ - ✏️ Rename Symbol
17
20
 
18
21
  ## Usage
19
22
 
@@ -24,8 +27,6 @@ npm i codemirror-languageserver
24
27
  ```js
25
28
  import { languageServer } from 'codemirror-languageserver';
26
29
 
27
- const transport = new WebSocketTransport(serverUri);
28
-
29
30
  var ls = languageServer({
30
31
  // WebSocket server uri and other client options.
31
32
  serverUri,
@@ -33,7 +34,7 @@ var ls = languageServer({
33
34
 
34
35
  // Alternatively, to share the same client across multiple instances of this plugin.
35
36
  client: new LanguageServerClient({
36
- serverUri,
37
+ transport: new WebSocketTransport(serverUri),
37
38
  rootUri: 'file:///',
38
39
  }),
39
40
 
@@ -52,6 +53,78 @@ var view = new EditorView({
52
53
  });
53
54
  ```
54
55
 
56
+ ### Custom Transport
57
+
58
+ Use `languageServerWithTransport` to provide a custom transport (e.g. for non-WebSocket connections):
59
+
60
+ ```js
61
+ import { languageServerWithTransport } from 'codemirror-languageserver';
62
+
63
+ var ls = languageServerWithTransport({
64
+ transport: myCustomTransport, // Must implement the Transport interface
65
+ rootUri: 'file:///',
66
+ documentUri: `file:///${filename}`,
67
+ languageId: 'cpp',
68
+ });
69
+ ```
70
+
71
+ The `Transport` interface requires `send`, `onMessage`, `onClose`, `onError`, and `close` methods. You can import `Transport` and `WebSocketTransport` from the package.
72
+
73
+ ### Document Highlight
74
+
75
+ Document highlights are enabled by default. When the cursor is on a symbol, all occurrences are highlighted. Style them with CSS:
76
+
77
+ ```css
78
+ .cm-lsp-highlight-text,
79
+ .cm-lsp-highlight-read { background-color: rgba(255, 255, 0, 0.2); }
80
+ .cm-lsp-highlight-write { background-color: rgba(255, 165, 0, 0.3); }
81
+ ```
82
+
83
+ ### Formatting
84
+
85
+ `formatDocument` and `formatSelection` are CodeMirror commands that you can bind to keys:
86
+
87
+ ```js
88
+ import { keymap } from '@codemirror/view';
89
+ import { formatDocument, formatSelection } from 'codemirror-languageserver';
90
+
91
+ keymap.of([
92
+ { key: 'Shift-Alt-f', run: formatDocument },
93
+ { key: 'Ctrl-k Ctrl-f', run: formatSelection },
94
+ ])
95
+ ```
96
+
97
+ To configure formatting options (tab size, spaces vs tabs, etc.), use the `formattingOptions` facet:
98
+
99
+ ```js
100
+ import { formattingOptions } from 'codemirror-languageserver';
101
+
102
+ // In extensions:
103
+ formattingOptions.of({
104
+ tabSize: 2,
105
+ insertSpaces: true,
106
+ trimTrailingWhitespace: true,
107
+ insertFinalNewline: true,
108
+ })
109
+ ```
110
+
111
+ By default, `tabSize` is read from the editor state.
112
+
113
+ ### Rename Symbol
114
+
115
+ `renameSymbol` is a CodeMirror command that opens a rename prompt at the top of the editor:
116
+
117
+ ```js
118
+ import { keymap } from '@codemirror/view';
119
+ import { renameSymbol } from 'codemirror-languageserver';
120
+
121
+ keymap.of([
122
+ { key: 'F2', run: renameSymbol },
123
+ ])
124
+ ```
125
+
126
+ Style the rename panel with CSS using the `cm-lsp-rename-panel` and `cm-lsp-rename-input` classes.
127
+
55
128
  ### Using with Initialization Options
56
129
 
57
130
  The plugin includes built-in TypeScript definitions for popular language servers:
@@ -0,0 +1,6 @@
1
+ import { Facet } from '@codemirror/state';
2
+ import type { EditorView } from '@codemirror/view';
3
+ import type * as LSP from 'vscode-languageserver-protocol';
4
+ export declare const formattingOptions: Facet<LSP.FormattingOptions, LSP.FormattingOptions>;
5
+ export declare function formatDocument(view: EditorView): boolean;
6
+ export declare function formatSelection(view: EditorView): boolean;
@@ -0,0 +1,2 @@
1
+ import { Extension } from '@codemirror/state';
2
+ export declare const documentHighlight: () => Extension;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export { LanguageServerClient, languageServerPlugin, SynchronizationMethod, } from './plugin';
2
+ export { WebSocketTransport } from './jsonrpc';
3
+ export type { Transport } from './jsonrpc';
2
4
  export { jumpToDefinition, jumpToDefinitionPos, jumpToDefinitionKeymap, } from './definition';
5
+ export { formatDocument, formatSelection, formattingOptions, } from './formatting';
6
+ export { renameSymbol } from './rename';
3
7
  export { PyrightInitializationOptions, RustAnalyzerInitializationOptions, TypeScriptInitializationOptions, ESLintInitializationOptions, ClangdInitializationOptions, GoplsInitializationOptions, } from './initialization';
4
8
  import { LanguageServerClient, SynchronizationMethod } from './plugin';
5
9
  import type { LanguageServerClientOptions, LanguageServerBaseOptions } from './plugin';
@@ -12,5 +16,5 @@ interface LanguageServerWebsocketOptions<InitializationOptions = unknown> extend
12
16
  serverUri: `ws://${string}` | `wss://${string}`;
13
17
  initializationOptions?: InitializationOptions;
14
18
  }
15
- export declare function languageServer<InitializationOptions = unknown>(options: LanguageServerWebsocketOptions<InitializationOptions>): import("@codemirror/state").Extension[];
16
- export declare function languageServerWithTransport<InitializationOptions = unknown>(options: LanguageServerOptions<InitializationOptions>): import("@codemirror/state").Extension[];
19
+ export declare function languageServer<InitializationOptions = unknown>(options: LanguageServerWebsocketOptions<InitializationOptions>): (import("@codemirror/state").Extension | import("@codemirror/state").StateField<string>)[];
20
+ export declare function languageServerWithTransport<InitializationOptions = unknown>(options: LanguageServerOptions<InitializationOptions>): (import("@codemirror/state").Extension | import("@codemirror/state").StateField<string>)[];
package/dist/index.js CHANGED
@@ -1,10 +1,120 @@
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
+ class WebSocketTransport {
9
+ constructor(uri) {
10
+ this.connection = new WebSocket(uri);
11
+ }
12
+ send(message) {
13
+ this.connection.send(message);
14
+ }
15
+ onMessage(callback) {
16
+ this.connection.addEventListener('message', (event) => {
17
+ callback(typeof event.data === 'string' ? event.data : '');
18
+ });
19
+ }
20
+ onClose(callback) {
21
+ this.connection.addEventListener('close', callback);
22
+ }
23
+ onError(callback) {
24
+ this.connection.addEventListener('error', () => {
25
+ callback(new Error('WebSocket error'));
26
+ });
27
+ }
28
+ close() {
29
+ this.connection.close();
30
+ }
31
+ }
32
+ class JSONRPCClient {
33
+ constructor(transport) {
34
+ this.nextId = 0;
35
+ this.pending = new Map();
36
+ this.notificationHandlers = [];
37
+ this.transport = transport;
38
+ this.ready = new Promise((resolve, reject) => {
39
+ if (transport instanceof WebSocketTransport) {
40
+ const ws = transport.connection;
41
+ if (ws.readyState === WebSocket.OPEN) {
42
+ resolve();
43
+ }
44
+ else {
45
+ ws.addEventListener('open', () => resolve());
46
+ ws.addEventListener('error', () => reject(new Error('WebSocket connection failed')));
47
+ }
48
+ }
49
+ else {
50
+ resolve();
51
+ }
52
+ });
53
+ transport.onMessage((raw) => {
54
+ let msg;
55
+ try {
56
+ msg = JSON.parse(raw);
57
+ }
58
+ catch {
59
+ return;
60
+ }
61
+ if ('id' in msg && msg.id != null && this.pending.has(msg.id)) {
62
+ const pending = this.pending.get(msg.id);
63
+ this.pending.delete(msg.id);
64
+ clearTimeout(pending.timer);
65
+ if (msg.error) {
66
+ pending.reject(new Error(msg.error.message || 'JSON-RPC error'));
67
+ }
68
+ else {
69
+ pending.resolve(msg.result);
70
+ }
71
+ }
72
+ else if ('method' in msg) {
73
+ for (const handler of this.notificationHandlers) {
74
+ handler(msg);
75
+ }
76
+ }
77
+ });
78
+ }
79
+ async request(req, timeout) {
80
+ await this.ready;
81
+ const id = this.nextId++;
82
+ const message = {
83
+ jsonrpc: '2.0',
84
+ id,
85
+ method: req.method,
86
+ params: req.params,
87
+ };
88
+ return new Promise((resolve, reject) => {
89
+ const timer = setTimeout(() => {
90
+ this.pending.delete(id);
91
+ reject(new Error(`Request ${req.method} timed out`));
92
+ }, timeout);
93
+ this.pending.set(id, { resolve, reject, timer });
94
+ this.transport.send(JSON.stringify(message));
95
+ });
96
+ }
97
+ async notify(notification) {
98
+ await this.ready;
99
+ const message = {
100
+ jsonrpc: '2.0',
101
+ method: notification.method,
102
+ params: notification.params,
103
+ };
104
+ this.transport.send(JSON.stringify(message));
105
+ }
106
+ onNotification(handler) {
107
+ this.notificationHandlers.push(handler);
108
+ }
109
+ close() {
110
+ for (const [id, pending] of this.pending) {
111
+ clearTimeout(pending.timer);
112
+ pending.reject(new Error('Client closed'));
113
+ }
114
+ this.pending.clear();
115
+ this.transport.close();
116
+ }
117
+ }
8
118
 
9
119
  function posToOffset(doc, pos) {
10
120
  if (pos.line >= doc.lines) {
@@ -46,6 +156,7 @@ function changeSetToEvents(doc, changes) {
46
156
  }
47
157
 
48
158
  const timeout = 10000;
159
+ const changesDelay = 500;
49
160
  const CompletionItemKindMap = Object.fromEntries(Object.entries(CompletionItemKind).map(([key, value]) => [value, key]));
50
161
  const useLast = (values) => values.reduce((_, v) => v, '');
51
162
  Facet.define({
@@ -61,26 +172,19 @@ class LanguageServerClient {
61
172
  this.autoClose = options.autoClose;
62
173
  this.plugins = [];
63
174
  this.transport = options.transport;
64
- this.requestManager = new RequestManager([this.transport]);
65
- this.client = new Client(this.requestManager);
175
+ this.client = new JSONRPCClient(this.transport);
66
176
  this.client.onNotification((data) => {
177
+ if (data.method && data.id) {
178
+ // Server-to-client request: respond with null result.
179
+ // https://github.com/FurqanSoftware/codemirror-languageserver/issues/9
180
+ this.transport.send(JSON.stringify({
181
+ jsonrpc: '2.0',
182
+ id: data.id,
183
+ result: null,
184
+ }));
185
+ }
67
186
  this.processNotification(data);
68
187
  });
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
188
  this.initializePromise = this.initialize();
85
189
  }
86
190
  getInitializeParams() {
@@ -135,6 +239,19 @@ class LanguageServerClient {
135
239
  dynamicRegistration: true,
136
240
  linkSupport: true,
137
241
  },
242
+ documentHighlight: {
243
+ dynamicRegistration: true,
244
+ },
245
+ formatting: {
246
+ dynamicRegistration: true,
247
+ },
248
+ rangeFormatting: {
249
+ dynamicRegistration: true,
250
+ },
251
+ rename: {
252
+ dynamicRegistration: true,
253
+ prepareSupport: true,
254
+ },
138
255
  },
139
256
  workspace: {
140
257
  didChangeConfiguration: {
@@ -143,6 +260,7 @@ class LanguageServerClient {
143
260
  },
144
261
  },
145
262
  initializationOptions: (_a = this.options.initializationOptions) !== null && _a !== void 0 ? _a : null,
263
+ locale: this.options.locale,
146
264
  processId: null,
147
265
  rootUri: this.rootUri,
148
266
  workspaceFolders: this.workspaceFolders,
@@ -178,6 +296,21 @@ class LanguageServerClient {
178
296
  async textDocumentTypeDefinition(params) {
179
297
  return await this.request('textDocument/typeDefinition', params, timeout);
180
298
  }
299
+ async textDocumentDocumentHighlight(params) {
300
+ return await this.request('textDocument/documentHighlight', params, timeout);
301
+ }
302
+ async textDocumentFormatting(params) {
303
+ return await this.request('textDocument/formatting', params, timeout);
304
+ }
305
+ async textDocumentRangeFormatting(params) {
306
+ return await this.request('textDocument/rangeFormatting', params, timeout);
307
+ }
308
+ async textDocumentPrepareRename(params) {
309
+ return await this.request('textDocument/prepareRename', params, timeout);
310
+ }
311
+ async textDocumentRename(params) {
312
+ return await this.request('textDocument/rename', params, timeout);
313
+ }
181
314
  async completionItemResolve(params) {
182
315
  return await this.request('completionItem/resolve', params, timeout);
183
316
  }
@@ -214,6 +347,8 @@ var SynchronizationMethod;
214
347
  class LanguageServerPlugin {
215
348
  constructor(view, { client, documentUri, languageId, allowHTMLContent, synchronizationMethod, }) {
216
349
  this.view = view;
350
+ this.changesTimeout = null;
351
+ this.pendingChanges = null;
217
352
  this.client = client;
218
353
  this.documentUri = documentUri;
219
354
  this.languageId = languageId;
@@ -230,20 +365,40 @@ class LanguageServerPlugin {
230
365
  if (!docChanged) {
231
366
  return;
232
367
  }
368
+ if (this.changesTimeout) {
369
+ clearTimeout(this.changesTimeout);
370
+ }
233
371
  switch (this.synchronizationMethod) {
234
372
  case SynchronizationMethod.Full:
235
- this.sendChanges([
373
+ this.pendingChanges = [
236
374
  {
237
375
  text: this.view.state.doc.toString(),
238
376
  },
239
- ]);
377
+ ];
240
378
  break;
241
379
  case SynchronizationMethod.Incremental:
242
- this.sendChanges(changeSetToEvents(startState.doc, changes));
380
+ if (!this.pendingChanges) {
381
+ this.pendingChanges = [];
382
+ }
383
+ this.pendingChanges.push(...changeSetToEvents(startState.doc, changes));
243
384
  break;
244
385
  }
386
+ this.changesTimeout = setTimeout(() => {
387
+ this.changesTimeout = null;
388
+ if (this.pendingChanges) {
389
+ this.sendChanges(this.pendingChanges);
390
+ this.pendingChanges = null;
391
+ }
392
+ }, changesDelay);
245
393
  }
246
394
  destroy() {
395
+ if (this.changesTimeout) {
396
+ clearTimeout(this.changesTimeout);
397
+ if (this.pendingChanges) {
398
+ this.sendChanges(this.pendingChanges);
399
+ this.pendingChanges = null;
400
+ }
401
+ }
247
402
  this.client.detachPlugin(this);
248
403
  }
249
404
  async initialize({ documentText }) {
@@ -302,11 +457,12 @@ class LanguageServerPlugin {
302
457
  }
303
458
  const dom = document.createElement('div');
304
459
  dom.classList.add('documentation');
305
- if (this.allowHTMLContent) {
306
- dom.innerHTML = await formatContents(contents);
460
+ const formatted = await formatContents(contents);
461
+ if (formatted.isHTML || this.allowHTMLContent) {
462
+ dom.innerHTML = formatted.value;
307
463
  }
308
464
  else {
309
- dom.textContent = await formatContents(contents);
465
+ dom.textContent = formatted.value;
310
466
  }
311
467
  return {
312
468
  pos,
@@ -366,56 +522,52 @@ class LanguageServerPlugin {
366
522
  label,
367
523
  detail,
368
524
  apply(view, completion, from, to) {
525
+ const changes = [];
369
526
  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)));
527
+ changes.push({
528
+ from: posToOffset(view.state.doc, textEdit.range.start),
529
+ to: Math.max(posToOffset(view.state.doc, textEdit.range.end), to),
530
+ insert: textEdit.newText,
531
+ });
371
532
  }
372
533
  else {
373
- view.dispatch(insertCompletionText(view.state, label, from, to));
374
- }
375
- if (!additionalTextEdits) {
376
- return;
534
+ changes.push({
535
+ from,
536
+ to,
537
+ insert: label,
538
+ });
377
539
  }
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;
540
+ if (additionalTextEdits) {
541
+ for (const edit of additionalTextEdits) {
542
+ changes.push({
543
+ from: posToOffset(view.state.doc, edit.range.start),
544
+ to: posToOffset(view.state.doc, edit.range.end),
545
+ insert: edit.newText,
546
+ });
383
547
  }
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
- });
548
+ }
549
+ view.dispatch(view.state.update({ changes }));
399
550
  },
400
551
  type: kind && CompletionItemKindMap[kind].toLowerCase(),
401
552
  };
402
553
  completion.info = async () => {
403
554
  var _a;
404
- let { documentation } = item;
405
- if (!documentation &&
555
+ if (!item.documentation &&
406
556
  ((_a = this.client.capabilities.completionProvider) === null || _a === void 0 ? void 0 : _a.resolveProvider)) {
407
557
  const resolvedItem = await this.client.completionItemResolve(item);
408
- documentation = resolvedItem.documentation;
558
+ item.documentation = resolvedItem.documentation;
409
559
  }
560
+ const { documentation } = item;
410
561
  if (!documentation)
411
562
  return null;
412
563
  const dom = document.createElement('div');
413
564
  dom.classList.add('documentation');
414
- if (this.allowHTMLContent) {
415
- dom.innerHTML = await formatContents(documentation);
565
+ const formatted = await formatContents(documentation);
566
+ if (formatted.isHTML || this.allowHTMLContent) {
567
+ dom.innerHTML = formatted.value;
416
568
  }
417
569
  else {
418
- dom.textContent = await formatContents(documentation);
570
+ dom.textContent = formatted.value;
419
571
  }
420
572
  return dom;
421
573
  };
@@ -511,19 +663,32 @@ class LanguageServerPlugin {
511
663
  const languageServerPlugin = ViewPlugin.fromClass(LanguageServerPlugin);
512
664
  async function formatContents(contents) {
513
665
  if (isLSPMarkupContent(contents)) {
514
- let value = contents.value;
515
666
  if (contents.kind === 'markdown') {
516
- value = await marked.parse(value);
667
+ return {
668
+ value: await marked.parse(contents.value),
669
+ isHTML: true,
670
+ };
517
671
  }
518
- return value;
672
+ return { value: contents.value, isHTML: false };
519
673
  }
520
674
  else if (Array.isArray(contents)) {
521
- return contents.map((c) => formatContents(c) + '\n\n').join('');
675
+ const parts = await Promise.all(contents.map((c) => formatContents(c)));
676
+ const isHTML = parts.some((p) => p.isHTML);
677
+ const value = parts
678
+ .map((p) => (isHTML && !p.isHTML ? escapeHTML(p.value) : p.value))
679
+ .join(isHTML ? '<br><br>' : '\n\n');
680
+ return { value, isHTML };
522
681
  }
523
682
  else if (typeof contents === 'string') {
524
- return contents;
683
+ return { value: contents, isHTML: false };
525
684
  }
526
685
  }
686
+ function escapeHTML(text) {
687
+ return text
688
+ .replace(/&/g, '&amp;')
689
+ .replace(/</g, '&lt;')
690
+ .replace(/>/g, '&gt;');
691
+ }
527
692
  function toSet(chars) {
528
693
  let preamble = '';
529
694
  let flat = Array.from(chars).join('');
@@ -579,6 +744,223 @@ const jumpToDefinitionKeymap = [
579
744
  { key: 'Mod-F12', run: jumpToTypeDefinition, preventDefault: true },
580
745
  ];
581
746
 
747
+ const formattingOptions = Facet.define({
748
+ combine(values) {
749
+ return Object.assign({ tabSize: 4, insertSpaces: true }, ...values);
750
+ },
751
+ });
752
+ function getFormattingOptions(view) {
753
+ var _a;
754
+ const opts = view.state.facet(formattingOptions);
755
+ return {
756
+ ...opts,
757
+ tabSize: (_a = opts.tabSize) !== null && _a !== void 0 ? _a : view.state.tabSize,
758
+ };
759
+ }
760
+ function applyTextEdits(view, edits) {
761
+ const changes = edits.map((edit) => ({
762
+ from: posToOffset(view.state.doc, edit.range.start),
763
+ to: posToOffset(view.state.doc, edit.range.end),
764
+ insert: edit.newText,
765
+ }));
766
+ view.dispatch(view.state.update({ changes }));
767
+ }
768
+ function formatDocument(view) {
769
+ var _a;
770
+ const plugin = view.plugin(languageServerPlugin);
771
+ if (!plugin)
772
+ return false;
773
+ if (!plugin.client.ready ||
774
+ !((_a = plugin.client.capabilities) === null || _a === void 0 ? void 0 : _a.documentFormattingProvider)) {
775
+ return false;
776
+ }
777
+ plugin.client
778
+ .textDocumentFormatting({
779
+ textDocument: { uri: plugin.documentUri },
780
+ options: getFormattingOptions(view),
781
+ })
782
+ .then((edits) => {
783
+ if (edits && edits.length > 0) {
784
+ applyTextEdits(view, edits);
785
+ }
786
+ });
787
+ return true;
788
+ }
789
+ function formatSelection(view) {
790
+ var _a;
791
+ const plugin = view.plugin(languageServerPlugin);
792
+ if (!plugin)
793
+ return false;
794
+ if (!plugin.client.ready ||
795
+ !((_a = plugin.client.capabilities) === null || _a === void 0 ? void 0 : _a.documentRangeFormattingProvider)) {
796
+ return false;
797
+ }
798
+ const { from, to } = view.state.selection.main;
799
+ plugin.client
800
+ .textDocumentRangeFormatting({
801
+ textDocument: { uri: plugin.documentUri },
802
+ range: {
803
+ start: offsetToPos(view.state.doc, from),
804
+ end: offsetToPos(view.state.doc, to),
805
+ },
806
+ options: getFormattingOptions(view),
807
+ })
808
+ .then((edits) => {
809
+ if (edits && edits.length > 0) {
810
+ applyTextEdits(view, edits);
811
+ }
812
+ });
813
+ return true;
814
+ }
815
+
816
+ const openRenamePanel = StateEffect.define();
817
+ const closeRenamePanel = StateEffect.define();
818
+ const renamePanelState = StateField.define({
819
+ create() {
820
+ return null;
821
+ },
822
+ update(value, tr) {
823
+ for (const effect of tr.effects) {
824
+ if (effect.is(openRenamePanel))
825
+ return effect.value;
826
+ if (effect.is(closeRenamePanel))
827
+ return null;
828
+ }
829
+ return value;
830
+ },
831
+ provide: (field) => showPanel.from(field, (value) => value != null ? (view) => createRenamePanel(view, value) : null),
832
+ });
833
+ function applyWorkspaceEdit(view, edit) {
834
+ const plugin = view.plugin(languageServerPlugin);
835
+ if (!plugin)
836
+ return;
837
+ const changes = [];
838
+ if (edit.changes) {
839
+ const edits = edit.changes[plugin.documentUri];
840
+ if (edits) {
841
+ for (const e of edits) {
842
+ changes.push({
843
+ from: posToOffset(view.state.doc, e.range.start),
844
+ to: posToOffset(view.state.doc, e.range.end),
845
+ insert: e.newText,
846
+ });
847
+ }
848
+ }
849
+ }
850
+ if (edit.documentChanges) {
851
+ for (const change of edit.documentChanges) {
852
+ if ('textDocument' in change) {
853
+ if (change.textDocument.uri !== plugin.documentUri)
854
+ continue;
855
+ for (const e of change.edits) {
856
+ if ('range' in e) {
857
+ changes.push({
858
+ from: posToOffset(view.state.doc, e.range.start),
859
+ to: posToOffset(view.state.doc, e.range.end),
860
+ insert: e.newText,
861
+ });
862
+ }
863
+ }
864
+ }
865
+ }
866
+ }
867
+ if (changes.length > 0) {
868
+ view.dispatch(view.state.update({ changes }));
869
+ }
870
+ }
871
+ function createRenamePanel(view, placeholder) {
872
+ const dom = document.createElement('div');
873
+ dom.className = 'cm-lsp-rename-panel';
874
+ const input = document.createElement('input');
875
+ input.className = 'cm-lsp-rename-input';
876
+ input.value = placeholder;
877
+ input.setAttribute('aria-label', 'New name');
878
+ const submit = () => {
879
+ const newName = input.value.trim();
880
+ if (!newName || newName === placeholder) {
881
+ view.dispatch({ effects: closeRenamePanel.of(null) });
882
+ view.focus();
883
+ return;
884
+ }
885
+ performRename(view, newName);
886
+ };
887
+ input.addEventListener('keydown', (e) => {
888
+ if (e.key === 'Enter') {
889
+ e.preventDefault();
890
+ submit();
891
+ }
892
+ else if (e.key === 'Escape') {
893
+ e.preventDefault();
894
+ view.dispatch({ effects: closeRenamePanel.of(null) });
895
+ view.focus();
896
+ }
897
+ });
898
+ dom.appendChild(input);
899
+ return {
900
+ dom,
901
+ top: true,
902
+ mount() {
903
+ input.select();
904
+ },
905
+ };
906
+ }
907
+ async function performRename(view, newName) {
908
+ const plugin = view.plugin(languageServerPlugin);
909
+ if (!plugin)
910
+ return;
911
+ const pos = view.state.selection.main.head;
912
+ view.dispatch({ effects: closeRenamePanel.of(null) });
913
+ view.focus();
914
+ const result = await plugin.client.textDocumentRename({
915
+ textDocument: { uri: plugin.documentUri },
916
+ position: offsetToPos(view.state.doc, pos),
917
+ newName,
918
+ });
919
+ if (result) {
920
+ applyWorkspaceEdit(view, result);
921
+ }
922
+ }
923
+ function renameSymbol(view) {
924
+ var _a;
925
+ const plugin = view.plugin(languageServerPlugin);
926
+ if (!plugin)
927
+ return false;
928
+ if (!plugin.client.ready ||
929
+ !((_a = plugin.client.capabilities) === null || _a === void 0 ? void 0 : _a.renameProvider)) {
930
+ return false;
931
+ }
932
+ const pos = view.state.selection.main.head;
933
+ const capabilities = plugin.client.capabilities;
934
+ const renameProvider = capabilities.renameProvider;
935
+ const supportsPrepare = typeof renameProvider === 'object' && renameProvider.prepareProvider;
936
+ if (supportsPrepare) {
937
+ plugin.client
938
+ .textDocumentPrepareRename({
939
+ textDocument: { uri: plugin.documentUri },
940
+ position: offsetToPos(view.state.doc, pos),
941
+ })
942
+ .then((result) => {
943
+ if (!result)
944
+ return;
945
+ const placeholder = 'placeholder' in result
946
+ ? result.placeholder
947
+ : view.state.doc.sliceString(posToOffset(view.state.doc, result.start), posToOffset(view.state.doc, result.end));
948
+ view.dispatch({
949
+ effects: openRenamePanel.of(placeholder),
950
+ });
951
+ });
952
+ }
953
+ else {
954
+ const word = view.state.wordAt(pos);
955
+ const placeholder = word
956
+ ? view.state.doc.sliceString(word.from, word.to)
957
+ : '';
958
+ view.dispatch({ effects: openRenamePanel.of(placeholder) });
959
+ }
960
+ return true;
961
+ }
962
+ const renameExtension = () => renamePanelState;
963
+
582
964
  const hoverTooltip = () => hoverTooltip$1((view, pos) => {
583
965
  var _a;
584
966
  const plugin = view.plugin(languageServerPlugin);
@@ -601,7 +983,8 @@ const autocompletion = () => autocompletion$1({
601
983
  trigKind = CompletionTriggerKind.TriggerCharacter;
602
984
  trigChar = line.text[pos - line.from - 1];
603
985
  }
604
- if (trigKind === CompletionTriggerKind.Invoked &&
986
+ if (!explicit &&
987
+ trigKind === CompletionTriggerKind.Invoked &&
605
988
  !context.matchBefore(/\w+$/)) {
606
989
  return null;
607
990
  }
@@ -613,6 +996,103 @@ const autocompletion = () => autocompletion$1({
613
996
  ],
614
997
  });
615
998
 
999
+ const highlightField = StateField.define({
1000
+ create() {
1001
+ return Decoration.none;
1002
+ },
1003
+ update(decorations, tr) {
1004
+ for (const effect of tr.effects) {
1005
+ if (effect.is(setHighlightsEffect)) {
1006
+ return effect.value;
1007
+ }
1008
+ }
1009
+ if (tr.docChanged) {
1010
+ return Decoration.none;
1011
+ }
1012
+ return decorations;
1013
+ },
1014
+ provide: (field) => EditorView.decorations.from(field),
1015
+ });
1016
+ const setHighlightsEffect = StateEffect.define();
1017
+ const textDecoration = Decoration.mark({
1018
+ class: 'cm-lsp-highlight-text',
1019
+ });
1020
+ const readDecoration = Decoration.mark({
1021
+ class: 'cm-lsp-highlight-read',
1022
+ });
1023
+ const writeDecoration = Decoration.mark({
1024
+ class: 'cm-lsp-highlight-write',
1025
+ });
1026
+ function decorationForKind(kind) {
1027
+ switch (kind) {
1028
+ case DocumentHighlightKind.Read:
1029
+ return readDecoration;
1030
+ case DocumentHighlightKind.Write:
1031
+ return writeDecoration;
1032
+ default:
1033
+ return textDecoration;
1034
+ }
1035
+ }
1036
+ const highlightPlugin = ViewPlugin.fromClass(class {
1037
+ constructor() {
1038
+ this.pending = -1;
1039
+ }
1040
+ update(update) {
1041
+ if (!update.selectionSet && !update.docChanged) {
1042
+ return;
1043
+ }
1044
+ if (update.docChanged) {
1045
+ return;
1046
+ }
1047
+ clearTimeout(this.pending);
1048
+ this.pending = setTimeout(() => this.requestHighlights(update.view), 100);
1049
+ }
1050
+ async requestHighlights(view) {
1051
+ var _a;
1052
+ const plugin = view.plugin(languageServerPlugin);
1053
+ if (!plugin)
1054
+ return;
1055
+ const { state } = view;
1056
+ const pos = state.selection.main.head;
1057
+ if (!plugin.client.ready ||
1058
+ !((_a = plugin.client.capabilities) === null || _a === void 0 ? void 0 : _a.documentHighlightProvider)) {
1059
+ view.dispatch({ effects: setHighlightsEffect.of(Decoration.none) });
1060
+ return;
1061
+ }
1062
+ const result = await plugin.client.textDocumentDocumentHighlight({
1063
+ textDocument: { uri: plugin.documentUri },
1064
+ position: offsetToPos(state.doc, pos),
1065
+ });
1066
+ if (view.state.selection.main.head !== pos) {
1067
+ return;
1068
+ }
1069
+ if (!result || result.length === 0) {
1070
+ view.dispatch({ effects: setHighlightsEffect.of(Decoration.none) });
1071
+ return;
1072
+ }
1073
+ const decorations = result
1074
+ .map((highlight) => {
1075
+ const from = posToOffset(view.state.doc, highlight.range.start);
1076
+ const to = posToOffset(view.state.doc, highlight.range.end);
1077
+ if (from == null || to == null)
1078
+ return null;
1079
+ return decorationForKind(highlight.kind).range(from, to);
1080
+ })
1081
+ .filter((d) => d != null)
1082
+ .sort((a, b) => a.from - b.from);
1083
+ view.dispatch({
1084
+ effects: setHighlightsEffect.of(Decoration.set(decorations)),
1085
+ });
1086
+ }
1087
+ destroy() {
1088
+ clearTimeout(this.pending);
1089
+ }
1090
+ });
1091
+ const documentHighlight = () => [
1092
+ highlightField,
1093
+ highlightPlugin,
1094
+ ];
1095
+
616
1096
  const mouseHandler = () => EditorView.domEventHandlers({
617
1097
  mousedown: (event, view) => {
618
1098
  if (!event.ctrlKey && !event.metaKey)
@@ -652,9 +1132,11 @@ function languageServerWithTransport(options) {
652
1132
  }),
653
1133
  hoverTooltip(),
654
1134
  autocompletion(),
1135
+ documentHighlight(),
1136
+ renameExtension(),
655
1137
  keymap.of([...jumpToDefinitionKeymap]),
656
1138
  mouseHandler(),
657
1139
  ];
658
1140
  }
659
1141
 
660
- export { LanguageServerClient, SynchronizationMethod, jumpToDefinition, jumpToDefinitionKeymap, jumpToDefinitionPos, languageServer, languageServerPlugin, languageServerWithTransport };
1142
+ export { LanguageServerClient, SynchronizationMethod, WebSocketTransport, formatDocument, formatSelection, formattingOptions, jumpToDefinition, jumpToDefinitionKeymap, jumpToDefinitionPos, languageServer, languageServerPlugin, languageServerWithTransport, renameSymbol };
@@ -0,0 +1,34 @@
1
+ export interface Transport {
2
+ send(message: string): void;
3
+ onMessage(callback: (message: string) => void): void;
4
+ onClose(callback: () => void): void;
5
+ onError(callback: (error: Error) => void): void;
6
+ close(): void;
7
+ }
8
+ export declare class WebSocketTransport implements Transport {
9
+ connection: WebSocket;
10
+ constructor(uri: string);
11
+ send(message: string): void;
12
+ onMessage(callback: (message: string) => void): void;
13
+ onClose(callback: () => void): void;
14
+ onError(callback: (error: Error) => void): void;
15
+ close(): void;
16
+ }
17
+ export declare class JSONRPCClient {
18
+ private transport;
19
+ private nextId;
20
+ private pending;
21
+ private notificationHandlers;
22
+ private ready;
23
+ constructor(transport: Transport);
24
+ request(req: {
25
+ method: string;
26
+ params: any;
27
+ }, timeout: number): Promise<any>;
28
+ notify(notification: {
29
+ method: string;
30
+ params: any;
31
+ }): Promise<void>;
32
+ onNotification(handler: (data: any) => void): void;
33
+ close(): void;
34
+ }
package/dist/plugin.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { EditorView, Tooltip, ViewPlugin } from '@codemirror/view';
2
+ import type { Transport } from './jsonrpc';
2
3
  import { CompletionTriggerKind } from 'vscode-languageserver-protocol';
3
4
  import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
4
5
  import type { PluginValue, ViewUpdate } from '@codemirror/view';
5
- import { Transport } from '@open-rpc/client-js/build/transports/Transport';
6
6
  import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol';
7
7
  import type * as LSP from 'vscode-languageserver-protocol';
8
8
  interface LSPRequestMap {
@@ -24,6 +24,26 @@ interface LSPRequestMap {
24
24
  LSP.TypeDefinitionParams,
25
25
  LSP.Location | LSP.Location[] | LSP.LocationLink[] | null
26
26
  ];
27
+ 'textDocument/documentHighlight': [
28
+ LSP.DocumentHighlightParams,
29
+ LSP.DocumentHighlight[] | null
30
+ ];
31
+ 'textDocument/formatting': [
32
+ LSP.DocumentFormattingParams,
33
+ LSP.TextEdit[] | null
34
+ ];
35
+ 'textDocument/rangeFormatting': [
36
+ LSP.DocumentRangeFormattingParams,
37
+ LSP.TextEdit[] | null
38
+ ];
39
+ 'textDocument/prepareRename': [
40
+ LSP.PrepareRenameParams,
41
+ LSP.Range | {
42
+ range: LSP.Range;
43
+ placeholder: string;
44
+ } | null
45
+ ];
46
+ 'textDocument/rename': [LSP.RenameParams, LSP.WorkspaceEdit | null];
27
47
  'completionItem/resolve': [LSP.CompletionItem, LSP.CompletionItem];
28
48
  }
29
49
  interface LSPNotifyMap {
@@ -55,7 +75,6 @@ export declare class LanguageServerClient<InitializationOptions = unknown> {
55
75
  private workspaceFolders;
56
76
  private autoClose?;
57
77
  private transport;
58
- private requestManager;
59
78
  private client;
60
79
  private plugins;
61
80
  private options;
@@ -63,18 +82,26 @@ export declare class LanguageServerClient<InitializationOptions = unknown> {
63
82
  protected getInitializeParams(): LSP.InitializeParams;
64
83
  initialize(): Promise<void>;
65
84
  close(): void;
66
- textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams): Promise<LSP.DidOpenTextDocumentParams>;
67
- textDocumentDidChange(params: LSP.DidChangeTextDocumentParams): Promise<LSP.DidChangeTextDocumentParams>;
85
+ textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams): Promise<void>;
86
+ textDocumentDidChange(params: LSP.DidChangeTextDocumentParams): Promise<void>;
68
87
  textDocumentHover(params: LSP.HoverParams): Promise<LSP.Hover>;
69
88
  textDocumentCompletion(params: LSP.CompletionParams): Promise<LSP.CompletionList | LSP.CompletionItem[]>;
70
89
  textDocumentDefinition(params: LSP.DefinitionParams): Promise<LSP.Location | LSP.Location[] | LSP.LocationLink[]>;
71
90
  textDocumentDeclaration(params: LSP.DeclarationParams): Promise<LSP.Location | LSP.Location[] | LSP.LocationLink[]>;
72
91
  textDocumentTypeDefinition(params: LSP.TypeDefinitionParams): Promise<LSP.Location | LSP.Location[] | LSP.LocationLink[]>;
92
+ textDocumentDocumentHighlight(params: LSP.DocumentHighlightParams): Promise<LSP.DocumentHighlight[]>;
93
+ textDocumentFormatting(params: LSP.DocumentFormattingParams): Promise<LSP.TextEdit[]>;
94
+ textDocumentRangeFormatting(params: LSP.DocumentRangeFormattingParams): Promise<LSP.TextEdit[]>;
95
+ textDocumentPrepareRename(params: LSP.PrepareRenameParams): Promise<LSP.Range | {
96
+ range: LSP.Range;
97
+ placeholder: string;
98
+ }>;
99
+ textDocumentRename(params: LSP.RenameParams): Promise<LSP.WorkspaceEdit>;
73
100
  completionItemResolve(params: LSP.CompletionItem): Promise<LSP.CompletionItem>;
74
101
  attachPlugin(plugin: LanguageServerPlugin): void;
75
102
  detachPlugin(plugin: LanguageServerPlugin): void;
76
103
  protected request<K extends keyof LSPRequestMap>(method: K, params: LSPRequestMap[K][0], timeout: number): Promise<LSPRequestMap[K][1]>;
77
- protected notify<K extends keyof LSPNotifyMap>(method: K, params: LSPNotifyMap[K]): Promise<LSPNotifyMap[K]>;
104
+ protected notify<K extends keyof LSPNotifyMap>(method: K, params: LSPNotifyMap[K]): Promise<void>;
78
105
  protected processNotification(notification: Notification): void;
79
106
  }
80
107
  export declare enum SynchronizationMethod {
@@ -84,11 +111,13 @@ export declare enum SynchronizationMethod {
84
111
  export declare class LanguageServerPlugin implements PluginValue {
85
112
  private view;
86
113
  client: LanguageServerClient;
87
- private documentUri;
114
+ documentUri: string;
88
115
  private languageId;
89
116
  private documentVersion;
90
117
  private allowHTMLContent;
91
118
  private synchronizationMethod;
119
+ private changesTimeout;
120
+ private pendingChanges;
92
121
  constructor(view: EditorView, { client, documentUri, languageId, allowHTMLContent, synchronizationMethod, }: {
93
122
  client: LanguageServerClient;
94
123
  documentUri: string;
@@ -162,5 +191,6 @@ export interface LanguageServerClientOptions<InitializationOptions = unknown> ex
162
191
  transport: Transport;
163
192
  autoClose?: boolean;
164
193
  initializationOptions?: InitializationOptions;
194
+ locale?: string;
165
195
  }
166
196
  export {};
@@ -0,0 +1,4 @@
1
+ import { StateField } from '@codemirror/state';
2
+ import { EditorView } from '@codemirror/view';
3
+ export declare function renameSymbol(view: EditorView): boolean;
4
+ export declare const renameExtension: () => StateField<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemirror-languageserver",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
4
4
  "description": "Language Server Plugin for CodeMirror 6",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -40,13 +40,14 @@
40
40
  "typescript": "^5.9.2"
41
41
  },
42
42
  "dependencies": {
43
+ "marked": "^16.1.2",
44
+ "vscode-languageserver-protocol": "^3.17.5"
45
+ },
46
+ "peerDependencies": {
43
47
  "@codemirror/autocomplete": "^6.18.6",
44
48
  "@codemirror/lint": "^6.8.5",
45
49
  "@codemirror/state": "^6.5.2",
46
- "@codemirror/view": "^6.38.1",
47
- "@open-rpc/client-js": "^1.8.1",
48
- "marked": "^16.1.2",
49
- "vscode-languageserver-protocol": "^3.17.5"
50
+ "@codemirror/view": "^6.38.1"
50
51
  },
51
52
  "prettier": {
52
53
  "tabWidth": 4,