diff --git a/DELETE.xml b/DELETE.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/XMLLanguageServer.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/XMLLanguageServer.java index 5e06d75e1b..1f1f951148 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/XMLLanguageServer.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/XMLLanguageServer.java @@ -31,6 +31,8 @@ import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.WorkspaceService; import org.eclipse.lsp4xml.commons.ParentProcessWatcher.ProcessLanguageServer; +import org.eclipse.lsp4xml.customservice.AutoCloseTagResponse; +import org.eclipse.lsp4xml.customservice.XMLCustomService; import org.eclipse.lsp4xml.commons.TextDocument; import org.eclipse.lsp4xml.dom.DOMDocument; import org.eclipse.lsp4xml.extensions.contentmodel.settings.ContentModelSettings; @@ -202,7 +204,7 @@ public long getParentProcessId() { } @Override - public CompletableFuture closeTag(TextDocumentPositionParams params) { + public CompletableFuture closeTag(TextDocumentPositionParams params) { return computeAsync((monitor) -> { TextDocument document = xmlTextDocumentService.getDocument(params.getTextDocument().getUri()); DOMDocument xmlDocument = xmlTextDocumentService.getXMLDocument(document); diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/customservice/AutoCloseTagResponse.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/customservice/AutoCloseTagResponse.java new file mode 100644 index 0000000000..ee5e7eff2d --- /dev/null +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/customservice/AutoCloseTagResponse.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2019 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 + * + * Contributors: Red Hat Inc. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.lsp4xml.customservice; + +import org.eclipse.lsp4j.Range; + +public class AutoCloseTagResponse { + public String snippet; + public Range range; + + public AutoCloseTagResponse(String snippet, Range range) { + this.snippet = snippet; + this.range = range; + } + + public AutoCloseTagResponse(String snippet) { + this.snippet = snippet; + } +} \ No newline at end of file diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/XMLCustomService.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/customservice/XMLCustomService.java similarity index 81% rename from org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/XMLCustomService.java rename to org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/customservice/XMLCustomService.java index 7e9cbbe92a..2c993a9746 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/XMLCustomService.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/customservice/XMLCustomService.java @@ -8,7 +8,7 @@ * Contributors: * Angelo Zerr - initial API and implementation */ -package org.eclipse.lsp4xml; +package org.eclipse.lsp4xml.customservice; import java.util.concurrent.CompletableFuture; @@ -24,5 +24,7 @@ public interface XMLCustomService { @JsonRequest - CompletableFuture closeTag(TextDocumentPositionParams params); + CompletableFuture closeTag(TextDocumentPositionParams params); } + + diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMElement.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMElement.java index 0ff0aae1fd..60a419f98c 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMElement.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/DOMElement.java @@ -253,6 +253,59 @@ public boolean isSelfClosed() { return selfClosed; } + + /** + * Will traverse backwards from the start offset + * returning an offset of the given character if it's found + * before another character. Whitespace is ignored. + * + * Returns null if the character is not found. + * + * The initial value for the start offset is not included. + * So have the offset 1 position after the character you want + * to start at. + */ + public Integer endsWith(char c, int startOffset) { + String text = this.getOwnerDocument().getText(); + if(startOffset > text.length() || startOffset < 0) { + return null; + } + startOffset--; + while(startOffset >= 0) { + char current = text.charAt(startOffset); + if(Character.isWhitespace(current)) { + startOffset--; + continue; + } + if(current != c) { + return null; + } + return startOffset; + } + return null; + } + + public Integer isNextChar(char c, int startOffset) { + String text = this.getOwnerDocument().getText(); + if(startOffset > text.length() || startOffset < 0) { + return null; + } + + while(startOffset < text.length()) { + char current = text.charAt(startOffset); + if(Character.isWhitespace(current)) { + startOffset++; + continue; + } + if(current != c) { + return null; + } + return startOffset; + } + return null; + } + + public boolean isSameTag(String tagInLowerCase) { return this.tag != null && tagInLowerCase != null && this.tag.length() == tagInLowerCase.length() && this.tag.toLowerCase().equals(tagInLowerCase); 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 bff7b4446b..4465fac6b9 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 @@ -73,7 +73,7 @@ public DOMDocument parse(TextDocument document, URIResolverExtensionManager reso switch (token) { case StartTagOpen: { if(!curr.isClosed() && curr.parent != null) { - //The next node's parent is not closed at this point + //The next node's parent (curr) is not closed at this point //so the node's parent (curr) will have its end position updated //to a newer end position. curr.end = scanner.getTokenOffset(); diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/TokenType.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/TokenType.java index 375cc97ac2..da9b56350b 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/TokenType.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/TokenType.java @@ -25,6 +25,7 @@ public enum TokenType { StartTagOpen, StartTagClose, StartTagSelfClose, + StartTagSelfCloseSlash, StartTag, EndTagOpen, EndTagClose, diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/XMLScanner.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/XMLScanner.java index 5793a0823e..fc158c7769 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/XMLScanner.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/dom/parser/XMLScanner.java @@ -274,9 +274,11 @@ TokenType internalScan() { return finishToken(offset, TokenType.AttributeName); } - if (stream.advanceIfChars(_FSL, _RAN)) { // /> + if (stream.advanceIfChar(_FSL)) { // / state = ScannerState.WithinContent; - return finishToken(offset, TokenType.StartTagSelfClose); + if(stream.advanceIfChar(_RAN)) { // > + return finishToken(offset, TokenType.StartTagSelfClose); + } } if (stream.advanceIfChar(_RAN)) { // > state = ScannerState.WithinContent; diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLCompletions.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLCompletions.java index 71caa562de..f26cc777bb 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLCompletions.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLCompletions.java @@ -29,6 +29,7 @@ import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4xml.commons.BadLocationException; import org.eclipse.lsp4xml.commons.TextDocument; +import org.eclipse.lsp4xml.customservice.AutoCloseTagResponse; import org.eclipse.lsp4xml.dom.DOMDocument; import org.eclipse.lsp4xml.dom.DOMElement; import org.eclipse.lsp4xml.dom.DOMNode; @@ -301,10 +302,13 @@ private static boolean isInsideDTDContent(DOMNode node, DOMDocument xmlDocument) return (node.getParentNode() != null && node.getParentNode().isDoctype()); } - public String doTagComplete(DOMDocument xmlDocument, Position position) { + public AutoCloseTagResponse doTagComplete(DOMDocument xmlDocument, Position position) { int offset; try { offset = xmlDocument.offsetAt(position); + if(offset - 2 < 0) { //There is not enough content for autoClose + return null; + } } catch (BadLocationException e) { LOGGER.log(Level.SEVERE, "doTagComplete failed", e); return null; @@ -313,37 +317,76 @@ public String doTagComplete(DOMDocument xmlDocument, Position position) { return null; } char c = xmlDocument.getText().charAt(offset - 1); + char cBefore = xmlDocument.getText().charAt(offset - 2); + String snippet = null; if (c == '>') { DOMNode node = xmlDocument.findNodeBefore(offset); - if (node != null && node.isElement() && ((DOMElement) node).getTagName() != null - && !isEmptyElement(((DOMElement) node).getTagName()) && node.getStart() < offset - && (!((DOMElement) node).hasEndTag() || ((DOMElement) node).getEndTagOpenOffset() > offset)) { - Scanner scanner = XMLScanner.createScanner(xmlDocument.getText(), node.getStart()); - TokenType token = scanner.scan(); - while (token != TokenType.EOS && scanner.getTokenEnd() <= offset) { - if (token == TokenType.StartTagClose && scanner.getTokenEnd() == offset) { - return "$0"; - } - token = scanner.scan(); - } + DOMElement element = ((DOMElement) node); + if (node != null + && node.isElement() + && !element.isSelfClosed() + && element.getTagName() != null + && !isEmptyElement(((DOMElement) node).getTagName()) + && node.getStart() < offset + && (!((DOMElement) node).hasEndTag())) { + snippet = "$0"; + } - } else if (c == '/') { + } else if (cBefore == '<' && c == '/') { DOMNode node = xmlDocument.findNodeBefore(offset); while (node != null && node.isClosed()) { node = node.getParentNode(); } if (node != null && node.isElement() && ((DOMElement) node).getTagName() != null) { - Scanner scanner = XMLScanner.createScanner(xmlDocument.getText(), node.getStart()); - TokenType token = scanner.scan(); - while (token != TokenType.EOS && scanner.getTokenEnd() <= offset) { - if (token == TokenType.EndTagOpen && scanner.getTokenEnd() == offset) { - return ((DOMElement) node).getTagName() + ">"; + snippet = ((DOMElement) node).getTagName() + ">$0"; + } + } else { + DOMNode node = xmlDocument.findNodeBefore(offset); + if(node.isElement() && node.getNodeName() != null) { + DOMElement element1 = (DOMElement) node; + Integer slashOffset = element1.endsWith('/', offset); + Position end = null; + if(slashOffset != null) { //The typed characted was '/' + Integer closeBracket = element1.isNextChar('>', offset); // After the slash is a close bracket + + // ' after slash + snippet = ">$0"; } - token = scanner.scan(); + // + DOMNode nextSibling = node.getNextSibling(); + if(nextSibling != null && nextSibling.isElement()){ + DOMElement element2 = (DOMElement) nextSibling; + if(!element2.hasStartTag() && node.getNodeName().equals(element2.getNodeName())) { + try { + snippet = ">$0"; + end = xmlDocument.positionAt(element2.getEnd()); + } catch (BadLocationException e) { + return null; + } + } + } + else { // + if(element1.hasEndTag()) { + try { + snippet = ">$0"; + end = xmlDocument.positionAt(element1.getEnd()); + } catch (BadLocationException e) { + return null; + } + } + } + if(snippet != null && end != null) { + return new AutoCloseTagResponse(snippet, new Range(position, end)); + } + } } } - return null; + if(snippet == null) { + return null; + } + return new AutoCloseTagResponse(snippet); } // ---------------- Tags completion diff --git a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLLanguageService.java b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLLanguageService.java index 47fbaf2de3..dcebfc3412 100644 --- a/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLLanguageService.java +++ b/org.eclipse.lsp4xml/src/main/java/org/eclipse/lsp4xml/services/XMLLanguageService.java @@ -40,8 +40,10 @@ import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4xml.commons.BadLocationException; import org.eclipse.lsp4xml.commons.TextDocument; +import org.eclipse.lsp4xml.customservice.AutoCloseTagResponse; import org.eclipse.lsp4xml.dom.DOMDocument; import org.eclipse.lsp4xml.dom.DOMElement; import org.eclipse.lsp4xml.extensions.contentmodel.settings.XMLValidationSettings; @@ -188,11 +190,11 @@ public List doCodeActions(CodeActionContext context, Range range, DO return codeActions.doCodeActions(context, range, document, formattingSettings); } - public String doTagComplete(DOMDocument xmlDocument, Position position) { + public AutoCloseTagResponse doTagComplete(DOMDocument xmlDocument, Position position) { return completions.doTagComplete(xmlDocument, position); } - public String doAutoClose(DOMDocument xmlDocument, Position position) { + public AutoCloseTagResponse doAutoClose(DOMDocument xmlDocument, Position position) { try { int offset = xmlDocument.offsetAt(position); String text = xmlDocument.getText(); diff --git a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/XMLAssert.java b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/XMLAssert.java index 5934163c84..eb25bc19c8 100644 --- a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/XMLAssert.java +++ b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/XMLAssert.java @@ -42,6 +42,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4xml.commons.BadLocationException; import org.eclipse.lsp4xml.commons.TextDocument; +import org.eclipse.lsp4xml.customservice.AutoCloseTagResponse; import org.eclipse.lsp4xml.dom.DOMDocument; import org.eclipse.lsp4xml.dom.DOMParser; import org.eclipse.lsp4xml.extensions.contentmodel.settings.ContentModelSettings; @@ -220,7 +221,12 @@ public static void testTagCompletion(String value, String expected) throws BadLo Position position = document.positionAt(offset); DOMDocument htmlDoc = DOMParser.getInstance().parse(document, ls.getResolverExtensionManager()); - String actual = ls.doTagComplete(htmlDoc, position); + AutoCloseTagResponse response = ls.doTagComplete(htmlDoc, position); + if(expected == null) { + Assert.assertNull(response); + return; + } + String actual = response.snippet; Assert.assertEquals(expected, actual); } diff --git a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/services/XMLCompletionTest.java b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/services/XMLCompletionTest.java index db8e7f3bc3..56cb6ba3bf 100644 --- a/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/services/XMLCompletionTest.java +++ b/org.eclipse.lsp4xml/src/test/java/org/eclipse/lsp4xml/services/XMLCompletionTest.java @@ -25,6 +25,7 @@ import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4xml.commons.BadLocationException; +import org.eclipse.lsp4xml.customservice.AutoCloseTagResponse; import org.eclipse.lsp4xml.dom.DOMDocument; import org.eclipse.lsp4xml.dom.DOMParser; import org.eclipse.lsp4xml.services.extensions.CompletionSettings; @@ -121,8 +122,8 @@ public void doTagComplete() throws BadLocationException { testTagCompletion("
|
", null); testTagCompletion("
|", "$0
"); testTagCompletion("|", null); - testTagCompletion("

"); - testTagCompletion("

"); + testTagCompletion("

$0"); + testTagCompletion("

$0"); // testTagCompletion("


", // "h1>"); } @@ -133,6 +134,15 @@ public void testAutoCloseTagCompletion() { assertAutoCloseEndTagCompletion("
|", "$0"); assertAutoCloseEndTagCompletion(" |", "$0"); assertAutoCloseEndTagCompletion("|", "$0"); + assertAutoCloseEndTagCompletion("$0"); + assertAutoCloseEndTagCompletion("$0"); + assertAutoCloseEndTagCompletion("", ">$0"); + } + + @Test + public void testAutoCloseTagCompletionWithRange() { + assertAutoCloseEndTagCompletionWithRange("", ">$0", new Range(new Position(0, 3), new Position(0,8))); + assertAutoCloseEndTagCompletionWithRange("", ">$0", new Range(new Position(0, 3), new Position(0,8))); } @Test @@ -208,6 +218,10 @@ public void assertOpenStartTagCompletion(String xmlText, int expectedStartTagOff } public void assertAutoCloseEndTagCompletion(String xmlText, String expectedTextEdit) { + assertAutoCloseEndTagCompletionWithRange(xmlText, expectedTextEdit, null); + } + + public void assertAutoCloseEndTagCompletionWithRange(String xmlText, String expectedTextEdit, Range range) { int offset = getOffset(xmlText); DOMDocument xmlDocument = initializeXMLDocument(xmlText, offset); Position position = null; @@ -216,8 +230,10 @@ public void assertAutoCloseEndTagCompletion(String xmlText, String expectedTextE } catch (Exception e) { fail("Couldn't get position at offset"); } - String completionList = languageService.doTagComplete(xmlDocument, position); + AutoCloseTagResponse response = languageService.doTagComplete(xmlDocument, position); + String completionList = response.snippet; assertEquals(expectedTextEdit, completionList); + assertEquals(range, response.range); } public int getOffset(String xmlText) {