Skip to content

Commit

Permalink
Quickfix to close open tag doesn't deal with attributes
Browse files Browse the repository at this point in the history
Fixes eclipse#646

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Jun 4, 2020
1 parent 84d2689 commit 6de6f97
Show file tree
Hide file tree
Showing 6 changed files with 412 additions and 212 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.apache.xerces.xni.XMLLocator;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMDocumentType;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.ETagRequiredCodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.ElementUnterminatedCodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.EqRequiredInAttributeCodeAction;
Expand All @@ -44,7 +46,8 @@ public enum XMLSyntaxErrorCode implements IXMLErrorCode {

AttributeNotUnique, // https://wiki.xmldation.com/Support/Validator/AttributeNotUnique
AttributeNSNotUnique, // https://wiki.xmldation.com/Support/Validator/AttributeNSNotUnique
AttributePrefixUnbound, ContentIllegalInProlog, // https://wiki.xmldation.com/Support/Validator/ContentIllegalInProlog
AttributePrefixUnbound, //
ContentIllegalInProlog, // https://wiki.xmldation.com/Support/Validator/ContentIllegalInProlog
DashDashInComment, // https://wiki.xmldation.com/Support/Validator/DashDashInComment
ElementUnterminated, // https://wiki.xmldation.com/Support/Validator/ElementUnterminated
ElementPrefixUnbound, // https://wiki.xmldation.com/Support/Validator/ElementPrefixUnbound
Expand All @@ -53,11 +56,30 @@ public enum XMLSyntaxErrorCode implements IXMLErrorCode {
ETagRequired, // https://wiki.xmldation.com/Support/Validator/ETagRequired
ETagUnterminated, // https://wiki.xmldation.com/Support/Validator/ETagUnterminated
EqRequiredInAttribute, // https://wiki.xmldation.com/Support/Validator/EqRequiredInAttribute
the_element_type_lmsg("the-element-type-lmsg"), EqRequiredInXMLDecl, IllegalQName, InvalidCommentStart,
LessthanInAttValue, MarkupEntityMismatch, MarkupNotRecognizedInContent, NameRequiredInReference, OpenQuoteExpected,
PITargetRequired, PseudoAttrNameExpected, QuoteRequiredInXMLDecl, RootElementTypeMustMatchDoctypedecl,
SDDeclInvalid, SemicolonRequiredInReference, SpaceRequiredBeforeEncodingInXMLDecl, SpaceRequiredBeforeStandalone, SpaceRequiredInPI,
VersionInfoRequired, VersionNotSupported, XMLDeclUnterminated, CustomETag, PrematureEOF, DoctypeNotAllowed, NoMorePseudoAttributes;
EqRequiredInXMLDecl, //
IllegalQName, //
InvalidCommentStart, //
LessthanInAttValue, //
MarkupEntityMismatch, //
MarkupNotRecognizedInContent, //
NameRequiredInReference, //
OpenQuoteExpected, //
PITargetRequired, //
PseudoAttrNameExpected, //
QuoteRequiredInXMLDecl, //
RootElementTypeMustMatchDoctypedecl, //
SDDeclInvalid, //
SemicolonRequiredInReference, //
SpaceRequiredBeforeEncodingInXMLDecl, //
SpaceRequiredBeforeStandalone, //
SpaceRequiredInPI, //
VersionInfoRequired, //
VersionNotSupported, //
XMLDeclUnterminated, //
CustomETag, //
PrematureEOF, //
DoctypeNotAllowed, //
NoMorePseudoAttributes;

private final String code;

Expand Down Expand Up @@ -108,13 +130,30 @@ public static Range toLSPRange(XMLLocator location, XMLSyntaxErrorCode code, Obj
case SpaceRequiredBeforeEncodingInXMLDecl:
case VersionInfoRequired:
case ElementPrefixUnbound:
case ElementUnterminated:
case RootElementTypeMustMatchDoctypedecl:
return XMLPositionUtility.selectStartTagName(offset, document);
case EqRequiredInAttribute: {
String attrName = getString(arguments[1]);
return XMLPositionUtility.selectAttributeNameFromGivenNameAt(attrName, offset, document);
}
case MarkupEntityMismatch:
case ElementUnterminated: {
String text = document.getText();
if (offset < text.length()) {
DOMNode element = document.findNodeAt(offset);
if (element.isElement() && !((DOMElement) element).isStartTagClosed()) {
// ex : <foo attr="" |
int endOffset = offset;
// remove spaces
while (Character.isWhitespace(text.charAt(endOffset))) {
endOffset--;
}
endOffset++;
return XMLPositionUtility.createRange(element.getStart() + 1, endOffset, document);
}
}
return XMLPositionUtility.selectRootStartTag(document);
}
case NoMorePseudoAttributes:
case EncodingDeclRequired:
case EqRequiredInXMLDecl:
Expand Down Expand Up @@ -201,16 +240,14 @@ public static Range toLSPRange(XMLLocator location, XMLSyntaxErrorCode code, Obj
case InvalidCommentStart:
case MarkupNotRecognizedInContent:
return XMLPositionUtility.createRange(offset, offset + 1, document);
case MarkupEntityMismatch:
return XMLPositionUtility.selectRootStartTag(document);
case NameRequiredInReference:
break;
case OpenQuoteExpected: {
return XMLPositionUtility.selectAttributeNameAt(offset - 1, document);
}
case DoctypeNotAllowed:
DOMDocumentType docType = document.getDoctype();
return XMLPositionUtility.createRange(docType);
return XMLPositionUtility.createRange(docType);
case PITargetRequired:
// Working
break;
Expand Down Expand Up @@ -238,6 +275,7 @@ public static void registerCodeActionParticipants(Map<String, ICodeActionPartici
codeActions.put(OpenQuoteExpected.getCode(), new OpenQuoteExpectedCodeAction());
codeActions.put(MarkupEntityMismatch.getCode(), new MarkupEntityMismatchCodeAction());
codeActions.put(ETagRequired.getCode(), new ETagRequiredCodeAction());
codeActions.put(RootElementTypeMustMatchDoctypedecl.getCode(), new RootElementTypeMustMatchDoctypedeclCodeAction());
codeActions.put(RootElementTypeMustMatchDoctypedecl.getCode(),
new RootElementTypeMustMatchDoctypedeclCodeAction());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*******************************************************************************
* Copyright (c) 2020 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.lemminx.extensions.contentmodel.participants.codeactions;

import java.util.List;

import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.commons.CodeActionFactory;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.dom.LineIndentInfo;
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.XMLPositionUtility;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;

/**
* Code action to fix close start tag element.
*
*/
public class CloseStartTagCodeAction implements ICodeActionParticipant {

@Override
public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List<CodeAction> codeActions,
SharedSettings sharedSettings, IComponentProvider componentProvider) {
Range diagnosticRange = diagnostic.getRange();
try {
int startOffset = document.offsetAt(diagnosticRange.getStart());
DOMNode node = document.findNodeAt(startOffset);
if (node != null && node.isElement()) {
int diagnosticEndOffset = document.offsetAt(diagnosticRange.getEnd());
DOMElement element = (DOMElement) node;
if (!element.hasStartTag()) {
// </foo>
DOMElement parent = element.getParentElement();
if (parent != null && parent.getTagName() != null) {
// <a><b></c>
// Replace with 'b' closing tag
String tagName = parent.getTagName();
Range replaceRange = XMLPositionUtility.selectEndTagName(element);
CodeAction replaceTagAction = CodeActionFactory.replace("Replace with '" + tagName + "' closing tag",
replaceRange, tagName, document.getTextDocument(), diagnostic);
codeActions.add(replaceTagAction);
}
} else {
// <foo>
boolean startTagClosed = element.isStartTagClosed();
char c = document.getText().charAt(diagnosticEndOffset - 1);
if (c != '/') {
if (startTagClosed) {
// ex : <foo attr="" >
// // Close with '</$tag>
String tagName = element.getTagName();
if (tagName != null) {
String label = "</" + tagName + ">";
String insertText = label;
Position endPosition = null;
if (!element.hasChildNodes()) {
int endOffset = element.getStartTagCloseOffset() + 1;
endPosition = document.positionAt(endOffset);
} else {
String text = document.getText();
// the element have some children(Text node, Element node, etc)
int endOffset = element.getLastChild().getEnd() - 1;
if (endOffset < text.length()) {
// remove whitespaces
char ch = text.charAt(endOffset);
while (Character.isWhitespace(ch)) {
endOffset--;
ch = text.charAt(endOffset);
}
}
endOffset++;
endPosition = document.positionAt(endOffset);
if (hasElements(element)) {
// The element have element node as children
// the </foo> must be inserted with a new line and indent
LineIndentInfo indentInfo = document
.getLineIndentInfo(diagnosticRange.getStart().getLine());
insertText = indentInfo.getLineDelimiter() + indentInfo.getWhitespacesIndent()
+ insertText;
}
}
CodeAction closeEndTagAction = CodeActionFactory.insert("Close with '" + label + "'",
endPosition, insertText, document.getTextDocument(), diagnostic);
codeActions.add(closeEndTagAction);
}

} else {
// ex : <foo attr="
// Close with '/>
CodeAction autoCloseAction = CodeActionFactory.insert("Close with '/>'",
diagnosticRange.getEnd(), "/>", document.getTextDocument(), diagnostic);
codeActions.add(autoCloseAction);
// // Close with '></$tag>
String tagName = element.getTagName();
if (tagName != null) {
String insertText = "></" + tagName + ">";
CodeAction closeEndTagAction = CodeActionFactory.insert(
"Close with '" + insertText + "'", diagnosticRange.getEnd(), insertText,
document.getTextDocument(), diagnostic);
codeActions.add(closeEndTagAction);
}
}
}

if (!startTagClosed) {
// Close with '>
CodeAction closeAction = CodeActionFactory.insert("Close with '>'", diagnosticRange.getEnd(),
">", document.getTextDocument(), diagnostic);
codeActions.add(closeAction);
}
}
}
} catch (BadLocationException e) {
// do nothing
}
}

/**
* Returns true if the given element has elements as children and false
* otherwise.
*
* @param element the DOM element.
*
* @return true if the given element has elements as children and false
* otherwise.
*/
private static boolean hasElements(DOMElement element) {
for (DOMNode node : element.getChildren()) {
if (node.isElement()) {
return true;
}
}
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,9 @@
*******************************************************************************/
package org.eclipse.lemminx.extensions.contentmodel.participants.codeactions;

import java.util.List;

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.services.extensions.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.IComponentProvider;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;

/**
* ETagRequiredCodeAction
* Code action to fix ETagRequired error.
*/
public class ETagRequiredCodeAction implements ICodeActionParticipant {

@Override
public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List<CodeAction> codeActions,
SharedSettings sharedSettings, IComponentProvider componentProvider) {
MarkupEntityMismatchCodeAction.createEndTagInsertCodeAction(diagnostic, range, document, codeActions, componentProvider);
}
public class ETagRequiredCodeAction extends CloseStartTagCodeAction {


}
Original file line number Diff line number Diff line change
Expand Up @@ -12,56 +12,9 @@
*/
package org.eclipse.lemminx.extensions.contentmodel.participants.codeactions;

import java.util.List;

import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.commons.CodeActionFactory;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.services.extensions.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.IComponentProvider;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;

/**
* Code action to fix ElementUnterminated error.
*
*/
public class ElementUnterminatedCodeAction implements ICodeActionParticipant {

@Override
public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List<CodeAction> codeActions,
SharedSettings sharedSettings, IComponentProvider componentProvider) {
Range diagnosticRange = diagnostic.getRange();
// Close with '/>
CodeAction autoCloseAction = CodeActionFactory.insert("Close with '/>'", diagnosticRange.getEnd(), "/>",
document.getTextDocument(), diagnostic);
codeActions.add(autoCloseAction);

// Close with '>
CodeAction closeAction = CodeActionFactory.insert("Close with '>'", diagnosticRange.getEnd(), ">",
document.getTextDocument(), diagnostic);
codeActions.add(closeAction);

// Close with '$tag>
try {
int offset = document.offsetAt(range.getStart());
DOMNode node = document.findNodeAt(offset);
if (node != null && node.isElement()) {
String tagName = ((DOMElement) node).getTagName();
if (tagName != null) {
String insertText = "></" + tagName + ">";
CodeAction closeEndTagAction = CodeActionFactory.insert("Close with '" + insertText + "'",
diagnosticRange.getEnd(), insertText, document.getTextDocument(), diagnostic);
codeActions.add(closeEndTagAction);
}
}
} catch (BadLocationException e) {
// do nothing
}
}

public class ElementUnterminatedCodeAction extends CloseStartTagCodeAction {
}
Loading

0 comments on commit 6de6f97

Please sign in to comment.