Skip to content

Commit

Permalink
Improved accuracy of attribute value quick fix suggestions
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Chen <alchen@redhat.com>
  • Loading branch information
Alexander Chen authored and angelozerr committed Dec 13, 2021
1 parent e41b18c commit d70604b
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 38 deletions.
Expand Up @@ -14,6 +14,8 @@

import java.util.Collection;
import java.util.List;
import java.text.Collator;
import java.util.TreeSet;

import org.eclipse.lemminx.commons.CodeActionFactory;
import org.eclipse.lemminx.dom.DOMAttr;
Expand All @@ -30,6 +32,8 @@
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;

import static org.eclipse.lemminx.utils.StringUtils.isSimilar;

/**
* Code action to fix cvc-attribute-3 error.
*
Expand All @@ -48,6 +52,7 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen
String attributeName = attr.getName();
ContentModelManager contentModelManager = componentProvider.getComponent(ContentModelManager.class);
Collection<CMDocument> cmDocuments = contentModelManager.findCMDocument(element);
String attributeValue = attr.getValue();
for (CMDocument cmDocument : cmDocuments) {
CMAttributeDeclaration cmAttribute = cmDocument.findCMAttribute(element, attributeName);
if (cmAttribute != null) {
Expand All @@ -56,14 +61,33 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen
diagnosticRange.getStart().getCharacter() + 1),
new Position(diagnosticRange.getEnd().getLine(),
diagnosticRange.getEnd().getCharacter() - 1));
cmAttribute.getEnumerationValues().forEach(value -> {
// Replace attribute value
// value = "${1:" + value + "}";
CodeAction replaceAttrValueAction = CodeActionFactory.replace(
"Replace with '" + value + "'", rangeValue, value, document.getTextDocument(),
diagnostic);
codeActions.add(replaceAttrValueAction);
});
Collection<String> similarValues = new TreeSet<String>(Collator.getInstance());
Collection<String> otherValues = new TreeSet<String>(Collator.getInstance());

for (String enumValue : cmAttribute.getEnumerationValues()) {
if (isSimilar(enumValue, attributeValue)) {
similarValues.add(enumValue);
} else {
otherValues.add(enumValue);
}
}
if (!similarValues.isEmpty()) {
// Add code actions for each similar value
for (String similarValue : similarValues) {
CodeAction similarCodeAction = CodeActionFactory.replace(
"Did you mean '" + similarValue + "'?", rangeValue, similarValue, document.getTextDocument(),
diagnostic);
codeActions.add(similarCodeAction);
}
} else {
// Add code actions for each possible elements
for (String otherValue : otherValues) {
CodeAction otherCodeAction = CodeActionFactory.replace(
"Replace with '" + otherValue + "'", rangeValue, otherValue, document.getTextDocument(),
diagnostic);
codeActions.add(otherCodeAction);
}
}
}
}
}
Expand Down
Expand Up @@ -27,19 +27,18 @@
import org.eclipse.lemminx.services.extensions.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.IComponentProvider;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lemminx.utils.LevenshteinDistance;
import org.eclipse.lemminx.utils.XMLPositionUtility;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;

import static org.eclipse.lemminx.utils.StringUtils.isSimilar;

/**
* cvc_complex_type_2_4_a
*/
public class cvc_complex_type_2_4_aCodeAction implements ICodeActionParticipant {

private static final float MAX_DISTANCE_DIFF_RATIO = 0.4f;

@Override
public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List<CodeAction> codeActions,
SharedSettings sharedSettings, IComponentProvider componentProvider) {
Expand Down Expand Up @@ -86,7 +85,7 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen
}

