Skip to content

Commit

Permalink
Stops autoClose of end tag if it already exists
Browse files Browse the repository at this point in the history
Fixes eclipse-lemminx#314, eclipse-lemminx#239

Signed-off-by: Nikolas Komonen <[email protected]>
  • Loading branch information
NikolasKomonen committed Mar 12, 2019
1 parent 4797365 commit f5a107c
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 32 deletions.
Empty file added DELETE.xml
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -202,7 +204,7 @@ public long getParentProcessId() {
}

@Override
public CompletableFuture<String> closeTag(TextDocumentPositionParams params) {
public CompletableFuture<AutoCloseTagResponse> closeTag(TextDocumentPositionParams params) {
return computeAsync((monitor) -> {
TextDocument document = xmlTextDocumentService.getDocument(params.getTextDocument().getUri());
DOMDocument xmlDocument = xmlTextDocumentService.getXMLDocument(document);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* Contributors:
* Angelo Zerr <[email protected]> - initial API and implementation
*/
package org.eclipse.lsp4xml;
package org.eclipse.lsp4xml.customservice;

import java.util.concurrent.CompletableFuture;

Expand All @@ -24,5 +24,7 @@
public interface XMLCustomService {

@JsonRequest
CompletableFuture<String> closeTag(TextDocumentPositionParams params);
CompletableFuture<AutoCloseTagResponse> closeTag(TextDocumentPositionParams params);
}


Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public enum TokenType {
StartTagOpen,
StartTagClose,
StartTagSelfClose,
StartTagSelfCloseSlash,
StartTag,
EndTagOpen,
EndTagClose,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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</" + ((DOMElement) node).getTagName() + ">";
}
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</" + ((DOMElement) node).getTagName() + ">";

}
} 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

// <a/|
if(closeBracket == null) { // no '>' after slash
snippet = ">$0";
}
token = scanner.scan();
//<a/|></a>
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 { //<a/ </a>
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -188,11 +190,11 @@ public List<CodeAction> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -121,8 +122,8 @@ public void doTagComplete() throws BadLocationException {
testTagCompletion("<div>|</div>", null);
testTagCompletion("<div class=\"\">|", "$0</div>");
testTagCompletion("<img />|", null);
testTagCompletion("<div><br /></|", "div>");
testTagCompletion("<div><br /><span></span></|", "div>");
testTagCompletion("<div><br /></|", "div>$0");
testTagCompletion("<div><br /><span></span></|", "div>$0");
// testTagCompletion("<div><h1><br /><span></span><img /></| </h1></div>",
// "h1>");
}
Expand All @@ -133,6 +134,15 @@ public void testAutoCloseTagCompletion() {
assertAutoCloseEndTagCompletion("<a><b>|</a>", "$0</b>");
assertAutoCloseEndTagCompletion("<a> <b>|</a>", "$0</b>");
assertAutoCloseEndTagCompletion("<a><b>|", "$0</b>");
assertAutoCloseEndTagCompletion("<a></|", "a>$0");
assertAutoCloseEndTagCompletion("<a/|", ">$0");
assertAutoCloseEndTagCompletion("<a/|</b>", ">$0");
}

@Test
public void testAutoCloseTagCompletionWithRange() {
assertAutoCloseEndTagCompletionWithRange("<a/|></a>", ">$0", new Range(new Position(0, 3), new Position(0,8)));
assertAutoCloseEndTagCompletionWithRange("<a/| </a>", ">$0", new Range(new Position(0, 3), new Position(0,8)));
}

@Test
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down

0 comments on commit f5a107c

Please sign in to comment.