diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java index ebc900d46c..51be2d5502 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java @@ -24,6 +24,8 @@ import org.eclipse.lemminx.dom.DOMNode; import org.eclipse.lemminx.dom.NoNamespaceSchemaLocation; import org.eclipse.lemminx.dom.SchemaLocation; +import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.TargetNamespace_1CodeAction; +import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.TargetNamespace_2CodeAction; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.cvc_attribute_3CodeAction; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.cvc_complex_type_2_1CodeAction; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.cvc_complex_type_2_3CodeAction; @@ -74,6 +76,7 @@ public enum XMLSchemaErrorCode implements IXMLErrorCode { cvc_maxInclusive_valid("cvc-maxInclusive-valid"), // https://wiki.xmldation.com/Support/validator/cvc-maxinclusive-valid cvc_minExclusive_valid("cvc-minExclusive-valid"), // https://wiki.xmldation.com/Support/validator/cvc-minexclusive-valid cvc_minInclusive_valid("cvc-minInclusive-valid"), // https://wiki.xmldation.com/Support/validator/cvc-mininclusive-valid + TargetNamespace_1("TargetNamespace.1"), // TargetNamespace_2("TargetNamespace.2"), SchemaLocation("SchemaLocation"), schema_reference_4("schema_reference.4"), // src_element_3("src-element.3"); @@ -138,7 +141,6 @@ public static Range toLSPRange(XMLLocator location, XMLSchemaErrorCode code, Obj case cvc_elt_1_a: case cvc_complex_type_4: case src_element_3: - case TargetNamespace_2: return XMLPositionUtility.selectStartTagName(offset, document); case cvc_complex_type_3_2_2: { String attrName = getString(arguments[1]); @@ -245,6 +247,10 @@ public static Range toLSPRange(XMLLocator location, XMLSchemaErrorCode code, Obj } case cvc_type_3_1_2: return XMLPositionUtility.selectStartTagName(offset, document); + case TargetNamespace_1: + return XMLPositionUtility.selectRootAttributeValue("xmlns", document); + case TargetNamespace_2: + return XMLPositionUtility.selectRootStartTag(document); default: } return null; @@ -260,5 +266,7 @@ public static void registerCodeActionParticipants(Map codeActions, + SharedSettings sharedSettings, IComponentProvider componentProvider) { + + String namespace = extractNamespace(diagnostic.getMessage()); + if (StringUtils.isEmpty(namespace)) { + return; + } + String quote = sharedSettings.getPreferences().getQuotationAsString(); + // @formatter:off + CodeAction replaceNamespace = CodeActionFactory.replace( + "Replace with '" + namespace + "'", + diagnostic.getRange(), + quote + namespace + quote, + document.getTextDocument(), diagnostic); + // @formatter:on + codeActions.add(replaceNamespace); + } + + private static String extractNamespace(String diagnosticMessage) { + // The error message has this form: + // TargetNamespace.1: Expecting namespace 'NaN', but the target namespace of the + // schema document is 'http://two-letter-name'. + Matcher nsMatcher = NAMESPACE_EXTRACTOR.matcher(diagnosticMessage); + if (nsMatcher.find()) { + return nsMatcher.group(1); + } + return null; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/TargetNamespace_2CodeAction.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/TargetNamespace_2CodeAction.java new file mode 100644 index 0000000000..102286a18b --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/TargetNamespace_2CodeAction.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2020 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * 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.codeactions; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.lemminx.commons.CodeActionFactory; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMNode; +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.XMLPositionUtility; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; + +/** + * Code action to add the missing xmlns declaration to the root element in an + * .xml document. + * + * Finds the namespace of the referenced .xsd. Adds the xmlns attribute to the + * root of .xml, and sets its value to the .xsd namespace. + */ +public class TargetNamespace_2CodeAction implements ICodeActionParticipant { + + private static final Pattern NAMESPACE_EXTRACTOR = Pattern.compile("'([^']+)'\\."); + + @Override + public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List codeActions, + SharedSettings sharedSettings, IComponentProvider componentProvider) { + + String namespace = extractNamespace(diagnostic.getMessage()); + if (StringUtils.isEmpty(namespace)) { + return; + } + DOMNode root = document.getDocumentElement(); + if (root == null) { + return; + } + Position tagEnd = XMLPositionUtility.selectStartTagName(root).getEnd(); + String quote = sharedSettings.getPreferences().getQuotationAsString(); + // @formatter:off + CodeAction addNamespaceDecl = CodeActionFactory.insert( + "Declare '" + namespace + "' as the namespace", + tagEnd, + " xmlns=" + quote + namespace + quote, + document.getTextDocument(), + diagnostic); + // @formatter:on + codeActions.add(addNamespaceDecl); + } + + private static String extractNamespace(String diagnosticMessage) { + // The error message has this form: + // TargetNamespace.2: Expecting no namespace, but the schema document has a + // target namespace of 'http://docbook.org/ns/docbook'. + Matcher nsMatcher = NAMESPACE_EXTRACTOR.matcher(diagnosticMessage); + if (nsMatcher.find()) { + return nsMatcher.group(1); + } + return null; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java index 5f1749faf7..8f9011b2e5 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java @@ -325,6 +325,34 @@ public static Range selectRootStartTag(DOMDocument document) { return selectStartTagName(root); } + /** + * Finds the root element of the given document and returns the + * attribute value Range for the attribute + * attrName. + * + * If attrName is not declared then null is returned. + * + * @param attrName The name of the attribute to find the range of the value for + * @param document The document to use the root element of + * @return The range in document where the declared value of + * attribute attrName resides (including quotations), + * or null if the attriubte is not declared. + */ + public static Range selectRootAttributeValue(String attrName, DOMDocument document) { + DOMNode root = document.getDocumentElement(); + if (root == null) { + root = document.getChild(0); + } + if (root == null) { + return null; + } + DOMAttr attr = root.getAttributeNode(attrName); + if (attr == null) { + return null; + } + return selectAttributeValue(attr); + } + public static Range selectStartTagName(int offset, DOMDocument document) { DOMNode element = document.findNodeAt(offset); if (element != null) { diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaDiagnosticsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaDiagnosticsTest.java index e6d811ad1b..b2fa045655 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaDiagnosticsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaDiagnosticsTest.java @@ -20,6 +20,9 @@ import org.eclipse.lemminx.XMLAssert; import org.eclipse.lemminx.extensions.contentmodel.participants.XMLSchemaErrorCode; import org.eclipse.lemminx.extensions.contentmodel.settings.ContentModelSettings; +import org.eclipse.lemminx.settings.EnforceQuoteStyle; +import org.eclipse.lemminx.settings.QuoteStyle; +import org.eclipse.lemminx.settings.SharedSettings; import org.eclipse.lsp4j.Diagnostic; import org.junit.jupiter.api.Test; @@ -577,6 +580,69 @@ public void cvc_complex_type_2_2_withText() throws Exception { testDiagnosticsFor(xml, diagnosticBob, diagnostic_cvc_2_2); } + @Test + public void testTargetNamespace_1Normal() throws Exception { + String xml = "\n" + // + "\n" + // + "Io"; + Diagnostic targetNamespace = d(2, 23, 2, 31, XMLSchemaErrorCode.TargetNamespace_1, "TargetNamespace.1: Expecting namespace 'BAD_NS', but the target namespace of the schema document is 'http://two-letter-name'."); + testDiagnosticsFor(xml, + targetNamespace, + d(2, 1, 2, 16, XMLSchemaErrorCode.cvc_elt_1_a, "cvc-elt.1.a: Cannot find the declaration of element 'two-letter-name'.") + ); + testCodeActionsFor(xml, targetNamespace, ca(targetNamespace, te(2, 23, 2, 31, "\"http://two-letter-name\""))); + } + + @Test + public void testTargetNamespace_1ShortNS() throws Exception { + String xml = "\n" + // + "\n" + // + "Io"; + Diagnostic targetNamespace = d(2, 23, 2, 26, XMLSchemaErrorCode.TargetNamespace_1, "TargetNamespace.1: Expecting namespace '_', but the target namespace of the schema document is 'http://two-letter-name'."); + testDiagnosticsFor(xml, + targetNamespace, + d(2, 1, 2, 16, XMLSchemaErrorCode.cvc_elt_1_a, "cvc-elt.1.a: Cannot find the declaration of element 'two-letter-name'.") + ); + testCodeActionsFor(xml, targetNamespace, ca(targetNamespace, te(2, 23, 2, 26, "\"http://two-letter-name\""))); + } + + @Test + public void testTargetNamespace_1SingleQuotes() throws Exception { + String xml = "\n" + // + "\n" + // + "Io"; + SharedSettings settings = new SharedSettings(); + settings.getFormattingSettings().setEnforceQuoteStyle(EnforceQuoteStyle.preferred); + settings.getPreferences().setQuoteStyle(QuoteStyle.singleQuotes); + Diagnostic targetNamespace = d(2, 23, 2, 26, XMLSchemaErrorCode.TargetNamespace_1, "TargetNamespace.1: Expecting namespace '_', but the target namespace of the schema document is 'http://two-letter-name'."); + testCodeActionsFor(xml, targetNamespace, settings, ca(targetNamespace, te(2, 23, 2, 26, "'http://two-letter-name'"))); + } + + @Test + public void testTargetNamespace_2() throws Exception { + String xml = "\n" + // + "\n" + // + "Io"; + Diagnostic targetNamespace = d(2, 1, 2, 16, XMLSchemaErrorCode.TargetNamespace_2, "TargetNamespace.2: Expecting no namespace, but the schema document has a target namespace of 'http://two-letter-name'."); + testDiagnosticsFor(xml, + targetNamespace, + d(2, 1, 2, 16, XMLSchemaErrorCode.cvc_elt_1_a, "cvc-elt.1.a: Cannot find the declaration of element 'two-letter-name'.") + ); + testCodeActionsFor(xml, targetNamespace, ca(targetNamespace, te(2, 16, 2, 16, " xmlns=\"http://two-letter-name\""))); + } + + @Test + public void testTargetNamespace_2SingleQuotes() throws Exception { + String xml = "\n" + // + "\n" + // + "Io"; + SharedSettings settings = new SharedSettings(); + settings.getFormattingSettings().setEnforceQuoteStyle(EnforceQuoteStyle.preferred); + settings.getPreferences().setQuoteStyle(QuoteStyle.singleQuotes); + Diagnostic targetNamespace = d(2, 1, 2, 16, XMLSchemaErrorCode.TargetNamespace_2, "TargetNamespace.2: Expecting no namespace, but the schema document has a target namespace of 'http://two-letter-name'."); + testCodeActionsFor(xml, targetNamespace, settings, ca(targetNamespace, te(2, 16, 2, 16, " xmlns='http://two-letter-name'"))); + } + private static void testDiagnosticsFor(String xml, Diagnostic... expected) { XMLAssert.testDiagnosticsFor(xml, "src/test/resources/catalogs/catalog.xml", expected); } diff --git a/org.eclipse.lemminx/src/test/resources/xsd/two-letter-name.xsd b/org.eclipse.lemminx/src/test/resources/xsd/two-letter-name.xsd new file mode 100644 index 0000000000..83ede5f3a1 --- /dev/null +++ b/org.eclipse.lemminx/src/test/resources/xsd/two-letter-name.xsd @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file