From 281c0853a772fd26cba28fd5a8a617212afe80dd Mon Sep 17 00:00:00 2001 From: azerr Date: Wed, 30 Sep 2020 18:29:46 +0200 Subject: [PATCH] Improve ETagRequired error range Fixes #876 Signed-off-by: azerr --- .../participants/XMLSyntaxErrorCode.java | 174 ++++++++-- .../codeactions/CloseStartTagCodeAction.java | 152 --------- .../codeactions/CloseTagCodeAction.java | 319 ++++++++++++++++++ .../codeactions/ETagRequiredCodeAction.java | 2 +- .../ETagUnterminatedCodeAction.java | 19 ++ .../ElementUnterminatedCodeAction.java | 2 +- .../MarkupEntityMismatchCodeAction.java | 2 +- .../AbstractXML2GrammarGenerator.java | 43 +-- .../lemminx/services/XMLCodeActions.java | 16 +- .../contentmodel/DTDDiagnosticsTest.java | 4 +- .../XMLSyntaxDiagnosticsTest.java | 167 ++++++--- .../xml2dtd/XML2DTDGeneratorTest.java | 22 ++ 12 files changed, 671 insertions(+), 251 deletions(-) delete mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/CloseStartTagCodeAction.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/CloseTagCodeAction.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ETagUnterminatedCodeAction.java diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSyntaxErrorCode.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSyntaxErrorCode.java index 49b7d82939..a47fecd138 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSyntaxErrorCode.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSyntaxErrorCode.java @@ -16,6 +16,7 @@ import static org.eclipse.lemminx.utils.XMLPositionUtility.selectCurrentTagOffset; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.xerces.xni.QName; @@ -25,6 +26,7 @@ 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.ETagUnterminatedCodeAction; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.ElementUnterminatedCodeAction; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.EqRequiredInAttributeCodeAction; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.MarkupEntityMismatchCodeAction; @@ -140,24 +142,6 @@ public static Range toLSPRange(XMLLocator location, XMLSyntaxErrorCode code, Obj 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 : + // + // + startTagElement = findChildTag(tag, element); + + } + return getRangeFromStartNodeToOffset(startTagElement, offset, document); + } + // Should never occurs + return null; } case SemicolonRequiredInReference: { EntityReferenceRange range = XMLPositionUtility.selectEntityReference(offset + 1, document, false); @@ -280,8 +308,102 @@ public static Range toLSPRange(XMLLocator location, XMLSyntaxErrorCode code, Obj } + /** + * Remove the offset of the first character from the left offset which is not a + * whitespace. + * + * @param initialOffset the initial offset. + * + * @param text the XML content. + * @return the offset of the first character from the left offset which is not a + * whitespace. + */ + private static int removeLeftSpaces(final int initialOffset, String text) { + int offset = initialOffset; + if (offset >= text.length()) { + return text.length(); + } + char ch = text.charAt(offset); + while (Character.isWhitespace(ch)) { + offset--; + ch = text.charAt(offset); + } + boolean enclosed = false; + if (ch == '/') { + // Usecases : + // - < + // - children = element.getChildren(); + for (int i = children.size() - 1; i >= 0; i--) { + DOMNode child = children.get(i); + if (child.isElement()) { + DOMElement childElement = ((DOMElement) child); + if (childElement.isSameTag(tagName)) { + return childElement; + } else { + DOMElement tagElement = findChildTag(tagName, childElement); + if (tagElement != null) { + return tagElement; + } + } + } + } + return null; + } + public static void registerCodeActionParticipants(Map codeActions, SharedSettings sharedSettings) { + codeActions.put(ETagUnterminated.getCode(), new ETagUnterminatedCodeAction()); codeActions.put(ElementUnterminated.getCode(), new ElementUnterminatedCodeAction()); codeActions.put(EqRequiredInAttribute.getCode(), new EqRequiredInAttributeCodeAction()); codeActions.put(OpenQuoteExpected.getCode(), new OpenQuoteExpectedCodeAction()); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/CloseStartTagCodeAction.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/CloseStartTagCodeAction.java deleted file mode 100644 index be478bf38b..0000000000 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/CloseStartTagCodeAction.java +++ /dev/null @@ -1,152 +0,0 @@ -/******************************************************************************* -* 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 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()) { - // - DOMElement parent = element.getParentElement(); - if (parent != null && parent.hasTagName()) { - // - // 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 { - // - boolean startTagClosed = element.isStartTagClosed(); - char c = document.getText().charAt(diagnosticEndOffset - 1); - if (c != '/') { - if (startTagClosed) { - // ex : - // // Close with ' - String tagName = element.getTagName(); - if (tagName != null) { - String label = ""; - 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 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 : '", - diagnosticRange.getEnd(), "/>", document.getTextDocument(), diagnostic); - codeActions.add(autoCloseAction); - // // Close with '> - String tagName = element.getTagName(); - if (tagName != null) { - String insertText = ">"; - 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; - } - -} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/CloseTagCodeAction.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/CloseTagCodeAction.java new file mode 100644 index 0000000000..9c391e042f --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/CloseTagCodeAction.java @@ -0,0 +1,319 @@ +/******************************************************************************* +* 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 CloseTagCodeAction implements ICodeActionParticipant { + + public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List codeActions, + SharedSettings sharedSettings, IComponentProvider componentProvider) { + Range diagnosticRange = diagnostic.getRange(); + try { + int startOffset = document.offsetAt(diagnosticRange.getStart()) + 1; + DOMNode node = document.findNodeAt(startOffset); + if (node == null || !node.isElement()) { + return; + } + DOMElement element = (DOMElement) node; + if (element.hasEndTag() && !element.isEndTagClosed() && element.hasTagName()) { + // Code actions for fixing end tag + doCodeActionsForEndTagUnclosed(element, document, diagnostic, codeActions); + } else { + // Code actions for fixing start tag + boolean startTagClosed = element.isStartTagClosed(); + String text = document.getText(); + if (!startTagClosed) { + doCodeActionsForStartTagUnclosed(element, document, diagnosticRange, text, diagnostic, codeActions); + } else { + doCodeActionsForStartTagClosed(element, document, diagnosticRange, text, diagnostic, codeActions); + } + } + } catch (BadLocationException e) { + // do nothing + } + } + + /** + * Add code actions to fix unclosed end-tag. + * + * @param element the end tag element to fix. + * @param document the owner DOM document. + * @param diagnostic the diagnostic. + * @param codeActions the code actions list to fill. + * + * @throws BadLocationException + */ + private void doCodeActionsForEndTagUnclosed(DOMElement element, DOMDocument document, Diagnostic diagnostic, + List codeActions) throws BadLocationException { + // ex: ' + CodeAction autoCloseAction = CodeActionFactory.insert("Close end-tag with '>'", endTagPosition, ">", + document.getTextDocument(), diagnostic); + codeActions.add(autoCloseAction); + } + + /** + * Add code actions to fix unclosed start-tag. + * + * @param element the start tag element to fix. + * @param document the owner DOM document. + * @param diagnostic the diagnostic. + * @param codeActions the code actions list to fill. + * + * @throws BadLocationException + */ + private void doCodeActionsForStartTagUnclosed(DOMElement element, DOMDocument document, Range diagnosticRange, + String text, Diagnostic diagnostic, List codeActions) throws BadLocationException { + // Here start tag element is not closed with '>'. + if (!element.hasEndTag()) { + // The element has no an end tag + // ex : ' + CodeAction autoCloseAction = CodeActionFactory.insert("Close with '/>'", diagnosticRange.getEnd(), "/>", + document.getTextDocument(), diagnostic); + codeActions.add(autoCloseAction); + // Close with '>' if element has tag name + String tagName = element.getTagName(); + if (tagName != null) { + String insertText = ">"; + CodeAction closeEndTagAction = CodeActionFactory.insert("Close with '" + insertText + "'", + diagnosticRange.getEnd(), insertText, document.getTextDocument(), diagnostic); + codeActions.add(closeEndTagAction); + } + } + } else { + // ex : + CodeAction autoCloseAction = insertGreaterThanCharacterCodeAction(document, diagnostic, diagnosticRange); + codeActions.add(autoCloseAction); + } + } + + /** + * Add code actions to fix closed start-tag. + * + * @param element the start tag element to fix. + * @param document the owner DOM document. + * @param diagnostic the diagnostic. + * @param codeActions the code actions list to fill. + * + * @throws BadLocationException + */ + private void doCodeActionsForStartTagClosed(DOMElement element, DOMDocument document, Range diagnosticRange, + String text, Diagnostic diagnostic, List codeActions) throws BadLocationException { + // Here start tag element is closed with '>'. + if (!element.hasEndTag()) { + // The element has no an end tag + // ex : + // // Close with '' + String tagName = element.getTagName(); + String label = ""; + final String initialInsertText = label; + String insertText = initialInsertText; + // ex: < + // ex: < + // ex: + int endOffset = element.getStartTagCloseOffset() + 1; + Position endPosition = document.positionAt(endOffset); + CodeAction closeEndTagAction = CodeActionFactory.insert("Close with '" + label + "'", endPosition, + insertText, document.getTextDocument(), diagnostic); + codeActions.add(closeEndTagAction); + } else { + // the element have some children(Text node, Element node, etc) + boolean hasChildWithNoTagName = false; + // Search orphan elements in the children to replace + List children = element.getChildren(); + for (DOMNode child : children) { + if (child.isElement()) { + DOMElement childElement = (DOMElement) child; + if (!childElement.hasTagName() || childElement.isOrphanEndTag()) { + // // ex : --> the breaks the element, it should be + // replaced with + // ex : the element, it should be replaced with + // + String replaceTagName = element.getTagName(); + CodeAction replaceTagAction = replaceEndTagNameCodeAction(childElement, replaceTagName, + diagnostic); + codeActions.add(replaceTagAction); + hasChildWithNoTagName = !childElement.hasTagName(); + } + } + } + if (!hasChildWithNoTagName) { + // Here, there are no child end tag element with ' must be inserted with a new line and indent + LineIndentInfo indentInfo = document.getLineIndentInfo(diagnosticRange.getStart().getLine()); + insertText = indentInfo.getLineDelimiter() + indentInfo.getWhitespacesIndent() + insertText; + } + // ex : bar --> the must be inserted after bar text + // ex : --> the must be inserted after the + Position endPosition = document.positionAt(endOffset); + CodeAction closeEndTagAction = CodeActionFactory.insert("Close with '" + label + "'", endPosition, + insertText, document.getTextDocument(), diagnostic); + codeActions.add(closeEndTagAction); + } + } + } else { + // The element has an end tag + // Search orphan end tag elements in the children which breaks the XML. + List children = element.getChildren(); + for (DOMNode child : children) { + if (child.isElement()) { + DOMElement childElement = (DOMElement) child; + if (!childElement.hasTagName() || childElement.isOrphanEndTag()) { + // ex: --> here the code action removes ' --> here the code action removes '' + CodeAction removeAction = removeTagCodeAction(childElement, document, diagnostic); + codeActions.add(removeAction); + } + } + } + } + } + + /** + * Create a code action which insert '>' at the end of the diagnostic error. + * + * @param diagnostic the diagnostic. + * @param document the DOM docment. + * @param diagnosticRange the diagnostic range + * @return a code action which insert '>' at the end of the diagnostic error. + */ + private static CodeAction insertGreaterThanCharacterCodeAction(DOMDocument document, Diagnostic diagnostic, + Range diagnosticRange) { + CodeAction autoCloseAction = CodeActionFactory.insert("Close with '>'", diagnosticRange.getEnd(), ">", + document.getTextDocument(), diagnostic); + return autoCloseAction; + } + + /** + * Create a code action which remove the content of the given DOM element. + * + * @param element the DOM element to remove. + * @param document the DOM document. + * @param diagnostic the diagnostic. + * @return + * @throws BadLocationException + */ + private static CodeAction removeTagCodeAction(DOMElement element, DOMDocument document, Diagnostic diagnostic) + throws BadLocationException { + String text = document.getText(); + Position startPosition = document.positionAt(element.getStart()); + Position endPosition = document.positionAt(element.getEnd()); + String contentToRemove = text.substring(element.getStart(), element.getEnd()); + CodeAction removeAction = CodeActionFactory.remove("Remove '" + contentToRemove + "'", + new Range(startPosition, endPosition), document.getTextDocument(), diagnostic); + return removeAction; + } + + /** + * Create a code action which replaces the end tag name of the given element + * with the given replace tag name. + * + * @param element the DOM element to replace + * @param replaceTagName the replace tag name + * @param diagnostic the diagnostic. + * @return a code action which replaces the end tag name of the given element + * with the given replace tag name. + */ + private static CodeAction replaceEndTagNameCodeAction(DOMElement element, String replaceTagName, + Diagnostic diagnostic) { + // + // Replace with 'b' closing tag + DOMDocument document = element.getOwnerDocument(); + Range replaceRange = XMLPositionUtility.selectEndTagName(element); + String tagName = element.getTagName(); + if (tagName == null) { + tagName = ""; + } + return CodeActionFactory.replace("Replace '" + tagName + "' with '" + replaceTagName + "' closing tag", + replaceRange, replaceText, document.getTextDocument(), diagnostic); + } + + /** + * 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; + } + + private static boolean isCharAt(String text, int offset, char ch) { + if (text.length() <= offset) { + return false; + } + return text.charAt(offset) == ch; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ETagRequiredCodeAction.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ETagRequiredCodeAction.java index b402822576..73daee49e2 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ETagRequiredCodeAction.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ETagRequiredCodeAction.java @@ -14,6 +14,6 @@ /** * Code action to fix ETagRequired error. */ -public class ETagRequiredCodeAction extends CloseStartTagCodeAction { +public class ETagRequiredCodeAction extends CloseTagCodeAction { } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ETagUnterminatedCodeAction.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ETagUnterminatedCodeAction.java new file mode 100644 index 0000000000..89ea8b7ec1 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ETagUnterminatedCodeAction.java @@ -0,0 +1,19 @@ +/******************************************************************************* +* 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; + +/** + * Code action to fix ETagUnterminated error. + * + */ +public class ETagUnterminatedCodeAction extends CloseTagCodeAction { +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ElementUnterminatedCodeAction.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ElementUnterminatedCodeAction.java index 2326d150f7..90574b09d9 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ElementUnterminatedCodeAction.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/ElementUnterminatedCodeAction.java @@ -16,5 +16,5 @@ * Code action to fix ElementUnterminated error. * */ -public class ElementUnterminatedCodeAction extends CloseStartTagCodeAction { +public class ElementUnterminatedCodeAction extends CloseTagCodeAction { } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/MarkupEntityMismatchCodeAction.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/MarkupEntityMismatchCodeAction.java index c0087e47d9..72824df268 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/MarkupEntityMismatchCodeAction.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/MarkupEntityMismatchCodeAction.java @@ -16,6 +16,6 @@ * tag of the root element is missing. This will provide a codeaction that * inserts that missing end tag. */ -public class MarkupEntityMismatchCodeAction extends CloseStartTagCodeAction { +public class MarkupEntityMismatchCodeAction extends CloseTagCodeAction { } \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/generators/AbstractXML2GrammarGenerator.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/generators/AbstractXML2GrammarGenerator.java index 9839b6abf1..9dcd1b456b 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/generators/AbstractXML2GrammarGenerator.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/generators/AbstractXML2GrammarGenerator.java @@ -142,29 +142,32 @@ private static void fillElements(Node node, Grammar grammar, ContainerDeclaratio Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element) child; - ElementDeclaration elementDecl = getElementDecl(grammar, parent, flat, element); - // Update has text - if (!elementDecl.hasCharacterContent()) { - elementDecl.setHasCharacterContent(hasCharacterContent(element)); - } - // Update element occurrences - elementDecl.incrementOccurrences(); - // Collect attributes - NamedNodeMap attributes = element.getAttributes(); - if (attributes != null) { - for (int j = 0; j < attributes.getLength(); j++) { - Attr attr = (Attr) attributes.item(j); - if (!isIgnore(attr)) { - // Attribute must be added in the grammar - AttributeDeclaration attributeDecl = elementDecl.getAttribute(attr.getName()); - // Update attribute occurrences - attributeDecl.incrementOccurrences(); - // Update attribute value - attributeDecl.addValue(attr.getValue()); + if (element.getLocalName() != null) { + // Element has tag name. + ElementDeclaration elementDecl = getElementDecl(grammar, parent, flat, element); + // Update has text + if (!elementDecl.hasCharacterContent()) { + elementDecl.setHasCharacterContent(hasCharacterContent(element)); + } + // Update element occurrences + elementDecl.incrementOccurrences(); + // Collect attributes + NamedNodeMap attributes = element.getAttributes(); + if (attributes != null) { + for (int j = 0; j < attributes.getLength(); j++) { + Attr attr = (Attr) attributes.item(j); + if (!isIgnore(attr)) { + // Attribute must be added in the grammar + AttributeDeclaration attributeDecl = elementDecl.getAttribute(attr.getName()); + // Update attribute occurrences + attributeDecl.incrementOccurrences(); + // Update attribute value + attributeDecl.addValue(attr.getValue()); + } } } + fillElements(element, grammar, elementDecl, flat); } - fillElements(element, grammar, elementDecl, flat); } } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCodeActions.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCodeActions.java index 41dca4152c..18057f0863 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCodeActions.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/XMLCodeActions.java @@ -14,6 +14,8 @@ import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.services.extensions.ICodeActionParticipant; @@ -30,6 +32,8 @@ */ public class XMLCodeActions { + private static final Logger LOGGER = Logger.getLogger(XMLCompletions.class.getName()); + private final XMLExtensionsRegistry extensionsRegistry; public XMLCodeActions(XMLExtensionsRegistry extensionsRegistry) { @@ -39,11 +43,17 @@ public XMLCodeActions(XMLExtensionsRegistry extensionsRegistry) { public List doCodeActions(CodeActionContext context, Range range, DOMDocument document, SharedSettings sharedSettings) { List codeActions = new ArrayList<>(); - if (context.getDiagnostics() != null) { + List diagnostics = context.getDiagnostics(); + if (diagnostics != null) { for (Diagnostic diagnostic : context.getDiagnostics()) { for (ICodeActionParticipant codeActionParticipant : extensionsRegistry.getCodeActionsParticipants()) { - codeActionParticipant.doCodeAction(diagnostic, range, document, codeActions, - sharedSettings, extensionsRegistry); + try { + codeActionParticipant.doCodeAction(diagnostic, range, document, codeActions, sharedSettings, + extensionsRegistry); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error while processing code action participant '" + + codeActionParticipant.getClass().getName() + "'", e); + } } } } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/DTDDiagnosticsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/DTDDiagnosticsTest.java index bc427c1bdb..9e4738d13a 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/DTDDiagnosticsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/DTDDiagnosticsTest.java @@ -577,7 +577,7 @@ public void testDTDNotFoundWithSYSTEM() throws Exception { XMLAssert.testDiagnosticsFor(xml, d(1, 29, 1, 46, DTDErrorCode.dtd_not_found), // [1] d(2, 1, 12, DTDErrorCode.MSG_ELEMENT_NOT_DECLARED), // [2] d(5, 4, 12, DTDErrorCode.MSG_ELEMENT_NOT_DECLARED), // [3] - d(5, 23, 5, 30, XMLSyntaxErrorCode.ETagRequired)); // [4] + d(5, 4, 5, 21, XMLSyntaxErrorCode.ETagRequired)); // [4] } @Test @@ -594,7 +594,7 @@ public void testDTDNotFoundWithPUBLIC() throws Exception { XMLAssert.testDiagnosticsFor(xml, d(1, 33, 1, 50, DTDErrorCode.dtd_not_found), // [1] d(2, 1, 12, DTDErrorCode.MSG_ELEMENT_NOT_DECLARED), // [2] d(5, 4, 12, DTDErrorCode.MSG_ELEMENT_NOT_DECLARED), // [3] - d(5, 23, 5, 30, XMLSyntaxErrorCode.ETagRequired)); // [4] + d(5, 4, 5, 21, XMLSyntaxErrorCode.ETagRequired)); // [4] } private static void testDiagnosticsFor(String xml, Diagnostic... expected) { diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSyntaxDiagnosticsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSyntaxDiagnosticsTest.java index 272a57b8b2..0e5ed87da2 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSyntaxDiagnosticsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSyntaxDiagnosticsTest.java @@ -153,8 +153,6 @@ public void elementUnterminated() throws Exception { Diagnostic d = d(1, 11, 1, 16, XMLSyntaxErrorCode.ElementUnterminated); testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, // - ca(d, te(1, 16, 1, 16, "/>")), // - ca(d, te(1, 16, 1, 16, ">")), // ca(d, te(1, 16, 1, 16, ">"))); } @@ -189,32 +187,29 @@ public void testElementUnterminatedEndsWithAttributes() throws Exception { testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, // ca(d, te(1, 13, 1, 13, "/>")), // - ca(d, te(1, 13, 1, 13, ">")), // - ca(d, te(1, 13, 1, 13, ">"))); + ca(d, te(1, 13, 1, 13, ">"))); } @Test - public void testElementUnterminatedEndsAndSpaces() throws Exception { + public void testElementUnterminatedEndsWithAttributesAndEndSlash() throws Exception { String xml = "\r\n" + // - " "; - Diagnostic d = d(1, 3, 1, 13, XMLSyntaxErrorCode.ElementUnterminated); + Diagnostic d = d(1, 3, 1, 24, XMLSyntaxErrorCode.ElementUnterminated); testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, // - ca(d, te(1, 13, 1, 13, "/>")), // - ca(d, te(1, 13, 1, 13, ">")), // - ca(d, te(1, 13, 1, 13, ">"))); + testCodeActionsFor(xml, d, ca(d, te(1, 24, 1, 24, ">"))); } @Test - public void testETagRequiredWithReplace() throws Exception { - String xml = "\r\n" + // - " \r\n" + // - " "; - Diagnostic d = d(2, 4, 2, 5, XMLSyntaxErrorCode.ETagRequired); + public void testElementUnterminatedEndsAndSpaces() throws Exception { + String xml = "\r\n" + // + " "; + Diagnostic d = d(1, 3, 1, 13, XMLSyntaxErrorCode.ElementUnterminated); testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, // - ca(d, te(2, 4, 2, 5, "b"))); + ca(d, te(1, 13, 1, 13, "/>")), // + ca(d, te(1, 13, 1, 13, ">"))); } /** @@ -276,7 +271,7 @@ public void testETagRequired() throws Exception { " Name\r\n" + // " \r\n" + // " "; - Diagnostic d = d(1, 5, 1, 7, XMLSyntaxErrorCode.ETagRequired); + Diagnostic d = d(1, 5, 2, 2, XMLSyntaxErrorCode.ETagRequired); testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, ca(d, te(1, 12, 1, 12, ""))); } @@ -286,7 +281,7 @@ public void testETagRequired2() throws Exception { String xml = "\r\n" + // " Nm>Name\r\n" + // " "; - testDiagnosticsFor(xml, d(1, 13, 1, 15, XMLSyntaxErrorCode.ETagRequired)); + testDiagnosticsFor(xml, d(0, 1, 1, 11, XMLSyntaxErrorCode.ETagRequired)); } @Test @@ -296,11 +291,67 @@ public void testETagRequired3() throws Exception { " \r\n" + // " \r\n" + // ""; - Diagnostic d = d(3, 5, 3, 7, XMLSyntaxErrorCode.ETagRequired); + Diagnostic d = d(3, 5, 4, 0, XMLSyntaxErrorCode.ETagRequired); testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, ca(d, te(3, 8, 3, 8, ""))); } + @Test + public void testETagRequiredWithReplace() throws Exception { + String xml = "\r\n" + // + " \r\n" + // + " "; + Diagnostic d = d(1, 2, 2, 2, XMLSyntaxErrorCode.ETagRequired); + testDiagnosticsFor(xml, d); + testCodeActionsFor(xml, d, // + ca(d, te(2, 4, 2, 5, "b")), ca(d, te(2, 6, 2, 6, "\r\n "))); + } + + @Test + public void testETagRequiredWithText() throws Exception { + String xml = "\r\n" + // + "def\r\n" + // + ""; + Diagnostic d = d(1, 1, 2, 0, XMLSyntaxErrorCode.ETagRequired); + testDiagnosticsFor(xml, d); + testCodeActionsFor(xml, d, ca(d, te(1, 8, 1, 8, ""))); + } + + @Test + public void testETagRequiredWithOrpheanEndTag() throws Exception { + String xml = "\r\n" + // + " \r\n" + // + " "; + Diagnostic d = d(1, 2, 2, 2, XMLSyntaxErrorCode.ETagRequired); + testDiagnosticsFor(xml, d); + testCodeActionsFor(xml, d, ca(d, te(2, 4, 2, 4, "foo>"))); + } + + @Test + public void testETagRequiredClosedWithOrpheanEndTag() throws Exception { + String xml = "\r\n" + // + " \r\n" + // + " \r\n" + // + ""; + Diagnostic d = d(1, 2, 2, 2, XMLSyntaxErrorCode.ETagRequired); + testDiagnosticsFor(xml, d); + testCodeActionsFor(xml, d, ca(d, te(2, 2, 2, 4, ""))); + } + + @Test + public void testETagRequiredClosedWithOrpheanEndTag2() throws Exception { + String xml = "\r\n" + // + " \r\n" + // + " \r\n" + // + " \r\n" + // + ""; + Diagnostic d = d(1, 2, 2, 2, XMLSyntaxErrorCode.ETagRequired); + testDiagnosticsFor(xml, d); + testCodeActionsFor(xml, d, ca(d, te(2, 2, 2, 8, ""))); + } + /** * Test ETagUnterminated * @@ -311,7 +362,9 @@ public void testETagRequired3() throws Exception { public void testETagUnterminated() throws Exception { String xml = "ABC/090928/CCT0012009-09-28T14:07:00"; - testDiagnosticsFor(xml, d(0, 26, 0, 31, XMLSyntaxErrorCode.ETagUnterminated)); + Diagnostic d = d(0, 26, 0, 31, XMLSyntaxErrorCode.ETagUnterminated); + testDiagnosticsFor(xml, d); + testCodeActionsFor(xml, d, ca(d, te(0, 31, 0, 31, ">"))); } /** @@ -327,7 +380,9 @@ public void testETagUnterminated2() throws Exception { " \r\n" + // " "; - testDiagnosticsFor(xml, d(3, 4, 3, 5, XMLSyntaxErrorCode.ETagUnterminated)); + Diagnostic d = d(3, 4, 3, 5, XMLSyntaxErrorCode.ETagUnterminated); + testDiagnosticsFor(xml, d); + testCodeActionsFor(xml, d, ca(d, te(3, 5, 3, 5, ">"))); } /** @@ -365,16 +420,6 @@ public void testETagUnterminated4() throws Exception { testDiagnosticsFor(xml, d(4, 6, 4, 16, XMLSyntaxErrorCode.ETagUnterminated)); } - @Test - public void testETagRequiredWithText() throws Exception { - String xml = "\r\n" + // - "def\r\n" + // - ""; - Diagnostic d = d(1, 1, 1, 4, XMLSyntaxErrorCode.ETagRequired); - testDiagnosticsFor(xml, d); - testCodeActionsFor(xml, d, ca(d, te(1, 8, 1, 8, ""))); - } - @Test public void testIllegalQName() throws Exception { String xml = "100"; @@ -400,7 +445,7 @@ public void testMarkupEntityMismatch() throws Exception { + // "\r\n" + // ""; - Diagnostic d = d(1, 1, 1, 9, XMLSyntaxErrorCode.MarkupEntityMismatch); + Diagnostic d = d(1, 1, 3, 19, XMLSyntaxErrorCode.MarkupEntityMismatch); testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, ca(d, te(3, 19, 3, 19, "\r\n"))); } @@ -408,15 +453,22 @@ public void testMarkupEntityMismatch() throws Exception { @Test public void testMarkupEntityMismatch2() throws Exception { String xml = ""; - Diagnostic d = d(0, 1, 0, 4, XMLSyntaxErrorCode.MarkupEntityMismatch); + Diagnostic d = d(0, 1, 0, 5, XMLSyntaxErrorCode.MarkupEntityMismatch); testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, ca(d, te(0, 5, 0, 5, ""))); } @Test public void testMarkupEntityMismatch3() throws Exception { + String xml = "<"; + Diagnostic d = d(0, 0, 0, 1, XMLSyntaxErrorCode.MarkupEntityMismatch); + testDiagnosticsFor(xml, d); + } + + @Test + public void testMarkupEntityMismatch4() throws Exception { String xml = "")), // - ca(d, te(0, 4, 0, 4, ">")), // - ca(d, te(0, 4, 0, 4, ">"))); + ca(d, te(0, 4, 0, 4, ">"))); } @Test @@ -438,8 +489,7 @@ public void testMarkupEntityMismatchWithoutCloseAndNewLine() throws Exception { testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, // ca(d, te(0, 4, 0, 4, "/>")), // - ca(d, te(0, 4, 0, 4, ">")), // - ca(d, te(0, 4, 0, 4, ">"))); + ca(d, te(0, 4, 0, 4, ">"))); } @Test @@ -449,8 +499,7 @@ public void testMarkupEntityMismatchWithoutCloseAndSpaces() throws Exception { testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, // ca(d, te(0, 4, 0, 4, "/>")), // - ca(d, te(0, 4, 0, 4, ">")), // - ca(d, te(0, 4, 0, 4, ">"))); + ca(d, te(0, 4, 0, 4, ">"))); } @Test @@ -460,8 +509,7 @@ public void testMarkupEntityMismatchWithAttributes() throws Exception { testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, // ca(d, te(0, 9, 0, 9, "/>")), // - ca(d, te(0, 9, 0, 9, ">")), // - ca(d, te(0, 9, 0, 9, ">"))); + ca(d, te(0, 9, 0, 9, ">"))); } @Test @@ -476,7 +524,7 @@ public void testMarkupEntityMismatchWithAttributesAndSlash() throws Exception { @Test public void testMarkupEntityMismatchWithText() throws Exception { String xml = "def"; - Diagnostic d = d(0, 1, 0, 4, XMLSyntaxErrorCode.MarkupEntityMismatch); + Diagnostic d = d(0, 1, 0, 8, XMLSyntaxErrorCode.MarkupEntityMismatch); testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, ca(d, te(0, 8, 0, 8, ""))); } @@ -484,7 +532,7 @@ public void testMarkupEntityMismatchWithText() throws Exception { @Test public void testMarkupEntityMismatchWithTextAndNewLine() throws Exception { String xml = "def\r\n"; - Diagnostic d = d(0, 1, 0, 4, XMLSyntaxErrorCode.MarkupEntityMismatch); + Diagnostic d = d(0, 1, 0, 8, XMLSyntaxErrorCode.MarkupEntityMismatch); testDiagnosticsFor(xml, d); testCodeActionsFor(xml, d, ca(d, te(0, 8, 0, 8, ""))); } @@ -702,4 +750,33 @@ public void testEntitySemicolonRequiredInReferenceShortName() throws Exception { ""; testDiagnosticsFor(xml, d(5, 4, 5, 6, XMLSyntaxErrorCode.SemicolonRequiredInReference)); } + + @Test + public void closeTag() throws Exception { + String xml = "")), // + ca(d, te(0, 2, 0, 2, ">"))); + + xml = ""; + d = d(0, 1, 0, 3, XMLSyntaxErrorCode.MarkupEntityMismatch); + testDiagnosticsFor(xml, d); + testCodeActionsFor(xml, d, ca(d, te(0, 3, 0, 3, ""))); + + xml = ""))); + + xml = ""))); + + xml = ""))); + } } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/generators/xml2dtd/XML2DTDGeneratorTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/generators/xml2dtd/XML2DTDGeneratorTest.java index b54484218e..612da125b1 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/generators/xml2dtd/XML2DTDGeneratorTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/generators/xml2dtd/XML2DTDGeneratorTest.java @@ -315,4 +315,26 @@ public void attrIDsAndEnums() { ""; assertGrammarGenerator(xml, new DTDGeneratorSettings(), dtd); } + + @Test + public void invalidStartTag() throws IOException { + String xml = "<"; + String dtd = ""; + assertGrammarGenerator(xml, new DTDGeneratorSettings(), dtd); + + xml = "bcd < "; + dtd = ""; + assertGrammarGenerator(xml, new DTDGeneratorSettings(), dtd); + } + + @Test + public void invalidEndTag() throws IOException { + String xml = ""; + String dtd = ""; + assertGrammarGenerator(xml, new DTDGeneratorSettings(), dtd); + + xml = "bcd "; + dtd = ""; + assertGrammarGenerator(xml, new DTDGeneratorSettings(), dtd); + } } \ No newline at end of file