if (!similarElementNames.isEmpty()) {
// // Add code actions for each similar elements
// Add code actions for each similar elements
for (String elementName : similarElementNames) {
CodeAction similarCodeAction = CodeActionFactory.replaceAt(
"Did you mean '" + elementName + "'?", elementName, document.getTextDocument(),
Expand All @@ -112,7 +111,7 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen

/**
* Returns the possible elements for the given DOM element.
*
*
* @param element the DOM element
* @param componentProvider the component provider
* @return the possible elements for the given DOM element.
Expand Down Expand Up @@ -149,9 +148,4 @@ private static Collection<CMElementDeclaration> getPossibleElements(DOMElement e
return possibleElements;
}

private static boolean isSimilar(String reference, String current) {
int threshold = Math.round(MAX_DISTANCE_DIFF_RATIO * reference.length());
LevenshteinDistance levenshteinDistance = new LevenshteinDistance(threshold);
return levenshteinDistance.apply(reference, current) != -1;
}
}
Expand Up @@ -28,6 +28,8 @@ public class StringUtils {
public static final String FALSE = "false";
public static final Collection<String> TRUE_FALSE_ARRAY = Arrays.asList(TRUE, FALSE);

private static final float MAX_DISTANCE_DIFF_RATIO = 0.4f;

private StringUtils() {
}

Expand Down Expand Up @@ -65,7 +67,7 @@ public static boolean isWhitespace(String value) {

/**
* Checks if a string is null or consists of only whitespace characters.
*
*
* @param value The string to check
* @return <code>true</code> if any of the below hold, and false otherwise:
* <ul>
Expand All @@ -81,7 +83,7 @@ public static boolean isBlank(String value) {
/**
* Normalizes the whitespace characters of a given string and applies it to the
* given string builder.
*
*
* @param str
* @return the result of normalize space of the given string.
*/
Expand All @@ -104,7 +106,7 @@ public static void normalizeSpace(String str, StringBuilder b) {

/**
* Returns the result of normalize space of the given string.
*
*
* @param str
* @return the result of normalize space of the given string.
*/
Expand All @@ -116,7 +118,7 @@ public static String normalizeSpace(String str) {

/**
* Returns the start whitespaces of the given line text.
*
*
* @param lineText
* @return the start whitespaces of the given line text.
*/
Expand All @@ -126,7 +128,7 @@ public static String getStartWhitespaces(String lineText) {

/**
* Returns the whitespaces from the given range start/end of the given text.
*
*
* @param start the range start
* @param end the range end
* @param text the text
Expand Down Expand Up @@ -214,10 +216,10 @@ public static String lTrim(String value) {
/**
* Given a string that is only whitespace, this will return the amount of
* newline characters.
*
*
* If the newLineCounter becomes > newLineLimit, then the value of newLineLimit
* is always returned.
*
*
* @param text
* @param isWhitespace
* @param delimiter
Expand Down Expand Up @@ -253,7 +255,7 @@ public static int getNumberOfNewLines(String text, boolean isWhitespace, String
/**
* Given a string will give back a non null string that is either the given
* string, or an empty string.
*
*
* @param text
* @return
*/
Expand All @@ -266,12 +268,12 @@ public static String getDefaultString(String text) {

/**
* Traverses backwards from the endOffset until it finds a whitespace character.
*
*
* The offset of the character after the whitespace is returned.
*
*
* (text = "abcd efg|h", endOffset = 8) -> 5
*
*
*
*
* @param text
* @param endOffset non-inclusive
* @return Start offset directly after the first whitespace.
Expand Down Expand Up @@ -300,7 +302,7 @@ public static int getOffsetAfterWhitespace(String text, int endOffset) {

/**
* Returns the number of consecutive whitespace characters in front of text
*
*
* @param text String of interest
* @return the number of consecutive whitespace characters in front of text
*/
Expand All @@ -320,7 +322,7 @@ public static int getFrontWhitespaceLength(String text) {

/**
* Returns the number of consecutive whitespace characters from the end of text
*
*
* @param text String of interest
* @return the number of consecutive whitespace characters from the end of text
*/
Expand Down Expand Up @@ -411,7 +413,7 @@ public static String getString(Object obj) {
/**
* Returns the start word offset from the left of the given <code>offset</code>
* and -1 if no word.
*
*
* @param text the text
* @param offset the offset
* @param isValidChar predicate to check if current character belong to the
Expand All @@ -434,7 +436,7 @@ public static int findStartWord(String text, int offset, Predicate<Character> is
/**
* Returns the end word offset from the right of the given <code>offset</code>
* and -1 if no word.
*
*
* @param text the text
* @param offset the offset
* @param isValidChar predicate to check if current character belong to the
Expand All @@ -456,10 +458,10 @@ public static int findEndWord(String text, int offset, Predicate<Character> isVa

/**
* Returns <code>value</code> without surrounding quotes.
*
*
* If <code>value</code> does not have matching surrounding quotes,
* returns <code>value</code>.
*
*
* @param value
* @return <code>value</code> without surrounding quotes.
*/
Expand All @@ -474,7 +476,7 @@ public static String convertToQuotelessValue(String value) {
/**
* Returns true if <code>value</code> has matching surrounding quotes
* and false otherwise.
*
*
* @param value
* @return true if <code>value</code> has matching surrounding quotes.
*/
Expand All @@ -490,4 +492,17 @@ public static boolean isQuoted(String value) {
char quoteValueEnd = value.charAt(value.length() - 1);
return quoteValueEnd == quoteValueStart;
}

/**
* Uses Levenshtein distance to determine similarity between strings
*
* @param reference the string being compared to
* @param current the string compared
* @return true if the two strings are similar, false otherwise
*/
public static boolean isSimilar(String reference, String current) {
int threshold = Math.round(MAX_DISTANCE_DIFF_RATIO * reference.length());
LevenshteinDistance levenshteinDistance = new LevenshteinDistance(threshold);
return levenshteinDistance.apply(reference, current) != -1;
}
}
Expand Up @@ -638,7 +638,6 @@ public void fuzzyElementNamesWithPrefix() throws Exception {
Diagnostic diagnostic = d(6, 5, 6, 16, XMLSchemaErrorCode.cvc_complex_type_2_4_c,
"cvc-complex-type.2.4.c: The matching wildcard is strict, but no declaration can be found for element 'camel:beani'.");
testDiagnosticsWithCatalogFor(xml, diagnostic);

testCodeActionsWithCatalogFor(xml, diagnostic, //
ca(diagnostic, te(6, 11, 6, 16, "bean"), te(6, 25, 6, 30, "bean")), //
ca(diagnostic, te(6, 11, 6, 16, "beanio"), te(6, 25, 6, 30, "beanio")));
Expand All @@ -658,6 +657,20 @@ public void fuzzyElementNamesWithPrefixAndNoMatch() throws Exception {
ca(diagnostic, te(5, 12, 5, 17, "AElement2"), te(5, 28, 5, 33, "AElement2")));
}

@Test
public void fuzzyElementMemberValueCodeActionTest() throws Exception {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + //
"<dress \r\n" +
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n" +
"xsi:noNamespaceSchemaLocation=\"src/test/resources/xsd/dressSize.xsd\"\r\n" +
"size=\"larg\"/>";
Diagnostic diagnostic1 = d(4, 5, 4, 11, XMLSchemaErrorCode.cvc_attribute_3,
"cvc-attribute.3: The value 'larg' of attribute 'size' on element 'dress' is not valid with respect to its type, 'SizeType'.");
testCodeActionsFor(xml, diagnostic1,
ca(diagnostic1, te(4, 6, 4, 10, "large")), ca(diagnostic1, te(4, 6, 4, 10, "x-large")));
}


@Test
public void cvc_complex_type_2_2_withElement() throws Exception {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + //
Expand Down

0 comments on commit d70604b

Please sign in to comment.