-
Notifications
You must be signed in to change notification settings - Fork 86
/
Copy pathregisterCommands.ts
490 lines (443 loc) · 20 KB
/
registerCommands.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
import * as path from 'path';
import * as vscode from 'vscode';
import { commands, ConfigurationTarget, env, ExtensionContext, OpenDialogOptions, Position, QuickPickItem, SnippetString, TextDocument, Uri, window, workspace, WorkspaceEdit, Selection } from "vscode";
import { CancellationToken, ExecuteCommandParams, ExecuteCommandRequest, ReferencesRequest, TextDocumentEdit, TextDocumentIdentifier, TextEdit } from "vscode-languageclient";
import { LanguageClient } from 'vscode-languageclient/node';
import { registerConfigurationUpdateCommand } from '../lsp-commands';
import { markdownPreviewProvider } from "../markdownPreviewProvider";
import { DEBUG } from '../server/java/javaServerStarter';
import { getDirectoryPath, getFileName, getRelativePath, getWorkspaceUri } from '../utils/fileUtils';
import * as ClientCommandConstants from "./clientCommandConstants";
import * as ServerCommandConstants from "./serverCommandConstants";
/**
* Register the commands for vscode-xml that don't require communication with the language server
*
* @param context the extension context
*/
export function registerClientOnlyCommands(context: ExtensionContext) {
registerDocsCommands(context);
registerOpenSettingsCommand(context);
registerOpenUriCommand(context);
}
/**
* Register the commands for vscode-xml that require communication with the language server
*
* @param context the extension context
* @param languageClient the language client
*/
export async function registerClientServerCommands(context: ExtensionContext, languageClient: LanguageClient) {
registerCodeLensReferencesCommands(context, languageClient);
registerValidationCommands(context);
registerRefactorCommands(context, languageClient);
registerAssociationCommands(context, languageClient);
registerRestartLanguageServerCommand(context, languageClient);
registerConfigurationUpdateCommand();
// Register client command to execute custom XML Language Server command
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND, (command, ...rest) => {
let token: CancellationToken;
let commandArgs: any[] = rest;
if (rest && rest.length && CancellationToken.is(rest[rest.length - 1])) {
token = rest[rest.length - 1];
commandArgs = rest.slice(0, rest.length - 1);
}
const params: ExecuteCommandParams = {
command,
arguments: commandArgs
};
if (token) {
return languageClient.sendRequest(ExecuteCommandRequest.type, params, token);
}
else {
return languageClient.sendRequest(ExecuteCommandRequest.type, params);
}
}));
}
/**
* Register commands used for the built-in documentation
*
* @param context the extension context
*/
function registerDocsCommands(context: ExtensionContext) {
context.subscriptions.push(markdownPreviewProvider);
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.OPEN_DOCS_HOME, async () => {
const uri = 'README.md';
const title = 'XML Documentation';
const sectionId = '';
markdownPreviewProvider.show(context.asAbsolutePath(path.join('docs', uri)), title, sectionId, context);
}));
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.OPEN_DOCS, async (params: { page: string, section: string }) => {
const page = params.page.endsWith('.md') ? params.page.substr(0, params.page.length - 3) : params.page;
const uri = page + '.md';
const sectionId = params.section || '';
const title = 'XML ' + page;
markdownPreviewProvider.show(context.asAbsolutePath(path.join('docs', uri)), title, sectionId, context);
}));
}
/**
* Registers a command that opens the settings page to a given setting
*
* @param context the extension context
*/
function registerOpenSettingsCommand(context: ExtensionContext) {
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.OPEN_SETTINGS, async (settingId?: string) => {
commands.executeCommand('workbench.action.openSettings', settingId);
}));
}
/**
* Registers a command that opens the settings page to a given setting
*
* @param context the extension context
*/
function registerOpenUriCommand(context: ExtensionContext) {
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.OPEN_URI, async (uri?: string) => {
commands.executeCommand('vscode.open', Uri.parse(uri));
}));
}
/**
* Register commands used for code lens "references"
*
* @param context the extension context
* @param languageClient the language server client
*/
function registerCodeLensReferencesCommands(context: ExtensionContext, languageClient: LanguageClient) {
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.SHOW_REFERENCES, (uriString: string, position: Position) => {
const uri = Uri.parse(uriString);
workspace.openTextDocument(uri).then(document => {
// Consume references service from the XML Language Server
const param = languageClient.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
languageClient.sendRequest(ReferencesRequest.type, param).then(locations => {
commands.executeCommand(ClientCommandConstants.EDITOR_SHOW_REFERENCES, uri, languageClient.protocol2CodeConverter.asPosition(position), locations.map(languageClient.protocol2CodeConverter.asLocation));
})
})
}));
}
/**
* Register commands used for revalidating XML files
*
* @param context the extension context
*/
function registerValidationCommands(context: ExtensionContext) {
// Revalidate current file
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.VALIDATE_CURRENT_FILE, async (identifierParam, validationArgs) => {
if (identifierParam) {
return await commands.executeCommand(ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND, ServerCommandConstants.VALIDATE_CURRENT_FILE, identifierParam, validationArgs);
}
const uri = window.activeTextEditor.document.uri;
const identifier = TextDocumentIdentifier.create(uri.toString());
commands.executeCommand(ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND, ServerCommandConstants.VALIDATE_CURRENT_FILE, identifier).
then(() => {
window.showInformationMessage('The current XML file was successfully validated.');
}, error => {
window.showErrorMessage('Error during XML validation ' + error.message);
});
}));
// Revalidate (Only XML syntax) current file
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.VALIDATE_ONLY_SYNTAX_CURRENT_FILE, async (identifierParam, validationArgs) => {
let identifier = identifierParam;
if (!identifier) {
const uri = window.activeTextEditor.document.uri;
identifier = TextDocumentIdentifier.create(uri.toString());
}
const configXML = workspace.getConfiguration().get('xml.validation');
const x = JSON.stringify(configXML); //configXML is not a JSON type
const validationSettings = JSON.parse(x);
validationSettings['dtd']['enabled'] = 'never';
validationSettings['schema']['enabled'] = 'never';
commands.executeCommand(ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND, ServerCommandConstants.VALIDATE_CURRENT_FILE, identifier, validationArgs, validationSettings).
then(() => {
window.showInformationMessage('The current XML file was successfully validated (XML Syntax Only).');
}, error => {
window.showErrorMessage('Error during XML validation (XML Syntax Only) ' + error.message);
});
}));
// Revalidate all open files
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.VALIDATE_ALL_FILES, async () => {
commands.executeCommand(ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND, ServerCommandConstants.VALIDATE_ALL_FILES).
then(() => {
window.showInformationMessage('All open XML files were successfully validated.');
}, error => {
window.showErrorMessage('Error during XML validation: ' + error.message);
});
}));
}
const bindingTypes = new Map<string, string>([
["Standard (xsi, DOCTYPE)", "standard"],
["XML Model association", "xml-model"],
["File association", "fileAssociation"]
]);
const bindingTypeOptions: QuickPickItem[] = [];
for (const label of bindingTypes.keys()) {
bindingTypeOptions.push({ "label": label });
}
const bindingTypeOptionsForRelaxNG: QuickPickItem[] = [];
for (const entry of bindingTypes.entries()) {
if (entry[1] !== 'standard') {
bindingTypeOptionsForRelaxNG.push({ "label": entry[0] });
}
}
/**
* The function passed to context subscriptions for grammar association
*
* @param documentURI the uri of the XML file path
* @param languageClient the language server client
*/
async function grammarAssociationCommand(documentURI: Uri, languageClient: LanguageClient) {
// A click on Bind to grammar/schema... has been processed in the XML document which is not bound to a grammar
// step 1: select local / remote Uri type
let grammarURI;
const uriType = await window.showQuickPick([{ "label": "local" }, { "label": "remote" }], { placeHolder: "Binding type" });
if (!uriType) {
return;
}
if (uriType.label === 'remote') {
let predefinedUrl = await env.clipboard.readText();
if (!predefinedUrl || !predefinedUrl.startsWith('http')) {
predefinedUrl = '';
}
grammarURI = await window.showInputBox({ title: 'Fill with schema / grammar URL', value: predefinedUrl });
} else {
// step 2.1: Open a dialog to select the XSD, DTD, RelaxNG file to bind.
const options: OpenDialogOptions = {
canSelectMany: false,
openLabel: 'Select XSD, DTD, RNG or RNC file',
filters: {
'Grammar files': ['xsd', 'dtd', 'rng', 'rnc']
}
};
const fileUri = await window.showOpenDialog(options);
grammarURI = fileUri && fileUri[0];
}
if (!grammarURI) {
return;
}
const grammarPath = grammarURI.fsPath ? grammarURI.fsPath : grammarURI;
const isRelaxNG = grammarPath.endsWith('.rng') || grammarPath.endsWith('.rnc');
// Step 3 : open a combo to select the binding type ("standard", "xml-model", "fileAssociation")
const bindingTypesQuickPick = isRelaxNG ? bindingTypeOptionsForRelaxNG : bindingTypeOptions;
const pickedBindingTypeOption = await window.showQuickPick(bindingTypesQuickPick, { placeHolder: "Binding type" });
if (!pickedBindingTypeOption) {
return;
}
const bindingType = bindingTypes.get(pickedBindingTypeOption.label);
// The XSD, DTD has been selected, get the proper syntax for binding this grammar file in the XML document.
try {
const currentFile = (window.activeTextEditor && window.activeTextEditor.document && window.activeTextEditor.document.languageId === 'xml') ? window.activeTextEditor.document : undefined;
if (bindingType === 'fileAssociation') {
// Bind grammar using file association
await bindWithFileAssociation(documentURI, grammarURI, currentFile);
} else {
// Bind grammar using standard binding
await bindWithStandard(documentURI, grammarURI, bindingType, languageClient);
}
} catch (error) {
window.showErrorMessage('Error during grammar binding: ' + error.message);
}
}
/**
* Perform grammar binding using file association through settings.json
*
* @param documentURI the URI of the current XML document
* @param grammarURI the URI of the user selected grammar file
* @param document the opened TextDocument
*/
async function bindWithFileAssociation(documentURI: Uri, grammarURI: Uri, document: TextDocument) {
const absoluteGrammarFilePath = grammarURI.toString();
const currentFilename = getFileName(documentURI.toString());
const currentWorkspaceUri = getWorkspaceUri(document);
// If the grammar file is in the same workspace, use the relative path, otherwise use the absolute path
const grammarFilePath = getDirectoryPath(absoluteGrammarFilePath).includes(currentWorkspaceUri.toString()) ? getRelativePath(currentWorkspaceUri.toString(), absoluteGrammarFilePath) : absoluteGrammarFilePath;
const defaultPattern = `**/${currentFilename}`;
const inputBoxOptions = {
title: "File Association Pattern",
value: defaultPattern,
placeHolder: defaultPattern,
prompt: "Enter the pattern of the XML document(s) to be bound.",
validateInput: async (pattern: string) => {
let hasMatch = false;
try {
hasMatch = await commands.executeCommand(ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND, ServerCommandConstants.CHECK_FILE_PATTERN, pattern, documentURI.toString());
} catch (error) {
console.log(`Error while validating file pattern : ${error}`);
}
return !hasMatch ? "The pattern will not match any file." : null
}
}
const inputPattern = (await window.showInputBox(inputBoxOptions));
if (!inputPattern) {
// User closed the input box with Esc
return;
}
const fileAssociation = {
"pattern": inputPattern,
"systemId": grammarFilePath
}
addToValueToSettingArray("xml.fileAssociations", fileAssociation);
}
/**
* Bind grammar file using standard XML document grammar
*
* @param documentURI the URI of the XML file path
* @param grammarURI the URI of the user selected grammar file
* @param bindingType the selected grammar binding type
* @param languageClient the language server client
*/
async function bindWithStandard(documentURI: Uri, grammarURI: Uri, bindingType: string, languageClient: LanguageClient) {
const identifier = TextDocumentIdentifier.create(documentURI.toString());
const result = await commands.executeCommand(ServerCommandConstants.ASSOCIATE_GRAMMAR_INSERT, identifier, grammarURI.toString(), bindingType);
// Insert the proper syntax for binding
const lspTextDocumentEdit = <TextDocumentEdit>result;
const workEdits = new WorkspaceEdit();
for (const edit of lspTextDocumentEdit.edits) {
workEdits.replace(documentURI, languageClient.protocol2CodeConverter.asRange(edit.range), edit.newText);
}
workspace.applyEdit(workEdits); // apply the edits
}
/**
* Add an entry, value, to the setting.json field, key
*
* @param key the filename/path of the xml document
* @param value the object to add to the config
*/
function addToValueToSettingArray<T>(key: string, value: T): void {
const settingArray: T[] = workspace.getConfiguration().get<T[]>(key, []);
if (settingArray.includes(value)) {
return;
}
settingArray.push(value);
workspace.getConfiguration().update(key, settingArray, ConfigurationTarget.Workspace);
}
/**
* Register commands used for associating grammar file (XSD,DTD) to a given XML file for command menu and CodeLens
*
* @param context the extension context
* @param languageClient the language server client
*/
function registerAssociationCommands(context: ExtensionContext, languageClient: LanguageClient) {
// For CodeLens
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.OPEN_BINDING_WIZARD, async (uriString: string) => {
const uri = Uri.parse(uriString);
grammarAssociationCommand(uri, languageClient)
}));
// For command menu
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.COMMAND_PALETTE_BINDING_WIZARD, async () => {
const uri = window.activeTextEditor.document.uri;
// Run check to ensure available grammar binding command should be executed, or if error is thrown
const canBind = await checkCanBindGrammar(uri);
if (canBind) {
grammarAssociationCommand(uri, languageClient)
} else {
window.showErrorMessage(`The document ${uri.toString()} is already bound with a grammar`);
}
}));
}
/**
* Change value of 'canBindGrammar' to determine if grammar/schema can be bound
*
* @param documentURI the text document
* @returns the `hasGrammar` check result from server
*/
async function checkCanBindGrammar(documentURI: Uri) {
// Retrieve the document uri and identifier
const identifier = TextDocumentIdentifier.create(documentURI.toString());
// Set the custom condition to watch if file already has bound grammar
let result = false;
try {
result = await commands.executeCommand(ServerCommandConstants.CHECK_BOUND_GRAMMAR, identifier);
} catch (error) {
console.log(`Error while checking bound grammar : ${error}`);
}
return result
}
/**
* Register command to restart the connection to the language server
*
* @param context the extension context
* @param languageClient the language client
*/
function registerRestartLanguageServerCommand(context: ExtensionContext, languageClient: LanguageClient) {
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.RESTART_LANGUAGE_SERVER, async () => {
// Can be replaced with `await languageClient.restart()` with vscode-languageclient ^8.0.1,
await languageClient.stop();
if (DEBUG) {
await new Promise((resolve) => setTimeout(resolve, 2000));
}
languageClient.start();
}));
}
interface SurroundWithResponse {
start: TextEdit;
end: TextEdit;
}
class SurroundWithKind {
static readonly tags = 'tags';
static readonly comments = 'comments';
static readonly cdata = 'cdata';
}
/**
* Register commands used for refactoring XML files
*
* @param context the extension context
*/
function registerRefactorCommands(context: ExtensionContext, languageClient: LanguageClient) {
// Surround with Tags (Wrap)
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.REFACTOR_SURROUND_WITH_TAGS, async () => {
await surroundWith(SurroundWithKind.tags, languageClient);
}));
// Surround with Comments
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.REFACTOR_SURROUND_WITH_COMMENTS, async () => {
await surroundWith(SurroundWithKind.comments, languageClient);
}));
// Surround with CDATA
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.REFACTOR_SURROUND_WITH_CDATA, async () => {
await surroundWith(SurroundWithKind.cdata, languageClient);
}));
}
async function surroundWith(surroundWithType: SurroundWithKind, languageClient: LanguageClient) {
const activeEditor = window.activeTextEditor;
if (!activeEditor) {
return;
}
const selection = activeEditor.selections[0];
if (!selection) {
return;
}
const uri = window.activeTextEditor.document.uri;
const identifier = TextDocumentIdentifier.create(uri.toString());
const range = languageClient.code2ProtocolConverter.asRange(selection);
const supportedSnippet: boolean = vscode.SnippetTextEdit ? true : false;
let result: SurroundWithResponse;
try {
result = await commands.executeCommand(ServerCommandConstants.REFACTOR_SURROUND_WITH, identifier, range, surroundWithType, supportedSnippet);
} catch (error) {
console.log(`Error while surround with : ${error}`);
}
if (!result) {
return;
}
const startTag = result.start.newText;
const endTag = result.end.newText;
if (supportedSnippet) {
// SnippetTextEdit is supported, uses snippet (with choice) to manage cursor.
const startRange = languageClient.protocol2CodeConverter.asRange(result.start.range);
const endRange = languageClient.protocol2CodeConverter.asRange(result.end.range);
const snippetEdits = [new vscode.SnippetTextEdit(startRange, new SnippetString(startTag)), new vscode.SnippetTextEdit(endRange, new SnippetString(endTag))];
const edit = new WorkspaceEdit();
edit.set(activeEditor.document.uri, snippetEdits);
await workspace.applyEdit(edit);
} else {
// SnippetTextEdit is not supported, update start / end tag
const startPos = languageClient.protocol2CodeConverter.asPosition(result.start.range.start);
const endPos = languageClient.protocol2CodeConverter.asPosition(result.end.range.start);
activeEditor.edit((selectedText) => {
selectedText.insert(startPos, startTag);
selectedText.insert(endPos, endTag);
})
if (surroundWithType === SurroundWithKind.tags) {
// Force the show of completion
const pos = languageClient.protocol2CodeConverter.asPosition(result.start.range.start);
const posAfterStartBracket = new Position(pos.line, pos.character + 1);
activeEditor.selections = [new Selection(posAfterStartBracket, posAfterStartBracket)];
commands.executeCommand("editor.action.triggerSuggest");
}
}
}