-
Notifications
You must be signed in to change notification settings - Fork 79
/
registerCommands.ts
346 lines (319 loc) · 14.3 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
import * as path from 'path';
import { commands, ConfigurationTarget, ExtensionContext, OpenDialogOptions, Position, QuickPickItem, TextDocument, Uri, window, workspace, WorkspaceEdit } from "vscode";
import { CancellationToken, ExecuteCommandParams, ExecuteCommandRequest, ReferencesRequest, TextDocumentEdit, TextDocumentIdentifier } from "vscode-languageclient";
import { LanguageClient } from 'vscode-languageclient/node';
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);
registerAssociationCommands(context, languageClient);
registerRestartLanguageServerCommand(context, languageClient);
// 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 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);
});
}));
}
export 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 });
}
/**
* 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 : open a combo to select the binding type ("standard", "xml-model", "fileAssociation")
const pickedBindingTypeOption = await window.showQuickPick(bindingTypeOptions, { placeHolder: "Binding type" });
if (!pickedBindingTypeOption) {
return;
}
const bindingType = bindingTypes.get(pickedBindingTypeOption.label);
// Open a dialog to select the XSD, DTD to bind.
const options: OpenDialogOptions = {
canSelectMany: false,
openLabel: 'Select XSD or DTD file',
filters: {
'Grammar files': ['xsd', 'dtd']
}
};
const fileUri = await window.showOpenDialog(options);
if (fileUri && fileUri[0]) {
// The XSD, DTD has been selected, get the proper syntax for binding this grammar file in the XML document.
const grammarURI = fileUri[0];
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();
}));
}