diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogURIResolverExtension.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogURIResolverExtension.java index 88961a4b9..cda11c086 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogURIResolverExtension.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/catalog/XMLCatalogURIResolverExtension.java @@ -83,6 +83,9 @@ public XMLInputSource resolveEntity(XMLResourceIdentifier resourceIdentifier) th } private boolean hasDTDorXMLSchema(String uri) { + if (extensionsRegistry.getDocumentProvider() == null) { + return false; + } DOMDocument document = extensionsRegistry.getDocumentProvider().getDocument(uri); if (document == null) { return false; diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/ContentModelHoverParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/ContentModelHoverParticipant.java index fe059d4fc..56bdfa86a 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/ContentModelHoverParticipant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/ContentModelHoverParticipant.java @@ -16,7 +16,9 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.eclipse.lemminx.dom.DOMAttr; import org.eclipse.lemminx.dom.DOMElement; @@ -49,7 +51,7 @@ public Hover onTag(IHoverRequest hoverRequest, CancelChecker cancelChecker) thro try { ContentModelManager contentModelManager = hoverRequest.getComponent(ContentModelManager.class); DOMElement element = (DOMElement) hoverRequest.getNode(); - Collection cmDocuments = contentModelManager.findCMDocument(element); + Collection cmDocuments = collectCMDocuments(element, contentModelManager); if (cmDocuments.isEmpty()) { // no bound grammar -> no documentation return null; @@ -75,7 +77,7 @@ public Hover onAttributeName(IHoverRequest hoverRequest, CancelChecker cancelChe DOMElement element = attribute.getOwnerElement(); try { ContentModelManager contentModelManager = hoverRequest.getComponent(ContentModelManager.class); - Collection cmDocuments = contentModelManager.findCMDocument(element); + Collection cmDocuments = collectCMDocuments(element, contentModelManager); if (cmDocuments.isEmpty()) { // no bound grammar -> no documentation return null; @@ -113,7 +115,7 @@ public Hover onAttributeValue(IHoverRequest hoverRequest, CancelChecker cancelCh DOMElement element = attribute.getOwnerElement(); try { ContentModelManager contentModelManager = hoverRequest.getComponent(ContentModelManager.class); - Collection cmDocuments = contentModelManager.findCMDocument(element); + Collection cmDocuments = collectCMDocuments(element, contentModelManager); if (cmDocuments.isEmpty()) { // no bound grammar -> no documentation return null; @@ -147,7 +149,7 @@ public Hover onText(IHoverRequest hoverRequest, CancelChecker cancelChecker) thr } try { ContentModelManager contentModelManager = hoverRequest.getComponent(ContentModelManager.class); - Collection cmDocuments = contentModelManager.findCMDocument(element); + Collection cmDocuments = collectCMDocuments(element, contentModelManager); if (cmDocuments.isEmpty()) { // no bound grammar -> no documentation return null; @@ -185,4 +187,43 @@ private static void fillHoverContent(MarkupContent content, List } } + /** + * Collect the content model documents which may declare the given element. When + * the element is inside an ancestor with an xsi:type attribute, the documents + * bound to the ancestor's namespace are also collected so that derived types + * declared in other schemas can be resolved. + * + * @param element the hovered XML element. + * @param contentModelManager the content model manager. + * @return the content model documents to search. + */ + private static Collection collectCMDocuments(DOMElement element, + ContentModelManager contentModelManager) { + Set documents = new LinkedHashSet<>(contentModelManager.findCMDocument(element)); + DOMElement current = element; + while (current != null) { + DOMElement parent = current.getParentNode() instanceof DOMElement ? (DOMElement) current.getParentNode() + : null; + if (parent != null && hasXSIType(parent)) { + documents.addAll(contentModelManager.findCMDocument(parent)); + } + current = parent; + } + return documents; + } + + private static boolean hasXSIType(DOMElement element) { + org.w3c.dom.NamedNodeMap attrs = element.getAttributes(); + if (attrs == null) { + return false; + } + for (int i = 0; i < attrs.getLength(); i++) { + org.w3c.dom.Node attr = attrs.item(i); + if ("type".equals(attr.getLocalName()) && XSISchemaModel.XSI_WEBSITE.equals(attr.getNamespaceURI())) { + return true; + } + } + return false; + } + } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/XSDPlugin.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/XSDPlugin.java index 88dcb2645..06724ca16 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/XSDPlugin.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/XSDPlugin.java @@ -14,7 +14,6 @@ import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager; -import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelProvider; import org.eclipse.lemminx.extensions.xsd.contentmodel.CMXSDContentModelProvider; import org.eclipse.lemminx.extensions.xsd.participants.XSDCodeLensParticipant; import org.eclipse.lemminx.extensions.xsd.participants.XSDCompletionParticipant; @@ -87,8 +86,9 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) { uiResolver = new XSDURIResolverExtension(registry.getDocumentProvider()); registry.getResolverExtensionManager().registerResolver(uiResolver); // register XSD content model provider - ContentModelProvider modelProvider = new CMXSDContentModelProvider(registry.getResolverExtensionManager()); + CMXSDContentModelProvider modelProvider = new CMXSDContentModelProvider(registry.getResolverExtensionManager()); contentModelManager = registry.getComponent(ContentModelManager.class); + modelProvider.setContentModelManager(contentModelManager); contentModelManager.registerModelProvider(modelProvider); // register completion, diagnostic participant registry.registerCompletionParticipant(completionParticipant); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/contentmodel/CMXSDContentModelProvider.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/contentmodel/CMXSDContentModelProvider.java index 0ba9c5497..3eaf73249 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/contentmodel/CMXSDContentModelProvider.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/contentmodel/CMXSDContentModelProvider.java @@ -26,6 +26,7 @@ import org.eclipse.lemminx.dom.SchemaLocation; import org.eclipse.lemminx.dom.SchemaLocationHint; import org.eclipse.lemminx.extensions.contentmodel.model.CMDocument; +import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager; import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelProvider; import org.eclipse.lemminx.extensions.xerces.AbstractLSPErrorReporter; import org.eclipse.lemminx.extensions.xerces.LSPXMLEntityManager; @@ -48,11 +49,22 @@ public class CMXSDContentModelProvider implements ContentModelProvider { private static final String XSI_NO_NAMESPACE_SCHEMA_LOCATION_BINDING_KIND = "xsi:noNamespaceSchemaLocation"; private final URIResolverExtensionManager resolverExtensionManager; + private ContentModelManager contentModelManager; public CMXSDContentModelProvider(URIResolverExtensionManager resolverExtensionManager) { this.resolverExtensionManager = resolverExtensionManager; } + /** + * Sets the content model manager used to resolve cross-schema type + * definitions (ex: xsi:type from a different namespace). + * + * @param contentModelManager the content model manager. + */ + public void setContentModelManager(ContentModelManager contentModelManager) { + this.contentModelManager = contentModelManager; + } + @Override public boolean adaptFor(DOMDocument document, boolean internal) { if (internal) { @@ -110,7 +122,7 @@ public CMDocument createCMDocument(String key, boolean resolveExternalEntities) XSModel model = loader.loadURI(key); if (model != null) { // XML Schema can be loaded - return new CMXSDDocument(model, loader); + return new CMXSDDocument(model, loader, contentModelManager); } return null; } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/contentmodel/CMXSDDocument.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/contentmodel/CMXSDDocument.java index 496b92513..9615f7e79 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/contentmodel/CMXSDDocument.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/contentmodel/CMXSDDocument.java @@ -58,6 +58,7 @@ import org.eclipse.lemminx.dom.DOMNode; import org.eclipse.lemminx.extensions.contentmodel.model.CMDocument; import org.eclipse.lemminx.extensions.contentmodel.model.CMElementDeclaration; +import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager; import org.eclipse.lemminx.extensions.contentmodel.model.FilesChangedTracker; import org.eclipse.lemminx.extensions.xerces.ReflectionUtils; import org.eclipse.lemminx.extensions.xsd.utils.XSDUtils; @@ -89,10 +90,16 @@ public class CMXSDDocument implements CMDocument, XSElementDeclHelper { private final FilesChangedTracker tracker; private final XSLoaderImpl xsLoader; + private final ContentModelManager contentModelManager; public CMXSDDocument(XSModel model, XSLoaderImpl xsLoaderImpl) { + this(model, xsLoaderImpl, null); + } + + public CMXSDDocument(XSModel model, XSLoaderImpl xsLoaderImpl, ContentModelManager contentModelManager) { this.model = model; this.xsLoader = xsLoaderImpl; + this.contentModelManager = contentModelManager; this.elementMappings = new HashMap<>(); this.refinedElementMappings = new HashMap<>(); this.tracker = createFilesChangedTracker(model); @@ -174,18 +181,14 @@ XSObjectList getSubstitutionGroup(XSElementDeclaration elementDeclaration) { @Override public CMElementDeclaration findCMElement(DOMElement element, String namespace) { - List paths = new ArrayList<>(); - while (element != null && (namespace == null || namespace.equals(element.getNamespaceURI()))) { - paths.add(0, element); - element = element.getParentNode() instanceof DOMElement ? (DOMElement) element.getParentNode() : null; - } + List paths = collectPath(element, namespace); CMXSDElementDeclaration declaration = null; for (int i = 0; i < paths.size(); i++) { DOMElement elt = paths.get(i); if (i == 0) { - declaration = (CMXSDElementDeclaration) findElementDeclaration(elt.getLocalName(), namespace); + declaration = (CMXSDElementDeclaration) findElementDeclaration(elt.getLocalName()); } else { - declaration = (CMXSDElementDeclaration) declaration.findCMElement(elt.getLocalName(), namespace); + declaration = (CMXSDElementDeclaration) declaration.findCMElement(elt.getLocalName(), elt.getNamespaceURI()); } if (declaration == null) { break; @@ -208,7 +211,38 @@ public CMElementDeclaration findCMElement(DOMElement element, String namespace) return declaration; } - private XSTypeDefinition findXsiType(DOMElement element) { + private static List collectPath(DOMElement element, String namespace) { + List paths = new ArrayList<>(); + DOMElement current = element; + while (current != null && (namespace == null || namespace.equals(current.getNamespaceURI()))) { + paths.add(0, current); + current = current.getParentNode() instanceof DOMElement ? (DOMElement) current.getParentNode() : null; + } + if (hasXSITypeAncestor(current)) { + while (current != null) { + paths.add(0, current); + current = current.getParentNode() instanceof DOMElement ? (DOMElement) current.getParentNode() : null; + } + } + return paths; + } + + private static boolean hasXSITypeAncestor(DOMElement element) { + DOMElement current = element; + while (current != null) { + if (hasXSITypeValue(current)) { + return true; + } + current = current.getParentNode() instanceof DOMElement ? (DOMElement) current.getParentNode() : null; + } + return false; + } + + private static boolean hasXSITypeValue(DOMElement element) { + return getXSITypeValue(element) != null; + } + + private static String getXSITypeValue(DOMElement element) { org.w3c.dom.NamedNodeMap attrs = element.getAttributes(); if (attrs == null) { return null; @@ -216,24 +250,53 @@ private XSTypeDefinition findXsiType(DOMElement element) { for (int i = 0; i < attrs.getLength(); i++) { Node attr = attrs.item(i); if (attr.getLocalName().equals("type") && XSISchemaModel.XSI_WEBSITE.equals(attr.getNamespaceURI())) { - String[] possiblyQualifiedType = attr.getNodeValue().split(":", 2); - javax.xml.namespace.QName qualifiedType; - if (possiblyQualifiedType.length == 1) { - qualifiedType = new javax.xml.namespace.QName( - null, - possiblyQualifiedType[0]); - } else { - qualifiedType = new javax.xml.namespace.QName( - element.getNamespaceURI(possiblyQualifiedType[0]), - possiblyQualifiedType[1]); + return attr.getNodeValue(); + } + } + return null; + } + + private XSTypeDefinition findXsiType(DOMElement element) { + String typeValue = getXSITypeValue(element); + if (typeValue == null) { + return null; + } + String[] possiblyQualifiedType = typeValue.split(":", 2); + javax.xml.namespace.QName qualifiedType; + if (possiblyQualifiedType.length == 1) { + qualifiedType = new javax.xml.namespace.QName(null, possiblyQualifiedType[0]); + } else { + qualifiedType = new javax.xml.namespace.QName( + element.getNamespaceURI(possiblyQualifiedType[0]), + possiblyQualifiedType[1]); + } + // Try to find the type in the current schema model. + XSTypeDefinition exactType = (XSTypeDefinition) model.getComponents(XSConstants.TYPE_DEFINITION) + .get(qualifiedType); + if (exactType != null) { + return exactType; + } + // Try to find the type in the models bound to the XML document for the type namespace. + if (contentModelManager != null && qualifiedType.getNamespaceURI() != null) { + DOMDocument xmlDocument = element.getOwnerDocument(); + if (xmlDocument != null) { + Collection documents = contentModelManager.findCMDocument(xmlDocument, + qualifiedType.getNamespaceURI()); + for (CMDocument document : documents) { + if (document instanceof CMXSDDocument) { + exactType = (XSTypeDefinition) ((CMXSDDocument) document).model + .getComponents(XSConstants.TYPE_DEFINITION).get(qualifiedType); + if (exactType != null) { + return exactType; + } + } } - return (XSTypeDefinition) model.getComponents(XSConstants.TYPE_DEFINITION).get(qualifiedType); } } return null; } - private CMElementDeclaration findElementDeclaration(String tag, String namespace) { + private CMElementDeclaration findElementDeclaration(String tag) { for (CMElementDeclaration cmElement : getElements()) { if (cmElement.getLocalName().equals(tag)) { return cmElement; diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaHoverExtensionsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaHoverExtensionsTest.java index 6c06fb40a..c3acf991d 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaHoverExtensionsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaHoverExtensionsTest.java @@ -425,12 +425,67 @@ public boolean isOpen() { })); } + /** + * See https://github.com/eclipse-lemminx/lemminx/issues/1787 + * + * Hover documentation for child elements of xsi:type-derived complex types + * across namespaces. + * + * @throws BadLocationException + * @throws MalformedURIException + */ + @Test + public void testHoverXSITypeDerivedChildAcrossNamespaces() throws BadLocationException, MalformedURIException { + String aSchemaURI = getXMLSchemaFileURI("xsi-type-derived/a.xsd"); + String bSchemaURI = getXMLSchemaFileURI("xsi-type-derived/b.xsd"); + + // Hover on should show documentation from a.xsd + String xmlA = ""; + assertHover(xmlA, "src/test/resources/xsd/xsi-type-derived/test.xml", + "Documentation for Title from namespace a" + // + System.lineSeparator() + // + System.lineSeparator() + "Source: [a.xsd](" + aSchemaURI + ")", + r(7, 5, 7, 12)); + + // Hover on should show documentation from b.xsd + String xmlB = ""; + assertHover(xmlB, "src/test/resources/xsd/xsi-type-derived/test.xml", + "Documentation for Title from namespace b" + // + System.lineSeparator() + // + System.lineSeparator() + "Source: [b.xsd](" + bSchemaURI + ")", + r(7, 5, 7, 12)); + } + private static void assertHover(String value, String expectedHoverLabel, Range expectedHoverRange) throws BadLocationException { XMLAssert.assertHover(new XMLLanguageService(), value, "src/test/resources/catalogs/catalog.xml", null, expectedHoverLabel, expectedHoverRange); } + private static void assertHover(String value, String fileURI, String expectedHoverLabel, Range expectedHoverRange) + throws BadLocationException { + XMLAssert.assertHover(new XMLLanguageService(), value, null, fileURI, + expectedHoverLabel, expectedHoverRange); + } + private static String getXMLSchemaFileURI(String schemaURI) throws MalformedURIException { return XMLEntityManager.expandSystemId("xsd/" + schemaURI, "src/test/resources/test.xml", true).replace("///", "/"); diff --git a/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/a.xsd b/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/a.xsd new file mode 100644 index 000000000..822af875d --- /dev/null +++ b/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/a.xsd @@ -0,0 +1,22 @@ + + + + + + + + + + + + Documentation for Title from namespace a + + + + + + + diff --git a/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/b.xsd b/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/b.xsd new file mode 100644 index 000000000..8fc1d3977 --- /dev/null +++ b/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/b.xsd @@ -0,0 +1,22 @@ + + + + + + + + + + + + Documentation for Title from namespace b + + + + + + + diff --git a/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/base.xsd b/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/base.xsd new file mode 100644 index 000000000..444a3ddc5 --- /dev/null +++ b/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/base.xsd @@ -0,0 +1,11 @@ + + + + + + Base Data type documentation + + + diff --git a/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/main.xsd b/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/main.xsd new file mode 100644 index 000000000..e909a7ab2 --- /dev/null +++ b/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/main.xsd @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/test.xml b/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/test.xml new file mode 100644 index 000000000..5c63c2f5a --- /dev/null +++ b/org.eclipse.lemminx/src/test/resources/xsd/xsi-type-derived/test.xml @@ -0,0 +1,13 @@ + + + + + + + + +