From ae9731520ed7a08c0d13bb14815c59cb1c13e484 Mon Sep 17 00:00:00 2001 From: azerr Date: Thu, 3 Jun 2021 10:24:22 +0200 Subject: [PATCH] Bind XSD, DTD with CodeLens See https://github.com/redhat-developer/vscode-xml/issues/395 Signed-off-by: azerr --- .../eclipse/lemminx/XMLWorkspaceService.java | 2 +- .../lemminx/client/ClientCommands.java | 10 +- .../eclipse/lemminx/client/CodeLensKind.java | 2 + .../lemminx/commons/CodeActionFactory.java | 12 +- .../contentmodel/ContentModelPlugin.java | 11 ++ .../commands/AssociateGrammarCommand.java | 174 ++++++++++++++++++ .../XMLValidationAllFilesCommand.java | 7 +- .../commands/XMLValidationFileCommand.java | 8 +- .../ContentModelCodeLensParticipant.java | 99 ++++++++++ .../participants/DTDErrorCode.java | 1 - .../NoGrammarConstraintsCodeAction.java | 155 ++++++++++++---- .../extensions/xsd/utils/XSDUtils.java | 11 +- .../AbstractDOMDocumentCommandHandler.java | 17 +- .../commands/IXMLCommandService.java | 4 +- .../lemminx/MockXMLLanguageServer.java | 10 + .../java/org/eclipse/lemminx/XMLAssert.java | 28 +-- ...ssociateGrammarCodeLensExtensionsTest.java | 45 +++++ .../commands/AssociateGrammarCommandTest.java | 148 +++++++++++++++ .../commands/XMLValidationCommandTest.java | 16 +- .../commands/CommandServiceTest.java | 2 +- 20 files changed, 675 insertions(+), 87 deletions(-) create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/AssociateGrammarCommand.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/ContentModelCodeLensParticipant.java create mode 100644 org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/AssociateGrammarCodeLensExtensionsTest.java create mode 100644 org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/commands/AssociateGrammarCommandTest.java diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLWorkspaceService.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLWorkspaceService.java index b9651d72a3..38720a3f0d 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLWorkspaceService.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLWorkspaceService.java @@ -63,7 +63,7 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { } return CompletableFutures.computeAsync(cancelChecker -> { try { - return handler.executeCommand(params, cancelChecker); + return handler.executeCommand(params, xmlLanguageServer.getSharedSettings(), cancelChecker); } catch (Exception e) { if (e instanceof ResponseErrorException) { throw (ResponseErrorException) e; diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/ClientCommands.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/ClientCommands.java index c3e9f85b63..cbe2fb54ff 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/ClientCommands.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/ClientCommands.java @@ -28,10 +28,14 @@ private ClientCommands() { public static final String SHOW_REFERENCES = "xml.show.references"; /** - * Open settings command. - * This custom command is sent to the client in order to have the client - * open its settings UI. + * Open settings command. This custom command is sent to the client in order to + * have the client open its settings UI. */ public static final String OPEN_SETTINGS = "xml.open.settings"; + /** + * Select file command. + */ + public static final String SELECT_FILE = "xml.select.file"; + } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/CodeLensKind.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/CodeLensKind.java index 1ac8891aa8..82d0c12c3e 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/CodeLensKind.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/CodeLensKind.java @@ -26,4 +26,6 @@ private CodeLensKind() { public static final String References = "references"; public static final String Implementations = "implementations"; + + public static final String Association = "association"; } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/CodeActionFactory.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/CodeActionFactory.java index 5b2dd6f417..eafaea4fe6 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/CodeActionFactory.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/CodeActionFactory.java @@ -85,10 +85,18 @@ public static CodeAction insert(String title, Position position, String insertTe * @return the text edit to insert a new content at the end of the given range. */ public static TextDocumentEdit insertEdit(String insertText, Position position, TextDocumentItem document) { - TextEdit edit = new TextEdit(new Range(position, position), insertText); + TextEdit edit = insertEdit(insertText, position); + return insertEdits(document, Collections.singletonList(edit)); + } + + public static TextEdit insertEdit(String insertText, Position position) { + return new TextEdit(new Range(position, position), insertText); + } + + public static TextDocumentEdit insertEdits(TextDocumentItem document, List edits) { VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier( document.getUri(), document.getVersion()); - return new TextDocumentEdit(versionedTextDocumentIdentifier, Collections.singletonList(edit)); + return new TextDocumentEdit(versionedTextDocumentIdentifier, edits); } public static CodeAction replace(String title, Range range, String replaceText, TextDocumentItem document, diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/ContentModelPlugin.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/ContentModelPlugin.java index 40f6f1f913..6f95811ab8 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/ContentModelPlugin.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/ContentModelPlugin.java @@ -15,10 +15,12 @@ import java.util.Objects; import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.extensions.contentmodel.commands.AssociateGrammarCommand; import org.eclipse.lemminx.extensions.contentmodel.commands.XMLValidationAllFilesCommand; import org.eclipse.lemminx.extensions.contentmodel.commands.XMLValidationFileCommand; import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager; import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelCodeActionParticipant; +import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelCodeLensParticipant; import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelCompletionParticipant; import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelDocumentLinkParticipant; import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelHoverParticipant; @@ -36,6 +38,7 @@ import org.eclipse.lemminx.services.extensions.ITypeDefinitionParticipant; import org.eclipse.lemminx.services.extensions.IXMLExtension; import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; +import org.eclipse.lemminx.services.extensions.codelens.ICodeLensParticipant; import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService; import org.eclipse.lemminx.services.extensions.diagnostics.IDiagnosticsParticipant; import org.eclipse.lemminx.services.extensions.save.ISaveContext; @@ -68,6 +71,8 @@ public class ContentModelPlugin implements IXMLExtension { private ContentModelSymbolsProviderParticipant symbolsProviderParticipant; + private final ICodeLensParticipant codeLensParticipant; + ContentModelManager contentModelManager; private ContentModelSettings cmSettings; @@ -80,6 +85,7 @@ public ContentModelPlugin() { diagnosticsParticipant = new ContentModelDiagnosticsParticipant(this); codeActionParticipant = new ContentModelCodeActionParticipant(); typeDefinitionParticipant = new ContentModelTypeDefinitionParticipant(); + codeLensParticipant = new ContentModelCodeLensParticipant(); } @Override @@ -176,6 +182,7 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) { registry.registerTypeDefinitionParticipant(typeDefinitionParticipant); symbolsProviderParticipant = new ContentModelSymbolsProviderParticipant(contentModelManager); registry.registerSymbolsProviderParticipant(symbolsProviderParticipant); + registry.registerCodeLensParticipant(codeLensParticipant); // Register custom commands to re-validate XML files IXMLCommandService commandService = registry.getCommandService(); @@ -186,6 +193,8 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) { new XMLValidationFileCommand(contentModelManager, documentProvider, validationService)); commandService.registerCommand(XMLValidationAllFilesCommand.COMMAND_ID, new XMLValidationAllFilesCommand(contentModelManager, documentProvider, validationService)); + commandService.registerCommand(AssociateGrammarCommand.COMMAND_ID, + new AssociateGrammarCommand(documentProvider)); } } @@ -198,12 +207,14 @@ public void stop(XMLExtensionsRegistry registry) { registry.unregisterDocumentLinkParticipant(documentLinkParticipant); registry.unregisterTypeDefinitionParticipant(typeDefinitionParticipant); registry.unregisterSymbolsProviderParticipant(symbolsProviderParticipant); + registry.unregisterCodeLensParticipant(codeLensParticipant); // Un-register custom commands to re-validate XML files IXMLCommandService commandService = registry.getCommandService(); if (commandService != null) { commandService.unregisterCommand(XMLValidationFileCommand.COMMAND_ID); commandService.unregisterCommand(XMLValidationAllFilesCommand.COMMAND_ID); + commandService.unregisterCommand(AssociateGrammarCommand.COMMAND_ID); } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/AssociateGrammarCommand.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/AssociateGrammarCommand.java new file mode 100644 index 0000000000..43f3e38ccb --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/AssociateGrammarCommand.java @@ -0,0 +1,174 @@ +/******************************************************************************* +* Copyright (c) 2021 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.contentmodel.commands; + +import static org.eclipse.lemminx.extensions.xsd.utils.XSDUtils.TARGET_NAMESPACE_ATTR; + +import java.net.URL; +import java.nio.file.Path; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.NoGrammarConstraintsCodeAction; +import org.eclipse.lemminx.services.IXMLDocumentProvider; +import org.eclipse.lemminx.services.extensions.commands.AbstractDOMDocumentCommandHandler; +import org.eclipse.lemminx.services.extensions.commands.ArgumentsUtils; +import org.eclipse.lemminx.settings.SharedSettings; +import org.eclipse.lemminx.utils.DOMUtils; +import org.eclipse.lemminx.utils.FilesUtils; +import org.eclipse.lemminx.utils.StringUtils; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * XML Command "xml.associate.grammar.insert" to associate a grammar to a given + * DOM document. + * + * The command parameters {@link ExecuteCommandParams} must be filled with 3 + * parameters: + * + *
    + *
  • document URI (String) : the DOM document file URI to bind with a grammar. + *
  • + *
  • grammar URI (String) : the XSD, DTD file URI to bind with the DOM + * document.
  • + *
  • binding type (String) : which can takes values "xsd", "dd", "xml-model" + * to know which binding type must be inserted in the DOM document.
  • + *
