Skip to content

Commit

Permalink
Adds editing of start/end tag simultaneously
Browse files Browse the repository at this point in the history
Under the preference xml.autoSelectMatchingTags

Fixes #redhat-developer/vscode-xml#130

Signed-off-by: Nikolas Komonen <[email protected]>
  • Loading branch information
NikolasKomonen committed Dec 9, 2019
1 parent 5606fa7 commit 206b3c5
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.InitializedParams;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.services.LanguageClient;
Expand Down Expand Up @@ -232,6 +233,13 @@ public CompletableFuture<AutoCloseTagResponse> closeTag(TextDocumentPositionPara
});
}

@Override
public CompletableFuture<Position> matchingTagPosition(TextDocumentPositionParams params) {
return xmlTextDocumentService.computeDOMAsync(params.getTextDocument(), (cancelChecker, xmlDocument) -> {
return getXMLLanguageService().getMatchingTagPosition(xmlDocument, params.getPosition(), cancelChecker);
});
}

@Override
public DOMDocument getDocument(String uri) {
ModelTextDocument<DOMDocument> document = xmlTextDocumentService.getDocument(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import java.util.concurrent.CompletableFuture;

import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
import org.eclipse.lsp4j.jsonrpc.services.JsonSegment;
Expand All @@ -25,6 +26,9 @@ public interface XMLCustomService {

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

@JsonRequest
CompletableFuture<Position> matchingTagPosition(TextDocumentPositionParams params);
}


Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public static void createEndTagInsertCodeAction(Diagnostic diagnostic, Range ran

DOMElement element = (DOMElement) node;
int startOffset = element.getStartTagOpenOffset();
if(startOffset == -1) {
return;
}
Position startPosition = document.positionAt(startOffset);
Position endPosition;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.eclipse.lsp4xml.customservice.AutoCloseTagResponse;
import org.eclipse.lsp4xml.dom.DOMDocument;
import org.eclipse.lsp4xml.dom.DOMElement;
import org.eclipse.lsp4xml.dom.DOMNode;
import org.eclipse.lsp4xml.extensions.contentmodel.settings.XMLValidationSettings;
import org.eclipse.lsp4xml.services.extensions.XMLExtensionsRegistry;
import org.eclipse.lsp4xml.settings.SharedSettings;
Expand Down Expand Up @@ -270,4 +271,8 @@ public AutoCloseTagResponse doAutoClose(DOMDocument xmlDocument, Position positi
}
}

public Position getMatchingTagPosition(DOMDocument xmlDocument, Position position, CancelChecker cancelChecker) {
return XMLPositionUtility.getMatchingTagPosition(xmlDocument, position);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -839,4 +839,38 @@ public static Location toLocation(LocationLink locationLink) {
return new Location(locationLink.getTargetUri(), locationLink.getTargetRange());
}

public static Position getMatchingTagPosition(DOMDocument xmlDocument, Position position) {
try {
int offset = xmlDocument.offsetAt(position);
DOMNode node = xmlDocument.findNodeAt(offset);

if(node.isElement()) {
DOMElement element = (DOMElement) node;

if(!element.hasEndTag() || element.isSelfClosed() || !element.hasStartTag()){
return null;
}
int tagNameLength = element.getTagName().length();
int startTagNameStart = element.getStartTagOpenOffset() + 1;
int startTagNameEnd = startTagNameStart + tagNameLength;
int endTagNameStart = element.getEndTagOpenOffset() + 2;
int endTagNameEnd = endTagNameStart + tagNameLength;

if(offset <= startTagNameEnd && offset >= startTagNameStart) {
int mirroredCursorOffset = endTagNameStart + (offset - startTagNameStart);
return xmlDocument.positionAt(mirroredCursorOffset);
}
else {
if(offset >= endTagNameStart && offset <= endTagNameEnd) {
int mirroredCursorOffset = startTagNameStart + (offset - endTagNameStart);
return xmlDocument.positionAt(mirroredCursorOffset);
}
}
}
} catch (BadLocationException e) {
return null;
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/*******************************************************************************
* 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.utils;

import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4xml.commons.BadLocationException;
import org.eclipse.lsp4xml.dom.DOMDocument;
import org.eclipse.lsp4xml.dom.DOMParser;
import org.junit.Assert;
import org.junit.Test;

/**
* XMLPositionUtilityTest
*/
public class XMLPositionUtilityTest {

@Test
public void testGetMatchingEndTagPositionMiddle() {
String initialText=
"<Apple>\n" +
" <Or|ange></Orange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <Orange></Or|ange>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingEndTagPositionBeginning() {
String initialText=
"<Apple>\n" +
" <|Orange></Orange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <Orange></|Orange>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingEndTagPositionEnd() {
String initialText=
"<Apple>\n" +
" <Orange|></Orange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <Orange></Orange|>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingEndTagPositionAttributes() {
String initialText=
"<Apple>\n" +
" <Orange| amount=\"1\"></Orange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <Orange amount=\"1\"></Orange|>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingEndTagNoResult() {
String initialText=
"<Apple>\n" +
" |<Orange></Orange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <Orange></Orange>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingEndTagNoResult2() {
String initialText=
"<Apple>\n" +
" <Orange |></Orange>\n" + // Because there is a space
"</Apple>";

String expectedText=
"<Apple>\n" +
" <Orange ></Orange>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingEndTagTextBetween() {
String initialText=
"<Apple>\n" +
" <Orange|>Text Between</Orange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <Orange>Text Between</Orange|>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingEndTagElementBetween() {
String initialText=
"<Apple>\n" +
" <Orange|><Lemon></Lemon></Orange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <Orange><Lemon></Lemon></Orange|>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingStartTagPositionMiddle() {
String initialText=
"<Apple>\n" +
" <Orange></Or|ange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <Or|ange></Orange>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingStartTagPositionBeginning() {
String initialText=
"<Apple>\n" +
" <Orange></|Orange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <|Orange></Orange>\n" +
"</Apple>";


testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingStartTagPositionEnd() {
String initialText=
"<Apple>\n" +
" <Orange></Orange|>\n" +
"</Apple>";


String expectedText=
"<Apple>\n" +
" <Orange|></Orange>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingStartTagPositionAttributes() {
String initialText=
"<Apple>\n" +
" <Orange amount=\"1\"></Orange|>\n" +
"</Apple>";


String expectedText=
"<Apple>\n" +
" <Orange| amount=\"1\"></Orange>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingEndTagPositionAttributesPrefixed() {
String initialText=
"<Apple>\n" +
" <prefix:Orange| amount=\"1\"></prefix:Orange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <prefix:Orange amount=\"1\"></prefix:Orange|>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

@Test
public void testGetMatchingEndTagPositionPrefixed() {
String initialText=
"<Apple>\n" +
" <pref|ix:Orange></prefix:Orange>\n" +
"</Apple>";

String expectedText=
"<Apple>\n" +
" <prefix:Orange></pref|ix:Orange>\n" +
"</Apple>";

testMatchingTagPosition(initialText, expectedText);
}

private static void testMatchingTagPosition(String initialCursorText, String expectedCursorText) {

int offset = initialCursorText.indexOf('|');
initialCursorText = initialCursorText.substring(0, offset) + initialCursorText.substring(offset + 1);
DOMDocument xmlDocument = DOMParser.getInstance().parse(initialCursorText, "testURI", null);
Position initialCursorPosition;
Position newCursorPosition;
int newCursorOffset = -1;
try {
initialCursorPosition = xmlDocument.positionAt(offset);
newCursorPosition = XMLPositionUtility.getMatchingTagPosition(xmlDocument, initialCursorPosition);
if(newCursorPosition != null) { // a result for a matching position was found
newCursorOffset = xmlDocument.offsetAt(newCursorPosition);
}
} catch (BadLocationException e) {
Assert.fail();
return;
}

StringBuffer sBuffer = new StringBuffer(initialCursorText);
String actualOutputString;
if(newCursorOffset > -1) {
actualOutputString = sBuffer.insert(newCursorOffset, "|").toString();
}
else { // no matching position was found
actualOutputString = initialCursorText;
}

Assert.assertEquals(expectedCursorText, actualOutputString);
}
}

0 comments on commit 206b3c5

Please sign in to comment.