From 7d09774bbed18f51b7d6c87b344134c0fbebcc68 Mon Sep 17 00:00:00 2001 From: Victor Rubezhny Date: Fri, 26 May 2023 19:46:42 +0200 Subject: [PATCH] Provide refactoring capabilities specific to maven properties. Rename... ... maven property. #383 --- lemminx-maven/pom.xml | 10 - .../maven/MavenLemminxExtension.java | 7 + .../extensions/maven/MavenProjectCache.java | 4 +- .../MavenPropertyRenameParticipant.java | 249 +++++++++++++++++ .../extensions/maven/utils/DOMUtils.java | 3 - .../MavenPropertyRenameParticipantTest.java | 250 ++++++++++++++++++ .../maven/utils/MavenLemminxTestsUtils.java | 2 +- .../property-refactoring/child/parent/pom.xml | 22 ++ .../property-refactoring/child/pom.xml | 21 ++ 9 files changed, 552 insertions(+), 16 deletions(-) create mode 100644 lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipant.java create mode 100644 lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipantTest.java create mode 100644 lemminx-maven/src/test/resources/property-refactoring/child/parent/pom.xml create mode 100644 lemminx-maven/src/test/resources/property-refactoring/child/pom.xml diff --git a/lemminx-maven/pom.xml b/lemminx-maven/pom.xml index 8242a706..1cb1ef54 100644 --- a/lemminx-maven/pom.xml +++ b/lemminx-maven/pom.xml @@ -223,16 +223,6 @@ true - - lemminx-snapshots - https://repo.eclipse.org/content/repositories/lemminx-snapshots/ - - true - - - false - - 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 73348361..a4538a27 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 @@ -84,6 +84,7 @@ import org.eclipse.lemminx.extensions.maven.participants.definition.MavenDefinitionParticipant; import org.eclipse.lemminx.extensions.maven.participants.diagnostics.MavenDiagnosticParticipant; import org.eclipse.lemminx.extensions.maven.participants.hover.MavenHoverParticipant; +import org.eclipse.lemminx.extensions.maven.participants.rename.MavenPropertyRenameParticipant; import org.eclipse.lemminx.extensions.maven.searcher.LocalRepositorySearcher; import org.eclipse.lemminx.extensions.maven.searcher.RemoteCentralRepositorySearcher; import org.eclipse.lemminx.extensions.maven.utils.DOMUtils; @@ -94,6 +95,7 @@ import org.eclipse.lemminx.services.extensions.completion.ICompletionParticipant; import org.eclipse.lemminx.services.extensions.diagnostics.IDiagnosticsParticipant; import org.eclipse.lemminx.services.extensions.hover.IHoverParticipant; +import org.eclipse.lemminx.services.extensions.rename.IRenameParticipant; import org.eclipse.lemminx.services.extensions.save.ISaveContext; import org.eclipse.lemminx.services.extensions.save.ISaveContext.SaveContextType; import org.eclipse.lemminx.settings.AllXMLSettings; @@ -119,6 +121,7 @@ public class MavenLemminxExtension implements IXMLExtension { private MavenDefinitionParticipant definitionParticipant; private MavenWorkspaceService workspaceServiceParticipant; private List codeActionParticipants = new ArrayList<>(); + private IRenameParticipant propertyRenameParticipant; private MavenProjectCache cache; private RemoteCentralRepositorySearcher centralSearcher; @@ -178,6 +181,8 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) { definitionParticipant = new MavenDefinitionParticipant(this); registry.registerDefinitionParticipant(definitionParticipant); registerCodeActionParticipants(registry); + propertyRenameParticipant = new MavenPropertyRenameParticipant(this); + registry.registerRenameParticipant(propertyRenameParticipant); } catch (Exception ex) { LOGGER.log(Level.SEVERE, ex.getCause().toString(), ex); } @@ -360,6 +365,8 @@ private DefaultPlexusContainer newPlexusContainer() throws PlexusContainerExcept @Override public void stop(XMLExtensionsRegistry registry) { + registry.unregisterRenameParticipant(propertyRenameParticipant); + this.propertyRenameParticipant = null; unregisterCodeActionParticipants(registry); registry.unregisterCompletionParticipant(completionParticipant); this.completionParticipant = null; diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java index 4e8c06c4..a0d3585d 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java @@ -126,7 +126,7 @@ private void check(DOMDocument document) { private void check(File pomFile) { Integer last = lastCheckedVersion.get(pomFile.toURI().normalize()); - if (last == null || last.intValue() < 1) { + if (last == null || last.intValue() < 0) { parseAndCache(pomFile); } } @@ -237,7 +237,7 @@ public InputStream getInputStream() throws IOException { private void parseAndCache(File pomFile) { URI uri = pomFile.toURI().normalize(); FileModelSource source = new FileModelSource(pomFile); - parseAndCache(uri, 1, source); + parseAndCache(uri, 0, source); } private ProjectBuildingRequest newProjectBuildingRequest() { diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipant.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipant.java new file mode 100644 index 00000000..a63ee001 --- /dev/null +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipant.java @@ -0,0 +1,249 @@ +/******************************************************************************* + * 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.rename; + +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PROPERTIES_ELT; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.maven.project.MavenProject; +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.extensions.maven.MavenLemminxExtension; +import org.eclipse.lemminx.extensions.maven.utils.DOMUtils; +import org.eclipse.lemminx.extensions.maven.utils.ParticipantUtils; +import org.eclipse.lemminx.services.extensions.rename.IPrepareRenameRequest; +import org.eclipse.lemminx.services.extensions.rename.IRenameParticipant; +import org.eclipse.lemminx.services.extensions.rename.IRenameRequest; +import org.eclipse.lemminx.services.extensions.rename.IRenameResponse; +import org.eclipse.lsp4j.PrepareRenameResult; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentEdit; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +public class MavenPropertyRenameParticipant implements IRenameParticipant { + private static final Logger LOGGER = Logger.getLogger(MavenPropertyRenameParticipant.class.getName()); + + private static final String PROPERTY_START = "${"; + private static final String PROPERTY_END = "}"; + + private final MavenLemminxExtension plugin; + + public MavenPropertyRenameParticipant(MavenLemminxExtension plugin) { + this.plugin = plugin; + } + + @Override + public Either prepareRename(IPrepareRenameRequest request, + CancelChecker cancelChecker) { + if (!(request.getNode() instanceof DOMElement element)) { + return null; + } + + cancelChecker.checkCanceled(); + MavenProject project = plugin.getProjectCache().getLastSuccessfulMavenProject(request.getXMLDocument()); + + cancelChecker.checkCanceled(); + Range range = getMavenPropertyRange(request.getOffset(), element, project); + + cancelChecker.checkCanceled(); + return range == null ? null : Either.forLeft(range); + } + + @Override + public void doRename(IRenameRequest request, IRenameResponse renameResponse, CancelChecker cancelChecker) { + DOMDocument document = request.getXMLDocument(); + String newPropertyName = request.getNewText(); + + if (!(request.getNode() instanceof DOMElement element)) { + return; + } + + cancelChecker.checkCanceled(); + MavenProject thisProject = plugin.getProjectCache().getLastSuccessfulMavenProject(document); + if (thisProject == null) { + return; + } + + cancelChecker.checkCanceled(); + Range range = getMavenPropertyRange(request.getOffset(), element, thisProject); + if (range == null) { + return; + } + + cancelChecker.checkCanceled(); + LinkedHashSet projects = new LinkedHashSet<>(); + projects.add(thisProject); + plugin.getProjectCache().getProjects().stream().forEach(child -> + projects.addAll(findParentsOfChildProject(thisProject, child))); + + String propertyName = element.getNodeName(); + URI thisProjectUri = ParticipantUtils.normalizedUri(document.getDocumentURI()); + projects.stream().forEach(project -> { + cancelChecker.checkCanceled(); + URI projectUri = ParticipantUtils.normalizedUri(project.getFile().toURI().toString()); + DOMDocument projectDocumentt = null; + if (projectUri.equals(thisProjectUri)) { + projectDocumentt = document; + } else { + projectDocumentt = org.eclipse.lemminx.utils.DOMUtils.loadDocument( + project.getFile().toURI().toString(), + request.getNode().getOwnerDocument().getResolverExtensionManager()); + } + + cancelChecker.checkCanceled(); + // Collect Text Edits for the document + List projectTextEdits = new ArrayList<>(); + collectPropertyElementTextEdits(projectDocumentt, propertyName, newPropertyName, projectTextEdits, cancelChecker); + collectPropertyUseTextEdits(projectDocumentt.getDocumentElement(), propertyName, newPropertyName, projectTextEdits, cancelChecker); + VersionedTextDocumentIdentifier projectVersionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier( + projectDocumentt.getTextDocument().getUri(), projectDocumentt.getTextDocument().getVersion()); + renameResponse.addTextDocumentEdit(new TextDocumentEdit(projectVersionedTextDocumentIdentifier, projectTextEdits)); + }); + + cancelChecker.checkCanceled(); + } + + /* + * Returns the list of parent projects between the given child and parent project, + * the list includes the given child, but not the parent. + */ + private static List findParentsOfChildProject(MavenProject parent, MavenProject child) { + List parents = new LinkedList<>(); + MavenProject currentChild = child; + URI childProjectUri = ParticipantUtils.normalizedUri(currentChild.getFile().toURI().toString()); + URI thisProjectUri = ParticipantUtils.normalizedUri(parent.getFile().toURI().toString()); + + while (currentChild != null) { + parents.add(currentChild); // Include the child as well + if (childProjectUri.equals(thisProjectUri)) { + break; // Stop searching + } + currentChild = currentChild.getParent(); + if (currentChild != null) { + childProjectUri = ParticipantUtils.normalizedUri(currentChild.getFile().toURI().toString()); + } + } + return currentChild != null ? parents : Collections.emptyList(); + } + + private static void collectPropertyElementTextEdits(DOMDocument document, String propertyName, String newPropertyName, + List textEdits, CancelChecker cancelChecker) { + DOMUtils.findChildElement(document.getDocumentElement(), PROPERTIES_ELT).ifPresent(properties -> { + DOMUtils.findChildElement(properties, propertyName).ifPresent(property -> { + int startTagOpenOffset = property.getStartTagOpenOffset() + 1; + int endTagOpenOffset = property.getEndTagOpenOffset() + 2; + try { + Range startRange = new Range(document.positionAt(startTagOpenOffset), + document.positionAt(startTagOpenOffset + propertyName.length())); + Range endRange = new Range(document.positionAt(endTagOpenOffset), + document.positionAt(endTagOpenOffset + propertyName.length())); + textEdits.add(new TextEdit(startRange, newPropertyName)); + textEdits.add(new TextEdit(endRange, newPropertyName)); + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + }); + }); + } + + private static void collectPropertyUseTextEdits(DOMElement rootElement, String property, String newPropertyName, + List textEditss, CancelChecker cancelChecker) throws CancellationException { + cancelChecker.checkCanceled(); + + // Check this element's text + collectInElementTextEdits(rootElement, property, newPropertyName, textEditss, cancelChecker); + + // collect in this element's children + rootElement.getChildren().stream().filter(DOMElement.class::isInstance).map(DOMElement.class::cast) + .forEach(child -> collectPropertyUseTextEdits(child, property, newPropertyName, textEditss, cancelChecker)); + } + + private static void collectInElementTextEdits(DOMElement element, String propertyName, String newPropertyName, + List textEditss, CancelChecker cancelChecker) throws CancellationException { + TextDocument textDocument = element.getOwnerDocument().getTextDocument(); + String propertyUse = PROPERTY_START + propertyName + PROPERTY_END; + DOMUtils.findElementTextChildren(element).stream() + .filter(text -> text.getData().contains(propertyUse)) + .forEach(text -> { + String data = text.getData(); + int index = 0; + for (index = data.indexOf(propertyUse); index != -1; index = data.indexOf(propertyUse, index + propertyUse.length())) { + cancelChecker.checkCanceled(); + try { + int propertyUseStart = text.getStart() + index; + int replaceStart = propertyUseStart + PROPERTY_START.length(); + int replaceEnd= replaceStart + propertyName.length(); + Range range = new Range(textDocument.positionAt(replaceStart), + textDocument.positionAt(replaceEnd)); + textEditss.add(new TextEdit(range, newPropertyName)); + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + return; + } + } + }); + } + + private static Range getMavenPropertyRange(int offset, DOMElement propertyElement, MavenProject project) { + if (propertyElement == null || project == null) { + return null; + } + DOMElement parentElement = propertyElement.getParentElement(); + if (parentElement == null || !PROPERTIES_ELT.equals(parentElement.getNodeName())) { + return null; + } + + if (!propertyElement.hasEndTag()) { + return null; + } + + int startTagOpenOffset = propertyElement.getStartTagOpenOffset() + 1; + int startTagCloseOffset = propertyElement.getStartTagCloseOffset(); + int endTagOpenOffset = propertyElement.getEndTagOpenOffset() + 2; + int endTagCloseOffset = propertyElement.getEndTagCloseOffset(); + + DOMDocument document = propertyElement.getOwnerDocument(); + Range range = null; + try { + if (offset >= startTagOpenOffset && offset < startTagCloseOffset) { + range = new Range(document.positionAt(startTagOpenOffset), document.positionAt(startTagCloseOffset)); + } else if (offset >= endTagOpenOffset && offset < endTagCloseOffset) { + range = new Range(document.positionAt(endTagOpenOffset), document.positionAt(endTagCloseOffset)); + } else { + return null; + } + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + return null; + } + + String mavenProperty = propertyElement.getNodeName(); + Map properties = ParticipantUtils.getMavenProjectProperties(project); + String value = properties.get(mavenProperty); + if (value == null) { + return null; + } + return range; + } +} diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/DOMUtils.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/DOMUtils.java index 9f5f23b5..c2f25837 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/DOMUtils.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/DOMUtils.java @@ -20,11 +20,8 @@ import org.eclipse.lemminx.dom.DOMElement; import org.eclipse.lemminx.dom.DOMNode; import org.eclipse.lemminx.dom.DOMText; -import org.eclipse.lemminx.extensions.contentmodel.utils.XMLGenerator; import org.eclipse.lemminx.services.extensions.IPositionRequest; import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionRequest; -import org.eclipse.lemminx.services.extensions.completion.ICompletionRequest; -import org.eclipse.lemminx.settings.XMLGeneralClientSettings; import org.w3c.dom.Text; public class DOMUtils { diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipantTest.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipantTest.java new file mode 100644 index 00000000..1627fdeb --- /dev/null +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/rename/MavenPropertyRenameParticipantTest.java @@ -0,0 +1,250 @@ +/******************************************************************************* + * 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.rename; + +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PROPERTIES_ELT; +import static org.eclipse.lemminx.extensions.maven.utils.MavenLemminxTestsUtils.createDOMDocument; +import static org.eclipse.lemminx.utils.TextEditUtils.creatTextDocumentEdit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.Optional; + +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.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.lsp4j.DidChangeWorkspaceFoldersParams; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.PrepareRenameResult; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(NoMavenCentralExtension.class) +public class MavenPropertyRenameParticipantTest { + private XMLLanguageService xmlLanguageService = new XMLLanguageService(); + + @Test + public void testRenameMavenProperty() throws Exception { + String propertyName = "myPropertyGroupId"; + String newPropertyName = "new-group-id"; + DOMDocument xmlDocument = createDOMDocument("/property-refactoring/pom-with-property.xml", xmlLanguageService); + + Optional properties = DOMUtils.findChildElement(xmlDocument.getDocumentElement(), PROPERTIES_ELT); + assertTrue(properties.isPresent(), "'' Element doesn't exist!"); + + Optional property = DOMUtils.findChildElement(properties.get(), propertyName); + assertTrue(properties.isPresent(), "'<" + propertyName + ">' Element doesn't exist!"); + + DOMElement propertyElement = property.get(); + + // Save property use ranges for testing + int startTagOpenOffset = propertyElement.getStartTagOpenOffset() + 1; + int startTagCloseOffset = propertyElement.getStartTagCloseOffset(); + int endTagOpenOffset = propertyElement.getEndTagOpenOffset() + 2; + int endTagCloseOffset = propertyElement.getEndTagCloseOffset(); + + Range expectedStartTagRange = + new Range(xmlDocument.positionAt(startTagOpenOffset), xmlDocument.positionAt(startTagCloseOffset)); + Range expectedEndTagRange = + new Range(xmlDocument.positionAt(endTagOpenOffset), xmlDocument.positionAt(endTagCloseOffset)); + List propertyUseRanges =collectMavenPropertyUsages(xmlDocument, propertyName); + + // Expected changes: + // - Start and and tags of maven property definition + // - one use of maven property + List expectedTextEdits = new ArrayList<>(); + expectedTextEdits.add(new TextEdit(expectedStartTagRange, newPropertyName)); + expectedTextEdits.add(new TextEdit(expectedEndTagRange, newPropertyName)); + propertyUseRanges.stream().map(r -> new TextEdit(r,newPropertyName)) + .forEach(expectedTextEdits::add); + // final List> documentChanges + WorkspaceEdit expectedRenameResult = new WorkspaceEdit(Arrays.asList( + Either.forLeft(creatTextDocumentEdit(xmlDocument, expectedTextEdits)))); + + // Test renaming start tag of maven property definition + Position startTagMiddle = xmlDocument.positionAt((startTagOpenOffset + startTagCloseOffset) / 2); + + Either prepareResult = + xmlLanguageService.prepareRename(xmlDocument, startTagMiddle, () -> {}); + assertNotNull(prepareResult, "Prepare Result is null!"); + assertNotNull(prepareResult.getLeft(), "Prepare Result Range is null!"); + + Range prepareResultRange = prepareResult.getLeft(); + assertEquals(expectedStartTagRange, prepareResultRange); + + WorkspaceEdit renameReult = xmlLanguageService.doRename(xmlDocument, startTagMiddle, newPropertyName, () -> {}); + assertNotNull(renameReult, "Prepare Result is null!"); + assertEquals(expectedRenameResult, renameReult); + + // Test renaming end tag of maven property definition + Position endTagMiddle = xmlDocument.positionAt((endTagOpenOffset + endTagCloseOffset) / 2); + + prepareResult = + xmlLanguageService.prepareRename(xmlDocument, endTagMiddle, () -> {}); + assertNotNull(prepareResult, "Prepare Result is null!"); + assertNotNull(prepareResult.getLeft(), "Prepare Result Range is null!"); + + prepareResultRange = prepareResult.getLeft(); + assertEquals(expectedEndTagRange, prepareResultRange); + + WorkspaceEdit workspaceEdit = xmlLanguageService.doRename(xmlDocument, startTagMiddle, newPropertyName, () -> {}); + assertNotNull(workspaceEdit, "Rename result is null!"); + assertNotNull(workspaceEdit.getDocumentChanges(), "Rename result document changes is null!"); + + workspaceEdit = xmlLanguageService.doRename(xmlDocument, endTagMiddle, newPropertyName, () -> {}); + assertNotNull(workspaceEdit, "Rename result is null!"); + assertNotNull(workspaceEdit.getDocumentChanges(), "Rename result document changes is null!"); + + renameReult = xmlLanguageService.doRename(xmlDocument, endTagMiddle, newPropertyName, () -> {}); + assertNotNull(renameReult, "Prepare Result is null!"); + assertEquals(expectedRenameResult, renameReult); + } + + @Test + public void testRenameMavenPropertyWithChildren() 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])))); + + String propertyName = "test-version"; + String newPropertyName = "new-test-version"; + DOMDocument xmlDocument = createDOMDocument("/property-refactoring/child/parent/pom.xml", xmlLanguageService); + assertNotNull(xmlDocument, "Parent document not found!"); + + Optional properties = DOMUtils.findChildElement(xmlDocument.getDocumentElement(), PROPERTIES_ELT); + assertTrue(properties.isPresent(), "'' Element doesn't exist!"); + + Optional property = DOMUtils.findChildElement(properties.get(), propertyName); + assertTrue(properties.isPresent(), "'<" + propertyName + ">' Element doesn't exist!"); + + DOMElement propertyElement = property.get(); + + // Save property use ranges for parent (this) document + int startTagOpenOffset = propertyElement.getStartTagOpenOffset() + 1; + int startTagCloseOffset = propertyElement.getStartTagCloseOffset(); + int endTagOpenOffset = propertyElement.getEndTagOpenOffset() + 2; + int endTagCloseOffset = propertyElement.getEndTagCloseOffset(); + + Range expectedStartTagRange = + new Range(xmlDocument.positionAt(startTagOpenOffset), xmlDocument.positionAt(startTagCloseOffset)); + Range expectedEndTagRange = + new Range(xmlDocument.positionAt(endTagOpenOffset), xmlDocument.positionAt(endTagCloseOffset)); + List propertyUseRanges =collectMavenPropertyUsages(xmlDocument, propertyName); + + // Save property use ranges for child document + DOMDocument childXmlDocument = createDOMDocument("/property-refactoring/child/pom.xml", xmlLanguageService); + assertNotNull(childXmlDocument, "Child document not found!"); + List childPropertyUseRanges =collectMavenPropertyUsages(childXmlDocument, propertyName); + + // Expected changes: + // - Start and and tags of maven property definition + // - one use of maven property + List expectedParentTextEdits = new ArrayList<>(); + expectedParentTextEdits.add(new TextEdit(expectedStartTagRange, newPropertyName)); + expectedParentTextEdits.add(new TextEdit(expectedEndTagRange, newPropertyName)); + propertyUseRanges.stream().map(r -> new TextEdit(r,newPropertyName)) + .forEach(expectedParentTextEdits::add); + + List expectedChildTextEdits = new ArrayList<>(); + childPropertyUseRanges.stream().map(r -> new TextEdit(r,newPropertyName)) + .forEach(expectedChildTextEdits::add); + + // final List> documentChanges + WorkspaceEdit expectedRenameResult = new WorkspaceEdit(Arrays.asList( + Either.forLeft(creatTextDocumentEdit(xmlDocument, expectedParentTextEdits)), + Either.forLeft(creatTextDocumentEdit(childXmlDocument, expectedChildTextEdits)))); + + // Test renaming start tag of maven property definition + Position startTagMiddle = xmlDocument.positionAt((startTagOpenOffset + startTagCloseOffset) / 2); + + Either prepareResult = + xmlLanguageService.prepareRename(xmlDocument, startTagMiddle, () -> {}); + assertNotNull(prepareResult, "Prepare Result is null!"); + assertNotNull(prepareResult.getLeft(), "Prepare Result Range is null!"); + + Range prepareResultRange = prepareResult.getLeft(); + assertEquals(expectedStartTagRange, prepareResultRange); + + WorkspaceEdit renameReult = xmlLanguageService.doRename(xmlDocument, startTagMiddle, newPropertyName, () -> {}); + assertNotNull(renameReult, "Prepare Result is null!"); + assertEquals(expectedRenameResult, renameReult); + + // Test renaming end tag of maven property definition + Position endTagMiddle = xmlDocument.positionAt((endTagOpenOffset + endTagCloseOffset) / 2); + + prepareResult = + xmlLanguageService.prepareRename(xmlDocument, endTagMiddle, () -> {}); + assertNotNull(prepareResult, "Prepare Result is null!"); + assertNotNull(prepareResult.getLeft(), "Prepare Result Range is null!"); + + prepareResultRange = prepareResult.getLeft(); + assertEquals(expectedEndTagRange, prepareResultRange); + + WorkspaceEdit workspaceEdit = xmlLanguageService.doRename(xmlDocument, startTagMiddle, newPropertyName, () -> {}); + assertNotNull(workspaceEdit, "Rename result is null!"); + assertNotNull(workspaceEdit.getDocumentChanges(), "Rename result document changes is null!"); + + workspaceEdit = xmlLanguageService.doRename(xmlDocument, endTagMiddle, newPropertyName, () -> {}); + assertNotNull(workspaceEdit, "Rename result is null!"); + assertNotNull(workspaceEdit.getDocumentChanges(), "Rename result document changes is null!"); + + renameReult = xmlLanguageService.doRename(xmlDocument, endTagMiddle, newPropertyName, () -> {}); + assertNotNull(renameReult, "Prepare Result is null!"); + assertEquals(expectedRenameResult, renameReult); + } + + private static final List collectMavenPropertyUsages(DOMDocument xmlDocument, String propertyName) { + String propertyUse = "${" + propertyName + "}"; + String text = xmlDocument.getText(); + TextDocument document = xmlDocument.getTextDocument(); + List propertyUseRanges = new ArrayList<>(); + int index = 0; + for (index = text.indexOf(propertyUse); index != -1; index = text.indexOf(propertyUse, index + propertyUse.length())) { + try { + int propertyUseStart = index + "${".length(); + int propertyUseEnd = propertyUseStart + propertyName.length(); + Position positionStart = document.positionAt(propertyUseStart); + Position positionEnd = document.positionAt(propertyUseEnd); + propertyUseRanges.add(new Range(positionStart, positionEnd)); + } catch (BadLocationException e) { + fail("Cannot find all property uses in test data", e); + } + } + return propertyUseRanges; + } +} \ No newline at end of file diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/MavenLemminxTestsUtils.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/MavenLemminxTestsUtils.java index f43b1245..b4bfcd1f 100644 --- a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/MavenLemminxTestsUtils.java +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/utils/MavenLemminxTestsUtils.java @@ -92,7 +92,7 @@ public static TextDocumentItem createTextDocumentItem(String resourcePath, Prope contents = contents.replaceAll((String)entry.getKey(), (String)entry.getValue()); } } - return new TextDocumentItem(uri.toString(), "xml", 1, contents); + return new TextDocumentItem(uri.toString(), "xml", 0, contents); } public static boolean completionContains(List completionItems, String searchString) { 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 new file mode 100644 index 00000000..499472e5 --- /dev/null +++ b/lemminx-maven/src/test/resources/property-refactoring/child/parent/pom.xml @@ -0,0 +1,22 @@ + + 4.0.0 + + org.test + 0.0.1-SNAPSHOT + + + org.test + test-parent + 0.0.1-SNAPSHOT + pom + + + + ${test-group-id} + test-dependency-1 + ${test-version} + + + \ No newline at end of file diff --git a/lemminx-maven/src/test/resources/property-refactoring/child/pom.xml b/lemminx-maven/src/test/resources/property-refactoring/child/pom.xml new file mode 100644 index 00000000..4215ff32 --- /dev/null +++ b/lemminx-maven/src/test/resources/property-refactoring/child/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + org.test + test-parent + 0.0.1-SNAPSHOT + ./parent/pom.xml + + test-child + pom + + + + ${test-group-id} + test-dependency-2 + ${test-version} + + + \ No newline at end of file