From cfe58d322b08a2cef49a2053c7a367f30f96eb2d Mon Sep 17 00:00:00 2001 From: azerr Date: Thu, 3 Jun 2021 15:49:35 +0200 Subject: [PATCH] Bind XSD, DTD with CodeLens Fixes #395 Signed-off-by: azerr --- src/client/xmlClient.ts | 7 ++- src/commands/commandConstants.ts | 98 ++++++++++++++++++++------------ src/commands/registerCommands.ts | 97 ++++++++++++++++++++++++------- src/markdownPreviewProvider.ts | 4 +- src/plugin.ts | 4 +- 5 files changed, 148 insertions(+), 62 deletions(-) diff --git a/src/client/xmlClient.ts b/src/client/xmlClient.ts index 0ed6c447..1e7153cb 100644 --- a/src/client/xmlClient.ts +++ b/src/client/xmlClient.ts @@ -3,7 +3,7 @@ import { commands, ExtensionContext, extensions, Position, TextDocument, TextEdi import { Command, ConfigurationParams, ConfigurationRequest, DidChangeConfigurationNotification, ExecuteCommandParams, LanguageClientOptions, MessageType, NotificationType, RequestType, RevealOutputChannelOn, TextDocumentPositionParams } from "vscode-languageclient"; import { Executable, LanguageClient } from 'vscode-languageclient/node'; import { XMLFileAssociation } from '../api/xmlExtensionApi'; -import { CommandConstants } from '../commands/commandConstants'; +import { ClientCommandConstants, ServerCommandConstants } from '../commands/commandConstants'; import { registerCommands } from '../commands/registerCommands'; import { onExtensionChange } from '../plugin'; import { RequirementsData } from "../server/requirements"; @@ -67,7 +67,7 @@ export async function startLanguageClient(context: ExtensionContext, executable: let text = languageClient.sendRequest(TagCloseRequest.type, param); return text; }; - context.subscriptions.push(activateTagClosing(tagProvider, { xml: true, xsl: true }, CommandConstants.AUTO_CLOSE_TAGS)); + context.subscriptions.push(activateTagClosing(tagProvider, { xml: true, xsl: true }, ServerCommandConstants.AUTO_CLOSE_TAGS)); if (extensions.onDidChange) {// Theia doesn't support this API yet context.subscriptions.push(extensions.onDidChange(() => { @@ -123,7 +123,8 @@ function getLanguageClientOptions(logfile: string, externalXmlSettings: External codeLens: { codeLensKind: { valueSet: [ - 'references' + 'references', + 'association' ] } }, diff --git a/src/commands/commandConstants.ts b/src/commands/commandConstants.ts index 13d1a130..fea37a11 100644 --- a/src/commands/commandConstants.ts +++ b/src/commands/commandConstants.ts @@ -11,52 +11,78 @@ 'use strict'; /** - * Commonly used commands + * VScode client commands. */ -export namespace CommandConstants { +export namespace ClientCommandConstants { - /** - * Auto close tags - */ - export const AUTO_CLOSE_TAGS = 'xml.completion.autoCloseTags'; + /** + * Show XML references + */ + export const SHOW_REFERENCES = 'xml.show.references'; - /** - * Show XML references - */ - export const SHOW_REFERENCES = 'xml.show.references'; + /** + * Show editor references + */ + export const EDITOR_SHOW_REFERENCES = 'editor.action.showReferences'; - /** - * Show editor references - */ - export const EDITOR_SHOW_REFERENCES = 'editor.action.showReferences'; + /** + * Reload VS Code window + */ + export const RELOAD_WINDOW = 'workbench.action.reloadWindow'; - /** - * Reload VS Code window - */ - export const RELOAD_WINDOW = 'workbench.action.reloadWindow'; + /** + * Open settings command + * + * A `settingId: string` parameter can be optionally provided + */ + export const OPEN_SETTINGS = 'xml.open.settings'; - /** - * Open settings command - * - * A `settingId: string` parameter can be optionally provided - */ - export const OPEN_SETTINGS = 'xml.open.settings'; + /** + * Render markdown string to html string + */ + export const MARKDOWN_API_RENDER = 'markdown.api.render'; - /** - * Render markdown string to html string - */ - export const MARKDOWN_API_RENDER = 'markdown.api.render'; + export const OPEN_DOCS = 'xml.open.docs'; - export const OPEN_DOCS = "xml.open.docs"; + /** + * Commands to revalidate files with an LSP command on the XML Language Server + */ + export const VALIDATE_CURRENT_FILE = 'xml.validation.current.file'; - export const OPEN_DOCS_HOME = "xml.open.docs.home"; + export const VALIDATE_ALL_FILES = 'xml.validation.all.files'; - /** - * VSCode client command to executes an LSP command on the XML Language Server - */ - export const EXECUTE_WORKSPACE_COMMAND = "xml.workspace.executeCommand"; + export const OPEN_DOCS_HOME = 'xml.open.docs.home'; - export const VALIDATE_CURRENT_FILE = "xml.validation.current.file"; + /** + * VSCode client commands to open the binding wizard to bind a XML to a grammar/schema. + */ + export const OPEN_BINDING_WIZARD = 'xml.open.binding.wizard'; - export const VALIDATE_ALL_FILES = "xml.validation.all.files"; + /** + * Client command to execute an XML command on XML Language Server side. + */ + export const EXECUTE_WORKSPACE_COMMAND = 'xml.workspace.executeCommand'; } + +/** + * XML Language Server commands. + */ +export namespace ServerCommandConstants { + + /** + * Auto close tags + */ + export const AUTO_CLOSE_TAGS = 'xml.completion.autoCloseTags'; + + /** + * Commands to revalidate files with an LSP command on the XML Language Server + */ + export const VALIDATE_CURRENT_FILE = ClientCommandConstants.VALIDATE_CURRENT_FILE; + + export const VALIDATE_ALL_FILES = ClientCommandConstants.VALIDATE_ALL_FILES; + + /** + * Command to associate a grammar in a XML document + */ + export const ASSOCIATE_GRAMMAR_INSERT = "xml.associate.grammar.insert"; +} \ No newline at end of file diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 7c38901a..e6b7fae1 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -1,23 +1,24 @@ import * as path from 'path'; -import { commands, ExtensionContext, Position, Uri, window, workspace } from "vscode"; -import { CancellationToken, ExecuteCommandParams, ExecuteCommandRequest, ReferencesRequest, TextDocumentIdentifier } from "vscode-languageclient"; +import { commands, ExtensionContext, OpenDialogOptions, Position, QuickPickItem, Uri, window, workspace, WorkspaceEdit } from "vscode"; +import { CancellationToken, ExecuteCommandParams, ExecuteCommandRequest, ReferencesRequest, TextDocumentIdentifier, TextDocumentEdit } from "vscode-languageclient"; import { LanguageClient } from 'vscode-languageclient/node'; import { markdownPreviewProvider } from "../markdownPreviewProvider"; -import { CommandConstants } from "./commandConstants"; +import { ClientCommandConstants, ServerCommandConstants } from "./commandConstants"; /** * Register the commands for vscode-xml * * @param context the extension context */ -export async function registerCommands(context: ExtensionContext, languageClient: LanguageClient) { +export function registerCommands(context: ExtensionContext, languageClient: LanguageClient) { registerDocsCommands(context); - registerCodeLensCommands(context, languageClient); + registerCodeLensReferencesCommands(context, languageClient); registerValidationCommands(context); + registerCodeLensAssociationCommands(context, languageClient); // Register client command to execute custom XML Language Server command - context.subscriptions.push(commands.registerCommand(CommandConstants.EXECUTE_WORKSPACE_COMMAND, (command, ...rest) => { + 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])) { @@ -36,7 +37,7 @@ export async function registerCommands(context: ExtensionContext, languageClient })); // Register command that open settings to a given setting - context.subscriptions.push(commands.registerCommand(CommandConstants.OPEN_SETTINGS, async (settingId?: string) => { + context.subscriptions.push(commands.registerCommand(ClientCommandConstants.OPEN_SETTINGS, async (settingId?: string) => { commands.executeCommand('workbench.action.openSettings', settingId); })); } @@ -46,15 +47,15 @@ export async function registerCommands(context: ExtensionContext, languageClient * * @param context the extension context */ -async function registerDocsCommands(context: ExtensionContext): Promise { +function registerDocsCommands(context: ExtensionContext) { context.subscriptions.push(markdownPreviewProvider); - context.subscriptions.push(commands.registerCommand(CommandConstants.OPEN_DOCS_HOME, async () => { + 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(CommandConstants.OPEN_DOCS, async (params: { page: string, section: string }) => { + 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 || ''; @@ -64,19 +65,19 @@ async function registerDocsCommands(context: ExtensionContext): Promise { } /** - * Register commands used for code lens + * Register commands used for code lens "references" * * @param context the extension context * @param languageClient the language server client */ -async function registerCodeLensCommands(context: ExtensionContext, languageClient: LanguageClient): Promise { - context.subscriptions.push(commands.registerCommand(CommandConstants.SHOW_REFERENCES, (uriString: string, position: Position) => { +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 let param = languageClient.code2ProtocolConverter.asTextDocumentPositionParams(document, position); languageClient.sendRequest(ReferencesRequest.type, param).then(locations => { - commands.executeCommand(CommandConstants.EDITOR_SHOW_REFERENCES, uri, languageClient.protocol2CodeConverter.asPosition(position), locations.map(languageClient.protocol2CodeConverter.asLocation)); + commands.executeCommand(ClientCommandConstants.EDITOR_SHOW_REFERENCES, uri, languageClient.protocol2CodeConverter.asPosition(position), locations.map(languageClient.protocol2CodeConverter.asLocation)); }) }) })); @@ -87,12 +88,12 @@ async function registerCodeLensCommands(context: ExtensionContext, languageClien * * @param context the extension context */ -async function registerValidationCommands(context: ExtensionContext): Promise { +function registerValidationCommands(context: ExtensionContext) { // Revalidate current file - context.subscriptions.push(commands.registerCommand(CommandConstants.VALIDATE_CURRENT_FILE, async (params) => { + context.subscriptions.push(commands.registerCommand(ClientCommandConstants.VALIDATE_CURRENT_FILE, async (params) => { const uri = window.activeTextEditor.document.uri; const identifier = TextDocumentIdentifier.create(uri.toString()); - commands.executeCommand(CommandConstants.EXECUTE_WORKSPACE_COMMAND, CommandConstants.VALIDATE_CURRENT_FILE, identifier). + commands.executeCommand(ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND, ServerCommandConstants.VALIDATE_CURRENT_FILE, identifier). then(() => { window.showInformationMessage('The current XML file was successfully validated.'); }, error => { @@ -100,12 +101,70 @@ async function registerValidationCommands(context: ExtensionContext): Promise { - commands.executeCommand(CommandConstants.EXECUTE_WORKSPACE_COMMAND, CommandConstants.VALIDATE_ALL_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([ + ["Standard (xsi, DOCTYPE)", "standard"], + ["XML Model association", "xml-model"] +]); + +const bindingTypeOptions: QuickPickItem[] = []; +for (const label of bindingTypes.keys()) { + bindingTypeOptions.push({ "label": label }); +} + +/** + * Register commands used for associating grammar file (XSD,DTD) to a given XML file + * + * @param context the extension context + */ +function registerCodeLensAssociationCommands(context: ExtensionContext, languageClient: LanguageClient) { + context.subscriptions.push(commands.registerCommand(ClientCommandConstants.OPEN_BINDING_WIZARD, async (uriString: string) => { + // A click on Bind to grammar/schema... has been processed in the XML document which is not bound to a grammar + const documentURI = Uri.parse(uriString); + + // Step 1 : open a combo to select the binding type ("standard", "xml-model") + 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 identifier = TextDocumentIdentifier.create(documentURI.toString()); + const grammarURI = fileUri[0]; + try { + const result = await commands.executeCommand(ServerCommandConstants.ASSOCIATE_GRAMMAR_INSERT, identifier, grammarURI.toString(), bindingType); + // Insert the proper syntax for binding + const lspTextDocumentEdit = 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 + } catch (error) { + window.showErrorMessage('Error during grammar binding: ' + error.message); + }; + } + })); + } \ No newline at end of file diff --git a/src/markdownPreviewProvider.ts b/src/markdownPreviewProvider.ts index d117a67b..9f9ba523 100644 --- a/src/markdownPreviewProvider.ts +++ b/src/markdownPreviewProvider.ts @@ -1,7 +1,7 @@ import { Disposable, WebviewPanel, window, ViewColumn, commands, Uri, Webview, ExtensionContext, env } from "vscode"; import * as fse from 'fs-extra'; import * as path from 'path'; -import { CommandConstants } from "./commands/commandConstants"; +import { ClientCommandConstants } from "./commands/commandConstants"; class MarkdownPreviewProvider implements Disposable { private panel: WebviewPanel | undefined; @@ -57,7 +57,7 @@ class MarkdownPreviewProvider implements Disposable { (_match: string, linkText: string, page: string, section: string) => { return `${linkText}` }); - body = await commands.executeCommand(CommandConstants.MARKDOWN_API_RENDER, markdownString); + body = await commands.executeCommand(ClientCommandConstants.MARKDOWN_API_RENDER, markdownString); this.documentCache.set(markdownFilePath, body); } return ` diff --git a/src/plugin.ts b/src/plugin.ts index 7cab223f..6bc7b1d8 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import { CommandConstants } from './commands/commandConstants'; +import { ClientCommandConstants } from './commands/commandConstants'; const glob = require('glob'); let existingExtensions: Array; @@ -49,7 +49,7 @@ export function onExtensionChange(extensions: readonly vscode.Extension[], if (hasChanged) { const msg = `Extensions to the XML Language Server changed, reloading ${vscode.env.appName} is required for the changes to take effect.`; const action = 'Reload'; - const restartId = CommandConstants.RELOAD_WINDOW; + const restartId = ClientCommandConstants.RELOAD_WINDOW; vscode.window.showWarningMessage(msg, action).then((selection) => { if (action === selection) { vscode.commands.executeCommand(restartId);