diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMCharacterData.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMCharacterData.java index 332d988ac..b6b811d6c 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMCharacterData.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMCharacterData.java @@ -30,16 +30,28 @@ public abstract class DOMCharacterData extends DOMNode implements org.w3c.dom.Ch private boolean isWhitespace; + private int newLineCount; + + private String delimiter; + public DOMCharacterData(int start, int end, DOMDocument ownerDocument) { super(start, end, ownerDocument); } public boolean hasMultiLine() { + return getData().contains(getDelimiter()); + } + + public String getDelimiter() { + if(delimiter != null) { + return delimiter; + } try { - String delimiter = getOwnerDocument().getTextDocument().lineDelimiter(0); - return getData().contains(delimiter); + delimiter = getOwnerDocument().getTextDocument().lineDelimiter(0); + return delimiter; } catch (BadLocationException e) { - return getData().contains(lineSeparator()); + delimiter = lineSeparator(); + return delimiter; } } diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMParser.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMParser.java index d688c5aa0..4c2224a80 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMParser.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMParser.java @@ -44,7 +44,15 @@ public DOMDocument parse(String text, String uri, URIResolverExtensionManager re return parse(new TextDocument(text, uri), resolverExtensionManager); } + public DOMDocument parse(String text, String uri, URIResolverExtensionManager resolverExtensionManager, boolean ignoreWhitespaceContent) { + return parse(new TextDocument(text, uri), resolverExtensionManager, ignoreWhitespaceContent); + } + public DOMDocument parse(TextDocument document, URIResolverExtensionManager resolverExtensionManager) { + return parse(document, resolverExtensionManager, true); + } + + public DOMDocument parse(TextDocument document, URIResolverExtensionManager resolverExtensionManager, boolean ignoreWhitespaceContent) { boolean isDTD = DOMUtils.isDTD(document.getUri()); boolean inDTDInternalSubset = false; String text = document.getText(); @@ -320,8 +328,8 @@ else if((curr.isClosed())) { case Content: { // FIXME: don't use getTokenText (substring) to know if the content is only // spaces or line feed (scanner should know that). - - if (curr instanceof DTDDeclNode) { + boolean currIsDeclNode = curr instanceof DTDDeclNode; + if (currIsDeclNode) { curr.end = scanner.getTokenOffset() - 1; while(!curr.isDoctype()) { curr = curr.getParentNode(); @@ -334,12 +342,22 @@ else if((curr.isClosed())) { String content = scanner.getTokenText(); if(StringUtils.isWhitespace(content)) { - if(curr.hasChildNodes()) { + if(ignoreWhitespaceContent) { + if(curr.hasChildNodes()) { + break; + } + + tempWhitespaceContent = textNode; break; + } - textNode.setWhitespace(true); - tempWhitespaceContent = textNode; - break; + else if(!currIsDeclNode) { + textNode.setWhitespace(true); + } + else { + break; + } + } curr.addChild(textNode); diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLFormatter.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLFormatter.java index 75fa102f8..9e8d19cd6 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLFormatter.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLFormatter.java @@ -11,7 +11,6 @@ package org.eclipse.lsp4xml.services; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -72,7 +71,7 @@ public List format(TextDocument document, Range range, XMLFo // Parse the content to format to create an XML document with full data (CData, // comments, etc) String text = document.getText().substring(start, end); - DOMDocument doc = DOMParser.getInstance().parse(text, document.getUri(), null); + DOMDocument doc = DOMParser.getInstance().parse(text, document.getUri(), null, false); // Format the content XMLBuilder xml = new XMLBuilder(formattingOptions, "", document.lineDelimiter(startPosition.getLine())); @@ -98,7 +97,7 @@ private void format(DOMNode node, int level, int end, XMLBuilder xml) { doLineFeed = false; } else { doLineFeed = !(node.isComment() && ((DOMComment) node).isCommentSameLineEndTag()) - && (!node.isText() || ((DOMText) node).hasSiblings()); + && (!node.isText() || (!((DOMText) node).isWhitespace() && ((DOMText) node).hasSiblings())); //&& (!isPreviousSiblingNodeType(node, DOMNode.TEXT_NODE) || !((DOMText) node.getPreviousSibling()).endsWithNewLine()) } @@ -209,7 +208,7 @@ private void format(DOMNode node, int level, int end, XMLBuilder xml) { // Generate content String content = textNode.getData(); - xml.addContent(content, textNode.isWhitespace(), textNode.hasSiblings()); + xml.addContent(content, textNode.isWhitespace(), textNode.hasSiblings(), textNode.getDelimiter(), level); return; } else if (node.isDoctype()) { boolean isDTD = node.getOwnerDocument().isDTD(); diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/settings/XMLFormattingOptions.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/settings/XMLFormattingOptions.java index 2b0487ba0..53a3905ed 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/settings/XMLFormattingOptions.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/settings/XMLFormattingOptions.java @@ -30,6 +30,7 @@ public class XMLFormattingOptions extends FormattingOptions { private static final String SPACE_BEFORE_EMPTY_CLOSE_TAG = "spaceBeforeEmptyCloseTag"; private static final String QUOTATIONS = "quotations"; private static final String JOIN_CONTENT_LINES = "joinContentLines"; + private static final String PRESERVED_NEWLINES = "preservedNewlines"; // Values for QUOTATIONS public static final String DOUBLE_QUOTES_VALUE = "doubleQuotes"; @@ -66,6 +67,7 @@ public void initializeDefaultSettings() { this.setSpaceBeforeEmptyCloseTag(true); this.setQuotations(DOUBLE_QUOTES_VALUE); this.setPreserveEmptyContent(false); + this.setPreservedNewlines(2); } public XMLFormattingOptions(int tabSize, boolean insertSpaces, boolean initializeDefaultSettings) { @@ -245,6 +247,20 @@ public boolean isPreserveEmptyContent() { } } + public void setPreservedNewlines(final int preservedNewlines) { + this.putNumber(XMLFormattingOptions.PRESERVED_NEWLINES, preservedNewlines); + } + + public int getPreservedNewlines() { + + final Number value = this.getNumber(XMLFormattingOptions.PRESERVED_NEWLINES); + if ((value != null)) { + return value.intValue(); + } else { + return 2; + } + } + public XMLFormattingOptions merge(FormattingOptions formattingOptions) { formattingOptions.entrySet().stream().forEach(entry -> { String key = entry.getKey(); diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/StringUtils.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/StringUtils.java index 3fdf15449..67aadbc46 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/StringUtils.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/StringUtils.java @@ -176,6 +176,43 @@ public static String lTrim(String value) { return value.substring(i, len); } - + /** + * 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 + * @return + */ + public static int getNumberOfNewLines(String text, boolean isWhitespace, String delimiter, int newLineLimit) { + if(!isWhitespace){ + return 0; + } + + int newLineCounter = 0; + boolean delimiterHasTwoCharacters = delimiter.length() == 2; + for(int i = 0; newLineCounter <= newLineLimit && i < text.length(); i++) { + String c; + if(delimiterHasTwoCharacters) { + if(i + 1 < text.length()) { + c = text.substring(i, i + 2); + if(delimiter.equals(c)) { + newLineCounter++; + i++; //skip the second char of the delimiter + } + } + } + else { + c = String.valueOf(text.charAt(i)); + if(delimiter.equals(c)) { + newLineCounter++; + } + } + } + return newLineCounter; + } } diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/XMLBuilder.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/XMLBuilder.java index 711bc0757..1f72159af 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/XMLBuilder.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/utils/XMLBuilder.java @@ -14,8 +14,6 @@ import org.eclipse.lsp4xml.dom.DOMAttr; import org.eclipse.lsp4xml.dom.DOMComment; -import org.eclipse.lsp4xml.dom.DOMDocumentType; -import org.eclipse.lsp4xml.dom.DOMNode; import org.eclipse.lsp4xml.dom.DTDDeclNode; import org.eclipse.lsp4xml.settings.XMLFormattingOptions; @@ -222,10 +220,10 @@ public XMLBuilder linefeed() { } public XMLBuilder addContent(String text) { - return addContent(text, false, false); + return addContent(text, false, false, null, 0); } - public XMLBuilder addContent(String text, Boolean isWhitespaceContent, Boolean hasSiblings) { + public XMLBuilder addContent(String text, Boolean isWhitespaceContent, Boolean hasSiblings, String delimiter, int level) { if(!isWhitespaceContent) { if(isJoinContentLines()) { text = StringUtils.normalizeSpace(text); @@ -238,6 +236,17 @@ else if(hasSiblings) { else if (!hasSiblings && isPreserveEmptyContent()) { xml.append(text); } + else if(hasSiblings) { + int preservedNewLines = getPreservedNewlines(); + if(preservedNewLines > 0) { + int newLineCount = StringUtils.getNumberOfNewLines(text, isWhitespaceContent, delimiter, preservedNewLines); + for (int i = 0; i < newLineCount - 1; i++) { // - 1 because the node after will insert a delimiter + xml.append(delimiter); + this.indent(level); + } + } + + } return this; } @@ -383,4 +392,11 @@ private boolean isPreserveEmptyContent() { return formattingOptions != null && formattingOptions.isPreserveEmptyContent(); } + private int getPreservedNewlines() { + if(formattingOptions != null) { + return formattingOptions.getPreservedNewlines(); + } + return 2; // default + } + } diff --git a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/dom/DOMParserTest.java b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/dom/DOMParserTest.java index 3d107306e..795637cb3 100644 --- a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/dom/DOMParserTest.java +++ b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/dom/DOMParserTest.java @@ -14,6 +14,7 @@ import java.util.ArrayList; +import org.eclipse.lsp4j.FormattingOptions; import org.eclipse.lsp4xml.dom.DOMDocumentType.DocumentTypeKind; import org.junit.Assert; import org.junit.Ignore; @@ -411,6 +412,34 @@ public void testUnclosedEndTagWithTrailingComment() { assertDocument(" Content ", root); } + @Test + public void testWhitespaceIsParsed() { + DOMNode textNodeBefore = createTextNode("\r\n\r\n", 3, 7, true); + DOMNode a = createElement("a", 0, 18, 22, true); + DOMNode b = createElement("b", 7, 10, 14, true); + DOMNode textNodeAfter = createTextNode("\r\n\r\n", 14, 18, true); + a.addChild(textNodeBefore); + a.addChild(b); + a.addChild(textNodeAfter); + + + assertDocument("\r\n\r\n\r\n\r\n", a, false); + } + + @Test + public void testPreserveWhitespaceContent() { + + DOMNode a = createElement("a", 0, 14, 18, true); + DOMNode b = createElement("b", 3, 10, 14, true); + DOMNode whitespaceContent = createTextNode("\r\n\r\n", 6, 10, true); + + a.addChild(b); + b.addChild(whitespaceContent); + + + assertDocument("\r\n\r\n", a); + } + @Test public void elementOffsets() { DOMDocument document = DOMParser.getInstance().parse("", null, null); @@ -988,6 +1017,12 @@ private static void assertDocument(String input, DOMNode expectedNode) { compareTrees(expectedNode, actualNode); } + private static void assertDocument(String input, DOMNode expectedNode, boolean ignoreWhitespace) { + DOMDocument document = DOMParser.getInstance().parse(input, "uri", null, ignoreWhitespace); + DOMNode actualNode = document.getChild(0); + compareTrees(expectedNode, actualNode); + } + private static void compareTrees(DOMNode expectedNode, DOMNode actualNode) { if (expectedNode.isElement()) { assertEquals(((DOMElement) expectedNode).getTagName(), ((DOMElement) actualNode).getTagName()); diff --git a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/services/XMLFormatterTest.java b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/services/XMLFormatterTest.java index 5d1df6223..61ae70601 100644 --- a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/services/XMLFormatterTest.java +++ b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/services/XMLFormatterTest.java @@ -906,7 +906,39 @@ public void testContentFormatting6() throws BadLocationException { String expected = "\r\n" + "\r\n" + + " Fred\r\n" + + " \r\n" + + " Jani\r\n" + + " \r\n" + + " Reminder\r\n" + + " \r\n" + + " Don't forget me this weekend\r\n" + + ""; + format(content, expected, formattingOptions); + } + + @Test + public void testDoctypeNoInternalSubsetNoNewlines() throws BadLocationException { + XMLFormattingOptions formattingOptions = createDefaultFormattingOptions(); + formattingOptions.setPreservedNewlines(0); + + String content = + "\r\n" + + "\r\n" + " Fred\r\n" + + "\r\n" + + " Jani\r\n" + + "\r\n" + + " Reminder\r\n" + + " \r\n" + + " Don't forget me this weekend\r\n" + + ""; + String expected = + "\r\n" + + "\r\n" + + " Fred\r\n" + " Jani\r\n" + " Reminder\r\n" + " Don't forget me this weekend\r\n" + @@ -946,8 +978,52 @@ public void testContentFormatting6() throws BadLocationException { " \r\n" + "]>\r\n" + "\r\n" + + " Fred\r\n" + + " \r\n" + + " \r\n" + + " Jani\r\n" + + " \r\n" + + " Reminder\r\n" + + " Don't forget me this weekend\r\n" + + ""; + format(content, expected, formattingOptions); + } + + @Test public void testDoctypeInternalSubsetNoNewlines() throws BadLocationException { + XMLFormattingOptions formattingOptions = createDefaultFormattingOptions(); + formattingOptions.setPreservedNewlines(0); + + String content = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "]>\r\n" + + "\r\n" + " Fred\r\n" + + "\r\n" + + "\r\n" + " Jani\r\n" + + " \r\n" + + " Reminder\r\n" + + " Don't forget me this weekend\r\n" + + ""; + String expected = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "]>\r\n" + + "\r\n" + + " Fred\r\n" + + " Jani\r\n" + " Reminder\r\n" + " Don't forget me this weekend\r\n" + ""; @@ -1012,8 +1088,10 @@ public void testContentFormatting6() throws BadLocationException { " \r\n" + " \r\n" + " \r\n" + - "]>\r\n" + - "\r\n" + + "]>\r\n" + + "\r\n" + + "\r\n" + + " \r\n" + " Fred\r\n" + ""; format(content, expected, formattingOptions); @@ -1188,7 +1266,8 @@ public void testContentFormatting6() throws BadLocationException { format(content, expected, formattingOptions, "test.dtd"); } - @Test public void testAllDoctypeParameters() throws BadLocationException { + @Test + public void testAllDoctypeParameters() throws BadLocationException { XMLFormattingOptions formattingOptions = createDefaultFormattingOptions(); String content = @@ -1226,8 +1305,10 @@ public void testContentFormatting6() throws BadLocationException { " \r\n" + "]>\r\n" + "\r\n" + - " sdsd\r\n" + + " sdsd\r\n" + + " \r\n" + " \r\n" + + " \r\n" + " er\r\n" + " dd\r\n" + " \r\n" + @@ -1309,7 +1390,8 @@ public void testUnclosedSystemId() throws BadLocationException { "\r\n" + "\r\n" + " \r\n" + - "]>\r\n" + + "]>\r\n" + + "\r\n" + ""; format(content, expected, formattingOptions); } @@ -1329,7 +1411,8 @@ public void testUnclosedPublicId() throws BadLocationException { "\r\n" + "\r\n" + " \r\n" + - "]>\r\n" + + "]>\r\n" + + "\r\n" + ""; format(content, expected, formattingOptions); } @@ -1350,7 +1433,8 @@ public void testCommentAfterMissingClosingBracket() throws BadLocationException "\r\n" + - "]>\r\n" + + "]>\r\n" + + "\r\n" + ""; format(content, expected, formattingOptions); } @@ -1633,6 +1717,158 @@ public void testAttributeNameTouchingPreviousValue() throws BadLocationException ""; format(content, expected, formattingOptions); } + + @Test + public void testPreserveNewlines() throws BadLocationException { + XMLFormattingOptions formattingOptions = createDefaultFormattingOptions(); + String content = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + String expected = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + format(content, expected, formattingOptions); + } + + @Test + public void testPreserveNewlines3Max() throws BadLocationException { + XMLFormattingOptions formattingOptions = createDefaultFormattingOptions(); + formattingOptions.setPreservedNewlines(3); + String content = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + String expected = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + format(content, expected, formattingOptions); + } + + @Test + public void testPreserveNewlines2() throws BadLocationException { + XMLFormattingOptions formattingOptions = createDefaultFormattingOptions(); + String content = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + String expected = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + format(content, expected, formattingOptions); + } + + @Test + public void testPreserveNewlinesBothSides() throws BadLocationException { + XMLFormattingOptions formattingOptions = createDefaultFormattingOptions(); + String content = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + String expected = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + format(content, expected, formattingOptions); + } + + @Test + public void testPreserveNewlinesBothSidesMultipleTags() throws BadLocationException { + XMLFormattingOptions formattingOptions = createDefaultFormattingOptions(); + String content = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + String expected = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + format(content, expected, formattingOptions); + } + + @Test + public void testPreserveNewlinesSingleLine() throws BadLocationException { + XMLFormattingOptions formattingOptions = createDefaultFormattingOptions(); + String content = + "\r\n" + + " \r\n" + + " \r\n" + + ""; + String expected = + "\r\n" + + " \r\n" + + " \r\n" + + ""; + format(content, expected, formattingOptions); + } + + @Test + public void testPreserveNewlines4() throws BadLocationException { + XMLFormattingOptions formattingOptions = createDefaultFormattingOptions(); + String content = + "\r\n" + + " \r\n" + + ""; + String expected = + "\r\n" + + " \r\n" + + ""; + format(content, expected, formattingOptions); + } + @Test @@ -1688,8 +1924,8 @@ private static void format(String unformatted, String expected, XMLFormattingOpt formatted = unformatted.substring(0, rangeStart) + formatted + unformatted.substring(rangeEnd - 1, unformatted.length()); } + Assert.assertEquals(expected, formatted); - }