diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/definition/MavenDefinitionParticipant.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/definition/MavenDefinitionParticipant.java index 424e5528..1f37c285 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/definition/MavenDefinitionParticipant.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/definition/MavenDefinitionParticipant.java @@ -19,7 +19,6 @@ import java.util.Map; import java.util.Optional; import java.util.function.Predicate; -import java.util.stream.Collectors; import org.apache.maven.Maven; import org.apache.maven.model.Dependency; @@ -158,16 +157,18 @@ private LocationLink findMavenPropertyLocation(IDefinitionRequest request) { DOMNode propertyDeclaration = null; Predicate isMavenProperty = (node) -> PROPERTIES_ELT.equals(node.getParentNode().getLocalName()); - if (childProj.getFile().toURI().toString().equals(xmlDocument.getDocumentURI())) { + URI childProjectUri = ParticipantUtils.normalizedUri(childProj.getFile().toURI().toString()); + URI thisProjectUri = ParticipantUtils.normalizedUri(xmlDocument.getDocumentURI()); + if (childProjectUri.equals(thisProjectUri)) { // Property is defined in the same file as the request propertyDeclaration = DOMUtils.findNodesByLocalName(xmlDocument, mavenProperty.getValue()).stream() - .filter(isMavenProperty).collect(Collectors.toList()).get(0); + .filter(isMavenProperty).findFirst().orElse(null); } else { DOMDocument propertyDeclaringDocument = org.eclipse.lemminx.utils.DOMUtils.loadDocument( childProj.getFile().toURI().toString(), request.getNode().getOwnerDocument().getResolverExtensionManager()); propertyDeclaration = DOMUtils.findNodesByLocalName(propertyDeclaringDocument, mavenProperty.getValue()) - .stream().filter(isMavenProperty).collect(Collectors.toList()).get(0); + .stream().filter(isMavenProperty).findFirst().orElse(null); } if (propertyDeclaration == null) { @@ -198,4 +199,4 @@ private static LocationLink toLocation(File target, DOMNode targetNode, Range or Range targetRange = XMLPositionUtility.createRange(targetNode); return new LocationLink(target.toURI().toString(), targetRange, targetRange, originRange); } -} +} \ No newline at end of file diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java index 3a1e15eb..97da2263 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java @@ -16,15 +16,17 @@ import static org.eclipse.lemminx.extensions.maven.DOMConstants.DEPENDENCY_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.PLUGINS_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.PLUGIN_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PROPERTIES_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.VERSION_ELT; import java.io.File; +import java.net.URI; import java.text.MessageFormat; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.function.UnaryOperator; import java.util.logging.Level; import java.util.logging.Logger; @@ -57,6 +59,7 @@ import org.eclipse.lemminx.services.extensions.HoverParticipantAdapter; import org.eclipse.lemminx.services.extensions.IHoverRequest; import org.eclipse.lemminx.services.extensions.IPositionRequest; +import org.eclipse.lemminx.utils.XMLPositionUtility; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.MarkupKind; @@ -139,6 +142,7 @@ yield hoverForProject(request, private static final String PomTextHover_managed_version_missing = "The managed version could not be determined."; private static final String PomTextHover_managed_location = "The artifact is managed in {0}"; private static final String PomTextHover_managed_location_missing = "The managed definition location could not be determined, probably defined by \"import\" scoped dependencies."; + private static final String PomTextHover_property_location = "The property is defined in {0}"; private static String getActualVersionText(boolean supportsMarkdown, MavenProject project) { if (project == null) { @@ -427,25 +431,56 @@ private Hover collectPluginConfiguration(IHoverRequest request) { private Hover collectProperty(IHoverRequest request, Map.Entry property) { boolean supportsMarkdown = request.canSupportMarkupKind(MarkupKind.MARKDOWN); + UnaryOperator toBold = supportsMarkdown ? MarkdownUtils::toBold : UnaryOperator.identity(); String lineBreak = MarkdownUtils.getLineBreak(supportsMarkdown); DOMDocument doc = request.getXMLDocument(); MavenProject project = plugin.getProjectCache().getLastSuccessfulMavenProject(doc); if (project != null) { Map allProps = ParticipantUtils.getMavenProjectProperties(project); - UnaryOperator toBold = supportsMarkdown ? MarkdownUtils::toBold : UnaryOperator.identity(); + return allProps.entrySet().stream() + .filter(prop -> property.getValue().equals(prop.getKey())) + .map(prop -> { + StringBuilder message = new StringBuilder(); + message.append(toBold.apply("Property: ")).append(prop.getKey()).append(lineBreak) + .append(toBold.apply("Value: ")).append(prop.getValue()).append(lineBreak); + + // Find location + MavenProject parentProject = project, childProj = project; + while (parentProject != null && parentProject.getProperties().containsKey(property.getValue())) { + childProj = parentProject; + parentProject = parentProject.getParent(); + } + + DOMNode propertyDeclaration = null; + Predicate isMavenProperty = (node) -> PROPERTIES_ELT.equals(node.getParentNode().getLocalName()); + + URI childProjectUri = ParticipantUtils.normalizedUri(childProj.getFile().toURI().toString()); + URI thisProjectUri = ParticipantUtils.normalizedUri(doc.getDocumentURI()); + if (childProjectUri.equals(thisProjectUri)) { + // Property is defined in the same file as the request + propertyDeclaration = DOMUtils.findNodesByLocalName(doc, property.getValue()).stream() + .filter(isMavenProperty).findFirst().orElse(null); + } else { + DOMDocument propertyDeclaringDocument = org.eclipse.lemminx.utils.DOMUtils.loadDocument( + childProj.getFile().toURI().toString(), + request.getNode().getOwnerDocument().getResolverExtensionManager()); + propertyDeclaration = DOMUtils.findNodesByLocalName(propertyDeclaringDocument, property.getValue()) + .stream().filter(isMavenProperty).findFirst().orElse(null); + } - for (Entry prop : allProps.entrySet()) { - String mavenProperty = prop.getKey(); - if (property.getValue().equals(mavenProperty)) { - String message = toBold.apply("Property: ") + mavenProperty + lineBreak + toBold.apply("Value: ") - + prop.getValue() + lineBreak; + if (propertyDeclaration != null) { + String uri = childProj.getFile().toURI().toString(); + Range targetRange = XMLPositionUtility.createRange(propertyDeclaration); + String sourceModelId = childProj.getGroupId() + ':' + childProj.getArtifactId() + ':' + childProj.getVersion(); + message.append(toBold.apply(MessageFormat.format(PomTextHover_property_location, + supportsMarkdown ? MarkdownUtils.toLink(uri, targetRange, sourceModelId, null) : sourceModelId))); + } Hover hover = new Hover( - new MarkupContent(supportsMarkdown ? MarkupKind.MARKDOWN : MarkupKind.PLAINTEXT, message)); + new MarkupContent(supportsMarkdown ? MarkupKind.MARKDOWN : MarkupKind.PLAINTEXT, message.toString())); hover.setRange(property.getKey()); return hover; - } - } + }).findAny().orElse(null); } return null; } diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MarkdownUtils.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MarkdownUtils.java index 8f9b0328..159f1462 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MarkdownUtils.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MarkdownUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2020, 2022 Red Hat Inc. and others. + * Copyright (c) 2020, 2023 Red Hat Inc. and others. * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ @@ -8,6 +8,11 @@ *******************************************************************************/ package org.eclipse.lemminx.extensions.maven.utils; +import java.util.Arrays; +import java.util.Objects; + +import org.eclipse.lsp4j.Range; + public class MarkdownUtils { public static final String LINE_BREAK = "\n\n"; @@ -47,12 +52,30 @@ public static String htmlXMLToMarkdown(String description) { } public static String toLink(String uri, String message, String title) { + return toLink(uri, null, message, title); + } + + public static String toLink(String uri, Range range, String message, String title) { StringBuilder link = new StringBuilder(); // [Message](http://example.com/ "Title") link.append('[').append(message != null ? message : "This link").append(']'); if (uri != null) { link.append('(').append(uri); + if(range != null && (range.getStart() != null || range.getEnd() != null)) { + // #L34,1-L35,3 + StringBuilder selection = new StringBuilder(); + Arrays.asList(range.getStart(), range.getEnd()).stream().filter(Objects::nonNull) + .forEach(r -> { + if (!selection.isEmpty()) { + selection.append('-'); + } + selection.append('L').append(r.getLine() + 1).append(',').append(r.getCharacter() + 1); + }); + if (!selection.isEmpty()) { + link.append('#').append(selection); + } + } if (title != null && title.trim().length() > 0) { link.append(' ').append('"').append(title.trim()).append('"'); } diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/ParticipantUtils.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/ParticipantUtils.java index 65022cdf..93cf28f2 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/ParticipantUtils.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/ParticipantUtils.java @@ -21,6 +21,7 @@ import static org.eclipse.lemminx.extensions.maven.DOMConstants.RELATIVE_PATH_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.VERSION_ELT; +import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -329,4 +330,13 @@ public static boolean match(Diagnostic diagnostic, String code) { return code == null ? diagnostic.getCode().getLeft() == null : code.equals(diagnostic.getCode().getLeft()); } + + public static URI normalizedUri(String uriString) { + try { + return URI.create(uriString).normalize(); + } catch (IllegalArgumentException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + return null; + } + } } diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenPropertyHoverTest.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenPropertyHoverTest.java new file mode 100644 index 00000000..9be5cf5e --- /dev/null +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenPropertyHoverTest.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.lemminx.extensions.maven.participants.hover; + +import static org.eclipse.lemminx.extensions.maven.utils.MavenLemminxTestsUtils.createDOMDocument; +import static org.eclipse.lemminx.XMLAssert.assertHover; +import static org.eclipse.lemminx.XMLAssert.r; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.concurrent.ExecutionException; + +import org.eclipse.lemminx.commons.BadLocationException; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.extensions.contentmodel.settings.ContentModelSettings; +import org.eclipse.lemminx.extensions.maven.NoMavenCentralExtension; +import org.eclipse.lemminx.extensions.maven.utils.ParticipantUtils; +import org.eclipse.lemminx.services.XMLLanguageService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(NoMavenCentralExtension.class) +public class MavenPropertyHoverTest { + private XMLLanguageService languageService; + + @BeforeEach + public void setUp() throws IOException { + languageService = new XMLLanguageService(); + } + + @AfterEach + public void tearDown() throws InterruptedException, ExecutionException { + languageService.dispose(); + languageService = null; + } + + @Test + public void testHpverForVariablePropertyDefinedInSameDocument() + throws IOException, InterruptedException, ExecutionException, URISyntaxException, BadLocationException { + DOMDocument document = createDOMDocument("/pom-with-properties-in-parent-for-definition.xml", languageService); + String text = document.getText(); + int offset = text.indexOf("${anotherProperty}") + "${".length(); + text = text.substring(0, offset) + '|' + text.substring(offset); + String expectedHoverText = """ + **Property:** anotherProperty + + **Value:** $ + + **The property is defined in [org.test:child:0.0.1-SNAPSHOT](%s#L16,3-L16,39)**"""; + + ContentModelSettings settings = new ContentModelSettings(); + settings.setUseCache(false); + assertHover(languageService, text, null, document.getDocumentURI(), + String.format(expectedHoverText, document.getDocumentURI()), + r(28, 15,28, 27), settings); + } + + @Test + public void testHpverForVariablePropertyDefinedInParentDocument() + throws IOException, InterruptedException, ExecutionException, URISyntaxException, BadLocationException { + DOMDocument document = createDOMDocument("/pom-with-properties-in-parent-for-definition.xml", languageService); + String text = document.getText(); + int offset = text.indexOf("${myProperty}") + "${".length(); + text = text.substring(0, offset) + '|' + text.substring(offset); + String expectedHoverText = """ + **Property:** myProperty + + **Value:** $ + + **The property is defined in [org.test:test:0.0.1-SNAPSHOT](%s#L12,3-L12,29)**"""; + + File documentFile = new File(ParticipantUtils.normalizedUri(document.getDocumentURI()).getPath()); + File expectedFile = new File(documentFile.getParent(), "pom-with-properties-for-definition.xml"); + ContentModelSettings settings = new ContentModelSettings(); + settings.setUseCache(false); + assertHover(languageService, text, null, document.getDocumentURI(), + String.format(expectedHoverText, expectedFile.toURI().toString()), + r(23, 15, 23, 22), settings); + } +} diff --git a/lemminx-maven/src/test/resources/pom-with-properties-in-parent-for-definition.xml b/lemminx-maven/src/test/resources/pom-with-properties-in-parent-for-definition.xml index e2aed4c7..20709dd0 100644 --- a/lemminx-maven/src/test/resources/pom-with-properties-in-parent-for-definition.xml +++ b/lemminx-maven/src/test/resources/pom-with-properties-in-parent-for-definition.xml @@ -23,6 +23,11 @@ someAritfact ${myProperty} + + test + anotherAritfact + ${anotherProperty} + \ No newline at end of file