Skip to content

Commit

Permalink
Provide refactoring capabilities specific to maven properties. Inline…
Browse files Browse the repository at this point in the history
… maven property. #383
  • Loading branch information
vrubezhny committed Jun 16, 2023
1 parent 1559908 commit 9a1dab8
Show file tree
Hide file tree
Showing 9 changed files with 409 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMParser;
import org.eclipse.lemminx.extensions.maven.participants.codeaction.InlinePropertyCodeAction;
import org.eclipse.lemminx.extensions.maven.participants.codeaction.MavenIdPartRemovalCodeAction;
import org.eclipse.lemminx.extensions.maven.participants.codeaction.MavenManagedVersionRemovalCodeAction;
import org.eclipse.lemminx.extensions.maven.participants.codeaction.MavenNoGrammarConstraintsCodeAction;
Expand Down Expand Up @@ -587,6 +588,9 @@ private void registerCodeActionParticipants(XMLExtensionsRegistry registry) {
codeActionParticipants.add(new MavenIdPartRemovalCodeAction());
codeActionParticipants.add(new MavenManagedVersionRemovalCodeAction());

// Refactoring
codeActionParticipants.add(new InlinePropertyCodeAction(this));

codeActionParticipants.stream().forEach(registry::registerCodeActionParticipant);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*******************************************************************************
* 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.codeaction;

import java.util.ArrayList;
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.CodeActionFactory;
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.codeaction.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionRequest;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

public class InlinePropertyCodeAction implements ICodeActionParticipant {
private static final Logger LOGGER = Logger.getLogger(InlinePropertyCodeAction.class.getName());
private final MavenLemminxExtension plugin;

public InlinePropertyCodeAction(MavenLemminxExtension plugin) {
this.plugin = plugin;
}

@Override
public void doCodeActionUnconditional(ICodeActionRequest request, List<CodeAction> codeActions, CancelChecker cancelChecker) throws CancellationException {
Range range = request.getRange();
if (range == null) {
return;
}

cancelChecker.checkCanceled();
try {
DOMDocument document = request.getDocument();
int startOffset = document.offsetAt(range.getStart());
Map.Entry<Range, String> mavenProperty = ParticipantUtils.getMavenProperty(document.findNodeAt(startOffset), startOffset);
if (mavenProperty == null) {
return;
}

cancelChecker.checkCanceled();
MavenProject project = plugin.getProjectCache().getLastSuccessfulMavenProject(document);
if (project != null) {
cancelChecker.checkCanceled();
Map<String, String> properties = ParticipantUtils.getMavenProjectProperties(project);
String value = properties.get(mavenProperty.getValue());
if (value != null) {
cancelChecker.checkCanceled();
// Replace the property with its value only in current node
codeActions.add(CodeActionFactory.replace(
"Replace property with its value",
range, value, document.getTextDocument(), null));

// Replace the property with its value in entire document
List<TextEdit> textEdits = new ArrayList<>();
collectInlinePropertyTextEdits(document.getDocumentElement(),
"${" + mavenProperty.getValue() + "}",
value, textEdits, cancelChecker);
if (textEdits.size() > 1) {
codeActions.add(CodeActionFactory.replace(
"Replace all property entries with its value",
textEdits, document.getTextDocument(), null));
}
}
}
} catch (CancellationException e) {
throw e;
} catch (Exception e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
}

void collectInlinePropertyTextEdits(DOMElement rootElement, String property, String value,
List<TextEdit> textEditss, CancelChecker cancelChecker) throws CancellationException {
cancelChecker.checkCanceled();

// Check this element's text
collectInElementTextEdits(rootElement, property, value, textEditss, cancelChecker);

// collect in this element's children
rootElement.getChildren().stream().filter(DOMElement.class::isInstance).map(DOMElement.class::cast)
.forEach(child -> collectInlinePropertyTextEdits(child, property, value, textEditss, cancelChecker));
}

void collectInElementTextEdits(DOMElement element, String property, String value,
List<TextEdit> textEditss, CancelChecker cancelChecker) throws CancellationException {
TextDocument textDocument = element.getOwnerDocument().getTextDocument();
DOMUtils.findElementTextChildren(element).stream()
.filter(text -> text.getData().contains(property))
.forEach(text -> {
String data = text.getData();
int index = 0;
for (index = data.indexOf(property); index != -1; index = data.indexOf(property, index + property.length())) {
cancelChecker.checkCanceled();
try {
int propertyUseStart = text.getStart() + index;
Range range = new Range(textDocument.positionAt(propertyUseStart),
textDocument.positionAt(propertyUseStart + property.length()));
textEditss.add(new TextEdit(range, value));
} catch (BadLocationException e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
return;
}
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.dom.DOMText;
import org.eclipse.lemminx.services.extensions.IPositionRequest;
import org.eclipse.lemminx.services.extensions.completion.ICompletionRequest;
import org.w3c.dom.Text;
Expand Down Expand Up @@ -99,6 +100,13 @@ public static Optional<String> findElementText(DOMElement element) {
.map(Text::getData)
.findFirst();
}

public static List<DOMText> findElementTextChildren(DOMElement element) {
return element.getChildren().stream()
.filter(DOMText.class::isInstance)
.map(DOMText.class::cast)
.collect(Collectors.toList());
}

public static String getOneLevelIndent(ICompletionRequest request) throws BadLocationException {
String oneLevelIndent = request.getLineIndentInfo().getWhitespacesIndent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,16 +268,20 @@ public static Map<String, String> getMavenProjectProperties(MavenProject project

return allProps;
}

public static Map.Entry<Range, String> getMavenPropertyInRequest(IPositionRequest request) {
DOMNode tag = request.getNode();
String tagText = tag.getNodeValue();
if (tagText == null) {
return getMavenProperty(request.getNode(), request.getOffset());
}

public static Map.Entry<Range, String> getMavenProperty(DOMNode tag, int offset) {
DOMElement element = tag.isElement() ? (DOMElement)tag : tag.getParentElement();
String tagText = DOMUtils.findElementText(element).orElse(null);
if (tagText == null || tagText.indexOf("${") == -1) {
return null;
}

int hoverLocation = request.getOffset();
int propertyOffset = request.getNode().getStart();
int hoverLocation = offset;
int propertyOffset = tag.getStart();
int beforeHover = hoverLocation - propertyOffset;

String beforeHoverText = tagText.substring(0, beforeHover);
Expand All @@ -289,9 +293,9 @@ public static Map.Entry<Range, String> getMavenPropertyInRequest(IPositionReques
if (indexOpen > indexCloseBefore) {

String propertyText = tagText.substring(indexOpen + 2, indexCloseAfter + beforeHover);
int textStart = request.getNode().getStart();
int textStart = tag.getStart();
Range propertyRange = XMLPositionUtility.createRange(textStart + indexOpen + 2,
textStart + indexCloseAfter - 1, request.getXMLDocument());
textStart + indexCloseAfter - 1, tag.getOwnerDocument());
return Map.entry(propertyRange, propertyText);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*******************************************************************************
* 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.codeaction;

import static org.eclipse.lemminx.XMLAssert.ca;
import static org.eclipse.lemminx.XMLAssert.te;
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.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.ArrayList;
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.extensions.maven.NoMavenCentralExtension;
import org.eclipse.lemminx.services.XMLLanguageService;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextEdit;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(NoMavenCentralExtension.class)
public class MavenCodeActionPropertyRefactoringTest {
private XMLLanguageService xmlLanguageService = new XMLLanguageService();
private SharedSettings sharedSettings = new SharedSettings();

@Test
public void testCodeActionsInlineSinglePropertyUse() throws Exception {
DOMDocument xmlDocument = createDOMDocument("/property-refactoring/pom-with-property.xml", xmlLanguageService);
String text = xmlDocument.getText();
TextDocument document = xmlDocument.getTextDocument();

// ${myPropertyGroupId}
String propertyUse = "${myPropertyGroupId}";
int propertyUseStart = text.indexOf(propertyUse);
int propertyUseEnd = propertyUseStart + propertyUse.length();

Position positionStart = document.positionAt(propertyUseStart);
Position positionEnd = document.positionAt(propertyUseEnd);

CodeAction expectedCodeAction = ca(null,
teOp(xmlDocument.getDocumentURI(), positionStart.getLine(), positionStart.getCharacter(),
positionEnd.getLine(), positionEnd.getCharacter(), "my-property-group-value"));
// Test for expected code actions returned
testCodeActionsFor(xmlDocument.getText(), null, new Range(positionStart, positionEnd),
(String) null, xmlDocument.getDocumentURI(), sharedSettings, xmlLanguageService, -1,
expectedCodeAction);
}

@Test
public void testCodeActionsInlineMultiplePropertyUses() throws Exception {
DOMDocument xmlDocument = createDOMDocument("/property-refactoring/pom-with-multiple-property-uses.xml", xmlLanguageService);
String text = xmlDocument.getText();
TextDocument document = xmlDocument.getTextDocument();

// ${myPropertyGroupId}
String propertyUse = "${myPropertyGroupId}";
List<Range> ranges = new ArrayList<>();

int index = 0;
for (index = text.indexOf(propertyUse); index != -1; index = text.indexOf(propertyUse, index + propertyUse.length())) {
try {
int propertyUseStart = index;
int propertyUseEnd = propertyUseStart + propertyUse.length();

Position positionStart = document.positionAt(propertyUseStart);
Position positionEnd = document.positionAt(propertyUseEnd);
ranges.add(new Range(positionStart, positionEnd));
} catch (BadLocationException e) {
fail("Cannot find all property uses in test data", e);
}
}
assertTrue(ranges.size() > 1, "There should be more than one prperty use");

// Inline for the selected single property use
CodeAction expectedCodeAction_1 = ca(null,
teOp(xmlDocument.getDocumentURI(),
ranges.get(0).getStart().getLine(), ranges.get(0).getStart().getCharacter(),
ranges.get(0).getEnd().getLine(), ranges.get(0).getEnd().getCharacter(),
"my-property-group-value"));

// Inline for all the property uses in pom.xml
List<TextEdit> textEdits = ranges.stream().map(range ->
te(range.getStart().getLine(), range.getStart().getCharacter(),
range.getEnd().getLine(), range.getEnd().getCharacter(),
"my-property-group-value")).collect(Collectors.toList()) ;
assertTrue(textEdits.size() == ranges.size(), "The TextEdits size should be equl to the size of ranges");
CodeAction expectedCodeAction_2 = ca(null, teOp(xmlDocument.getDocumentURI(), textEdits.toArray(new TextEdit[textEdits.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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

import org.eclipse.lemminx.commons.TextDocument;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.extensions.contentmodel.settings.ContentModelSettings;
import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationRootSettings;
import org.eclipse.lemminx.extensions.contentmodel.uriresolver.XMLCacheResolverExtension;
import org.eclipse.lemminx.services.XMLLanguageService;
import org.eclipse.lemminx.uriresolver.CacheResourceDownloadingException;
Expand Down Expand Up @@ -126,4 +128,15 @@ public static void prefetchDavenXSD() {
}
});
}

public static ContentModelSettings createContentModelSettings(boolean ignoreNoGrammar) {
ContentModelSettings settings = new ContentModelSettings();
settings.setUseCache(false);
XMLValidationRootSettings problems = new XMLValidationRootSettings();
if (ignoreNoGrammar) {
problems.setNoGrammar("ignore");
}
settings.setValidation(problems);
return settings;
}
}
Loading

0 comments on commit 9a1dab8

Please sign in to comment.