@hydranium/client-browser 1.0.0-next.303 → 1.0.0-next.304
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 +63 -14
- package/lib/language-client.d.ts +1 -1
- package/lib/language-client.d.ts.map +1 -1
- package/lib/language-client.js +6 -2
- package/lib/language-client.js.map +1 -1
- package/lib/monaco/index.d.ts +10 -0
- package/lib/monaco/index.d.ts.map +1 -0
- package/lib/monaco/index.js +29 -0
- package/lib/monaco/index.js.map +1 -0
- package/lib/monaco/monaco-lsp-client.d.ts +495 -0
- package/lib/monaco/monaco-lsp-client.d.ts.map +1 -0
- package/lib/monaco/monaco-lsp-client.js +1277 -0
- package/lib/monaco/monaco-lsp-client.js.map +1 -0
- package/package.json +17 -4
- package/src/language-client.ts +6 -3
- package/src/monaco/index.ts +13 -0
- package/src/monaco/monaco-lsp-client.ts +1462 -0
|
@@ -0,0 +1,1277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/********************************************************************************
|
|
3
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
4
|
+
*
|
|
5
|
+
* This program and the accompanying materials are made available under the
|
|
6
|
+
* terms of the MIT License which is available in the project root.
|
|
7
|
+
*
|
|
8
|
+
* SPDX-License-Identifier: MIT
|
|
9
|
+
********************************************************************************/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.MonacoLspClient = exports.MONACO_CLIENT_CAPABILITIES = exports.DEFAULT_MARKER_OWNER = void 0;
|
|
45
|
+
exports.saveDirtyDocuments = saveDirtyDocuments;
|
|
46
|
+
/**
|
|
47
|
+
* The client half of the LSP head for a Monaco editor, over the connection the
|
|
48
|
+
* page already holds for the head.
|
|
49
|
+
*
|
|
50
|
+
* Hand-written rather than `monaco-languageclient`, which drags in the
|
|
51
|
+
* `@codingame/monaco-vscode-*` shim stack: at `platform: 'browser'` a bundler
|
|
52
|
+
* refuses a `node:*` builtin rather than shimming it, and that refusal is what
|
|
53
|
+
* keeps a page bundle a neutrality gate over the head composition it carries.
|
|
54
|
+
*
|
|
55
|
+
* A Theia or VS Code integration hands `vscode-languageclient` a document
|
|
56
|
+
* store, a marker service and a provider registry. Here Monaco is all three,
|
|
57
|
+
* and the three translations between it and LSP are the whole of the work:
|
|
58
|
+
*
|
|
59
|
+
* - **Identity.** A Monaco model URI must be string-identical to the LSP URI, or
|
|
60
|
+
* `publishDiagnostics` addresses a document Monaco does not have and the
|
|
61
|
+
* markers land nowhere, silently.
|
|
62
|
+
* - **Coordinates.** LSP lines and characters are 0-based; Monaco lines and
|
|
63
|
+
* columns are 1-based. Every range crossing this file is converted.
|
|
64
|
+
* - **Versions.** Monaco's `versionId` is monotonic per model, which is exactly
|
|
65
|
+
* what the server's text store wants for its stale-write gate, so it is used
|
|
66
|
+
* directly rather than counted here in parallel.
|
|
67
|
+
*/
|
|
68
|
+
const protocol_1 = require("@hydranium/protocol");
|
|
69
|
+
const monaco = __importStar(require("monaco-editor-core"));
|
|
70
|
+
const browser_1 = require("vscode-jsonrpc/browser");
|
|
71
|
+
const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
|
|
72
|
+
/** The marker owner a client sets diagnostics under when its options name none. */
|
|
73
|
+
exports.DEFAULT_MARKER_OWNER = 'lsp';
|
|
74
|
+
/**
|
|
75
|
+
* The client capabilities for a host whose editor is a {@link MonacoLspClient},
|
|
76
|
+
* to pass as `initializeLanguageClient`'s `capabilities`: the two workspace
|
|
77
|
+
* answers of the default, plus every form the client renders, so a server that
|
|
78
|
+
* gates a form on its capability sends it. Multi-line semantic tokens stay
|
|
79
|
+
* undeclared, since Monaco cannot render one, and Langium then splits them
|
|
80
|
+
* per line. Frozen: a host that declares more spreads it.
|
|
81
|
+
*/
|
|
82
|
+
exports.MONACO_CLIENT_CAPABILITIES = deepFreeze({
|
|
83
|
+
workspace: { applyEdit: true, configuration: true, workspaceEdit: { documentChanges: true } },
|
|
84
|
+
textDocument: {
|
|
85
|
+
synchronization: { willSave: false, willSaveWaitUntil: false, didSave: false },
|
|
86
|
+
completion: {
|
|
87
|
+
contextSupport: true,
|
|
88
|
+
completionItem: {
|
|
89
|
+
snippetSupport: true,
|
|
90
|
+
commitCharactersSupport: true,
|
|
91
|
+
documentationFormat: [vscode_languageserver_protocol_1.MarkupKind.Markdown, vscode_languageserver_protocol_1.MarkupKind.PlainText],
|
|
92
|
+
deprecatedSupport: true,
|
|
93
|
+
preselectSupport: true,
|
|
94
|
+
tagSupport: { valueSet: [vscode_languageserver_protocol_1.CompletionItemTag.Deprecated] },
|
|
95
|
+
insertReplaceSupport: true,
|
|
96
|
+
insertTextModeSupport: { valueSet: [vscode_languageserver_protocol_1.InsertTextMode.asIs, vscode_languageserver_protocol_1.InsertTextMode.adjustIndentation] },
|
|
97
|
+
labelDetailsSupport: true
|
|
98
|
+
},
|
|
99
|
+
completionItemKind: { valueSet: Object.values(vscode_languageserver_protocol_1.CompletionItemKind) }
|
|
100
|
+
},
|
|
101
|
+
hover: { contentFormat: [vscode_languageserver_protocol_1.MarkupKind.Markdown, vscode_languageserver_protocol_1.MarkupKind.PlainText] },
|
|
102
|
+
definition: { linkSupport: true },
|
|
103
|
+
documentHighlight: {},
|
|
104
|
+
publishDiagnostics: {
|
|
105
|
+
relatedInformation: true,
|
|
106
|
+
tagSupport: { valueSet: [vscode_languageserver_protocol_1.DiagnosticTag.Unnecessary, vscode_languageserver_protocol_1.DiagnosticTag.Deprecated] },
|
|
107
|
+
codeDescriptionSupport: true
|
|
108
|
+
},
|
|
109
|
+
semanticTokens: {
|
|
110
|
+
requests: { full: true },
|
|
111
|
+
tokenTypes: Object.values(vscode_languageserver_protocol_1.SemanticTokenTypes),
|
|
112
|
+
tokenModifiers: Object.values(vscode_languageserver_protocol_1.SemanticTokenModifiers),
|
|
113
|
+
formats: ['relative']
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
/** `value` and everything it holds, frozen, so a shared constant cannot be changed in place. */
|
|
118
|
+
function deepFreeze(value) {
|
|
119
|
+
for (const child of Object.values(value)) {
|
|
120
|
+
if (typeof child === 'object' && child !== null) {
|
|
121
|
+
deepFreeze(child);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return Object.freeze(value);
|
|
125
|
+
}
|
|
126
|
+
function toMarkerSeverity(severity) {
|
|
127
|
+
switch (severity) {
|
|
128
|
+
case vscode_languageserver_protocol_1.DiagnosticSeverity.Error:
|
|
129
|
+
return monaco.MarkerSeverity.Error;
|
|
130
|
+
case vscode_languageserver_protocol_1.DiagnosticSeverity.Warning:
|
|
131
|
+
return monaco.MarkerSeverity.Warning;
|
|
132
|
+
case vscode_languageserver_protocol_1.DiagnosticSeverity.Information:
|
|
133
|
+
return monaco.MarkerSeverity.Info;
|
|
134
|
+
case vscode_languageserver_protocol_1.DiagnosticSeverity.Hint:
|
|
135
|
+
return monaco.MarkerSeverity.Hint;
|
|
136
|
+
// An absent severity is an ERROR by the LSP spec's own wording ("the
|
|
137
|
+
// client is free to decide"), and treating it as a hint would hide a
|
|
138
|
+
// deliberate error behind a barely-visible squiggle.
|
|
139
|
+
default:
|
|
140
|
+
return monaco.MarkerSeverity.Error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* LSP completion kinds to Monaco's.
|
|
145
|
+
*
|
|
146
|
+
* **Written out rather than passed through, because the two enums are different
|
|
147
|
+
* NUMBER SPACES over the same names.** LSP `Class` is 7 and Monaco's 7 is
|
|
148
|
+
* `Interface`; LSP `Text` is 1 and Monaco's 1 is `Function`. A pass-through
|
|
149
|
+
* therefore compiles, runs, and puts the wrong icon on every entry in the list —
|
|
150
|
+
* a defect nobody reports as a bug because the list is otherwise correct.
|
|
151
|
+
*
|
|
152
|
+
* Written by NAME on both sides so the map stays readable and a future enum
|
|
153
|
+
* addition on either side is a compile error rather than a silent gap.
|
|
154
|
+
*/
|
|
155
|
+
const COMPLETION_KINDS = {
|
|
156
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Text]: monaco.languages.CompletionItemKind.Text,
|
|
157
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Method]: monaco.languages.CompletionItemKind.Method,
|
|
158
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Function]: monaco.languages.CompletionItemKind.Function,
|
|
159
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Constructor]: monaco.languages.CompletionItemKind.Constructor,
|
|
160
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Field]: monaco.languages.CompletionItemKind.Field,
|
|
161
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Variable]: monaco.languages.CompletionItemKind.Variable,
|
|
162
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Class]: monaco.languages.CompletionItemKind.Class,
|
|
163
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Interface]: monaco.languages.CompletionItemKind.Interface,
|
|
164
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Module]: monaco.languages.CompletionItemKind.Module,
|
|
165
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Property]: monaco.languages.CompletionItemKind.Property,
|
|
166
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Unit]: monaco.languages.CompletionItemKind.Unit,
|
|
167
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Value]: monaco.languages.CompletionItemKind.Value,
|
|
168
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Enum]: monaco.languages.CompletionItemKind.Enum,
|
|
169
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Keyword]: monaco.languages.CompletionItemKind.Keyword,
|
|
170
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Snippet]: monaco.languages.CompletionItemKind.Snippet,
|
|
171
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Color]: monaco.languages.CompletionItemKind.Color,
|
|
172
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.File]: monaco.languages.CompletionItemKind.File,
|
|
173
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Reference]: monaco.languages.CompletionItemKind.Reference,
|
|
174
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Folder]: monaco.languages.CompletionItemKind.Folder,
|
|
175
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.EnumMember]: monaco.languages.CompletionItemKind.EnumMember,
|
|
176
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Constant]: monaco.languages.CompletionItemKind.Constant,
|
|
177
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Struct]: monaco.languages.CompletionItemKind.Struct,
|
|
178
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Event]: monaco.languages.CompletionItemKind.Event,
|
|
179
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.Operator]: monaco.languages.CompletionItemKind.Operator,
|
|
180
|
+
[vscode_languageserver_protocol_1.CompletionItemKind.TypeParameter]: monaco.languages.CompletionItemKind.TypeParameter
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* LSP document-highlight kinds to Monaco's.
|
|
184
|
+
*
|
|
185
|
+
* **The same three names over two number spaces, offset by one.** LSP counts
|
|
186
|
+
* from 1 and Monaco from 0, so a pass-through renders every `Read` as a `Text`
|
|
187
|
+
* and every `Write` as a `Read` — and `Write`, LSP's 3, is outside Monaco's enum
|
|
188
|
+
* entirely. The decoration differs per kind, so the result is a file highlighted
|
|
189
|
+
* in the wrong colours rather than an error.
|
|
190
|
+
*/
|
|
191
|
+
const HIGHLIGHT_KINDS = {
|
|
192
|
+
[vscode_languageserver_protocol_1.DocumentHighlightKind.Text]: monaco.languages.DocumentHighlightKind.Text,
|
|
193
|
+
[vscode_languageserver_protocol_1.DocumentHighlightKind.Read]: monaco.languages.DocumentHighlightKind.Read,
|
|
194
|
+
[vscode_languageserver_protocol_1.DocumentHighlightKind.Write]: monaco.languages.DocumentHighlightKind.Write
|
|
195
|
+
};
|
|
196
|
+
/** LSP 0-based line/character to Monaco 1-based line/column. */
|
|
197
|
+
function toMonacoRange(range) {
|
|
198
|
+
return {
|
|
199
|
+
startLineNumber: range.start.line + 1,
|
|
200
|
+
startColumn: range.start.character + 1,
|
|
201
|
+
endLineNumber: range.end.line + 1,
|
|
202
|
+
endColumn: range.end.character + 1
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function toMarker(diagnostic) {
|
|
206
|
+
return {
|
|
207
|
+
...toMonacoRange(diagnostic.range),
|
|
208
|
+
// LSP 3.18 widened `message` to `string | MarkupContent`, and Monaco's
|
|
209
|
+
// marker takes a plain string. Flattening to the markup's `value` rather
|
|
210
|
+
// than stringifying keeps a rich message readable instead of rendering
|
|
211
|
+
// `[object Object]` in the hover.
|
|
212
|
+
message: typeof diagnostic.message === 'string' ? diagnostic.message : diagnostic.message.value,
|
|
213
|
+
severity: toMarkerSeverity(diagnostic.severity),
|
|
214
|
+
source: diagnostic.source,
|
|
215
|
+
// Stringified because LSP allows a numeric code and Monaco's marker does
|
|
216
|
+
// not, and `undefined` is preserved rather than becoming `"undefined"` in
|
|
217
|
+
// the problem list.
|
|
218
|
+
code: diagnostic.code === undefined
|
|
219
|
+
? undefined
|
|
220
|
+
: diagnostic.codeDescription === undefined
|
|
221
|
+
? String(diagnostic.code)
|
|
222
|
+
: { value: String(diagnostic.code), target: monaco.Uri.parse(diagnostic.codeDescription.href) },
|
|
223
|
+
tags: diagnostic.tags?.flatMap(toMarkerTag),
|
|
224
|
+
relatedInformation: diagnostic.relatedInformation?.map(related => ({
|
|
225
|
+
resource: monaco.Uri.parse(related.location.uri),
|
|
226
|
+
message: related.message,
|
|
227
|
+
...toMonacoRange(related.location.range)
|
|
228
|
+
}))
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** A tag Monaco renders, or none for a tag a later LSP version adds. */
|
|
232
|
+
function toMarkerTag(tag) {
|
|
233
|
+
switch (tag) {
|
|
234
|
+
case vscode_languageserver_protocol_1.DiagnosticTag.Unnecessary:
|
|
235
|
+
return [monaco.MarkerTag.Unnecessary];
|
|
236
|
+
case vscode_languageserver_protocol_1.DiagnosticTag.Deprecated:
|
|
237
|
+
return [monaco.MarkerTag.Deprecated];
|
|
238
|
+
default:
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Monaco's content changes as LSP incremental changes, ordered so that applying
|
|
244
|
+
* them in sequence is correct.
|
|
245
|
+
*
|
|
246
|
+
* **Monaco reports every range against the state BEFORE the edit; LSP reads each
|
|
247
|
+
* range against the state after the previous one.** The two agree only if the
|
|
248
|
+
* changes are applied back-to-front, which is why they are sorted here descending
|
|
249
|
+
* by position. Monaco is documented to deliver them in that order already, and
|
|
250
|
+
* relying on it would be free — but the failure of relying on it wrongly is a
|
|
251
|
+
* multi-cursor edit silently corrupting the server's copy of the document while
|
|
252
|
+
* the editor looks correct, which is not a defect anyone finds from the symptom.
|
|
253
|
+
*/
|
|
254
|
+
function toContentChanges(changes) {
|
|
255
|
+
return [...changes]
|
|
256
|
+
.sort((left, right) => right.rangeOffset - left.rangeOffset)
|
|
257
|
+
.map(change => ({
|
|
258
|
+
range: {
|
|
259
|
+
start: { line: change.range.startLineNumber - 1, character: change.range.startColumn - 1 },
|
|
260
|
+
end: { line: change.range.endLineNumber - 1, character: change.range.endColumn - 1 }
|
|
261
|
+
},
|
|
262
|
+
rangeLength: change.rangeLength,
|
|
263
|
+
text: change.text
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
/** Monaco 1-based line/column to an LSP 0-based position. */
|
|
267
|
+
function toLspPosition(position) {
|
|
268
|
+
return { line: position.lineNumber - 1, character: position.column - 1 };
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Where an editor-opener request wants the caret.
|
|
272
|
+
*
|
|
273
|
+
* Monaco hands the opener a RANGE where it has one and a bare position where it
|
|
274
|
+
* does not, and the two spell their line under different names — so a reader of
|
|
275
|
+
* `lineNumber` alone gets `undefined` for every navigation that carried a range,
|
|
276
|
+
* which is all of the ones a definition result produces: Monaco collapses a
|
|
277
|
+
* target to its start before it reaches here.
|
|
278
|
+
*
|
|
279
|
+
* Line 1, column 1 for a request naming no position at all, which a request to
|
|
280
|
+
* open a document merely by URI is. The top of the file is that request's own
|
|
281
|
+
* answer rather than a fallback.
|
|
282
|
+
*/
|
|
283
|
+
function toEditorPosition(selectionOrPosition) {
|
|
284
|
+
if (selectionOrPosition === undefined) {
|
|
285
|
+
return { lineNumber: 1, column: 1 };
|
|
286
|
+
}
|
|
287
|
+
return 'startLineNumber' in selectionOrPosition
|
|
288
|
+
? { lineNumber: selectionOrPosition.startLineNumber, column: selectionOrPosition.startColumn }
|
|
289
|
+
: { lineNumber: selectionOrPosition.lineNumber, column: selectionOrPosition.column };
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Whether `error` says the connection is closed or disposed, by its code
|
|
293
|
+
* rather than by `instanceof`: a connection the host hands in may come from
|
|
294
|
+
* another `vscode-jsonrpc` copy, whose `ConnectionError` is a different class.
|
|
295
|
+
*/
|
|
296
|
+
function isConnectionClosedError(error) {
|
|
297
|
+
return error instanceof Error && 'code' in error && (error.code === browser_1.ConnectionErrors.Closed || error.code === browser_1.ConnectionErrors.Disposed);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Send a notification that a closed connection does not fail: the server's
|
|
301
|
+
* side of the sync ended with the connection. A write that fails later is
|
|
302
|
+
* logged by the connection itself.
|
|
303
|
+
*/
|
|
304
|
+
function notifyUnlessClosed(send) {
|
|
305
|
+
try {
|
|
306
|
+
void send().catch(() => undefined);
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
if (!isConnectionClosedError(error)) {
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Whether a request failed because nobody wants its answer any more: the
|
|
316
|
+
* server gave it up (`RequestCancelled`, `ServerCancelled`, or
|
|
317
|
+
* `ContentModified` for a document changed under it), or the connection
|
|
318
|
+
* closed before or while it ran.
|
|
319
|
+
*/
|
|
320
|
+
function isAbandonedRequest(error) {
|
|
321
|
+
if (isConnectionClosedError(error)) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
if (!(0, protocol_1.isResponseError)(error)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
return (error.code === vscode_languageserver_protocol_1.LSPErrorCodes.RequestCancelled ||
|
|
328
|
+
error.code === vscode_languageserver_protocol_1.LSPErrorCodes.ServerCancelled ||
|
|
329
|
+
error.code === vscode_languageserver_protocol_1.LSPErrorCodes.ContentModified ||
|
|
330
|
+
error.code === vscode_languageserver_protocol_1.ErrorCodes.PendingResponseRejected ||
|
|
331
|
+
error.code === vscode_languageserver_protocol_1.ErrorCodes.ConnectionInactive);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Monaco's cancellation, which it recognises by name and message: Monaco's API
|
|
335
|
+
* does not export the class, and drops this error silently where it reports
|
|
336
|
+
* any other from a timer as an uncaught page error.
|
|
337
|
+
*/
|
|
338
|
+
function monacoCancellation() {
|
|
339
|
+
const error = new Error('Canceled');
|
|
340
|
+
error.name = 'Canceled';
|
|
341
|
+
return error;
|
|
342
|
+
}
|
|
343
|
+
function isMarkupContent(value) {
|
|
344
|
+
return typeof value === 'object' && value !== null && 'kind' in value && 'value' in value;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* A `textDocument/definition` result as Monaco location links.
|
|
348
|
+
*
|
|
349
|
+
* **Both wire shapes are live, and reading only one of them fails quietly.** The
|
|
350
|
+
* response is a `Location`, an array of them, or an array of `LocationLink`s,
|
|
351
|
+
* and the framework's head answers with the last — to a client that declared no
|
|
352
|
+
* capabilities at all, so `linkSupport` cannot be relied on to narrow it. The
|
|
353
|
+
* two spell the target under different keys, so a client that reads a link's
|
|
354
|
+
* `uri` parses a URI out of nothing, which addresses no model and navigates
|
|
355
|
+
* nowhere without raising anything.
|
|
356
|
+
*
|
|
357
|
+
* The link form is the richer one and is kept whole rather than flattened to a
|
|
358
|
+
* bare location. `targetRange` is the declaration's full extent and
|
|
359
|
+
* `targetSelectionRange` the name inside it, which is what puts the caret on the
|
|
360
|
+
* name rather than on the keyword in front of it. `originSelectionRange` is the
|
|
361
|
+
* span underlined under the Ctrl key, and dropping it leaves Monaco underlining
|
|
362
|
+
* its own word-at-position instead — a guess about where the reference the
|
|
363
|
+
* server resolved actually begins.
|
|
364
|
+
*/
|
|
365
|
+
function toDefinitionLinks(result) {
|
|
366
|
+
const entries = Array.isArray(result) ? result : [result];
|
|
367
|
+
return entries.map(entry => vscode_languageserver_protocol_1.LocationLink.is(entry)
|
|
368
|
+
? {
|
|
369
|
+
uri: monaco.Uri.parse(entry.targetUri),
|
|
370
|
+
range: toMonacoRange(entry.targetRange),
|
|
371
|
+
targetSelectionRange: toMonacoRange(entry.targetSelectionRange),
|
|
372
|
+
originSelectionRange: entry.originSelectionRange === undefined ? undefined : toMonacoRange(entry.originSelectionRange)
|
|
373
|
+
}
|
|
374
|
+
: { uri: monaco.Uri.parse(entry.uri), range: toMonacoRange(entry.range) });
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* `Hover.contents` as Monaco markdown blocks.
|
|
378
|
+
*
|
|
379
|
+
* The LSP type is a union of four shapes — a `MarkupContent`, a plain string, a
|
|
380
|
+
* `{ language, value }` code block, and an array of the latter two — and all
|
|
381
|
+
* four are still live in 3.18 despite the last two being deprecated. Monaco
|
|
382
|
+
* takes markdown, so the code-block form is fenced with its language rather than
|
|
383
|
+
* dropped: rendering it as prose would silently lose the formatting that was the
|
|
384
|
+
* point of sending it that way.
|
|
385
|
+
*/
|
|
386
|
+
function toMarkdown(contents) {
|
|
387
|
+
if (isMarkupContent(contents)) {
|
|
388
|
+
return [{ value: contents.kind === 'plaintext' ? escapePlaintext(contents.value) : contents.value }];
|
|
389
|
+
}
|
|
390
|
+
const blocks = Array.isArray(contents) ? contents : [contents];
|
|
391
|
+
return blocks.map(block => typeof block === 'string' ? { value: block } : { value: `\`\`\`${block.language}\n${block.value}\n\`\`\`` });
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Plaintext as markdown that renders it verbatim, escaped as Monaco's own
|
|
395
|
+
* `MarkdownString.appendText` does, which Monaco does not export.
|
|
396
|
+
*/
|
|
397
|
+
function escapePlaintext(text) {
|
|
398
|
+
return text
|
|
399
|
+
.replace(/[\\`*_{}[\]()#+!~]/g, '\\$&')
|
|
400
|
+
.replace(/^([ \t]*)-/gm, '$1\\-')
|
|
401
|
+
.replace(/[ \t]+/g, whitespace => ' '.repeat(whitespace.length))
|
|
402
|
+
.replace(/>/g, '\\>')
|
|
403
|
+
.replace(/\n/g, '\n\n');
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* The range a completion item applies to.
|
|
407
|
+
*
|
|
408
|
+
* **Monaco REQUIRES a range and LSP does not**, which is the one real asymmetry
|
|
409
|
+
* in this conversion. An LSP item may carry a `textEdit` (either a plain
|
|
410
|
+
* `TextEdit` or an `InsertReplaceEdit`), or nothing at all — in which case the
|
|
411
|
+
* client is expected to replace the word being typed. Getting the fallback wrong
|
|
412
|
+
* does not fail: accepting an item inserts alongside the prefix instead of over
|
|
413
|
+
* it, so `Order.stastatus` appears and reads as a server sending a bad label.
|
|
414
|
+
*
|
|
415
|
+
* **An `InsertReplaceEdit` is forwarded as BOTH ranges, not collapsed to one.**
|
|
416
|
+
* Monaco's `range` accepts the `{ insert, replace }` pair and then honours
|
|
417
|
+
* `suggest.insertMode`, which is the whole point of the server sending two: the
|
|
418
|
+
* server states what a suggestion could stand in for and the USER's setting
|
|
419
|
+
* decides whether accepting it overwrites that or inserts before it. Collapsing
|
|
420
|
+
* to `replace` here would silently overrule that setting, and collapsing to
|
|
421
|
+
* `insert` would throw away the wider range the server went to the trouble of
|
|
422
|
+
* computing.
|
|
423
|
+
*/
|
|
424
|
+
function toCompletionRange(item, model, position) {
|
|
425
|
+
const edit = item.textEdit;
|
|
426
|
+
if (edit !== undefined) {
|
|
427
|
+
return 'range' in edit ? toMonacoRange(edit.range) : { insert: toMonacoRange(edit.insert), replace: toMonacoRange(edit.replace) };
|
|
428
|
+
}
|
|
429
|
+
const word = model.getWordUntilPosition(position);
|
|
430
|
+
return {
|
|
431
|
+
startLineNumber: position.lineNumber,
|
|
432
|
+
endLineNumber: position.lineNumber,
|
|
433
|
+
startColumn: word.startColumn,
|
|
434
|
+
endColumn: position.column
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/** Monaco's completion trigger in LSP's numbering, which starts at one where Monaco's starts at zero. */
|
|
438
|
+
function toCompletionContext(context) {
|
|
439
|
+
switch (context.triggerKind) {
|
|
440
|
+
case monaco.languages.CompletionTriggerKind.TriggerCharacter:
|
|
441
|
+
return { triggerKind: vscode_languageserver_protocol_1.CompletionTriggerKind.TriggerCharacter, triggerCharacter: context.triggerCharacter };
|
|
442
|
+
case monaco.languages.CompletionTriggerKind.TriggerForIncompleteCompletions:
|
|
443
|
+
return { triggerKind: vscode_languageserver_protocol_1.CompletionTriggerKind.TriggerForIncompleteCompletions };
|
|
444
|
+
default:
|
|
445
|
+
return { triggerKind: vscode_languageserver_protocol_1.CompletionTriggerKind.Invoked };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/** Monaco's insert rules for `item`: a snippet, and whitespace kept as sent for an as-is item. */
|
|
449
|
+
function toInsertTextRules(item) {
|
|
450
|
+
const rules = monaco.languages.CompletionItemInsertTextRule;
|
|
451
|
+
const snippet = item.insertTextFormat === vscode_languageserver_protocol_1.InsertTextFormat.Snippet ? rules.InsertAsSnippet : rules.None;
|
|
452
|
+
const keepWhitespace = item.insertTextMode === vscode_languageserver_protocol_1.InsertTextMode.asIs ? rules.KeepWhitespace : rules.None;
|
|
453
|
+
const combined = snippet | keepWhitespace;
|
|
454
|
+
return combined === rules.None ? undefined : combined;
|
|
455
|
+
}
|
|
456
|
+
/** The deprecated tag Monaco strikes through, from the tag or the older `deprecated` flag. */
|
|
457
|
+
function toCompletionTags(item) {
|
|
458
|
+
return item.tags?.includes(vscode_languageserver_protocol_1.CompletionItemTag.Deprecated) || item.deprecated === true
|
|
459
|
+
? [monaco.languages.CompletionItemTag.Deprecated]
|
|
460
|
+
: undefined;
|
|
461
|
+
}
|
|
462
|
+
function toCompletionItem(item, model, position) {
|
|
463
|
+
return {
|
|
464
|
+
label: item.labelDetails === undefined
|
|
465
|
+
? item.label
|
|
466
|
+
: { label: item.label, detail: item.labelDetails.detail, description: item.labelDetails.description },
|
|
467
|
+
// `Text` for an item with no kind, which is Monaco's plainest icon. LSP
|
|
468
|
+
// leaves the kind optional and Monaco does not.
|
|
469
|
+
kind: item.kind === undefined ? monaco.languages.CompletionItemKind.Text : COMPLETION_KINDS[item.kind],
|
|
470
|
+
insertText: item.textEdit?.newText ?? item.insertText ?? item.label,
|
|
471
|
+
insertTextRules: toInsertTextRules(item),
|
|
472
|
+
range: toCompletionRange(item, model, position),
|
|
473
|
+
detail: item.detail,
|
|
474
|
+
// A plain string is Monaco's plaintext.
|
|
475
|
+
documentation: isMarkupContent(item.documentation)
|
|
476
|
+
? item.documentation.kind === 'plaintext'
|
|
477
|
+
? item.documentation.value
|
|
478
|
+
: { value: item.documentation.value }
|
|
479
|
+
: item.documentation,
|
|
480
|
+
sortText: item.sortText,
|
|
481
|
+
filterText: item.filterText,
|
|
482
|
+
tags: toCompletionTags(item),
|
|
483
|
+
preselect: item.preselect,
|
|
484
|
+
commitCharacters: item.commitCharacters,
|
|
485
|
+
// Monaco runs it after the item is accepted, if a command of that id is
|
|
486
|
+
// registered: an editor action such as `editor.action.triggerSuggest`.
|
|
487
|
+
command: item.command === undefined
|
|
488
|
+
? undefined
|
|
489
|
+
: { id: item.command.command, title: item.command.title, arguments: item.command.arguments },
|
|
490
|
+
// Forwarded rather than dropped: an item that needs a companion edit (an
|
|
491
|
+
// import, a sibling declaration) is silently half-applied without them,
|
|
492
|
+
// and the half that lands is the one that parses.
|
|
493
|
+
additionalTextEdits: item.additionalTextEdits?.map(edit => ({ range: toMonacoRange(edit.range), text: edit.newText }))
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* One LSP text edit as a Monaco edit operation.
|
|
498
|
+
*
|
|
499
|
+
* An `AnnotatedTextEdit` is a plain edit plus a change-annotation id — a hint
|
|
500
|
+
* that a client with a confirmation UI could group and prompt on. This client
|
|
501
|
+
* has no such UI, so the annotation is dropped and the text applied, which is
|
|
502
|
+
* the behaviour LSP prescribes for a client that does not implement
|
|
503
|
+
* `changeAnnotationSupport`. Dropping the EDIT instead would silently lose part
|
|
504
|
+
* of a server write.
|
|
505
|
+
*/
|
|
506
|
+
function toEditOperation(edit) {
|
|
507
|
+
return { range: toMonacoRange(edit.range), text: edit.newText };
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* The editor settings a server-driven language needs, under whatever the host
|
|
511
|
+
* passes to {@link MonacoLspClient.createEditor}.
|
|
512
|
+
*
|
|
513
|
+
* - `fixedOverflowWidgets`: Monaco sizes the suggest list against the document
|
|
514
|
+
* body rather than the editor, so in a pane smaller than the list the pane's
|
|
515
|
+
* clipping ancestor cuts it off part-way down and the list draws no
|
|
516
|
+
* scrollbar: a nine-item completion reads as a two-item one.
|
|
517
|
+
* - `wordBasedSuggestions` off: Monaco's own word-based suggestions offer every
|
|
518
|
+
* word already in the document, so in a language whose identifiers are scoped
|
|
519
|
+
* cross-references they propose names not in scope at the cursor, mixed
|
|
520
|
+
* indistinguishably into the server's list.
|
|
521
|
+
* - `suggest.insertMode` replace: the client forwards both ranges of an
|
|
522
|
+
* `InsertReplaceEdit`, and this is the setting that chooses between them. An
|
|
523
|
+
* identifier in a modelling language is a whole reference, not a prefix
|
|
524
|
+
* someone is part-way through typing. Monaco's own default is `'insert'`.
|
|
525
|
+
* - `semanticHighlighting.enabled`: the default, `'configuredByTheme'`, leaves
|
|
526
|
+
* highlighting off for the built-in themes, which is indistinguishable from
|
|
527
|
+
* the server never answering. Without a grammar underneath, the server's
|
|
528
|
+
* semantic tokens are the only source of highlighting.
|
|
529
|
+
*/
|
|
530
|
+
const LSP_EDITOR_OPTIONS = {
|
|
531
|
+
fixedOverflowWidgets: true,
|
|
532
|
+
wordBasedSuggestions: 'off',
|
|
533
|
+
suggest: { insertMode: 'replace' },
|
|
534
|
+
'semanticHighlighting.enabled': true
|
|
535
|
+
};
|
|
536
|
+
/**
|
|
537
|
+
* Registers the server's languages with Monaco and keeps its models in step
|
|
538
|
+
* with the LSP head on the other end of `connection`.
|
|
539
|
+
*
|
|
540
|
+
* Construction registers the languages, the providers and the inbound
|
|
541
|
+
* `workspace/applyEdit` handler; {@link openDocument} opens one document and
|
|
542
|
+
* {@link closeDocument} closes it; {@link dispose} closes every document and
|
|
543
|
+
* releases every registration. Diagnostics are NOT subscribed here — see
|
|
544
|
+
* {@link applyDiagnostics}.
|
|
545
|
+
*/
|
|
546
|
+
class MonacoLspClient {
|
|
547
|
+
options;
|
|
548
|
+
/**
|
|
549
|
+
* The models this client opened, by URI in Monaco's form. Every inbound
|
|
550
|
+
* edit, diagnostic and dirty read goes through this map rather than
|
|
551
|
+
* Monaco's global lookup: a page may hold a second client, and a model
|
|
552
|
+
* that client holds is not this one's to write, mark or save, however
|
|
553
|
+
* its URI reads.
|
|
554
|
+
*/
|
|
555
|
+
models = new Map();
|
|
556
|
+
/** Per URI, the buffer version last known to be persisted. */
|
|
557
|
+
savedVersions = new Map();
|
|
558
|
+
/** Everything construction and {@link registerEditorOpener} registered, released in reverse by {@link dispose}. */
|
|
559
|
+
registrations = [];
|
|
560
|
+
markerOwner;
|
|
561
|
+
/**
|
|
562
|
+
* The host's connection, sending by method name: the LSP message types come
|
|
563
|
+
* from `vscode-languageserver-protocol`'s copy of `vscode-jsonrpc`, which
|
|
564
|
+
* need not be the connection's.
|
|
565
|
+
*/
|
|
566
|
+
connection;
|
|
567
|
+
constructor(connection, capabilities, options) {
|
|
568
|
+
this.options = options;
|
|
569
|
+
this.connection = (0, protocol_1.sendByMethodName)(connection);
|
|
570
|
+
this.markerOwner = options.markerOwner ?? exports.DEFAULT_MARKER_OWNER;
|
|
571
|
+
// First: it is the one registration on the connection, which throws once
|
|
572
|
+
// closed, and a constructor that throws leaves nothing to dispose.
|
|
573
|
+
this.registerApplyEdit();
|
|
574
|
+
for (const language of options.languages) {
|
|
575
|
+
monaco.languages.register(language);
|
|
576
|
+
}
|
|
577
|
+
this.registerSemanticTokens(capabilities);
|
|
578
|
+
this.registerCompletion(capabilities);
|
|
579
|
+
this.registerHover();
|
|
580
|
+
this.registerDefinition();
|
|
581
|
+
this.registerDocumentHighlight();
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* The language id for `uri`, by the extensions the options name.
|
|
585
|
+
*
|
|
586
|
+
* Throws rather than falling back to a default id. An unknown extension means
|
|
587
|
+
* the host named a document the server has no grammar for, and the failure of
|
|
588
|
+
* the guessing version is that the document opens, parses as the wrong
|
|
589
|
+
* language and reports a page of syntax errors that look like the file being
|
|
590
|
+
* broken.
|
|
591
|
+
*/
|
|
592
|
+
languageIdFor(uri) {
|
|
593
|
+
// The path alone: a query or a fragment follows the extension.
|
|
594
|
+
const path = monaco.Uri.parse(uri).path;
|
|
595
|
+
const language = this.options.languages.find(candidate => candidate.extensions?.some(extension => path.endsWith(extension)));
|
|
596
|
+
if (language === undefined) {
|
|
597
|
+
throw new Error(`No language registered for ${uri}`);
|
|
598
|
+
}
|
|
599
|
+
return language.id;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Apply one `publishDiagnostics` notification to the model it addresses.
|
|
603
|
+
*
|
|
604
|
+
* **Called by the host's single notification handler rather than subscribing
|
|
605
|
+
* here, and that is forced rather than stylistic.**
|
|
606
|
+
* `MessageConnection.onNotification` stores handlers in a map keyed by method,
|
|
607
|
+
* so a second registration for `textDocument/publishDiagnostics` REPLACES the
|
|
608
|
+
* first and returns a disposable as though it had worked. A host with a
|
|
609
|
+
* handler for its own diagnostics view would have it taken over by a client
|
|
610
|
+
* that registered its own, and its view would freeze at whatever it had
|
|
611
|
+
* reached — with nothing on the console to say so.
|
|
612
|
+
*
|
|
613
|
+
* A URI with no model of this client's is not an error: a workspace has more
|
|
614
|
+
* documents than the host opens, so most publishes address a file Monaco
|
|
615
|
+
* has never heard of, and a model another client holds is that client's.
|
|
616
|
+
*/
|
|
617
|
+
applyDiagnostics(params) {
|
|
618
|
+
const model = this.modelOf(params.uri);
|
|
619
|
+
if (model === undefined) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
monaco.editor.setModelMarkers(model, this.markerOwner, params.diagnostics.map(toMarker));
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Answer `workspace/applyEdit` — the server→text direction, and the half of
|
|
626
|
+
* the sync a client that only sends `didOpen` silently loses.
|
|
627
|
+
*
|
|
628
|
+
* A server-side write (a diagram drag, a form edit, an integrity repair) goes
|
|
629
|
+
* through the framework's `ModelService`, which rewrites the in-memory text
|
|
630
|
+
* document and then mirrors the settled text to whichever client holds it
|
|
631
|
+
* open, as a MINIMAL `workspace/applyEdit` computed against a shadow of that
|
|
632
|
+
* client's buffer. So the request goes out on every such write the moment the
|
|
633
|
+
* host opens a document — and a client with no handler makes
|
|
634
|
+
* `vscode-jsonrpc` answer `MethodNotFound`, which the framework logs through
|
|
635
|
+
* its tracer and therefore onto `window/logMessage`. A host that has no
|
|
636
|
+
* handler for that either loses the whole inbound direction with nothing on
|
|
637
|
+
* the console, in the editor or in the server's own report to say so.
|
|
638
|
+
*
|
|
639
|
+
* **Registered here rather than by the host, unlike `publishDiagnostics`.**
|
|
640
|
+
* `onRequest` keys handlers by method exactly as `onNotification` does, so a
|
|
641
|
+
* second registration would silently replace this one — but nothing else
|
|
642
|
+
* answers `workspace/applyEdit`, so there is no fan-out to arbitrate, and the
|
|
643
|
+
* handler needs the model store, which is this class's business.
|
|
644
|
+
*
|
|
645
|
+
* **The framework does not gate this on a client capability.**
|
|
646
|
+
* `HydraniumTextDocuments.applyEditToLanguageClient` checks that an LSP
|
|
647
|
+
* connection is bound and that the client has the document open, never a
|
|
648
|
+
* capability, and `vscode-languageserver`'s own
|
|
649
|
+
* `RemoteWorkspaceImpl.applyEdit` forwards unconditionally — so declaring
|
|
650
|
+
* `workspace.applyEdit` in `initialize` does not turn the request on and
|
|
651
|
+
* omitting it does not turn it off.
|
|
652
|
+
*
|
|
653
|
+
* **The echo does not LOOP, and is deliberately not suppressed here.**
|
|
654
|
+
* Applying the edit makes Monaco fire `onDidChangeContent`, which
|
|
655
|
+
* {@link openDocument} turns into a `didChange`. The server answers that
|
|
656
|
+
* echo with no push of its own, so nothing ping-pongs.
|
|
657
|
+
* The server correlates that `didChange` against the push still in flight
|
|
658
|
+
* and reconstructs it against the buffer this client held BEFORE the push —
|
|
659
|
+
* which is the only text its incremental ranges address — so an echo mints
|
|
660
|
+
* no version and drives no rebuild.
|
|
661
|
+
*
|
|
662
|
+
* Suppressing it would break what that correlation runs on: `didChange` is
|
|
663
|
+
* what keeps the server's per-URI shadow and its per-client version aligned
|
|
664
|
+
* with this buffer, so withholding it makes the next outbound diff wrong and
|
|
665
|
+
* makes every versioned push arrive stale. A conforming client echoes.
|
|
666
|
+
*
|
|
667
|
+
* **This client echoes the ranges it was handed, and a server must not assume
|
|
668
|
+
* that.** A host is free to minimise a coarse edit against its own buffer
|
|
669
|
+
* before applying it — Theia routes every workspace edit through
|
|
670
|
+
* `computeMoreMinimalEdits` — so the echo of a single full-document replace
|
|
671
|
+
* can arrive as a set of small ranges keyed to text the server never sent.
|
|
672
|
+
*/
|
|
673
|
+
registerApplyEdit() {
|
|
674
|
+
this.registrations.push(this.connection.onRequest(vscode_languageserver_protocol_1.ApplyWorkspaceEditRequest.type, (params) => this.applyWorkspaceEdit(params.edit)));
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Apply `edit` to the models it addresses, or refuse all of it.
|
|
678
|
+
*
|
|
679
|
+
* **Every target is resolved before any is written.** An LSP workspace edit
|
|
680
|
+
* is a unit — `failureHandling` exists precisely because a partially applied
|
|
681
|
+
* one leaves the workspace in a state no participant described — and here the
|
|
682
|
+
* server's text shadow is what would carry the damage: it tracks one text per
|
|
683
|
+
* URI, so a half-applied multi-document edit desynchronises the untouched
|
|
684
|
+
* half's baseline and every later minimal diff for it addresses the wrong
|
|
685
|
+
* lines.
|
|
686
|
+
*
|
|
687
|
+
* The framework's own egress never exercises that: it sends a write that
|
|
688
|
+
* touches two documents as two requests of one `TextDocumentEdit` each,
|
|
689
|
+
* because `applyEditToLanguageClient` is per-URI and coalesced per-URI.
|
|
690
|
+
* The loop is therefore for an adopter's server that batches, and the
|
|
691
|
+
* all-or-nothing shape is what keeps this client honest for one.
|
|
692
|
+
*/
|
|
693
|
+
applyWorkspaceEdit(edit) {
|
|
694
|
+
const resolved = [];
|
|
695
|
+
// A document named twice is refused whole: each entry is resolved against
|
|
696
|
+
// the buffer as it is now, so a second entry would be refused or applied
|
|
697
|
+
// at positions the first one moved.
|
|
698
|
+
const named = new Set();
|
|
699
|
+
const nameOnce = (uri) => {
|
|
700
|
+
const key = monaco.Uri.parse(uri).toString();
|
|
701
|
+
if (named.has(key)) {
|
|
702
|
+
return `An edit names ${key} more than once`;
|
|
703
|
+
}
|
|
704
|
+
named.add(key);
|
|
705
|
+
return undefined;
|
|
706
|
+
};
|
|
707
|
+
for (const change of edit.documentChanges ?? []) {
|
|
708
|
+
if (!vscode_languageserver_protocol_1.TextDocumentEdit.is(change)) {
|
|
709
|
+
// A create / rename / delete of a FILE. The framework never sends
|
|
710
|
+
// one, and a page has no filesystem of its own to perform it in —
|
|
711
|
+
// the worker's does, and it is not addressable from here. Refused by
|
|
712
|
+
// name so the reason names the operation rather than the shape.
|
|
713
|
+
return { applied: false, failureReason: `Resource operations are not supported by this client: ${change.kind}` };
|
|
714
|
+
}
|
|
715
|
+
const repeated = nameOnce(change.textDocument.uri);
|
|
716
|
+
if (repeated !== undefined) {
|
|
717
|
+
return { applied: false, failureReason: repeated };
|
|
718
|
+
}
|
|
719
|
+
const resolution = this.resolveDocumentEdit(change.textDocument.uri, change.textDocument.version, change.edits);
|
|
720
|
+
if (!resolution.applicable) {
|
|
721
|
+
return { applied: false, failureReason: resolution.reason };
|
|
722
|
+
}
|
|
723
|
+
resolved.push(resolution.edit);
|
|
724
|
+
}
|
|
725
|
+
// The `changes` map is the pre-3.13 form, carrying no version to check
|
|
726
|
+
// against. Handled rather than refused because it is still live in the
|
|
727
|
+
// spec and an adopter's own server may well send it; the framework's
|
|
728
|
+
// egress does not. Read only without `documentChanges`: the spec makes
|
|
729
|
+
// the two alternatives, and an edit carrying both would apply twice.
|
|
730
|
+
for (const [uri, edits] of Object.entries(edit.documentChanges === undefined ? (edit.changes ?? {}) : {})) {
|
|
731
|
+
const repeated = nameOnce(uri);
|
|
732
|
+
if (repeated !== undefined) {
|
|
733
|
+
return { applied: false, failureReason: repeated };
|
|
734
|
+
}
|
|
735
|
+
const resolution = this.resolveDocumentEdit(uri, null, edits);
|
|
736
|
+
if (!resolution.applicable) {
|
|
737
|
+
return { applied: false, failureReason: resolution.reason };
|
|
738
|
+
}
|
|
739
|
+
resolved.push(resolution.edit);
|
|
740
|
+
}
|
|
741
|
+
for (const target of resolved) {
|
|
742
|
+
// `pushEditOperations`, not `setValue`: the edit joins Monaco's undo
|
|
743
|
+
// stack, so Ctrl+Z reverses a server write from the editor. `setValue`
|
|
744
|
+
// would reset the model — dropping the undo history and the cursor
|
|
745
|
+
// with it, and re-tokenising the whole document for an edit the server
|
|
746
|
+
// went to the trouble of making minimal.
|
|
747
|
+
//
|
|
748
|
+
// An undo stop on either side, so the write is an entry of its own.
|
|
749
|
+
// A typing run leaves Monaco's last element open and an edit pushed
|
|
750
|
+
// then is appended to it, so without the stop before, one Ctrl+Z
|
|
751
|
+
// reverts the write together with the word typed before it; without
|
|
752
|
+
// the stop after, the next keystroke joins the write.
|
|
753
|
+
//
|
|
754
|
+
// `() => null` for the cursor computer: the edit is the SERVER's, not a
|
|
755
|
+
// user gesture, so it must not move a caret the user placed. Returning
|
|
756
|
+
// no selection leaves the cursor where it was.
|
|
757
|
+
target.model.pushStackElement();
|
|
758
|
+
target.model.pushEditOperations(null, [...target.operations], () => null);
|
|
759
|
+
target.model.pushStackElement();
|
|
760
|
+
}
|
|
761
|
+
return { applied: true };
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Resolve one document's worth of an inbound edit against the model it names.
|
|
765
|
+
*
|
|
766
|
+
* Refuses, rather than dropping quietly, in three cases:
|
|
767
|
+
*
|
|
768
|
+
* - **No model for the URI.** `applied: true` for an edit that went nowhere
|
|
769
|
+
* would leave the server's shadow believing this client holds text it does
|
|
770
|
+
* not, and every later minimal diff for that URI would be computed against
|
|
771
|
+
* the wrong baseline. The framework pushes only to documents the language
|
|
772
|
+
* client has opened, and stages or keeps in the store a write to any
|
|
773
|
+
* other, so this branch should stay unreached — it exists so that if it is
|
|
774
|
+
* ever reached the failure is loud rather than a slow desynchronisation.
|
|
775
|
+
* - **A stale version.** The framework addresses a position-dependent
|
|
776
|
+
* (line-keyed) push at its last known version of this buffer, precisely so
|
|
777
|
+
* the client can refuse a push its buffer has outrun: applying a stale
|
|
778
|
+
* range does not fail, it splices the file at the wrong lines. Refusing is
|
|
779
|
+
* not a dead branch — `ModelService` re-pushes a full-range replace on
|
|
780
|
+
* `applied: false`, which is position-independent and therefore correct
|
|
781
|
+
* against whatever the buffer now holds. That retry arrives with
|
|
782
|
+
* `version: null` and passes this gate by construction.
|
|
783
|
+
* - **A snippet edit.** Monaco applies snippets through a controller rather
|
|
784
|
+
* than through the model, so a `SnippetTextEdit` pushed as text would
|
|
785
|
+
* insert its placeholder syntax literally.
|
|
786
|
+
*/
|
|
787
|
+
resolveDocumentEdit(uri, version, edits) {
|
|
788
|
+
const model = this.modelOf(uri);
|
|
789
|
+
if (model === undefined) {
|
|
790
|
+
return { applicable: false, reason: `No editor open for ${uri}` };
|
|
791
|
+
}
|
|
792
|
+
if (version !== null && version !== model.getVersionId()) {
|
|
793
|
+
return {
|
|
794
|
+
applicable: false,
|
|
795
|
+
reason: `Stale edit for ${uri}: addressed at version ${version}, buffer is at ${model.getVersionId()}`
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
const operations = [];
|
|
799
|
+
for (const edit of edits) {
|
|
800
|
+
if (vscode_languageserver_protocol_1.SnippetTextEdit.is(edit)) {
|
|
801
|
+
return { applicable: false, reason: `Snippet edits are not supported by this client: ${uri}` };
|
|
802
|
+
}
|
|
803
|
+
operations.push(toEditOperation(edit));
|
|
804
|
+
}
|
|
805
|
+
return { applicable: true, edit: { model, operations } };
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Create a model for `uri` holding `text`, and tell the server the document is
|
|
809
|
+
* open. Returns the model this client already holds for the URI; a model
|
|
810
|
+
* another client of this page holds is refused, since this client would
|
|
811
|
+
* neither announce it nor send its changes.
|
|
812
|
+
*
|
|
813
|
+
* **`text` must be the bytes the server already has.** A browser host takes
|
|
814
|
+
* it from the worker's own filesystem, so `didOpen` carries what the heads
|
|
815
|
+
* came up on. Inventing placeholder text would not fail — it would
|
|
816
|
+
* OVERWRITE the server's copy through the text-document store and quietly
|
|
817
|
+
* change the diagnostics the server reports.
|
|
818
|
+
*
|
|
819
|
+
* `didOpen` carries the model's text rather than `text` itself: Monaco drops
|
|
820
|
+
* a BOM and rewrites mixed line endings to the majority form when it builds
|
|
821
|
+
* the model, as any editor does, and a server holding the bytes instead
|
|
822
|
+
* would be one character off every position and never match the buffer a
|
|
823
|
+
* save compares it with.
|
|
824
|
+
*
|
|
825
|
+
* Which is also why the second call for a URI is a no-op rather than a
|
|
826
|
+
* refresh: by then the buffer may hold a server write the snapshot never had,
|
|
827
|
+
* and the snapshot is the older claim.
|
|
828
|
+
*
|
|
829
|
+
* The URI is sent in Monaco's form: `Uri.parse(uri).toString()` encodes a
|
|
830
|
+
* space or a drive-letter colon, and the providers name the document by the
|
|
831
|
+
* model's URI, so a `didOpen` with the host's string would open a document
|
|
832
|
+
* no provider request ever addresses.
|
|
833
|
+
*/
|
|
834
|
+
openDocument(uri, text) {
|
|
835
|
+
const key = monaco.Uri.parse(uri).toString();
|
|
836
|
+
// Idempotent, and that is what makes two panes able to swap content: a
|
|
837
|
+
// model is Monaco's per-URI singleton, so a second `createModel` for the
|
|
838
|
+
// same URI THROWS — and re-announcing `didOpen` would push the host's
|
|
839
|
+
// startup snapshot over whatever the server's copy has since become.
|
|
840
|
+
const held = this.models.get(key);
|
|
841
|
+
const existing = monaco.editor.getModel(monaco.Uri.parse(key));
|
|
842
|
+
if (held !== undefined && existing === held) {
|
|
843
|
+
return held;
|
|
844
|
+
}
|
|
845
|
+
if (held !== undefined) {
|
|
846
|
+
// The host disposed this client's model itself. The server still has
|
|
847
|
+
// the document open, so it is closed there before anything else:
|
|
848
|
+
// a second `didOpen` without a `didClose` is a client out of step.
|
|
849
|
+
this.closeDocument(key);
|
|
850
|
+
}
|
|
851
|
+
if (existing !== null) {
|
|
852
|
+
throw new Error(`A model for ${key} exists that this client did not open`);
|
|
853
|
+
}
|
|
854
|
+
const languageId = this.languageIdFor(key);
|
|
855
|
+
const model = monaco.editor.createModel(text, languageId, monaco.Uri.parse(key));
|
|
856
|
+
this.models.set(key, model);
|
|
857
|
+
// The opened content came out of the server's filesystem, so it is by
|
|
858
|
+
// definition already persisted — a document that reported itself dirty on
|
|
859
|
+
// open would make the first save rewrite every file it did not change.
|
|
860
|
+
this.savedVersions.set(key, model.getAlternativeVersionId());
|
|
861
|
+
notifyUnlessClosed(() => this.connection.sendNotification(vscode_languageserver_protocol_1.DidOpenTextDocumentNotification.type, {
|
|
862
|
+
textDocument: { uri: key, languageId, version: model.getVersionId(), text: model.getValue() }
|
|
863
|
+
}));
|
|
864
|
+
// Subscribed before the editor is created, so an editor contribution that
|
|
865
|
+
// touches the model on attach cannot slip a change past the server.
|
|
866
|
+
// Typing goes on after the worker dies: the buffer is the page's.
|
|
867
|
+
model.onDidChangeContent(event => {
|
|
868
|
+
notifyUnlessClosed(() => this.connection.sendNotification(vscode_languageserver_protocol_1.DidChangeTextDocumentNotification.type, {
|
|
869
|
+
textDocument: { uri: key, version: event.versionId },
|
|
870
|
+
contentChanges: toContentChanges(event.changes)
|
|
871
|
+
}));
|
|
872
|
+
});
|
|
873
|
+
return model;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Dispose the model for `uri` and tell the server the document is closed, so
|
|
877
|
+
* the server drops this client's shadow of it and stops pushing to it. A URI
|
|
878
|
+
* this client never opened is left alone. An editor still showing the model
|
|
879
|
+
* is the host's to re-point first: Monaco detaches a disposed model from it.
|
|
880
|
+
* A model the host disposed itself is closed on the server all the same,
|
|
881
|
+
* here or by the next {@link openDocument} of its URI.
|
|
882
|
+
*
|
|
883
|
+
* The markers go first: Monaco drops a disposed model's markers only for its
|
|
884
|
+
* own schemes, so a `file:` document reopened later would start with the
|
|
885
|
+
* squiggles the server published before the close.
|
|
886
|
+
*
|
|
887
|
+
* The model goes before `didClose`, and a connection already closed does not
|
|
888
|
+
* fail the call: the page's side of the sync is this client's to release
|
|
889
|
+
* whatever became of the worker, and the server's side ends with its
|
|
890
|
+
* connection.
|
|
891
|
+
*/
|
|
892
|
+
closeDocument(uri) {
|
|
893
|
+
const key = monaco.Uri.parse(uri).toString();
|
|
894
|
+
const model = this.models.get(key);
|
|
895
|
+
if (model === undefined) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
this.models.delete(key);
|
|
899
|
+
this.savedVersions.delete(key);
|
|
900
|
+
// Cleared whether or not the host disposed the model already: the marker
|
|
901
|
+
// service keys by URI and keeps a disposed `file:` model's markers.
|
|
902
|
+
monaco.editor.setModelMarkers(model, this.markerOwner, []);
|
|
903
|
+
if (!model.isDisposed()) {
|
|
904
|
+
model.dispose();
|
|
905
|
+
}
|
|
906
|
+
notifyUnlessClosed(() => this.connection.sendNotification(vscode_languageserver_protocol_1.DidCloseTextDocumentNotification.type, { textDocument: { uri: key } }));
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Close every open document and release every registration, in reverse
|
|
910
|
+
* order, the `workspace/applyEdit` handler included: a later request is
|
|
911
|
+
* answered as an unknown method, as for a client that never registered.
|
|
912
|
+
* The languages stay registered, since Monaco cannot unregister one.
|
|
913
|
+
*/
|
|
914
|
+
dispose() {
|
|
915
|
+
try {
|
|
916
|
+
for (const uri of [...this.models.keys()]) {
|
|
917
|
+
this.closeDocument(uri);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
finally {
|
|
921
|
+
for (const registration of this.registrations.splice(0).reverse()) {
|
|
922
|
+
registration.dispose();
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Create one editor over `model`, with the settings a server-driven language
|
|
928
|
+
* needs under `options`: the host's theme, layout and chrome spread over
|
|
929
|
+
* them, so a host cannot lose one by spreading in the wrong order.
|
|
930
|
+
*
|
|
931
|
+
* Separate from {@link openDocument} because a host may hold a FIXED number
|
|
932
|
+
* of editors and swap their models rather than create an editor per
|
|
933
|
+
* document. That is what keeps a `didOpen` a statement about the workspace
|
|
934
|
+
* instead of about which pane happens to be showing what.
|
|
935
|
+
*
|
|
936
|
+
* `suggest` is merged one level deep: it is the one nested setting here, and
|
|
937
|
+
* a host's own suggest options would otherwise replace the insert mode the
|
|
938
|
+
* forwarded `InsertReplaceEdit` ranges depend on.
|
|
939
|
+
*/
|
|
940
|
+
createEditor(container, model, options = {}) {
|
|
941
|
+
return monaco.editor.create(container, {
|
|
942
|
+
...LSP_EDITOR_OPTIONS,
|
|
943
|
+
...options,
|
|
944
|
+
suggest: { ...LSP_EDITOR_OPTIONS.suggest, ...options.suggest },
|
|
945
|
+
model
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* The open documents whose buffer has moved since it was opened or last
|
|
950
|
+
* saved, with the text to persist.
|
|
951
|
+
*
|
|
952
|
+
* **The editor's buffer is the right source for a save**, not the host's copy
|
|
953
|
+
* of the workspace: a server write reaches this buffer through
|
|
954
|
+
* `workspace/applyEdit`, so Monaco's model is the one place both directions of
|
|
955
|
+
* the sync are already merged.
|
|
956
|
+
*
|
|
957
|
+
* **Dirty rather than every open document, and that is not cosmetic.** A save
|
|
958
|
+
* of an unmodified file writes its current bytes into the backing store, and
|
|
959
|
+
* a store that wins over a seed on the next load would then pin a file nobody
|
|
960
|
+
* changed.
|
|
961
|
+
*
|
|
962
|
+
* `getAlternativeVersionId` rather than `getVersionId`, because it is the one
|
|
963
|
+
* that returns to its old value when an edit is UNDONE — undoing a server
|
|
964
|
+
* write leaves nothing to save, which `getVersionId` (monotonic) would report
|
|
965
|
+
* as a change.
|
|
966
|
+
*/
|
|
967
|
+
dirtyDocuments() {
|
|
968
|
+
return [...this.models.keys()].flatMap(uri => this.dirtyDocument(uri) ?? []);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* The document at `uri`, in any form, if it has unsaved changes, read from
|
|
972
|
+
* its model alone; nothing for a document that is saved or that this client
|
|
973
|
+
* does not hold.
|
|
974
|
+
*/
|
|
975
|
+
dirtyDocument(uri) {
|
|
976
|
+
const key = monaco.Uri.parse(uri).toString();
|
|
977
|
+
const model = this.modelOf(key);
|
|
978
|
+
if (model === undefined) {
|
|
979
|
+
return undefined;
|
|
980
|
+
}
|
|
981
|
+
const version = model.getAlternativeVersionId();
|
|
982
|
+
return version === this.savedVersions.get(key) ? undefined : { uri: key, text: model.getValue(), version, model };
|
|
983
|
+
}
|
|
984
|
+
/** The model this client holds for a URI in any form, if it still exists. */
|
|
985
|
+
modelOf(uri) {
|
|
986
|
+
const model = this.models.get(monaco.Uri.parse(uri).toString());
|
|
987
|
+
return model === undefined || model.isDisposed() ? undefined : model;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Run one provider's request. A request nobody wants an answer to any more,
|
|
991
|
+
* since the server gave it up or the connection closed, rejects with
|
|
992
|
+
* Monaco's cancellation, which Monaco drops silently. Any other rejection
|
|
993
|
+
* reaches Monaco, which reports it as an uncaught page error.
|
|
994
|
+
*/
|
|
995
|
+
async request(send) {
|
|
996
|
+
try {
|
|
997
|
+
return await send();
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
throw isAbandonedRequest(error) ? monacoCancellation() : error;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Whether `model` is one this client opened. Monaco picks a provider by
|
|
1005
|
+
* language id, so a second client of the same language is handed this
|
|
1006
|
+
* client's models and this client the other's; a request for one of those
|
|
1007
|
+
* would name a document this client's server never opened.
|
|
1008
|
+
*/
|
|
1009
|
+
owns(model) {
|
|
1010
|
+
return this.models.get(model.uri.toString()) === model;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Record that `document`'s content has been persisted.
|
|
1014
|
+
*
|
|
1015
|
+
* Takes the document rather than the URI so the version recorded is the one
|
|
1016
|
+
* the TEXT was read at: a keystroke landing while the save was in flight must
|
|
1017
|
+
* leave the document dirty, and marking it against the buffer's current
|
|
1018
|
+
* version would silently swallow that edit. A record of a model since closed
|
|
1019
|
+
* marks nothing: a reopened URI is a new model whose versions start over, so
|
|
1020
|
+
* the old record's version could name an edit of the new one.
|
|
1021
|
+
*/
|
|
1022
|
+
markSaved(document) {
|
|
1023
|
+
if (this.models.get(document.uri) === document.model) {
|
|
1024
|
+
this.savedVersions.set(document.uri, document.version);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Register one `DocumentSemanticTokensProvider` per language, reading the
|
|
1029
|
+
* legend off the `initialize` result.
|
|
1030
|
+
*
|
|
1031
|
+
* The legend has to come from the server rather than being restated here: it
|
|
1032
|
+
* is the index space the token stream is encoded in, so a client legend that
|
|
1033
|
+
* merely LOOKS right renders every token as the wrong kind — a colouring bug
|
|
1034
|
+
* with no error anywhere. Langium builds it by merging every registered
|
|
1035
|
+
* language's provider options, which is why one legend serves them all.
|
|
1036
|
+
*
|
|
1037
|
+
* Absent capability means no provider at all rather than an empty legend: an
|
|
1038
|
+
* empty one would decode every token index to nothing and produce a file with
|
|
1039
|
+
* no highlighting, which is what a broken server looks like.
|
|
1040
|
+
*/
|
|
1041
|
+
registerSemanticTokens(capabilities) {
|
|
1042
|
+
const legend = capabilities.semanticTokensProvider?.legend;
|
|
1043
|
+
if (legend === undefined) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
for (const language of this.options.languages) {
|
|
1047
|
+
this.registrations.push(monaco.languages.registerDocumentSemanticTokensProvider(language.id, {
|
|
1048
|
+
getLegend: () => legend,
|
|
1049
|
+
provideDocumentSemanticTokens: async (model, _lastResultId, token) => {
|
|
1050
|
+
if (!this.owns(model)) {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
const tokens = await this.request(() => this.connection.sendRequest(vscode_languageserver_protocol_1.SemanticTokensRequest.type, { textDocument: { uri: model.uri.toString() } }, token));
|
|
1054
|
+
// `data` arrives as a plain array over JSON-RPC; Monaco requires
|
|
1055
|
+
// a `Uint32Array` and reads garbage from anything else.
|
|
1056
|
+
return tokens === null ? null : { data: new Uint32Array(tokens.data), resultId: tokens.resultId };
|
|
1057
|
+
},
|
|
1058
|
+
// Nothing to release: no `resultId`-keyed state is kept here, because
|
|
1059
|
+
// this provider does not implement the delta request.
|
|
1060
|
+
releaseDocumentSemanticTokens: () => undefined
|
|
1061
|
+
}));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Register one `CompletionItemProvider` per language.
|
|
1066
|
+
*
|
|
1067
|
+
* `triggerCharacters` is taken from the server rather than guessed. It is what
|
|
1068
|
+
* makes the list appear on `.` without a letter after it; a client that
|
|
1069
|
+
* invents its own set either misses a trigger the grammar defines or asks on a
|
|
1070
|
+
* character the server has nothing to say about.
|
|
1071
|
+
*/
|
|
1072
|
+
registerCompletion(capabilities) {
|
|
1073
|
+
for (const language of this.options.languages) {
|
|
1074
|
+
this.registrations.push(monaco.languages.registerCompletionItemProvider(language.id, {
|
|
1075
|
+
triggerCharacters: capabilities.completionProvider?.triggerCharacters,
|
|
1076
|
+
provideCompletionItems: async (model, position, context, token) => {
|
|
1077
|
+
if (!this.owns(model)) {
|
|
1078
|
+
return { suggestions: [] };
|
|
1079
|
+
}
|
|
1080
|
+
const result = await this.request(() => this.connection.sendRequest(vscode_languageserver_protocol_1.CompletionRequest.type, {
|
|
1081
|
+
textDocument: { uri: model.uri.toString() },
|
|
1082
|
+
position: toLspPosition(position),
|
|
1083
|
+
context: toCompletionContext(context)
|
|
1084
|
+
}, token));
|
|
1085
|
+
if (result === null) {
|
|
1086
|
+
return { suggestions: [] };
|
|
1087
|
+
}
|
|
1088
|
+
// `CompletionList | CompletionItem[]`, and both forms are live —
|
|
1089
|
+
// Langium answers with a list, but an adopter's own provider is
|
|
1090
|
+
// free to return the array.
|
|
1091
|
+
const items = Array.isArray(result) ? result : result.items;
|
|
1092
|
+
return {
|
|
1093
|
+
suggestions: items.map(item => toCompletionItem(item, model, position)),
|
|
1094
|
+
incomplete: Array.isArray(result) ? undefined : result.isIncomplete
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
}));
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Register one `HoverProvider` per language.
|
|
1102
|
+
*
|
|
1103
|
+
* Registered unconditionally rather than behind `capabilities.hoverProvider`,
|
|
1104
|
+
* unlike the semantic-token provider above, because the two failure modes are
|
|
1105
|
+
* not alike: a hover request a server does not implement answers `null` and
|
|
1106
|
+
* Monaco shows nothing, whereas a semantic-token provider without the
|
|
1107
|
+
* server's legend would decode the token stream against the wrong index space
|
|
1108
|
+
* and mis-colour the file.
|
|
1109
|
+
*/
|
|
1110
|
+
registerHover() {
|
|
1111
|
+
for (const language of this.options.languages) {
|
|
1112
|
+
this.registrations.push(monaco.languages.registerHoverProvider(language.id, {
|
|
1113
|
+
provideHover: async (model, position, token) => {
|
|
1114
|
+
if (!this.owns(model)) {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
const hover = await this.request(() => this.connection.sendRequest(vscode_languageserver_protocol_1.HoverRequest.type, { textDocument: { uri: model.uri.toString() }, position: toLspPosition(position) }, token));
|
|
1118
|
+
if (hover === null) {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
contents: toMarkdown(hover.contents),
|
|
1123
|
+
range: hover.range === undefined ? undefined : toMonacoRange(hover.range)
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
}));
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Route a definition target in ANOTHER document to `open`, which answers
|
|
1131
|
+
* whether it could show it.
|
|
1132
|
+
*
|
|
1133
|
+
* **Monaco's standalone editor service can only navigate within the editor
|
|
1134
|
+
* that was clicked in**, and it declines the rest by doing nothing: it looks
|
|
1135
|
+
* the target resource up on the source editor alone, finds a different model,
|
|
1136
|
+
* and returns — no message, no marker, no console line. That is the same
|
|
1137
|
+
* observable as a server that resolved no reference, which is what makes the
|
|
1138
|
+
* seam worth stating rather than discovering. A host with a workbench behind
|
|
1139
|
+
* it supplies this step; a plain page supplies it by hand.
|
|
1140
|
+
*
|
|
1141
|
+
* `false` from `open` leaves the reader where they are, and that is the
|
|
1142
|
+
* honest answer for a target the host holds no text for rather than a
|
|
1143
|
+
* failure: a `virtual:` in-code contribution is a document to the server and
|
|
1144
|
+
* no file to anyone.
|
|
1145
|
+
*
|
|
1146
|
+
* **A source editor on a model this client does not own is left to the next
|
|
1147
|
+
* opener.** Monaco calls every opener for every editor, so on a page with
|
|
1148
|
+
* two clients this one would otherwise claim a jump the other client's
|
|
1149
|
+
* server resolved, and hand its host a document that host never opened.
|
|
1150
|
+
*
|
|
1151
|
+
* **A target in the SOURCE editor's own model is deliberately NOT routed
|
|
1152
|
+
* here.** Monaco calls the last-registered opener first and falls through to
|
|
1153
|
+
* its own on a `false`, and its own is better for that case: it selects the
|
|
1154
|
+
* declaration's whole range where this seam could only place a caret, and it
|
|
1155
|
+
* is the path the editor's navigation history is recorded on.
|
|
1156
|
+
*
|
|
1157
|
+
* Released by {@link dispose} as well as by the returned disposable.
|
|
1158
|
+
*/
|
|
1159
|
+
registerEditorOpener(open) {
|
|
1160
|
+
const registration = monaco.editor.registerEditorOpener({
|
|
1161
|
+
openCodeEditor: (source, resource, selectionOrPosition) => {
|
|
1162
|
+
const model = source.getModel();
|
|
1163
|
+
if (model === null || !this.owns(model) || model.uri.toString() === resource.toString()) {
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
return open(resource.toString(), toEditorPosition(selectionOrPosition));
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
this.registrations.push(registration);
|
|
1170
|
+
return registration;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Register one `DefinitionProvider` per language.
|
|
1174
|
+
*
|
|
1175
|
+
* **This is half of go-to-definition, and on its own it moves nothing for a
|
|
1176
|
+
* target in another document.** A provider answers where the declaration is;
|
|
1177
|
+
* going there is the host's job, and {@link registerEditorOpener} is the
|
|
1178
|
+
* other half. Registered unconditionally, for the reason {@link registerHover}
|
|
1179
|
+
* states.
|
|
1180
|
+
*/
|
|
1181
|
+
registerDefinition() {
|
|
1182
|
+
for (const language of this.options.languages) {
|
|
1183
|
+
this.registrations.push(monaco.languages.registerDefinitionProvider(language.id, {
|
|
1184
|
+
provideDefinition: async (model, position, token) => {
|
|
1185
|
+
if (!this.owns(model)) {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
const result = await this.request(() => this.connection.sendRequest(vscode_languageserver_protocol_1.DefinitionRequest.type, { textDocument: { uri: model.uri.toString() }, position: toLspPosition(position) }, token));
|
|
1189
|
+
return result === null ? null : toDefinitionLinks(result);
|
|
1190
|
+
}
|
|
1191
|
+
}));
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Register one `DocumentHighlightProvider` per language.
|
|
1196
|
+
*
|
|
1197
|
+
* **Document-scoped by the protocol, unlike every other reference-following
|
|
1198
|
+
* request here**, so it answers what a name does in the file being read
|
|
1199
|
+
* rather than where it comes from — the declaration and its uses in one view,
|
|
1200
|
+
* with no jump and nothing opened. Registered unconditionally, for the reason
|
|
1201
|
+
* {@link registerHover} states.
|
|
1202
|
+
*/
|
|
1203
|
+
registerDocumentHighlight() {
|
|
1204
|
+
for (const language of this.options.languages) {
|
|
1205
|
+
this.registrations.push(monaco.languages.registerDocumentHighlightProvider(language.id, {
|
|
1206
|
+
provideDocumentHighlights: async (model, position, token) => {
|
|
1207
|
+
if (!this.owns(model)) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
const highlights = await this.request(() => this.connection.sendRequest(vscode_languageserver_protocol_1.DocumentHighlightRequest.type, { textDocument: { uri: model.uri.toString() }, position: toLspPosition(position) }, token));
|
|
1211
|
+
if (highlights === null) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
// An absent kind is `Text` by the protocol, which is also what
|
|
1215
|
+
// Monaco assumes for an absent one — stated rather than left off,
|
|
1216
|
+
// so the default is this client's and not a coincidence.
|
|
1217
|
+
return highlights.map(highlight => ({
|
|
1218
|
+
range: toMonacoRange(highlight.range),
|
|
1219
|
+
kind: HIGHLIGHT_KINDS[highlight.kind ?? vscode_languageserver_protocol_1.DocumentHighlightKind.Text]
|
|
1220
|
+
}));
|
|
1221
|
+
}
|
|
1222
|
+
}));
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
exports.MonacoLspClient = MonacoLspClient;
|
|
1227
|
+
/**
|
|
1228
|
+
* Persist the dirty documents of `client`, or those of them `uris` names, in
|
|
1229
|
+
* the host's form or Monaco's, through `session`, one after another, and
|
|
1230
|
+
* return what was saved.
|
|
1231
|
+
*
|
|
1232
|
+
* **A save persists; it does not author.** Every keystroke already reached the
|
|
1233
|
+
* server as a `didChange`, and every server write reached the buffer as a
|
|
1234
|
+
* `workspace/applyEdit`, so the server's text is the buffer and disk is the
|
|
1235
|
+
* only thing behind. `persistDocument` at `'any'` writes that text without a
|
|
1236
|
+
* model: a write of the buffer's text would be authored against no read, so
|
|
1237
|
+
* no `ModelVersion` could gate it, and `'any'` on such a write could put an
|
|
1238
|
+
* older buffer over a server write still in flight.
|
|
1239
|
+
*
|
|
1240
|
+
* **The open's text hash is the gate.** A `didChange` travels the LSP port and
|
|
1241
|
+
* the persist the data port, and nothing orders one port's messages before
|
|
1242
|
+
* the other's, so the open can answer with text a keystroke has not reached.
|
|
1243
|
+
* The open answer's hash is `textHash` of the server's text; when it is not
|
|
1244
|
+
* the buffer's, the save throws, naming the document, and the document stays
|
|
1245
|
+
* dirty for the next save. A keystroke landing after the open persists as
|
|
1246
|
+
* newer text, which leaves the buffer dirty the same way.
|
|
1247
|
+
*
|
|
1248
|
+
* Each document's text is read just before its own open, not once for the
|
|
1249
|
+
* set: an earlier document's persist takes time, and a keystroke in a later
|
|
1250
|
+
* one during it would otherwise fail that one's hash check for nothing. Each
|
|
1251
|
+
* is marked saved at the version its text was read at, one at a time, so a
|
|
1252
|
+
* failure part-way through leaves the documents it never reached dirty and a
|
|
1253
|
+
* second call retries exactly those. Sequential, not concurrent: each persist
|
|
1254
|
+
* runs over the server's shared store.
|
|
1255
|
+
*/
|
|
1256
|
+
async function saveDirtyDocuments(client, session, uris) {
|
|
1257
|
+
const wanted = uris?.map(uri => monaco.Uri.parse(uri).toString());
|
|
1258
|
+
const dirty = client.dirtyDocuments().filter(document => wanted === undefined || wanted.includes(document.uri));
|
|
1259
|
+
const saved = [];
|
|
1260
|
+
for (const { uri } of dirty) {
|
|
1261
|
+
// Read again now: an undo during an earlier save may have cleaned it.
|
|
1262
|
+
const document = client.dirtyDocument(uri);
|
|
1263
|
+
if (document === undefined) {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
await session.withOpenDocument({ uri }, opened => {
|
|
1267
|
+
if (opened.text?.hash !== (0, protocol_1.textHash)(document.text)) {
|
|
1268
|
+
throw new Error(`The server's text for ${uri} differs from the buffer this save read; the document stays unsaved`);
|
|
1269
|
+
}
|
|
1270
|
+
return session.persistDocument({ uri, baseVersion: 'any' });
|
|
1271
|
+
});
|
|
1272
|
+
client.markSaved(document);
|
|
1273
|
+
saved.push(document);
|
|
1274
|
+
}
|
|
1275
|
+
return saved;
|
|
1276
|
+
}
|
|
1277
|
+
//# sourceMappingURL=monaco-lsp-client.js.map
|