+ * + * @author Angelo ZERR + * + */ +public class AssociateGrammarCommand extends AbstractDOMDocumentCommandHandler { + + public static final String COMMAND_ID = "xml.associate.grammar.insert"; + + public AssociateGrammarCommand(IXMLDocumentProvider documentProvider) { + super(documentProvider); + } + + public enum GrammarBindingType { + + XSD("xsd"), // + DTD("dtd"), // + XML_MODEL("xml-model"); + + private String name; + + private GrammarBindingType(String name) { + this.name = name != null ? name : name(); + } + + public String getName() { + return name; + } + } + + @SuppressWarnings({ "serial" }) + public class UnknownBindingTypeException extends Exception { + + public UnknownBindingTypeException(String bindingType) { + super("Unknown binding type '" + bindingType + "'. Allowed values are " + // + Stream.of(GrammarBindingType.values()) // + .map(GrammarBindingType::getName) // + .collect(Collectors.joining(", ", "[", "]"))); + } + + } + + @Override + protected Object executeCommand(DOMDocument document, ExecuteCommandParams params, SharedSettings sharedSettings, + CancelChecker cancelChecker) throws Exception { + String documentURI = document.getDocumentURI(); + String fullPathGrammarURI = ArgumentsUtils.getArgAt(params, 1, String.class); + String bindingType = ArgumentsUtils.getArgAt(params, 2, String.class); + String grammarURI = getRelativeURI(fullPathGrammarURI, documentURI); + + if (GrammarBindingType.XSD.getName().equals(bindingType)) { + // Check if XSD to bind declares a target namespace + String targetNamespace = getTargetNamespace(fullPathGrammarURI); + if (StringUtils.isEmpty(targetNamespace)) { + // Insert inside -> + // xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance" + // xsi:noNamespaceSchemaLocation=\"xsd/tag.xsd\" + return NoGrammarConstraintsCodeAction.createXSINoNamespaceSchemaLocationEdit(grammarURI, document); + } + // Insert inside -> + // xmlns="team_namespace" + // xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + // xsi:schemaLocation="team_namespace xsd/team.xsd" + return NoGrammarConstraintsCodeAction.createXSISchemaLocationEdit(grammarURI, targetNamespace, document); + } else if (GrammarBindingType.DTD.getName().equals(bindingType)) { + // Insert before -> + return NoGrammarConstraintsCodeAction.createDocTypeEdit(grammarURI, document, sharedSettings); + } else if (GrammarBindingType.XML_MODEL.getName().equals(bindingType)) { + String targetNamespace = DOMUtils.isXSD(fullPathGrammarURI) ? getTargetNamespace(fullPathGrammarURI) : null; + // Insert before -> + return NoGrammarConstraintsCodeAction.createXmlModelEdit(grammarURI, targetNamespace, document, + sharedSettings); + } + throw new UnknownBindingTypeException(bindingType); + } + + private static String getRelativeURI(String fullPathGrammarURI, String documentURI) { + try { + Path grammarPath = FilesUtils.getPath(fullPathGrammarURI); + Path documentPath = FilesUtils.getPath(documentURI); + Path relativePath = documentPath.getParent().relativize(grammarPath); + return relativePath.toString().replaceAll("\\\\", "/"); + } catch (Exception e) { + return fullPathGrammarURI; + } + } + + private static String getTargetNamespace(String xsdURI) { + TargetNamespaceHandler handler = new TargetNamespaceHandler(); + try { + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser = factory.newSAXParser(); + saxParser.parse(new URL(xsdURI).openStream(), handler); + } catch (Exception e) { + + } + return handler.getTargetNamespace(); + } + + /** + * SAX handler which extract the targetNamespace attribute from the xs:schema + * root tag element and null otherwise. + * + */ + private static class TargetNamespaceHandler extends DefaultHandler { + + private String targetNamespace; + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + super.startElement(uri, localName, qName, attributes); + this.targetNamespace = attributes.getValue(TARGET_NAMESPACE_ATTR); + throw new SAXException(); + } + + public String getTargetNamespace() { + return targetNamespace; + } + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationAllFilesCommand.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationAllFilesCommand.java index ef05dd92a1..b5a7e694d0 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationAllFilesCommand.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationAllFilesCommand.java @@ -18,11 +18,13 @@ import org.eclipse.lemminx.services.IXMLDocumentProvider; import org.eclipse.lemminx.services.IXMLValidationService; import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService.IDelegateCommandHandler; +import org.eclipse.lemminx.settings.SharedSettings; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.jsonrpc.CancelChecker; /** - * XML Command to revalidate all opened XML files which means: + * XML Command "xml.validation.all.files" to revalidate all opened XML files + * which means: * *
    *
  • clear the Xerces grammar pool (used by the Xerces validation) and the @@ -52,7 +54,8 @@ public XMLValidationAllFilesCommand(ContentModelManager contentModelManager, IXM } @Override - public Object executeCommand(ExecuteCommandParams params, CancelChecker cancelChecker) throws Exception { + public Object executeCommand(ExecuteCommandParams params, SharedSettings sharedSettings, + CancelChecker cancelChecker) throws Exception { // 1. clear the Xerces grammar pool // (used by the Xerces validation) and the content model documents cache (used // by the XML completion/hover based on the grammar) diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationFileCommand.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationFileCommand.java index b6d4d56698..3c9b07e1bd 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationFileCommand.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationFileCommand.java @@ -16,11 +16,13 @@ import org.eclipse.lemminx.services.IXMLDocumentProvider; import org.eclipse.lemminx.services.IXMLValidationService; import org.eclipse.lemminx.services.extensions.commands.AbstractDOMDocumentCommandHandler; +import org.eclipse.lemminx.settings.SharedSettings; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.jsonrpc.CancelChecker; /** - * XML Command to revalidate a give XML file which means: + * XML Command "xml.validation.current.file" to revalidate a give XML file which + * means: * *
      *
    • remove the referenced grammar in the XML file from the Xerces grammar @@ -48,8 +50,8 @@ public XMLValidationFileCommand(ContentModelManager contentModelManager, IXMLDoc } @Override - protected Object executeCommand(DOMDocument document, ExecuteCommandParams params, CancelChecker cancelChecker) - throws Exception { + protected Object executeCommand(DOMDocument document, ExecuteCommandParams params, SharedSettings sharedSettings, + CancelChecker cancelChecker) throws Exception { // 1. remove the referenced grammar in the XML file from the Xerces grammar pool // (used by the Xerces validation) and the content model documents cache (used // by the XML completion/hover based on the grammar) diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/ContentModelCodeLensParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/ContentModelCodeLensParticipant.java new file mode 100644 index 0000000000..1ab3f5f47d --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/ContentModelCodeLensParticipant.java @@ -0,0 +1,99 @@ +/******************************************************************************* +* Copyright (c) 2021 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ + +package org.eclipse.lemminx.extensions.contentmodel.participants; + +import static org.eclipse.lemminx.client.ClientCommands.SELECT_FILE; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.lemminx.client.CodeLensKind; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMElement; +import org.eclipse.lemminx.extensions.contentmodel.commands.AssociateGrammarCommand; +import org.eclipse.lemminx.extensions.contentmodel.commands.AssociateGrammarCommand.GrammarBindingType; +import org.eclipse.lemminx.services.extensions.codelens.ICodeLensParticipant; +import org.eclipse.lemminx.services.extensions.codelens.ICodeLensRequest; +import org.eclipse.lemminx.utils.XMLPositionUtility; +import org.eclipse.lsp4j.CodeLens; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; + +/** + * At first this participant is enabled only when LSP client can support the + * client command "xml.file.select" to open a file dialog and + * select it. + * + * In this case, when XML file is not associated to a grammar (XSD, DTD), this + * class generates several CodeLenses on the root of the DOM Document: + * + *
        + *
      • [Bind with XSD] : click on this Codelens open a file dialog to select the + * XSD to bind.
      • + *
      • [Bind with DTD] : click on this Codelens open a file dialog to select the + * DTD to bind.
      • + *
      • [Bind with xml-model] : click on this Codelens open a file dialog to + * select the XSD, DTD to bind.
      • + *
      + * + *

      + * Once the LSP client select the DTD, XSD, it should call the + * {@link AssociateGrammarCommand} to generate the proper syntax for binding. + *

      + * + */ +public class ContentModelCodeLensParticipant implements ICodeLensParticipant { + + @Override + public void doCodeLens(ICodeLensRequest request, List lenses, CancelChecker cancelChecker) { + if (!request.isSupportedByClient(CodeLensKind.Association)) { + // The LSP client can support Association, when DOM document is not bound to a + // grammar, code lenses appears: + + // [Bind with XSD] [Bind with DTD] [Bind with xml-model] + // + + // A click on codelens consume the LSP client command + // "xml.file.select" which should open a file dialog to select + // a XSD, DTD + // Once the file is selected, the LSP client must consume the XML language + // server command "xml.associate.grammar.insert" by passing as parameters + // - the document uri + // - the selected file uri + // - the binding type coming from code lens arguments. + + // The XML language server command return a TextDocumentEdit which must be + // applied on LSP client side. + return; + } + DOMDocument document = request.getDocument(); + DOMElement documentElement = document.getDocumentElement(); + if (documentElement == null || document.hasGrammar()) { + return; + } + String documentURI = document.getDocumentURI(); + Range range = XMLPositionUtility.selectRootStartTag(document); + + lenses.add(createAssociateLens(documentURI, "Bind with XSD", GrammarBindingType.XSD.getName(), range)); + lenses.add(createAssociateLens(documentURI, "Bind with DTD", GrammarBindingType.DTD.getName(), range)); + lenses.add( + createAssociateLens(documentURI, "Bind with xml-model", GrammarBindingType.XML_MODEL.getName(), range)); + } + + private static CodeLens createAssociateLens(String documentURI, String title, String bindingType, Range range) { + Command command = new Command(title, SELECT_FILE, Arrays.asList(documentURI, bindingType)); + return new CodeLens(range, command, null); + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/DTDErrorCode.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/DTDErrorCode.java index 9ca2845b4e..64cfe221b2 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/DTDErrorCode.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/DTDErrorCode.java @@ -20,7 +20,6 @@ import org.apache.xerces.xni.XMLLocator; import org.eclipse.lemminx.commons.BadLocationException; import org.eclipse.lemminx.dom.DOMDocument; -import org.eclipse.lemminx.dom.DOMDocumentType; import org.eclipse.lemminx.dom.DOMElement; import org.eclipse.lemminx.dom.DOMRange; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.ElementDeclUnterminatedCodeAction; diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/NoGrammarConstraintsCodeAction.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/NoGrammarConstraintsCodeAction.java index ce5ced722b..4e51b823fc 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/NoGrammarConstraintsCodeAction.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/NoGrammarConstraintsCodeAction.java @@ -15,6 +15,7 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Arrays; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -30,12 +31,14 @@ import org.eclipse.lemminx.services.extensions.ICodeActionParticipant; import org.eclipse.lemminx.services.extensions.IComponentProvider; import org.eclipse.lemminx.settings.SharedSettings; +import org.eclipse.lemminx.utils.StringUtils; import org.eclipse.lemminx.utils.XMLBuilder; import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextDocumentEdit; +import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; /** @@ -55,9 +58,6 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen } FileContentGeneratorManager generator = componentProvider.getComponent(FileContentGeneratorManager.class); - String delimiter = document.lineDelimiter(0); - int beforeTagOffset = documentElement.getStartTagOpenOffset(); - int afterTagOffset = beforeTagOffset + 1 + documentElement.getTagName().length(); // ---------- XSD @@ -67,23 +67,18 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen // xsi:noNamespaceSchemaLocation // Create code action to create the XSD file with the generated XSD content - String insertText = " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" - + document.getTextDocument().lineDelimiter(0); - insertText += " xsi:noNamespaceSchemaLocation=\"" + schemaFileName + "\""; + TextDocumentEdit noNamespaceSchemaLocationEdit = createXSINoNamespaceSchemaLocationEdit(schemaFileName, + document); CodeAction noNamespaceSchemaLocationAction = createGrammarFileAndBindIt( "Generate '" + schemaFileName + "' and bind with xsi:noNamespaceSchemaLocation", schemaURI, - schemaTemplate, insertText, afterTagOffset, document, diagnostic); + schemaTemplate, noNamespaceSchemaLocationEdit, diagnostic); codeActions.add(noNamespaceSchemaLocationAction); // xml-model - XMLBuilder xsdWithXmlModel = new XMLBuilder(sharedSettings, null, delimiter); - xsdWithXmlModel.startPrologOrPI("xml-model"); - xsdWithXmlModel.addSingleAttribute("href", schemaFileName, true); - xsdWithXmlModel.endPrologOrPI(); - xsdWithXmlModel.linefeed(); + TextDocumentEdit xsdWithXMLModelEdit = createXmlModelEdit(schemaFileName, null, document, sharedSettings); CodeAction xsdWithXmlModelAction = createGrammarFileAndBindIt( "Generate '" + schemaFileName + "' and bind with xml-model", schemaURI, schemaTemplate, - xsdWithXmlModel.toString(), beforeTagOffset, document, diagnostic); + xsdWithXMLModelEdit, diagnostic); codeActions.add(xsdWithXmlModelAction); // ---------- DTD @@ -93,28 +88,17 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen String dtdTemplate = generator.generate(document, sharedSettings, new DTDGeneratorSettings()); // - XMLBuilder docType = new XMLBuilder(sharedSettings, null, delimiter); - docType.startDoctype(); - docType.addParameter(documentElement.getLocalName()); - docType.addContent(" SYSTEM \""); - docType.addContent(dtdFileName); - docType.addContent("\""); - docType.endDoctype(); - docType.linefeed(); + TextDocumentEdit dtdWithDocType = createDocTypeEdit(dtdFileName, document, sharedSettings); CodeAction docTypeAction = createGrammarFileAndBindIt( - "Generate '" + dtdFileName + "' and bind with DOCTYPE", dtdURI, dtdTemplate, docType.toString(), - beforeTagOffset, document, diagnostic); + "Generate '" + dtdFileName + "' and bind with DOCTYPE", dtdURI, dtdTemplate, dtdWithDocType, + diagnostic); codeActions.add(docTypeAction); // xml-model - XMLBuilder dtdWithXmlModel = new XMLBuilder(sharedSettings, null, delimiter); - dtdWithXmlModel.startPrologOrPI("xml-model"); - dtdWithXmlModel.addSingleAttribute("href", dtdFileName, true); - dtdWithXmlModel.endPrologOrPI(); - dtdWithXmlModel.linefeed(); + TextDocumentEdit dtdWithXMLModelEdit = createXmlModelEdit(dtdFileName, null, document, sharedSettings); CodeAction dtdWithXmlModelAction = createGrammarFileAndBindIt( - "Generate '" + dtdFileName + "' and bind with xml-model", dtdURI, dtdTemplate, - dtdWithXmlModel.toString(), beforeTagOffset, document, diagnostic); + "Generate '" + dtdFileName + "' and bind with xml-model", dtdURI, dtdTemplate, dtdWithXMLModelEdit, + diagnostic); codeActions.add(dtdWithXmlModelAction); } catch (BadLocationException e) { @@ -153,18 +137,113 @@ static String getFileName(String schemaURI) { return new File(schemaURI).getName(); } - private static CodeAction createGrammarFileAndBindIt(String title, String grammarURI, String grammarContent, - String insertText, int insertOffset, DOMDocument document, Diagnostic diagnostic) - throws BadLocationException { - Position position = document.positionAt(insertOffset); - TextDocumentEdit insertEdit = CodeActionFactory.insertEdit(insertText, position, document.getTextDocument()); - return createGrammarFileAndBindIt(title, grammarURI, grammarContent, insertEdit, diagnostic); - } - private static CodeAction createGrammarFileAndBindIt(String title, String grammarURI, String grammarContent, TextDocumentEdit boundEdit, Diagnostic diagnostic) { CodeAction codeAction = CodeActionFactory.createFile(title, grammarURI, grammarContent, diagnostic); codeAction.getEdit().getDocumentChanges().add(Either.forLeft(boundEdit)); return codeAction; } + + public static TextDocumentEdit createXSINoNamespaceSchemaLocationEdit(String schemaFileName, DOMDocument document) + throws BadLocationException { + String delimiter = document.getTextDocument().lineDelimiter(0); + DOMElement documentElement = document.getDocumentElement(); + int beforeTagOffset = documentElement.getStartTagOpenOffset(); + int afterTagOffset = beforeTagOffset + 1 + documentElement.getTagName().length(); + + StringBuilder insertText = new StringBuilder(); + + insertText.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""); + insertText.append(delimiter); + + insertText.append(" xsi:noNamespaceSchemaLocation=\""); + insertText.append(schemaFileName); + insertText.append("\""); + + Position position = document.positionAt(afterTagOffset); + return CodeActionFactory.insertEdit(insertText.toString(), position, document.getTextDocument()); + } + + public static TextDocumentEdit createXSISchemaLocationEdit(String schemaFileName, String targetNamespace, + DOMDocument document) throws BadLocationException { + String delimiter = document.getTextDocument().lineDelimiter(0); + DOMElement documentElement = document.getDocumentElement(); + int beforeTagOffset = documentElement.getStartTagOpenOffset(); + int afterTagOffset = beforeTagOffset + 1 + documentElement.getTagName().length(); + + StringBuilder insertText = new StringBuilder(); + insertText.append(" xmlns=\""); + insertText.append(targetNamespace); + insertText.append("\""); + insertText.append(delimiter); + + insertText.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""); + insertText.append(delimiter); + + insertText.append(" xsi:schemaLocation=\""); + insertText.append(targetNamespace); + insertText.append(" "); + insertText.append(schemaFileName); + insertText.append("\""); + + Position position = document.positionAt(afterTagOffset); + return CodeActionFactory.insertEdit(insertText.toString(), position, document.getTextDocument()); + } + + public static TextDocumentEdit createXmlModelEdit(String schemaFileName, String targetNamespace, + DOMDocument document, SharedSettings sharedSettings) throws BadLocationException { + String delimiter = document.getTextDocument().lineDelimiter(0); + DOMElement documentElement = document.getDocumentElement(); + int beforeTagOffset = documentElement.getStartTagOpenOffset(); + + // Insert Text edit for xml-model + XMLBuilder xsdWithXmlModel = new XMLBuilder(sharedSettings, null, delimiter); + xsdWithXmlModel.startPrologOrPI("xml-model"); + xsdWithXmlModel.addSingleAttribute("href", schemaFileName, true); + xsdWithXmlModel.endPrologOrPI(); + xsdWithXmlModel.linefeed(); + + String xmlModelInsertText = xsdWithXmlModel.toString(); + Position xmlModelPosition = document.positionAt(beforeTagOffset); + + if (StringUtils.isEmpty(targetNamespace)) { + return CodeActionFactory.insertEdit(xmlModelInsertText, xmlModelPosition, document.getTextDocument()); + } + + StringBuilder xmlNamespaceInsertText = new StringBuilder(); + xmlNamespaceInsertText.append(" xmlns=\""); + xmlNamespaceInsertText.append(targetNamespace); + xmlNamespaceInsertText.append("\" "); + + int afterTagOffset = beforeTagOffset + 1 + documentElement.getTagName().length(); + Position xmlNamespacePosition = document.positionAt(afterTagOffset); + + List edits = Arrays.asList( // + // insert xml-model before root tag element + CodeActionFactory.insertEdit(xmlModelInsertText, xmlModelPosition), + // insert xml namespace inside root tag element + CodeActionFactory.insertEdit(xmlNamespaceInsertText.toString(), xmlNamespacePosition)); + return CodeActionFactory.insertEdits(document.getTextDocument(), edits); + } + + public static TextDocumentEdit createDocTypeEdit(String dtdFileName, DOMDocument document, + SharedSettings sharedSettings) throws BadLocationException { + String delimiter = document.getTextDocument().lineDelimiter(0); + DOMElement documentElement = document.getDocumentElement(); + int beforeTagOffset = documentElement.getStartTagOpenOffset(); + + XMLBuilder docType = new XMLBuilder(sharedSettings, null, delimiter); + docType.startDoctype(); + docType.addParameter(documentElement.getLocalName()); + docType.addContent(" SYSTEM \""); + docType.addContent(dtdFileName); + docType.addContent("\""); + docType.endDoctype(); + docType.linefeed(); + + String insertText = docType.toString(); + Position position = document.positionAt(beforeTagOffset); + return CodeActionFactory.insertEdit(insertText, position, document.getTextDocument()); + } + } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/utils/XSDUtils.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/utils/XSDUtils.java index 911bc5b279..f7ea55e0b2 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/utils/XSDUtils.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/utils/XSDUtils.java @@ -47,6 +47,9 @@ */ public class XSDUtils { + public static final String SCHEMA_LOCATION_ATTR = "schemaLocation"; + public static final String TARGET_NAMESPACE_ATTR = "targetNamespace"; + /** * Binding type of xs attribute. * @@ -152,7 +155,7 @@ public static void searchXSTargetAttributes(DOMAttr originAttr, BindingType bind // - String targetNamespace = documentElement.getAttribute("targetNamespace"); // -> + String targetNamespace = documentElement.getAttribute(TARGET_NAMESPACE_ATTR); // -> // http://camel.apache.org/schema/spring String targetNamespacePrefix = documentElement.getPrefix(targetNamespace); // -> tns @@ -190,7 +193,7 @@ private static void searchXSTargetAttributes(DOMAttr originAttr, BindingType bin } } else if (isXSInclude(targetElement)) { // collect xs:include XML Schema location - String schemaLocation = targetElement.getAttribute("schemaLocation"); + String schemaLocation = targetElement.getAttribute(SCHEMA_LOCATION_ATTR); if (schemaLocation != null) { if (externalURIS == null) { externalURIS = new HashSet<>(); @@ -271,7 +274,7 @@ public static void searchXSOriginAttributes(DOMNode targetNode, BiConsumer - String targetNamespace = documentElement.getAttribute("targetNamespace"); // -> + String targetNamespace = documentElement.getAttribute(TARGET_NAMESPACE_ATTR); // -> // http://camel.apache.org/schema/spring String targetNamespacePrefix = documentElement.getPrefix(targetNamespace); // -> tns @@ -464,7 +467,7 @@ public static DOMAttr getSchemaLocation(DOMElement element) { if (!(isXSInclude(element) || isXSImport(element))) { return null; } - return element.getAttributeNode("schemaLocation"); + return element.getAttributeNode(SCHEMA_LOCATION_ATTR); } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/commands/AbstractDOMDocumentCommandHandler.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/commands/AbstractDOMDocumentCommandHandler.java index 140a47af31..a031fed6d8 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/commands/AbstractDOMDocumentCommandHandler.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/commands/AbstractDOMDocumentCommandHandler.java @@ -14,6 +14,7 @@ import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.services.IXMLDocumentProvider; import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService.IDelegateCommandHandler; +import org.eclipse.lemminx.settings.SharedSettings; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.CancelChecker; @@ -34,7 +35,8 @@ public AbstractDOMDocumentCommandHandler(IXMLDocumentProvider documentProvider) } @Override - public final Object executeCommand(ExecuteCommandParams params, CancelChecker cancelChecker) throws Exception { + public final Object executeCommand(ExecuteCommandParams params, SharedSettings sharedSettings, + CancelChecker cancelChecker) throws Exception { TextDocumentIdentifier identifier = ArgumentsUtils.getArgAt(params, 0, TextDocumentIdentifier.class); String uri = identifier.getUri(); DOMDocument document = documentProvider.getDocument(uri); @@ -42,23 +44,24 @@ public final Object executeCommand(ExecuteCommandParams params, CancelChecker ca throw new UnsupportedOperationException(String .format("Command '%s' cannot find the DOM document with the URI '%s'.", params.getCommand(), uri)); } - return executeCommand(document, params, cancelChecker); + return executeCommand(document, params, sharedSettings, cancelChecker); } /** * Executes a command * - * @param document the DOM document retrieve by the - * {@link TextDocumentIdentifier} argument. + * @param document the DOM document retrieve by the + * {@link TextDocumentIdentifier} argument. * - * @param params command execution parameters - * @param cancelChecker check if cancel has been requested + * @param params command execution parameters + * @param sharedSettings the shared settings + * @param cancelChecker check if cancel has been requested * @return the result of the command * @throws Exception the unhandled exception will be wrapped in * org.eclipse.lsp4j.jsonrpc.ResponseErrorException * and be wired back to the JSON-RPC protocol caller */ protected abstract Object executeCommand(DOMDocument document, ExecuteCommandParams params, - CancelChecker cancelChecker) throws Exception; + SharedSettings sharedSettings, CancelChecker cancelChecker) throws Exception; } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/commands/IXMLCommandService.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/commands/IXMLCommandService.java index 3940660de7..6e95461683 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/commands/IXMLCommandService.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/commands/IXMLCommandService.java @@ -14,6 +14,7 @@ import java.util.concurrent.CompletableFuture; +import org.eclipse.lemminx.settings.SharedSettings; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.jsonrpc.CancelChecker; @@ -37,13 +38,14 @@ public interface IDelegateCommandHandler { * Executes a command * * @param params command execution parameters + * @param sharedSettings the shared settings. * @param cancelChecker check if cancel has been requested * @return the result of the command * @throws Exception the unhandled exception will be wrapped in * org.eclipse.lsp4j.jsonrpc.ResponseErrorException * and be wired back to the JSON-RPC protocol caller */ - Object executeCommand(ExecuteCommandParams params, CancelChecker cancelChecker) throws Exception; + Object executeCommand(ExecuteCommandParams params, SharedSettings sharedSettings, CancelChecker cancelChecker) throws Exception; } /** diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/MockXMLLanguageServer.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/MockXMLLanguageServer.java index 5196ad64d0..e5118c1645 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/MockXMLLanguageServer.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/MockXMLLanguageServer.java @@ -17,9 +17,12 @@ import org.eclipse.lemminx.customservice.ActionableNotification; import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService.IDelegateCommandHandler; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.MessageParams; import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentItem; /** * Mock XML Language server which helps to track show messages, actionable @@ -65,4 +68,11 @@ public CompletableFuture executeCommand(String command, Object... argume return getWorkspaceService().executeCommand(params); } + public TextDocumentIdentifier didOpen(String fileURI, String xml) { + TextDocumentIdentifier xmlIdentifier = new TextDocumentIdentifier(fileURI); + DidOpenTextDocumentParams params = new DidOpenTextDocumentParams( + new TextDocumentItem(xmlIdentifier.getUri(), "xml", 1, xml)); + getTextDocumentService().didOpen(params); + return xmlIdentifier; + } } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java index 1dcdc7a932..e9316f56be 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/XMLAssert.java @@ -18,7 +18,6 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Path; @@ -94,7 +93,7 @@ import org.junit.jupiter.api.Assertions; /** - * XML + * XML Assert * */ public class XMLAssert { @@ -123,7 +122,8 @@ public class XMLAssert { private static final String FILE_URI = "test.xml"; - private static final CancelChecker NULL_CHECKER = () -> {}; + private static final CancelChecker NULL_CHECKER = () -> { + }; public static class SettingsSaveContext extends AbstractSaveContext { @@ -314,7 +314,8 @@ public static void testTagCompletion(String value, String expected) throws BadLo testTagCompletion(value, expected, new SharedSettings()); } - public static void testTagCompletion(String value, String expected, SharedSettings settings) throws BadLocationException { + public static void testTagCompletion(String value, String expected, SharedSettings settings) + throws BadLocationException { int offset = value.indexOf('|'); value = value.substring(0, offset) + value.substring(offset + 1); @@ -333,7 +334,8 @@ public static void testTagCompletion(String value, String expected, SharedSettin assertEquals(expected, actual); } - public static void testTagCompletion(String value, AutoCloseTagResponse expected, SharedSettings settings) throws BadLocationException { + public static void testTagCompletion(String value, AutoCloseTagResponse expected, SharedSettings settings) + throws BadLocationException { int offset = value.indexOf('|'); value = value.substring(0, offset) + value.substring(offset + 1); @@ -628,15 +630,18 @@ public static CodeAction ca(Diagnostic d, TextEdit... te) { codeAction.setTitle(""); codeAction.setDiagnostics(Arrays.asList(d)); - VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier(FILE_URI, - 0); - - TextDocumentEdit textDocumentEdit = new TextDocumentEdit(versionedTextDocumentIdentifier, Arrays.asList(te)); + TextDocumentEdit textDocumentEdit = tde(FILE_URI, 0, te); WorkspaceEdit workspaceEdit = new WorkspaceEdit(Collections.singletonList(Either.forLeft(textDocumentEdit))); codeAction.setEdit(workspaceEdit); return codeAction; } + public static TextDocumentEdit tde(String uri, int version, TextEdit... te) { + VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier(uri, + version); + return new TextDocumentEdit(versionedTextDocumentIdentifier, Arrays.asList(te)); + } + public static CodeAction ca(Diagnostic d, Either... ops) { CodeAction codeAction = new CodeAction(); codeAction.setDiagnostics(Collections.singletonList(d)); @@ -1077,7 +1082,7 @@ public static void testCodeLensFor(String value, String fileURI, XMLLanguageServ XMLCodeLensSettings codeLensSettings = new XMLCodeLensSettings(); ExtendedCodeLensCapabilities codeLensCapabilities = new ExtendedCodeLensCapabilities( - new CodeLensKindCapabilities(Arrays.asList(CodeLensKind.References))); + new CodeLensKindCapabilities(Arrays.asList(CodeLensKind.References, CodeLensKind.Association))); codeLensSettings.setCodeLens(codeLensCapabilities); List actual = xmlLanguageService.getCodeLens(xmlDocument, codeLensSettings, () -> { }); @@ -1325,7 +1330,8 @@ public static void testSelectionRange(String xml, SelectionRange... selectionRan stringBuilder.deleteCharAt(nextPipe); nextPipe = stringBuilder.indexOf("|"); } - assertEquals(selectionRanges.length, cursorOffsets.size(), "Number of cursors and SelectionRanges should be equal"); + assertEquals(selectionRanges.length, cursorOffsets.size(), + "Number of cursors and SelectionRanges should be equal"); testSelectionRange(stringBuilder.toString(), cursorOffsets, selectionRanges); } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/AssociateGrammarCodeLensExtensionsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/AssociateGrammarCodeLensExtensionsTest.java new file mode 100644 index 0000000000..f27a4feeaa --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/AssociateGrammarCodeLensExtensionsTest.java @@ -0,0 +1,45 @@ +/******************************************************************************* +* Copyright (c) 2021 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.contentmodel; + +import static org.eclipse.lemminx.XMLAssert.cl; +import static org.eclipse.lemminx.XMLAssert.r; +import static org.eclipse.lemminx.XMLAssert.testCodeLensFor; +import static org.eclipse.lemminx.client.ClientCommands.SELECT_FILE; + +import org.eclipse.lemminx.commons.BadLocationException; +import org.junit.jupiter.api.Test; + +/** + * Associate grammar codelens tests + * + */ +public class AssociateGrammarCodeLensExtensionsTest { + + @Test + public void noGrammar() throws BadLocationException { + String xml = "\r\n" + // + ""; + testCodeLensFor(xml, "test.xml", // + cl(r(1, 1, 1, 4), "Bind with XSD", SELECT_FILE), // + cl(r(1, 1, 1, 4), "Bind with DTD", SELECT_FILE), // + cl(r(1, 1, 1, 4), "Bind with xml-model", SELECT_FILE)); + } + + @Test + public void withGrammar() throws BadLocationException { + String xml = "\r\n" + // + ""; + testCodeLensFor(xml, "test.xml"); + } + +} diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/commands/AssociateGrammarCommandTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/commands/AssociateGrammarCommandTest.java new file mode 100644 index 0000000000..b3dcb2f674 --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/commands/AssociateGrammarCommandTest.java @@ -0,0 +1,148 @@ +/******************************************************************************* +* Copyright (c) 2021 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.contentmodel.commands; + +import static org.eclipse.lemminx.XMLAssert.tde; +import static org.eclipse.lemminx.XMLAssert.te; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.util.concurrent.ExecutionException; + +import org.eclipse.lemminx.MockXMLLanguageServer; +import org.eclipse.lemminx.extensions.contentmodel.commands.AssociateGrammarCommand.GrammarBindingType; +import org.eclipse.lemminx.utils.platform.Platform; +import org.eclipse.lsp4j.TextDocumentEdit; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.jupiter.api.Test; + +/** + * Test for associating a grammar to a given XML command. + * + * @author Angelo ZERR + * + */ +public class AssociateGrammarCommandTest { + + @Test + public void associateWithXSDNoNamespaceShemaLocation() throws InterruptedException, ExecutionException { + MockXMLLanguageServer languageServer = new MockXMLLanguageServer(); + + String xml = "\r\n" + // + ""; + String xmlPath = getFileURI("src/test/resources/tag.xml"); + TextDocumentIdentifier xmlIdentifier = languageServer.didOpen(xmlPath, xml); + String xsdPath = getFileURI("src/test/resources/xsd/tag.xsd"); + String bindingType = GrammarBindingType.XSD.getName(); + + TextDocumentEdit actual = (TextDocumentEdit) languageServer + .executeCommand(AssociateGrammarCommand.COMMAND_ID, xmlIdentifier, xsdPath, bindingType).get(); + assertNotNull(actual); + + assertEquals(actual, tde(xmlPath, 1, te(1, 4, 1, 4, // + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n" + // + " xsi:noNamespaceSchemaLocation=\"xsd/tag.xsd\""))); + } + + @Test + public void associateWithXSDSchemaLocation() throws Exception { + MockXMLLanguageServer languageServer = new MockXMLLanguageServer(); + + String xml = "\r\n" + // + ""; + String xmlPath = getFileURI("src/test/resources/tag.xml"); + TextDocumentIdentifier xmlIdentifier = languageServer.didOpen(xmlPath, xml); + + String xsdPath = getFileURI("src/test/resources/xsd/team.xsd"); + String bindingType = GrammarBindingType.XSD.getName(); + + TextDocumentEdit actual = (TextDocumentEdit) languageServer + .executeCommand(AssociateGrammarCommand.COMMAND_ID, xmlIdentifier, xsdPath, bindingType).get(); + assertNotNull(actual); + + assertEquals(actual, tde(xmlPath, 1, te(1, 4, 1, 4, // + " xmlns=\"team_namespace\"\r\n" + // + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n" + // + " xsi:schemaLocation=\"team_namespace xsd/team.xsd\""))); + } + + @Test + public void associateWithDTD() throws InterruptedException, ExecutionException { + MockXMLLanguageServer languageServer = new MockXMLLanguageServer(); + + String xml = "\r\n" + // + ""; + String xmlPath = getFileURI("src/test/resources/tag.xml"); + TextDocumentIdentifier xmlIdentifier = languageServer.didOpen(xmlPath, xml); + String dtdPath = getFileURI("src/test/resources/dtd/tag.dtd"); + String bindingType = GrammarBindingType.DTD.getName(); + + TextDocumentEdit actual = (TextDocumentEdit) languageServer + .executeCommand(AssociateGrammarCommand.COMMAND_ID, xmlIdentifier, dtdPath, bindingType).get(); + assertNotNull(actual); + + assertEquals(actual, tde(xmlPath, 1, te(1, 0, 1, 0, // + "\r\n"))); + } + + @Test + public void associateWithXMLModel() throws InterruptedException, ExecutionException { + MockXMLLanguageServer languageServer = new MockXMLLanguageServer(); + + String xml = "\r\n" + // + ""; + String xmlPath = getFileURI("src/test/resources/tag.xml"); + TextDocumentIdentifier xmlIdentifier = languageServer.didOpen(xmlPath, xml); + String dtdPath = getFileURI("src/test/resources/dtd/tag.dtd"); + String bindingType = GrammarBindingType.XML_MODEL.getName(); + + TextDocumentEdit actual = (TextDocumentEdit) languageServer + .executeCommand(AssociateGrammarCommand.COMMAND_ID, xmlIdentifier, dtdPath, bindingType).get(); + assertNotNull(actual); + + assertEquals(actual, tde(xmlPath, 1, te(1, 0, 1, 0, // + "\r\n"))); + } + + @Test + public void unknownBindingTypeException() throws InterruptedException, ExecutionException { + + MockXMLLanguageServer languageServer = new MockXMLLanguageServer(); + + String xml = "\r\n" + // + ""; + String xmlPath = getFileURI("src/test/resources/tag.xml"); + TextDocumentIdentifier xmlIdentifier = languageServer.didOpen(xmlPath, xml); + String dtdPath = getFileURI("src/test/resources/dtd/tag.dtd"); + String bindingType = "BAD"; + + try { + languageServer.executeCommand(AssociateGrammarCommand.COMMAND_ID, xmlIdentifier, dtdPath, bindingType) + .get(); + fail("Unknown binding type should throw an exception."); + } catch (Exception e) { + assertEquals("Unknown binding type 'BAD'. Allowed values are [xsd, dtd, xml-model]", + e.getCause().getMessage()); + } + + } + + private static String getFileURI(String fileName) { + String uri = new File(fileName).toURI().toString(); + if (Platform.isWindows && !uri.startsWith("file://")) { + uri = uri.replace("file:/", "file:///"); + } + return uri; + } +} diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationCommandTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationCommandTest.java index db87a4953f..3bf8cd6aaf 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationCommandTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/commands/XMLValidationCommandTest.java @@ -32,11 +32,9 @@ import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.DiagnosticSeverity; -import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.TextDocumentIdentifier; -import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.junit.jupiter.api.Test; @@ -83,7 +81,7 @@ public void validationFileCommand() throws Exception { ""; // Open the XML document, the validation is triggered asynchronously - TextDocumentIdentifier xmlIdentifier = didOpen(languageServer, "test.xml", xml); + TextDocumentIdentifier xmlIdentifier = languageServer.didOpen("test.xml", xml); // Wait for: // - downloading of XSD from the HTTP server to lemminx cache @@ -209,7 +207,7 @@ public void validationAllFilesCommand() throws Exception { ""; // Open the XML document, the validation is triggered asynchronously - TextDocumentIdentifier xml1Identifier = didOpen(languageServer, "test1.xml", xml1); + TextDocumentIdentifier xml1Identifier = languageServer.didOpen("test1.xml", xml1); // Wait for to collect diagnostics in the proper order (XSD diagnostics followed // by DTD diagnostics) @@ -223,7 +221,7 @@ public void validationAllFilesCommand() throws Exception { ""; // Open the XML document, the validation is triggered asynchronously - TextDocumentIdentifier xml2Identifier = didOpen(languageServer, "test2.xml", xml2); + TextDocumentIdentifier xml2Identifier = languageServer.didOpen("test2.xml", xml2); // Wait for: // - downloading of XSD from the HTTP server to lemminx cache @@ -331,14 +329,6 @@ public void validationAllFilesCommand() throws Exception { } } - private static TextDocumentIdentifier didOpen(MockXMLLanguageServer languageServer, String fileURI, String xml) { - TextDocumentIdentifier xmlIdentifier = new TextDocumentIdentifier(fileURI); - DidOpenTextDocumentParams params = new DidOpenTextDocumentParams( - new TextDocumentItem(xmlIdentifier.getUri(), "xml", 1, xml)); - languageServer.getTextDocumentService().didOpen(params); - return xmlIdentifier; - } - private static CompletionList completion(MockXMLLanguageServer languageServer, TextDocumentIdentifier xmlIdentifier) throws BadLocationException, InterruptedException, ExecutionException { DOMDocument document = languageServer.getDocument(xmlIdentifier.getUri()); diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/commands/CommandServiceTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/commands/CommandServiceTest.java index 14fe5d13bb..fa9dea9997 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/commands/CommandServiceTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/services/extensions/commands/CommandServiceTest.java @@ -36,7 +36,7 @@ public CommandServiceTest() { } private void registerCommand(String command) { - languageServer.registerCommand(command, (params, cancelChecker) -> { + languageServer.registerCommand(command, (params, sharedSettings, cancelChecker) -> { return params.getCommand() + (params.getArguments().isEmpty() ? "" : ": " + params.getArguments().stream().map(a -> a.toString()).collect(Collectors.joining(" "))); });