diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenLemminxExtension.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenLemminxExtension.java index a4538a27..c7575931 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenLemminxExtension.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenLemminxExtension.java @@ -135,7 +135,8 @@ public class MavenLemminxExtension implements IXMLExtension { XMLMavenSettings settings = new XMLMavenSettings(); private URIResolverExtensionManager resolverExtensionManager; private List initialWorkspaceFolders = List.of(); - + private LinkedHashSet currentWorkspaceFolders = new LinkedHashSet<>(); + @Override public void doSave(ISaveContext context) { if (context.getType() == SaveContextType.SETTINGS) { @@ -445,8 +446,14 @@ public URIResolverExtensionManager getUriResolveExtentionManager() { return resolverExtensionManager; } + public LinkedHashSet getCurrentWorkspaceFolders() { + return currentWorkspaceFolders; + } + public void didChangeWorkspaceFolders(URI[] added, URI[] removed) { initialize(); + currentWorkspaceFolders.addAll(List.of(added != null? added : new URI[0])); + currentWorkspaceFolders.removeAll(List.of(removed != null ? removed : new URI[0])); WorkspaceReader workspaceReader = mavenRequest.getWorkspaceReader(); if (workspaceReader instanceof MavenLemminxWorkspaceReader reader) { Collection projectsToAdd = computeAddedWorkspaceProjects(added != null? added : new URI[0]); @@ -516,18 +523,42 @@ private static DOMDocument createDOMDocument(URI uri) { return null; } - private static String key(Dependency artifact) { + /** + * Creates a GAV key for a given Artifact + * + * @param artifact + * @return GAV key string + */ + public static String key(Dependency artifact) { return Optional.ofNullable(artifact.getGroupId()).orElse("") + ':' + Optional.ofNullable(artifact.getArtifactId()).orElse("") + ':' + Optional.ofNullable(artifact.getVersion()).orElse(""); } - - private static String key(Parent parent) { + + /** + * Creates a GAV key for a given Parent + * + * @param artifact + * @return GAV key string + */ + public static String key(Parent parent) { return Optional.ofNullable(parent.getGroupId()).orElse("") + ':' + Optional.ofNullable(parent.getArtifactId()).orElse("") + ':' + Optional.ofNullable(parent.getVersion()).orElse(""); } - + + /** + * Creates a GAV key for a given Maven Project + * + * @param artifact + * @return GAV key string + */ + public static String key(MavenProject project) { + return Optional.ofNullable(project.getGroupId()).orElse("") + ':' + + Optional.ofNullable(project.getArtifactId()).orElse("") + ':' + + Optional.ofNullable(project.getVersion()).orElse(""); + } + private void adUrisdParentFirst(String artifactKey, LinkedHashMap parentByDep, HashMap uriByDep, diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/ExtractPropertyCodeAction.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/ExtractPropertyCodeAction.java index 6a406c41..0632b79f 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/ExtractPropertyCodeAction.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/ExtractPropertyCodeAction.java @@ -8,10 +8,24 @@ *******************************************************************************/ package org.eclipse.lemminx.extensions.maven.participants.codeaction; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.ID_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PARENT_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PROJECT_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PROPERTIES_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.VERSION_ELT; +import static org.eclipse.lemminx.extensions.maven.MavenLemminxExtension.key; + +import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CancellationException; import java.util.logging.Level; import java.util.logging.Logger; @@ -20,6 +34,7 @@ import org.apache.maven.project.MavenProject; import org.eclipse.lemminx.commons.BadLocationException; import org.eclipse.lemminx.commons.CodeActionFactory; +import org.eclipse.lemminx.commons.TextDocument; import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.dom.DOMElement; import org.eclipse.lemminx.dom.DOMNode; @@ -30,10 +45,17 @@ import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionParticipant; import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionRequest; import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.ResourceOperation; +import org.eclipse.lsp4j.TextDocumentEdit; import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; public class ExtractPropertyCodeAction implements ICodeActionParticipant { private static final Logger LOGGER = Logger.getLogger(ExtractPropertyCodeAction.class.getName()); @@ -76,6 +98,13 @@ public void doCodeActionUnconditional(ICodeActionRequest request, List`, ``, + // `` etc. + // The only value extraction available for project's `` element. + if(!checkParentElement(node)) { + return; + } + String propertyValue = node.getNodeValue(); if (propertyValue == null || propertyValue.trim().isBlank()) { return; @@ -94,36 +123,116 @@ public void doCodeActionUnconditional(ICodeActionRequest request, List properties = ParticipantUtils.getMavenProjectProperties(project); String propertyName = constructPropertyName(project, node, properties, cancelChecker); + // Gather ` header text edits cancelChecker.checkCanceled(); - Optional propettiesElement = DOMUtils.findChildElement(document.getDocumentElement(), "properties"); + LinkedHashSet parentProjects = findParentProjects(project); + LinkedHashMap headerTextEdits = new LinkedHashMap<>(); + String newline = request.getXMLGenerator().getLineDelimiter(); + String indent = DOMUtils.getOneLevelIndent(request); + URI thisProjectUri = ParticipantUtils.normalizedUri(document.getDocumentURI()); + parentProjects.stream().forEach(p -> { + cancelChecker.checkCanceled(); + URI projectUri = ParticipantUtils.normalizedUri(p.getFile().toURI().toString()); + DOMDocument projectDocumentt = null; + if (projectUri.equals(thisProjectUri)) { + projectDocumentt = document; + } else { + projectDocumentt = org.eclipse.lemminx.utils.DOMUtils.loadDocument( + p.getFile().toURI().toString(), + document.getResolverExtensionManager()); + } - TextEdit propertiesHeaderTextEdit = createPropertiesHeaderTextEdit(request, - propettiesElement.isPresent() ? propettiesElement.get() : document.getDocumentElement(), + TextEdit projectHeaderTextEdit = createPropertiesHeaderTextEdit(projectDocumentt, newline, indent, + propertyName, propertyValue, cancelChecker); + if (projectHeaderTextEdit != null) { + headerTextEdits.put(p, createProjectTextDocumentEdit( + projectDocumentt.getTextDocument(), Arrays.asList(projectHeaderTextEdit))); + } + }); + + cancelChecker.checkCanceled(); + List exactValueProperties = properties.entrySet().stream().filter(e -> propertyValue.equals(e.getValue())) + .map(Entry::getKey).toList(); + + // Create code action to extract a single value as new property in current module + TextEdit headerTextEdit = createPropertiesHeaderTextEdit(document, newline, indent, propertyName, propertyValue, cancelChecker); + Range singleRange = getSingleExtractPropertyRange(request, (DOMText)node, propertyName); + cancelChecker.checkCanceled(); - // Create code action to extract a single value into a property - List singleTextEdits = new ArrayList<>(); - singleTextEdits.add(propertiesHeaderTextEdit); - collectExtractPropertyTextEdit(request, (DOMText)node, propertyName, singleTextEdits, cancelChecker); - // The list should contain header and one extracted property - if (singleTextEdits.size() > 1) { + if (singleRange != null) { + // Extract as new property in current module + List singleTextEdits = new ArrayList<>(); + singleTextEdits.add(headerTextEdit); + singleTextEdits.add(new TextEdit(singleRange, "${" + propertyName + "}")); codeActions.add(CodeActionFactory.replace( - "Extract text value into a property", + "Extract as new property in current module", singleTextEdits, document.getTextDocument(), null)); + + // Replace with existing "${already.existing.property}" property + exactValueProperties.stream().forEach(p -> { + List singleReplaceTextEdits = new ArrayList<>(); + singleReplaceTextEdits.add(new TextEdit(singleRange, "${" + p + "}")); + codeActions.add(CodeActionFactory.replace( + "Replace with existing \"$" + p + "}\" property", + singleReplaceTextEdits, document.getTextDocument(), null)); + }); + + // Extract as new property in parent ggg:aaa:vvv + parentProjects.stream().forEach(p -> { + TextDocumentEdit projectHeaderEdit = headerTextEdits.get(p); + if (projectHeaderEdit != null) { + TextDocumentEdit singleBodyEdit = createProjectTextDocumentEdit(document.getTextDocument(), + Arrays.asList(new TextEdit(singleRange, "${" + propertyName + "}"))); + + codeActions.add(createReplaceCodeActione("Extract as new property in parent \"" + key(p) + "\"", + Arrays.asList(projectHeaderEdit, singleBodyEdit), null)); + } + }); } // Create code action to extract all the value entries into a properties - List multipleTextEdits = new ArrayList<>(); - multipleTextEdits.add(propertiesHeaderTextEdit); + List multipleRanges = new ArrayList<>(); collectExtractPropertyTextEdits(request, document.getDocumentElement(), (DOMText)node, - propertyName, propertyValue, multipleTextEdits, cancelChecker); + propertyName, propertyValue, multipleRanges, cancelChecker); // The list should contain header and two or more extracted properties - if (multipleTextEdits.size() > 2) { + if (multipleRanges.size() > 1) { + // Extract all values as new property in current module + List multipleTextEdits = new ArrayList<>(); + multipleTextEdits.add(headerTextEdit); + multipleRanges.stream().forEach(r -> { + multipleTextEdits.add(new TextEdit(r, "${" + propertyName + "}")); + }); codeActions.add(CodeActionFactory.replace( - "Extract all the text value entries into a property", + "Extract all values as new property in current module", multipleTextEdits, document.getTextDocument(), null)); + + // Replace all values with existing "${already.existing.property}" property + exactValueProperties.stream().forEach(p -> { + List multipleReplaceTextEdits = new ArrayList<>(); + multipleRanges.stream().forEach(r -> { + multipleReplaceTextEdits.add(new TextEdit(r, "${" + p + "}")); + }); + codeActions.add(CodeActionFactory.replace( + "Replace all values with existing \"$" + p + "}\" property", + multipleReplaceTextEdits, document.getTextDocument(), null)); + }); + + // Extract all values as new property in parent ggg:aaa:vvv + parentProjects.stream().forEach(p -> { + TextDocumentEdit projectHeaderEdit = headerTextEdits.get(p); + List multipleBodyEdits = new ArrayList<>(); + multipleRanges.stream().forEach(r -> { + multipleBodyEdits.add(new TextEdit(r, "${" + propertyName + "}")); + }); + if (projectHeaderEdit != null) { + codeActions.add(createReplaceCodeActione("Extract all values as new property in parent \"" + key(p) + "\"", + Arrays.asList(projectHeaderEdit, + createProjectTextDocumentEdit(document.getTextDocument(), multipleBodyEdits)), null)); + } + }); } } } catch (CancellationException e) { @@ -134,7 +243,7 @@ public void doCodeActionUnconditional(ICodeActionRequest request, List textEditss, CancelChecker cancelChecker) throws CancellationException { + List ranges, CancelChecker cancelChecker) throws CancellationException { cancelChecker.checkCanceled(); String textElementName = text.getParentElement().getNodeName(); @@ -143,59 +252,56 @@ void collectExtractPropertyTextEdits(ICodeActionRequest request, DOMElement root .filter(DOMText.class::isInstance).map(DOMText.class::cast) .filter(textNode -> value.equals(textNode.getNodeValue())) .findFirst(); - rootElementTextNode.ifPresent(rootTextNode -> - collectExtractPropertyTextEdit(request, rootTextNode, property, textEditss, cancelChecker)); + rootElementTextNode.stream().map(rootTextNode -> getSingleExtractPropertyRange(request, rootTextNode, property)) + .filter(Objects::nonNull).forEach(ranges::add); } // collect in this element's children rootElement.getChildren().stream().filter(DOMElement.class::isInstance).map(DOMElement.class::cast) - .forEach(child -> collectExtractPropertyTextEdits(request, child, text, property, value, textEditss, cancelChecker)); + .forEach(child -> collectExtractPropertyTextEdits(request, child, text, property, value, ranges, cancelChecker)); } - private static TextEdit createPropertiesHeaderTextEdit(ICodeActionRequest request, DOMElement parent, String propertyName, String propertyValue, - CancelChecker cancelChecker) throws CancellationException { + private static TextEdit createPropertiesHeaderTextEdit(DOMDocument document, String newline, String indent , + String propertyName, String propertyValue, CancelChecker cancelChecker) throws CancellationException { + DOMElement root = DOMUtils.findChildElement(document.getDocumentElement(), "properties") + .orElse(document.getDocumentElement()); cancelChecker.checkCanceled(); try { - String newline = request.getXMLGenerator().getLineDelimiter(); - String indent = DOMUtils.getOneLevelIndent(request); - // Properties section - int start = parent.getStartTagCloseOffset() + 1; + int start = root.getStartTagCloseOffset() + 1; StringBuilder sb = new StringBuilder(); sb.append(newline); - if ("project".equals(parent.getNodeName())) { + if (PROJECT_ELT.equals(root.getNodeName())) { // Create a new properties section header - sb.append(indent).append("").append(newline); + sb.append(indent).append('<').append(PROPERTIES_ELT).append('>').append(newline); } // Add property sb.append(indent).append(indent) .append('<').append(propertyName).append('>').append(propertyValue) - .append('<').append('/').append(propertyName).append('>').append(newline); + .append('<').append('/').append(propertyName).append('>'); - if ("project".equals(parent.getNodeName())) { + if (PROJECT_ELT.equals( root.getNodeName())) { // Create a new properties section footer - sb.append(indent).append("").append(newline); + sb.append(newline).append(indent).append("').append(newline); } - Position startPosition = request.getDocument().positionAt(start); + Position startPosition = document.positionAt(start); cancelChecker.checkCanceled(); - return new TextEdit(new Range(startPosition, startPosition), - sb.toString()); + return new TextEdit(new Range(startPosition, startPosition), sb.toString()); } catch (BadLocationException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } return null; } - - private static void collectExtractPropertyTextEdit(ICodeActionRequest request, DOMText text, String propertyName, - List textEdits, CancelChecker cancelChecker) throws CancellationException { - cancelChecker.checkCanceled(); + + private static Range getSingleExtractPropertyRange(ICodeActionRequest request, DOMText text, String propertyName) { try { Position startPosition = request.getDocument().positionAt(text.getStart()); Position endPosition = request.getDocument().positionAt(text.getEnd()); - textEdits.add(new TextEdit(new Range(startPosition, endPosition), "${" + propertyName + "}")); + return new Range(startPosition, endPosition); } catch (BadLocationException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); + return null; } } @@ -214,9 +320,9 @@ private static String constructPropertyName(MavenProject project, DOMNode node, if (property == null) { cancelChecker.checkCanceled(); DOMElement grarndParent = parent.getParentElement(); - Optional id = DOMUtils.findChildElementText(grarndParent, "id"); + Optional id = DOMUtils.findChildElementText(grarndParent, ID_ELT); if (id.isEmpty()) { // Checking only for two steps back - id = DOMUtils.findChildElementText(grarndParent.getParentNode(), "id"); + id = DOMUtils.findChildElementText(grarndParent.getParentNode(), ID_ELT); } if (id.isPresent() && !id.get().isBlank()) { @@ -241,7 +347,7 @@ private static String constructPropertyName(MavenProject project, DOMNode node, cancelChecker.checkCanceled(); return clearPropertyName(property); } - + private static String clearPropertyName(String value) { StringBuilder sb = new StringBuilder(); value.trim().chars() @@ -258,4 +364,53 @@ private static String clearPropertyName(String value) { }); return sb.toString(); } + + /* + * Returns the list of parent projects starting from an existing Workspace Root + */ + private LinkedHashSet findParentProjects(MavenProject child) { + LinkedHashSet parents = new LinkedHashSet<>(); + if (child != null) { + LinkedHashSet workspaceRoots = plugin.getCurrentWorkspaceFolders(); + MavenProject parent = child; + while ((parent = parent.getParent()) != null) { + URI parentUri = ParticipantUtils.normalizedUri(parent.getFile().toURI().toString()); + if (isInWorkspacet(workspaceRoots, parentUri)) { + parents.add(parent); + } + } + } + return parents; + } + + private static boolean isInWorkspacet(Set wsRoots, URI uri) { + String uriPath = uri.normalize().getPath(); + return wsRoots.stream().map(URI::normalize) + .filter(u -> u.getScheme().equals(uri.getScheme())) + .map(URI::getPath).filter(uriPath::startsWith).findAny().isPresent(); + } + + private static TextDocumentEdit createProjectTextDocumentEdit(TextDocument textDocument, List projectTextEdits) { + VersionedTextDocumentIdentifier projectVersionedTextDocumentIdentifier = + new VersionedTextDocumentIdentifier(textDocument.getUri(), textDocument.getVersion()); + return new TextDocumentEdit(projectVersionedTextDocumentIdentifier, projectTextEdits); + } + + private static CodeAction createReplaceCodeActione(String title, List replace, Diagnostic diagnostic) { + CodeAction insertContentAction = new CodeAction(title); + insertContentAction.setKind(CodeActionKind.QuickFix); + insertContentAction.setDiagnostics(Arrays.asList(diagnostic)); + + List> documentChanges = new ArrayList<>(); + replace.stream().forEach(change -> documentChanges.add(Either.forLeft(change))); + insertContentAction.setEdit( new WorkspaceEdit(documentChanges)); + return insertContentAction; + } + + private static boolean checkParentElement(DOMNode node) { + DOMElement parent = node.getParentElement(); + return parent != null + && (!PARENT_ELT.equals(parent.getNodeName()) + || VERSION_ELT.equals(node.getNodeName())); + } } diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/InlinePropertyCodeAction.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/InlinePropertyCodeAction.java index 57804d2f..a5f42b70 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/InlinePropertyCodeAction.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/InlinePropertyCodeAction.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.logging.Level; import java.util.logging.Logger; @@ -27,6 +28,7 @@ import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionParticipant; import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionRequest; import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.jsonrpc.CancelChecker; @@ -63,19 +65,28 @@ public void doCodeActionUnconditional(ICodeActionRequest request, List textEdits = new ArrayList<>(); collectInlinePropertyTextEdits(document.getDocumentElement(), "${" + mavenProperty.getValue() + "}", value, textEdits, cancelChecker); + + if (textEdits.size() > 0) { + // Replace the property with its value only in current node + TextEdit thisEdit = textEdits.stream() + .filter(e -> rangeContains(e.getRange(), range.getStart())) + .findFirst().orElse(null); + if (thisEdit != null) { + codeActions.add(CodeActionFactory.replace( + "Inline Property", thisEdit.getRange(), thisEdit.getNewText(), + document.getTextDocument(), null)); + } + } + if (textEdits.size() > 1) { + // Replace the property with its value in entire document codeActions.add(CodeActionFactory.replace( - "Replace all property entries with its value", + "Inline all Properties", textEdits, document.getTextDocument(), null)); } } @@ -87,6 +98,24 @@ public void doCodeActionUnconditional(ICodeActionRequest request, List position.getLine()) { + return false; + } + if (start.getLine() == position.getLine() && start.getCharacter() > position.getCharacter()) { + return false; + } + Position end = range.getEnd(); + if (end.getLine() < position.getLine()) { + return false; + } + if (end.getLine() == position.getLine() && end.getCharacter() < position.getLine()) { + return false; + } + + return true; + } void collectInlinePropertyTextEdits(DOMElement rootElement, String property, String value, List textEditss, CancelChecker cancelChecker) throws CancellationException { cancelChecker.checkCanceled(); diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/MavenCodeActionPropertyRefactoringTest.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/MavenCodeActionPropertyRefactoringTest.java index 663792b1..fbd9b8cd 100644 --- a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/MavenCodeActionPropertyRefactoringTest.java +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/codeaction/MavenCodeActionPropertyRefactoringTest.java @@ -13,24 +13,34 @@ import static org.eclipse.lemminx.XMLAssert.teOp; import static org.eclipse.lemminx.XMLAssert.testCodeActionsFor; import static org.eclipse.lemminx.extensions.maven.utils.MavenLemminxTestsUtils.createDOMDocument; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import org.eclipse.lemminx.commons.BadLocationException; import org.eclipse.lemminx.commons.TextDocument; import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.dom.DOMElement; import org.eclipse.lemminx.dom.LineIndentInfo; +import org.eclipse.lemminx.extensions.maven.MavenWorkspaceService; import org.eclipse.lemminx.extensions.maven.NoMavenCentralExtension; +import org.eclipse.lemminx.extensions.maven.utils.DOMUtils; import org.eclipse.lemminx.services.XMLLanguageService; +import org.eclipse.lemminx.services.extensions.IWorkspaceServiceParticipant; import org.eclipse.lemminx.settings.SharedSettings; import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -226,5 +236,110 @@ public void testCodeActionsExtracMultiplePropertyUses() throws Exception { testCodeActionsFor(xmlDocument.getText(), null, ranges.get(0), (String) null, xmlDocument.getDocumentURI(), sharedSettings, xmlLanguageService, -1, expectedCodeAction_1, expectedCodeAction_2); - } + } + + @Test + public void testCodeActionsExtracPropertyUsesWithChildren() throws Exception { + // We need the WORKSPACE projects to be placed to MavenProjectCache + IWorkspaceServiceParticipant workspaceService = xmlLanguageService.getWorkspaceServiceParticipants().stream().filter(MavenWorkspaceService.class::isInstance).findAny().get(); + assertNotNull(workspaceService); + + URI folderUri = getClass().getResource("/property-refactoring/child").toURI(); + WorkspaceFolder wsFolder = new WorkspaceFolder(folderUri.toString()); + + // Add folders to MavenProjectCache + workspaceService.didChangeWorkspaceFolders( + new DidChangeWorkspaceFoldersParams( + new WorkspaceFoldersChangeEvent ( + Arrays.asList(new WorkspaceFolder[] {wsFolder}), + Arrays.asList(new WorkspaceFolder[0])))); + + DOMDocument xmlDocument = createDOMDocument("/property-refactoring/child/pom.xml", xmlLanguageService); + String text = xmlDocument.getText(); + TextDocument document = xmlDocument.getTextDocument(); + + // 0.0.1 + String valueUse = "0.0.1-SNAPSHOT"; + List ranges = new ArrayList<>(); + + int index = 0; + for (index = text.indexOf(valueUse); index != -1; index = text.indexOf(valueUse, index + valueUse.length())) { + try { + int valueStart = index; + int valueEnd = valueStart + valueUse.length(); + + Position valueStartPosition = document.positionAt(valueStart + "".length()); + Position valueEndPosition = document.positionAt(valueEnd - "/".length()); + ranges.add(new Range(valueStartPosition, valueEndPosition)); + } catch (BadLocationException e) { + fail("Cannot find all value uses in test data", e); + } + } + assertTrue(ranges.size() > 0, "There should be at least one value use"); + + // Code action for extracting a single value into a property + + // TextEdit to add a header properties + int propertiesOffset = xmlDocument.getDocumentElement().getStartTagCloseOffset() + 1; + Position propertiesPosition = xmlDocument.positionAt(propertiesOffset); + LineIndentInfo indentInfo = xmlDocument.getLineIndentInfo(propertiesPosition.getLine()); + String lineDelimiter = indentInfo.getLineDelimiter(); + String indent = indentInfo.getWhitespacesIndent(); + + StringBuilder propertiesValue = new StringBuilder(); + propertiesValue.append(lineDelimiter) + .append(indent).append("").append(lineDelimiter) + .append(indent).append(indent) + .append("0.0.1-SNAPSHOT").append(lineDelimiter) + .append(indent).append("").append(lineDelimiter); + + List singleTextEdits = new ArrayList<>(); + singleTextEdits.add(new TextEdit(new Range(propertiesPosition, propertiesPosition), propertiesValue.toString())); + singleTextEdits.add(new TextEdit(ranges.get(0), "${test-dependency-2-version}")); + assertTrue(singleTextEdits.size() == 2, "The TextEdits size should be 2"); + CodeAction expectedCodeAction_1 = ca(null, + teOp(xmlDocument.getDocumentURI(), singleTextEdits.toArray(new TextEdit[singleTextEdits.size()]))); + + // Code action for replacing a single value with an existing property + + List singleReplaceTextEdits = new ArrayList<>(); + singleReplaceTextEdits.add(new TextEdit(ranges.get(0), "${test-version}")); + assertTrue(singleReplaceTextEdits.size() == 1, "The TextEdits size should be 1"); + CodeAction expectedCodeAction_2 = ca(null, + teOp(xmlDocument.getDocumentURI(), singleReplaceTextEdits.toArray(new TextEdit[singleReplaceTextEdits.size()]))); + + // Code action for extracting a single value into a property in parent project + + DOMDocument parentXmlDocument = createDOMDocument("/property-refactoring/child/parent/pom.xml", xmlLanguageService); + + // TextEdit to add a header properties + DOMElement properttiesElement = DOMUtils.findChildElement(parentXmlDocument.getDocumentElement(), "properties").orElse(null); + assertNotNull(properttiesElement, "Properties element not found in parent document"); + + propertiesOffset = properttiesElement.getStartTagCloseOffset() + 1; + propertiesPosition = parentXmlDocument.positionAt(propertiesOffset); + indentInfo = parentXmlDocument.getLineIndentInfo(propertiesPosition.getLine()); + lineDelimiter = indentInfo.getLineDelimiter(); + indent = indentInfo.getWhitespacesIndent(); + + propertiesValue = new StringBuilder(); + propertiesValue.append(lineDelimiter).append(indent).append(indent) + .append("0.0.1-SNAPSHOT"); + + List headerTextEdits = new ArrayList<>(); + headerTextEdits.add(new TextEdit(new Range(propertiesPosition, propertiesPosition), propertiesValue.toString())); + assertTrue(headerTextEdits.size() == 1, "The TextEdits size should be 2"); + + List bodyTextEdits = new ArrayList<>(); + bodyTextEdits.add(new TextEdit(ranges.get(0), "${test-dependency-2-version}")); + assertTrue(bodyTextEdits.size() == 1, "The TextEdits size should be 2"); + CodeAction expectedCodeAction_3 = ca(null, + teOp(parentXmlDocument.getDocumentURI(), headerTextEdits.toArray(new TextEdit[headerTextEdits.size()])), + teOp(xmlDocument.getDocumentURI(), bodyTextEdits.toArray(new TextEdit[bodyTextEdits.size()]))); + + // Test for expected code actions returned + testCodeActionsFor(xmlDocument.getText(), null, ranges.get(0), + (String) null, xmlDocument.getDocumentURI(), sharedSettings, xmlLanguageService, -1, + expectedCodeAction_1, expectedCodeAction_2, expectedCodeAction_3); + } } diff --git a/lemminx-maven/src/test/resources/property-refactoring/child/parent/pom.xml b/lemminx-maven/src/test/resources/property-refactoring/child/parent/pom.xml index 499472e5..82a026dc 100644 --- a/lemminx-maven/src/test/resources/property-refactoring/child/parent/pom.xml +++ b/lemminx-maven/src/test/resources/property-refactoring/child/parent/pom.xml @@ -9,7 +9,7 @@ org.test test-parent - 0.0.1-SNAPSHOT + 0.0.1 pom diff --git a/lemminx-maven/src/test/resources/property-refactoring/child/pom.xml b/lemminx-maven/src/test/resources/property-refactoring/child/pom.xml index 4215ff32..2c0b2922 100644 --- a/lemminx-maven/src/test/resources/property-refactoring/child/pom.xml +++ b/lemminx-maven/src/test/resources/property-refactoring/child/pom.xml @@ -5,7 +5,7 @@ org.test test-parent - 0.0.1-SNAPSHOT + 0.0.1 ./parent/pom.xml test-child @@ -15,7 +15,7 @@ ${test-group-id} test-dependency-2 - ${test-version} + 0.0.1-SNAPSHOT \ No newline at end of file