From 888ee23c95d7e8ed4232424cafcda14c4d6d6dfa Mon Sep 17 00:00:00 2001 From: David Kwon Date: Mon, 4 May 2020 14:52:23 -0400 Subject: [PATCH] Add code action for RootElementTypeMustMatchDoctypedecl Signed-off-by: David Kwon --- .../lemminx/commons/CodeActionFactory.java | 16 ++- .../participants/XMLSyntaxErrorCode.java | 2 + ...entTypeMustMatchDoctypedeclCodeAction.java | 121 ++++++++++++++++++ .../XMLSyntaxDiagnosticsTest.java | 23 +++- 4 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/RootElementTypeMustMatchDoctypedeclCodeAction.java diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/CodeActionFactory.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/CodeActionFactory.java index c62f5cf52..add9d1d1e 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/CodeActionFactory.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/commons/CodeActionFactory.java @@ -16,6 +16,7 @@ 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; @@ -76,16 +77,23 @@ public static CodeAction insert(String title, Position position, String insertTe public static CodeAction replace(String title, Range range, String replaceText, TextDocumentItem document, Diagnostic diagnostic) { + TextEdit replace = new TextEdit(range, replaceText); + return replace(title, Collections.singletonList(replace), document, + diagnostic); + } + + public static CodeAction replace(String title, List replace, TextDocumentItem document, + Diagnostic diagnostic) { + CodeAction insertContentAction = new CodeAction(title); insertContentAction.setKind(CodeActionKind.QuickFix); insertContentAction.setDiagnostics(Arrays.asList(diagnostic)); - TextEdit edit = new TextEdit(range, replaceText); + VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier( document.getUri(), document.getVersion()); - - TextDocumentEdit textDocumentEdit = new TextDocumentEdit(versionedTextDocumentIdentifier, Collections.singletonList(edit)); + TextDocumentEdit textDocumentEdit = new TextDocumentEdit(versionedTextDocumentIdentifier, + replace); WorkspaceEdit workspaceEdit = new WorkspaceEdit(Collections.singletonList(Either.forLeft(textDocumentEdit))); - insertContentAction.setEdit(workspaceEdit); return insertContentAction; } 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 3ee0fff04..03c3d65e0 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 @@ -27,6 +27,7 @@ import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.EqRequiredInAttributeCodeAction; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.MarkupEntityMismatchCodeAction; import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.OpenQuoteExpectedCodeAction; +import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.RootElementTypeMustMatchDoctypedeclCodeAction; import org.eclipse.lemminx.services.extensions.ICodeActionParticipant; import org.eclipse.lemminx.services.extensions.diagnostics.IXMLErrorCode; import org.eclipse.lemminx.utils.XMLPositionUtility; @@ -232,5 +233,6 @@ public static void registerCodeActionParticipants(Map", must match DOCTYPE root "". + */ +public class RootElementTypeMustMatchDoctypedeclCodeAction implements ICodeActionParticipant { + + @Override + public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List codeActions, + XMLFormattingOptions formattingSettings, IComponentProvider componentProvider) { + DOMElement root = document.getDocumentElement(); + if (root == null) { + return; + } + Range rootStartRange = XMLPositionUtility.selectStartTagName(root); + if (!range.equals(rootStartRange)) { + return; + } + + String currentRootText = getCurrentRoot(diagnostic.getMessage()); + if (currentRootText == null || !currentRootText.equals(root.getNodeName())) { + return; + } + + String doctypeRootText = getDoctypeRoot(diagnostic.getMessage()); + if (doctypeRootText == null) { + return; + } + + List replace= new ArrayList<>(); + addTextEdits(root, doctypeRootText, replace); + + CodeAction action = CodeActionFactory.replace( + "Replace with '" + doctypeRootText + "'", + replace, document.getTextDocument(), diagnostic); + codeActions.add(action); + } + + private void addTextEdits(DOMElement root, String newText, List replace) { + replace.add(new TextEdit(XMLPositionUtility.selectStartTagName(root), newText)); + + if (root.isClosed() && !root.isSelfClosed()) { + replace.add(new TextEdit(XMLPositionUtility.selectEndTagName(root), newText)); + } + } + + /** + * Returns the current root name, extracted from message + * + * The provided message must match this format: + * ... root element "", must match DOCTYPE ... + * + * @param message the message to extract the root name from + * @return the current root name, extracted from message + */ + private static String getCurrentRoot(String message) { + String preText = "root element \""; + String postText = "\", must match DOCTYPE"; + int preMatch = message.indexOf(preText); + if (preMatch < 0) { + return null; + } + int postMatch = message.indexOf(postText); + if (postMatch < 0) { + return null; + } + + return message.substring(preMatch + preText.length(), postMatch); + } + + /** + * Returns the doctype root name, extracted from message + * + * The provided message must match this format: + * ... DOCTYPE root "". + * + * @param message the message to extract the root name from + * @return the DOCTYPE root name, extracted from message + */ + private static String getDoctypeRoot(String message) { + String preText = "DOCTYPE root \""; + int preMatch = message.indexOf(preText); + if (preMatch < 0) { + return null; + } + return message.substring(preMatch + preText.length(), message.length() - 2); + } + +} \ No newline at end of file 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 2b8e08ddf..f74921d4f 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 @@ -424,7 +424,7 @@ public void testQuoteRequiredInXMLDecl() throws Exception { } @Test - public void testRootElementTypeMustMatchDoctypedecl() { + public void testRootElementTypeMustMatchDoctypedecl() throws Exception { String xml = "\r\n" + " \r\n" + ""; - testDiagnosticsFor(xml, d(5, 1, 5, 5, XMLSyntaxErrorCode.RootElementTypeMustMatchDoctypedecl)); + String expectedMessage = "Document root element \"abcd\", must match DOCTYPE root \"efgh\"."; + Diagnostic d = d(5, 1, 5, 5, XMLSyntaxErrorCode.RootElementTypeMustMatchDoctypedecl, expectedMessage); + testDiagnosticsFor(xml, d); + testCodeActionsFor(xml, d, ca(d, te(5, 1, 5, 5, "efgh"))); + } + + @Test + public void testRootElementTypeMustMatchDoctypedecl2() throws Exception { + String xml = + "\r\n" + + "\r\n" + + "\r\n" + + "]> \r\n" + + "test"; + + String expectedMessage = "Document root element \"abcd\", must match DOCTYPE root \"efgh\"."; + Diagnostic d = d(5, 1, 5, 5, XMLSyntaxErrorCode.RootElementTypeMustMatchDoctypedecl, expectedMessage); + testDiagnosticsFor(xml, d); + testCodeActionsFor(xml, d, ca(d, te(5, 1, 5, 5, "efgh"), te(5, 12, 5, 16, "efgh"))); } @Test