codemirror-languageserver 1.18.0 → 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,11 +347,14 @@ 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;
220
355
  this.allowHTMLContent = allowHTMLContent;
221
- this.synchronizationMethod = synchronizationMethod;
356
+ this.synchronizationMethod =
357
+ synchronizationMethod !== null && synchronizationMethod !== void 0 ? synchronizationMethod : SynchronizationMethod.Full;
222
358
  this.documentVersion = 0;
223
359
  this.client.attachPlugin(this);
224
360
  this.initialize({
@@ -229,20 +365,40 @@ class LanguageServerPlugin {
229
365
  if (!docChanged) {
230
366
  return;
231
367
  }
368
+ if (this.changesTimeout) {
369
+ clearTimeout(this.changesTimeout);
370
+ }
232
371
  switch (this.synchronizationMethod) {
233
372
  case SynchronizationMethod.Full:
234
- this.sendChanges([
373
+ this.pendingChanges = [
235
374
  {
236
375
  text: this.view.state.doc.toString(),
237
376
  },
238
- ]);
377
+ ];
239
378
  break;
240
379
  case SynchronizationMethod.Incremental:
241
- this.sendChanges(changeSetToEvents(startState.doc, changes));
380
+ if (!this.pendingChanges) {
381
+ this.pendingChanges = [];
382
+ }
383
+ this.pendingChanges.push(...changeSetToEvents(startState.doc, changes));
242
384
  break;
243
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);
244
393
  }
245
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
+ }
246
402
  this.client.detachPlugin(this);
247
403
  }
248
404
  async initialize({ documentText }) {
@@ -301,11 +457,12 @@ class LanguageServerPlugin {
301
457
  }
302
458
  const dom = document.createElement('div');
303
459
  dom.classList.add('documentation');
304
- if (this.allowHTMLContent) {
305
- dom.innerHTML = await formatContents(contents);
460
+ const formatted = await formatContents(contents);
461
+ if (formatted.isHTML || this.allowHTMLContent) {
462
+ dom.innerHTML = formatted.value;
306
463
  }
307
464
  else {
308
- dom.textContent = await formatContents(contents);
465
+ dom.textContent = formatted.value;
309
466
  }
310
467
  return {
311
468
  pos,
@@ -365,56 +522,52 @@ class LanguageServerPlugin {
365
522
  label,
366
523
  detail,
367
524
  apply(view, completion, from, to) {
525
+ const changes = [];
368
526
  if (isLSPTextEdit(textEdit)) {
369
- 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
+ });
370
532
  }
371
533
  else {
372
- view.dispatch(insertCompletionText(view.state, label, from, to));
373
- }
374
- if (!additionalTextEdits) {
375
- return;
534
+ changes.push({
535
+ from,
536
+ to,
537
+ insert: label,
538
+ });
376
539
  }
377
- additionalTextEdits
378
- .sort(({ range: { end: a } }, { range: { end: b } }) => {
379
- if (posToOffset(view.state.doc, a) <
380
- posToOffset(view.state.doc, b)) {
381
- 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
+ });
382
547
  }
383
- else if (posToOffset(view.state.doc, a) >
384
- posToOffset(view.state.doc, b)) {
385
- return -1;
386
- }
387
- return 0;
388
- })
389
- .forEach((textEdit) => {
390
- view.dispatch(view.state.update({
391
- changes: {
392
- from: posToOffset(view.state.doc, textEdit.range.start),
393
- to: posToOffset(view.state.doc, textEdit.range.end),
394
- insert: textEdit.newText,
395
- },
396
- }));
397
- });
548
+ }
549
+ view.dispatch(view.state.update({ changes }));
398
550
  },
399
551
  type: kind && CompletionItemKindMap[kind].toLowerCase(),
400
552
  };
