Skip to content

Commit

Permalink
Fuzzy Element name codeaction
Browse files Browse the repository at this point in the history
Handles regular element names and ones with prefixes

Fixes #589

Refer to #589 for code to test

Signed-off-by: Nikolas Komonen <[email protected]>
  • Loading branch information
NikolasKomonen committed Nov 11, 2019
1 parent bc9f68b commit 655e17c
Show file tree
Hide file tree
Showing 14 changed files with 696 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
*/
package org.eclipse.lsp4xml.commons;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionKind;
Expand Down Expand Up @@ -85,4 +88,24 @@ public static CodeAction replace(String title, Range range, String replaceText,
insertContentAction.setEdit(workspaceEdit);
return insertContentAction;
}

public static CodeAction replaceAt(String title, String replaceText, TextDocumentItem document,
Diagnostic diagnostic, Collection<Range> ranges) {
CodeAction insertContentAction = new CodeAction(title);
insertContentAction.setKind(CodeActionKind.QuickFix);
insertContentAction.setDiagnostics(Arrays.asList(diagnostic));

VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier(
document.getUri(), document.getVersion());
ArrayList<TextEdit> edits = new ArrayList<TextEdit>();
for (Range range : ranges) {
TextEdit edit = new TextEdit(range, replaceText);
edits.add(edit);
}
TextDocumentEdit textDocumentEdit = new TextDocumentEdit(versionedTextDocumentIdentifier, edits);
WorkspaceEdit workspaceEdit = new WorkspaceEdit(Collections.singletonList(Either.forLeft(textDocumentEdit)));

insertContentAction.setEdit(workspaceEdit);
return insertContentAction;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,27 @@ public String getPrefix() {
return prefix;
}

/**
* Returns the suffix of an element name with a namespace
*
* eg: returns <code>dog</code> from and element that looks like <code>&lt;animal:dog ...&gt;</code>.
*
* If there is no prefix then it just returns the regular name.
* @return the suffix of an element name if it has a namespace, else the regular name
*/
public String getSuffix() {
String name = getTagName();
if (name == null) {
return null;
}
String prefix = name;
int index = name.indexOf(":"); //$NON-NLS-1$
if (index != -1) {
prefix = name.substring(index + 1, name.length());
}
return prefix;
}

/*
* (non-Javadoc)
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public static Range toLSPRange(XMLLocator location, DTDErrorCode code, Object[]
case MSG_REQUIRED_ATTRIBUTE_NOT_SPECIFIED:
case MSG_ELEMENT_NOT_DECLARED:
case MSG_CONTENT_INVALID: {
return XMLPositionUtility.selectStartTag(offset, document);
return XMLPositionUtility.selectStartTagName(offset, document);
}
case MSG_ATTRIBUTE_NOT_DECLARED: {
return XMLPositionUtility.selectAttributeValueAt(getString(arguments[1]), offset, document);
Expand All @@ -134,7 +134,7 @@ public static Range toLSPRange(XMLLocator location, DTDErrorCode code, Object[]
case MSG_ELEMENT_WITH_ID_REQUIRED: {
DOMElement element = document.getDocumentElement();
if (element != null) {
return XMLPositionUtility.selectStartTag(element);
return XMLPositionUtility.selectStartTagName(element);
}
}
case IDREFSInvalid:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.eclipse.lsp4xml.extensions.contentmodel.participants.codeactions.cvc_attribute_3CodeAction;
import org.eclipse.lsp4xml.extensions.contentmodel.participants.codeactions.cvc_complex_type_2_1CodeAction;
import org.eclipse.lsp4xml.extensions.contentmodel.participants.codeactions.cvc_complex_type_2_3CodeAction;
import org.eclipse.lsp4xml.extensions.contentmodel.participants.codeactions.cvc_complex_type_2_4_aCodeAction;
import org.eclipse.lsp4xml.extensions.contentmodel.participants.codeactions.cvc_complex_type_3_2_2CodeAction;
import org.eclipse.lsp4xml.extensions.contentmodel.participants.codeactions.cvc_complex_type_4CodeAction;
import org.eclipse.lsp4xml.extensions.contentmodel.participants.codeactions.cvc_enumeration_validCodeAction;
Expand Down Expand Up @@ -134,7 +135,7 @@ public static Range toLSPRange(XMLLocator location, XMLSchemaErrorCode code, Obj
case cvc_complex_type_4:
case src_element_3:
case TargetNamespace_2:
return XMLPositionUtility.selectStartTag(offset, document);
return XMLPositionUtility.selectStartTagName(offset, document);
case cvc_complex_type_3_2_2: {
String attrName = getString(arguments[1]);
return XMLPositionUtility.selectAttributeNameFromGivenNameAt(attrName, offset, document);
Expand Down Expand Up @@ -237,13 +238,15 @@ public static Range toLSPRange(XMLLocator location, XMLSchemaErrorCode code, Obj
}
}
case cvc_type_3_1_2:
return XMLPositionUtility.selectStartTag(offset, document);
return XMLPositionUtility.selectStartTagName(offset, document);
default:
}
return null;
}

public static void registerCodeActionParticipants(Map<String, ICodeActionParticipant> codeActions) {
codeActions.put(cvc_complex_type_2_4_a.getCode(), new cvc_complex_type_2_4_aCodeAction());
codeActions.put(cvc_complex_type_2_4_c.getCode(), new cvc_complex_type_2_4_aCodeAction());
codeActions.put(cvc_complex_type_2_3.getCode(), new cvc_complex_type_2_3CodeAction());
codeActions.put(cvc_complex_type_4.getCode(), new cvc_complex_type_4CodeAction());
codeActions.put(cvc_type_3_1_1.getCode(), new cvc_type_3_1_1CodeAction());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public static Range toLSPRange(XMLLocator location, XMLSyntaxErrorCode code, Obj
case ElementPrefixUnbound:
case ElementUnterminated:
case RootElementTypeMustMatchDoctypedecl:
return XMLPositionUtility.selectStartTag(offset, document);
return XMLPositionUtility.selectStartTagName(offset, document);
case EqRequiredInAttribute: {
String attrName = getString(arguments[1]);
return XMLPositionUtility.selectAttributeNameFromGivenNameAt(attrName, offset, document);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.eclipse.lsp4xml.extensions.contentmodel.participants.codeactions;

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

import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4xml.commons.CodeActionFactory;
import org.eclipse.lsp4xml.dom.DOMDocument;
import org.eclipse.lsp4xml.dom.DOMElement;
import org.eclipse.lsp4xml.dom.DOMNode;
import org.eclipse.lsp4xml.extensions.contentmodel.model.CMElementDeclaration;
import org.eclipse.lsp4xml.extensions.contentmodel.model.ContentModelManager;
import org.eclipse.lsp4xml.services.extensions.ICodeActionParticipant;
import org.eclipse.lsp4xml.services.extensions.IComponentProvider;
import org.eclipse.lsp4xml.settings.XMLFormattingOptions;
import org.eclipse.lsp4xml.utils.LevenshteinDistance;
import org.eclipse.lsp4xml.utils.XMLPositionUtility;

/**
* 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,
XMLFormattingOptions formattingSettings, IComponentProvider componentProvider) {
try {
int offset = document.offsetAt(diagnostic.getRange().getStart());
DOMNode node = document.findNodeAt(offset);
if (node != null && node.isElement()) {
// Get element from the diagnostic
DOMElement element = (DOMElement) node;
DOMElement parentElement = element.getParentElement();
String tagName = element.getSuffix();
boolean suffixOnly = element.getPrefix() != null;

// Get the XSD element declaration of the parent element.
ContentModelManager contentModelManager = componentProvider.getComponent(ContentModelManager.class);
CMElementDeclaration cmElement = contentModelManager.findCMElement(parentElement);
if (cmElement != null) {
// Collect all possible elements from the parent element upon the offset start
// of the element
Collection<CMElementDeclaration> possibleElements = cmElement.getPossibleElements(parentElement,
element.getStart());

// When added to these collections, the names will be ordered alphabetically
Collection<String> otherElementNames = new TreeSet<String>(Collator.getInstance());
Collection<String> similarElementNames = new TreeSet<String>(Collator.getInstance());

// Try to collect similar names coming from tag name
for (CMElementDeclaration possibleElement : possibleElements) {
String possibleElementName = possibleElement.getName();
if (isSimilar(possibleElementName, tagName)) {
similarElementNames.add(possibleElementName);
}
else {
otherElementNames.add(possibleElementName);
}
}

// Create ranges for the replace.
List<Range> ranges = new ArrayList<>();
ranges.add(XMLPositionUtility.selectStartTagName(element, suffixOnly));
Range end = XMLPositionUtility.selectEndTag(element, suffixOnly);
if (end != null) {
ranges.add(end);
}

if (!similarElementNames.isEmpty()) {
// // Add code actions for each similar elements
for (String elementName : similarElementNames) {
CodeAction similarCodeAction = CodeActionFactory.replaceAt(
"Did you mean '" + elementName + "'?", elementName, document.getTextDocument(),
diagnostic, ranges);
codeActions.add(similarCodeAction);
}
} else {
// Add code actions for each possible elements
for (String elementName : otherElementNames) {
CodeAction otherCodeAction = CodeActionFactory.replaceAt(
"Replace with '" + elementName + "'", elementName, document.getTextDocument(),
diagnostic, ranges);
codeActions.add(otherCodeAction);
}
}
}
}

} catch (Exception e) {
// Do nothing
}
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ private static void warnNoGrammar(DOMDocument document, List<Diagnostic> diagnos
Range range = null;
DOMElement documentElement = document.getDocumentElement();
if (documentElement != null) {
range = XMLPositionUtility.selectStartTag(documentElement);
range = XMLPositionUtility.selectStartTagName(documentElement);
}
if (range == null) {
range = new Range(new Position(0, 0), new Position(0, 0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public static Range toLSPRange(XMLLocator location, XSDErrorCode code, Object[]
List<DOMNode> children = parent.getChildrenWithAttributeValue("name", nameValue);

if (children.isEmpty()) {
return XMLPositionUtility.selectStartTag(offset, document);
return XMLPositionUtility.selectStartTagName(offset, document);
}

offset = children.get(0).getStart() + 1;
Expand All @@ -128,7 +128,7 @@ public static Range toLSPRange(XMLLocator location, XSDErrorCode code, Object[]
case src_element_2_1:
case src_element_3:
case src_import_1_2:
return XMLPositionUtility.selectStartTag(offset, document);
return XMLPositionUtility.selectStartTagName(offset, document);
case s4s_att_not_allowed: {
String attrName = getString(arguments[1]);
return XMLPositionUtility.selectAttributeNameFromGivenNameAt(attrName, offset, document);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ private static void findStartEndTagDefinition(IDefinitionRequest request, List<L
if (element.hasStartTag() && element.hasEndTag()) {
// The DOM element has end and start tag
DOMDocument document = element.getOwnerDocument();
Range startRange = XMLPositionUtility.selectStartTag(element);
Range startRange = XMLPositionUtility.selectStartTagName(element);
Range endRange = XMLPositionUtility.selectEndTag(element);
int offset = request.getOffset();
if (element.isInStartTag(offset)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ private static void publishOneDiagnosticInRoot(DOMDocument document, String mess
Consumer<PublishDiagnosticsParams> publishDiagnostics) {
String uri = document.getDocumentURI();
DOMElement documentElement = document.getDocumentElement();
Range range = XMLPositionUtility.selectStartTag(documentElement);
Range range = XMLPositionUtility.selectStartTagName(documentElement);
List<Diagnostic> diagnostics = new ArrayList<>();
diagnostics.add(new Diagnostic(range, message, severity, "XML"));
publishDiagnostics.accept(new PublishDiagnosticsParams(uri, diagnostics));
Expand Down
Loading

0 comments on commit 655e17c

Please sign in to comment.