401
553
  completion.info = async () => {
402
554
  var _a;
403
- let { documentation } = item;
404
- if (!documentation &&
555
+ if (!item.documentation &&
405
556
  ((_a = this.client.capabilities.completionProvider) === null || _a === void 0 ? void 0 : _a.resolveProvider)) {
406
557
  const resolvedItem = await this.client.completionItemResolve(item);
407
- documentation = resolvedItem.documentation;
558
+ item.documentation = resolvedItem.documentation;
408
559
  }
560
+ const { documentation } = item;
409
561
  if (!documentation)
410
562
  return null;
411
563
  const dom = document.createElement('div');
412
564
  dom.classList.add('documentation');
413
- if (this.allowHTMLContent) {
414
- dom.innerHTML = await formatContents(documentation);
565
+ const formatted = await formatContents(documentation);
566
+ if (formatted.isHTML || this.allowHTMLContent) {
567
+ dom.innerHTML = formatted.value;
415
568
  }
416
569
  else {
417
- dom.textContent = await formatContents(documentation);
570
+ dom.textContent = formatted.value;
418
571
  }
419
572
  return dom;
420
573
  };
@@ -510,19 +663,32 @@ class LanguageServerPlugin {
510
663
  const languageServerPlugin = ViewPlugin.fromClass(LanguageServerPlugin);
511
664
  async function formatContents(contents) {
512
665
  if (isLSPMarkupContent(contents)) {
513
- let value = contents.value;
514
666
  if (contents.kind === 'markdown') {
515
- value = await marked.parse(value);
667
+ return {
668
+ value: await marked.parse(contents.value),
669
+ isHTML: true,
670
+ };
516
671
  }
517
- return value;
672
+ return { value: contents.value, isHTML: false };
518
673
  }
519
674
  else if (Array.isArray(contents)) {
520
- 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 };
521
681
  }
522
682
  else if (typeof contents === 'string') {
523
- return contents;
683
+ return { value: contents, isHTML: false };
524
684
  }
525
685
  }
686
+ function escapeHTML(text) {
687
+ return text
688
+ .replace(/&/g, '&amp;')
689
+ .replace(/</g, '&lt;')
690
+ .replace(/>/g, '&gt;');
691
+ }
526
692
  function toSet(chars) {
527
693
  let preamble = '';
528
694
  let flat = Array.from(chars).join('');
@@ -578,6 +744,223 @@ const jumpToDefinitionKeymap = [
578
744
  { key: 'Mod-F12', run: jumpToTypeDefinition, preventDefault: true },
579
745
  ];
580
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
+
581
964
  const hoverTooltip = () => hoverTooltip$1((view, pos) => {
582
965
  var _a;
583
966
  const plugin = view.plugin(languageServerPlugin);
@@ -600,7 +983,8 @@ const autocompletion = () => autocompletion$1({
600
983
  trigKind = CompletionTriggerKind.TriggerCharacter;
601
984
  trigChar = line.text[pos - line.from - 1];
602
985
  }
603
- if (trigKind === CompletionTriggerKind.Invoked &&
986
+ if (!explicit &&
987
+ trigKind === CompletionTriggerKind.Invoked &&
604
988
  !context.matchBefore(/\w+$/)) {
605
989
  return null;
606
990
  }
@@ -612,6 +996,103 @@ const autocompletion = () => autocompletion$1({
612
996
  ],
613
997
  });
614
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
+
615
1096
  const mouseHandler = () => EditorView.domEventHandlers({
616
1097
  mousedown: (event, view) => {
617
1098
  if (!event.ctrlKey && !event.metaKey)
@@ -651,9 +1132,11 @@ function languageServerWithTransport(options) {
651
1132
  }),
652
1133
  hoverTooltip(),
653
1134
  autocompletion(),
1135
+ documentHighlight(),
1136
+ renameExtension(),
654
1137
  keymap.of([...jumpToDefinitionKeymap]),
655
1138
  mouseHandler(),
656
1139
  ];
657
1140
  }
658
1141
 
659
- 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.0",
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,