1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-03-07 12:32:52 +01:00

Merge pull request from Vvalter/master

Fix VIM-1090 and VIM-1100 tag motion with duplicate tags.
This commit is contained in:
Alex Plate 2019-04-29 13:59:15 +03:00 committed by GitHub
commit af79066c26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 801 additions and 406 deletions
src/com/maddyhome/idea/vim/helper
test/org/jetbrains/plugins/ideavim/action

View File

@ -18,7 +18,6 @@
package com.maddyhome.idea.vim.helper;
import com.google.common.collect.Lists;
import com.intellij.lang.CodeDocumentationAwareCommenter;
import com.intellij.lang.Commenter;
import com.intellij.lang.Language;
@ -27,7 +26,6 @@ import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
@ -42,6 +40,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -432,80 +431,192 @@ public class SearchHelper {
return -1;
}
/** returns new position which ignore whitespaces at beginning of the line*/
private static int ignoreWhitespaceAtLineStart(CharSequence seq, int lineStart, int pos) {
if (seq.subSequence(lineStart, pos).chars().allMatch(Character::isWhitespace)) {
while (pos < seq.length() && seq.charAt(pos) != '\n' && Character.isWhitespace(seq.charAt(pos))) {
pos++;
}
}
return pos;
}
@Nullable
public static TextRange findBlockTagRange(@NotNull Editor editor, @NotNull Caret caret, int count, boolean isOuter) {
final int cursorOffset = caret.getOffset();
int pos = cursorOffset;
int currentCount = count;
final int position = caret.getOffset();
final CharSequence sequence = editor.getDocument().getCharsSequence();
final int selectionStart = caret.getSelectionStart();
final int selectionEnd = caret.getSelectionEnd();
final boolean isRangeSelection = selectionEnd - selectionStart > 1;
int searchStartPosition;
if (!isRangeSelection) {
final int line = caret.getLogicalPosition().line;
final int lineBegin = editor.getDocument().getLineStartOffset(line);
searchStartPosition = ignoreWhitespaceAtLineStart(sequence, lineBegin, position);
} else {
searchStartPosition = selectionEnd;
}
if (isInHTMLTag(sequence, searchStartPosition, false)) {
// caret is inside opening tag. Move to closing '>'.
while (searchStartPosition < sequence.length() && sequence.charAt(searchStartPosition) != '>') {
searchStartPosition ++;
}
}
else if (isInHTMLTag(sequence, searchStartPosition, true)) {
// caret is inside closing tag. Move to starting '<'.
while (searchStartPosition > 0 && sequence.charAt(searchStartPosition) != '<') {
searchStartPosition --;
}
}
while (true) {
final Pair<TextRange, String> closingTagResult = findClosingTag(sequence, pos);
if (closingTagResult == null) {
final Pair<TextRange, String> closingTag = findUnmatchedClosingTag(sequence, searchStartPosition, count);
if (closingTag == null) {
return null;
}
final TextRange closingTagTextRange = closingTagResult.getFirst();
final String tagName = closingTagResult.getSecond();
final TextRange openingTagTextRange = findOpeningTag(sequence, closingTagTextRange.getStartOffset(), tagName);
if (openingTagTextRange != null && openingTagTextRange.getStartOffset() <= cursorOffset && --currentCount == 0) {
if (isOuter) {
return new TextRange(openingTagTextRange.getStartOffset(), closingTagTextRange.getEndOffset());
final TextRange closingTagTextRange = closingTag.getFirst();
final String tagName = closingTag.getSecond();
TextRange openingTag = findUnmatchedOpeningTag(sequence, closingTagTextRange.getStartOffset(), tagName);
if (openingTag == null) {
return null;
}
if (isRangeSelection && openingTag.getEndOffset() - 1 >= selectionStart) {
// If there was already some text selected and the new selection would not extend further, we try again
searchStartPosition = closingTagTextRange.getEndOffset();
count = 1;
continue;
}
int selectionEndWithoutNewline = selectionEnd;
while (selectionEndWithoutNewline < sequence.length() && sequence.charAt(selectionEndWithoutNewline) == '\n') {
selectionEndWithoutNewline ++;
}
if (closingTagTextRange.getStartOffset() == selectionEndWithoutNewline && openingTag.getEndOffset() == selectionStart) {
// Special case: if the inner tag is already selected we should like isOuter is active
// Note that we need to ignore newlines, because their selection is lost between multiple "it" invocations
isOuter = true;
} else
if (openingTag.getEndOffset() == closingTagTextRange.getStartOffset() && selectionStart == openingTag.getEndOffset()) {
// Special case: for an empty tag pair (e.g. <a></a>) the whole tag is selected if the caret is in the middle.
isOuter = true;
}
if (isOuter) {
return new TextRange(openingTag.getStartOffset(), closingTagTextRange.getEndOffset() - 1);
} else {
return new TextRange(openingTag.getEndOffset(), Math.max(closingTagTextRange.getStartOffset() - 1, openingTag.getEndOffset()));
}
}
}
/**
* Returns true if there is a html at the given position. Ignores tags with a trailing slash like <aaa/>.
*/
private static boolean isInHTMLTag(@NotNull final CharSequence sequence, final int position, final boolean isEndtag) {
int openingBracket = -1;
for (int i = position; i >= 0 && i < sequence.length(); i--) {
if (sequence.charAt(i) == '<') {
openingBracket = i;
break;
}
if (sequence.charAt(i) == '>' && i != position) {
return false;
}
}
if (openingBracket == -1) {
return false;
}
boolean hasSlashAfterOpening = openingBracket + 1 < sequence.length() && sequence.charAt(openingBracket + 1) == '/';
if ((isEndtag && !hasSlashAfterOpening) || (!isEndtag && hasSlashAfterOpening)) {
return false;
}
int closingBracket = -1;
for (int i = openingBracket; i < sequence.length(); i++) {
if (sequence.charAt(i) == '>') {
closingBracket = i;
break;
}
}
return closingBracket != -1 && sequence.charAt(closingBracket - 1) != '/';
}
@Nullable
private static Pair<TextRange,String> findUnmatchedClosingTag(@NotNull final CharSequence sequence, final int position, int count) {
// The tag name may contain any characters except slashes, whitespace and '>'
final String tagNamePattern = "([^/\\s>]+)";
// An opening tag consists of '<' followed by a tag name, optionally some additional text after whitespace and a '>'
final String openingTagPattern = String.format("<%s(?:\\s[^>]*)?>", tagNamePattern);
final String closingTagPattern = String.format("</%s>", tagNamePattern);
final Pattern tagPattern = Pattern.compile(String.format("(?:%s)|(?:%s)", openingTagPattern, closingTagPattern));
final Matcher matcher = tagPattern.matcher(sequence.subSequence(position, sequence.length()));
final Stack<String> openTags = new Stack<>();
while (matcher.find()) {
boolean isClosingTag = matcher.group(1) == null;
if (isClosingTag) {
final String tagName = matcher.group(2);
// Ignore unmatched open tags. Either the file is malformed or it might be a tag like <br> that does not need to be closed.
while (!openTags.isEmpty() && !openTags.peek().equalsIgnoreCase(tagName)) {
openTags.pop();
}
else {
return new TextRange(openingTagTextRange.getEndOffset() + 1, closingTagTextRange.getStartOffset() - 1);
if (openTags.isEmpty()) {
if (count <= 1) {
return Pair.create(new TextRange(position + matcher.start(), position + matcher.end()), tagName);
} else {
count--;
}
} else {
openTags.pop();
}
} else {
final String tagName = matcher.group(1);
openTags.push(tagName);
}
}
return null;
}
@Nullable
private static TextRange findUnmatchedOpeningTag(@NotNull CharSequence sequence, int position, @NotNull String tagName) {
final String quotedTagName = Pattern.quote(tagName);
final String patternString = "(</%s>)" // match closing tags
+ "|(<%s" // or opening tags starting with tagName
+ "(\\s([^>]*" // After at least one whitespace there might be additional text in the tag. E.g. <html lang="en">
+ "[^/])?)?>)"; // Slash is not allowed as last character (this would be a self closing tag).
final Pattern tagPattern = Pattern.compile(String.format(patternString, quotedTagName, quotedTagName), Pattern.CASE_INSENSITIVE);
final Matcher matcher = tagPattern.matcher(sequence.subSequence(0, position+1));
final Stack<TextRange> openTags = new Stack<>();
while (matcher.find()) {
final TextRange match = new TextRange(matcher.start(), matcher.end());
if (sequence.charAt(matcher.start() + 1) == '/') {
if (!openTags.isEmpty()) {
openTags.pop();
}
}
else {
pos = closingTagTextRange.getEndOffset() + 1;
openTags.push(match);
}
}
}
@Nullable
private static TextRange findOpeningTag(@NotNull CharSequence sequence, int position, @NotNull String tagName) {
final String tagBeginning = "<" + tagName;
final Pattern pattern = Pattern.compile(Pattern.quote(tagBeginning), Pattern.CASE_INSENSITIVE);
final Matcher matcher = pattern.matcher(sequence.subSequence(0, position));
final List<Integer> possibleBeginnings = Lists.newArrayList();
while (matcher.find()) {
possibleBeginnings.add(matcher.start());
if (openTags.isEmpty()) {
return null;
} else {
return openTags.pop();
}
final List<Integer> reversedBeginnings = Lists.reverse(possibleBeginnings);
for (int openingTagPos : reversedBeginnings) {
final int openingTagEndPos = openingTagPos + tagBeginning.length();
final int closeBracketPos = StringUtil.indexOf(sequence, '>', openingTagEndPos);
if (closeBracketPos > 0 && (closeBracketPos == openingTagEndPos || sequence.charAt(openingTagEndPos) == ' ')) {
return new TextRange(openingTagPos, closeBracketPos);
}
}
return null;
}
@Nullable
private static Pair<TextRange, String> findClosingTag(@NotNull CharSequence sequence, int pos) {
int closeBracketPos = pos;
int openBracketPos;
while (closeBracketPos < sequence.length()) {
closeBracketPos = StringUtil.indexOf(sequence, '>', closeBracketPos);
if (closeBracketPos < 0) {
return null;
}
openBracketPos = closeBracketPos - 1;
while (openBracketPos >= 0) {
openBracketPos = StringUtil.lastIndexOf(sequence, '<', 0, openBracketPos);
if (openBracketPos >= 0 &&
openBracketPos + 1 < sequence.length() &&
sequence.charAt(openBracketPos + 1) == '/') {
final String tagName = String.valueOf(sequence.subSequence(openBracketPos + "</".length(), closeBracketPos));
if (tagName.length() > 0 && tagName.charAt(0) != ' ') {
TextRange textRange = new TextRange(openBracketPos, closeBracketPos);
return Pair.create(textRange, tagName);
}
}
openBracketPos--;
}
closeBracketPos++;
}
return null;
}

View File

@ -383,350 +383,6 @@ public class MotionActionTest extends VimTestCase {
myFixture.checkResult("foo = ;\n");
}
//|d| |v_it|
public void testDeleteInnerTagBlockCaretInHtml() {
typeTextInFile(parseKeys("dit"), "<template <caret>name=\"hello\">\n" +
" <button>Click Me</button>\n" +
" <p>You've pressed the button {{counter}} times.</p>\n" +
"</template>\n");
myFixture.checkResult("<template name=\"hello\"></template>\n");
}
//|d| |v_it|
public void testDeleteInnerTagBlockCaretInHtmlUnclosedTag() {
typeTextInFile(parseKeys("dit"), "<template <caret>name=\"hello\">\n" +
" <button>Click Me</button>\n" +
" <br>\n" +
" <p>You've pressed the button {{counter}} times.</p>\n" +
"</template>\n");
myFixture.checkResult("<template name=\"hello\"></template>\n");
}
public void testDeleteInnerTagBlockCaretEdgeTag() {
typeTextInFile(parseKeys("dit"), "<template name=\"hello\"<caret>>\n" +
" <button>Click Me</button>\n" +
" <br>\n" +
" <p>You've pressed the button {{counter}} times.</p>\n" +
"</template>\n");
myFixture.checkResult("<template name=\"hello\"></template>\n");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBefore() {
typeTextInFile(parseKeys("dit"), "abc<caret>de<tag>fg</tag>hi");
myFixture.checkResult("abcde<tag>fg</tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockInOpen() {
typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fg</tag>hi");
myFixture.checkResult("abcde<tag></tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockInOpenEndOfLine() {
typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fg</tag>");
myFixture.checkResult("abcde<tag></tag>");
}
//|d| |v_it|
public void testDeleteInnerTagBlockInOpenStartOfLine() {
typeTextInFile(parseKeys("dit"), "<ta<caret>g>fg</tag>hi");
myFixture.checkResult("<tag></tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockInOpenWithArgs() {
typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g name = \"name\">fg</tag>hi");
myFixture.checkResult("abcde<tag name = \"name\"></tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBetween() {
typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>g</tag>hi");
myFixture.checkResult("abcde<tag></tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBetweenTagWithRegex() {
typeTextInFile(parseKeys("dit"), "abcde<[abc]*>af<caret>gbc</[abc]*>hi");
myFixture.checkResult("abcde<[abc]*></[abc]*>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBetweenCamelCase() {
typeTextInFile(parseKeys("dit"), "abcde<tAg>f<caret>g</tag>hi");
myFixture.checkResult("abcde<tAg></tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBetweenCaps() {
typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>g</TAG>hi");
myFixture.checkResult("abcde<tag></TAG>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBetweenWithSpaceBeforeTag() {
typeTextInFile(parseKeys("dit"), "abcde< tag>f<caret>g</ tag>hi");
myFixture.checkResult("abcde< tag>fg</ tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBetweenWithSpaceAfterTag() {
typeTextInFile(parseKeys("dit"), "abcde<tag >f<caret>g</tag>hi");
myFixture.checkResult("abcde<tag ></tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBetweenWithArgs() {
typeTextInFile(parseKeys("dit"), "abcde<tag name = \"name\">f<caret>g</tag>hi");
myFixture.checkResult("abcde<tag name = \"name\"></tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockInClose() {
typeTextInFile(parseKeys("dit"), "abcde<tag>fg</ta<caret>g>hi");
myFixture.checkResult("abcde<tag></tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockAfter() {
typeTextInFile(parseKeys("dit"), "abcde<tag>fg</tag>h<caret>i");
myFixture.checkResult("abcde<tag>fg</tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockInAlone() {
typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fghi");
myFixture.checkResult("abcde<tag>fghi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockWithoutTags() {
typeTextInFile(parseKeys("dit"), "abc<caret>de");
myFixture.checkResult("abcde");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBeforeWithoutOpenTag() {
typeTextInFile(parseKeys("dit"), "abc<caret>defg</tag>hi");
myFixture.checkResult("abcdefg</tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockInCloseWithoutOpenTag() {
typeTextInFile(parseKeys("dit"), "abcdefg</ta<caret>g>hi");
myFixture.checkResult("abcdefg</tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockAfterWithoutOpenTag() {
typeTextInFile(parseKeys("dit"), "abcdefg</tag>h<caret>i");
myFixture.checkResult("abcdefg</tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBeforeWithoutCloseTag() {
typeTextInFile(parseKeys("dit"), "abc<caret>defg<tag>hi");
myFixture.checkResult("abcdefg<tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockInOpenWithoutCloseTag() {
typeTextInFile(parseKeys("dit"), "abcdefg<ta<caret>g>hi");
myFixture.checkResult("abcdefg<tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockAfterWithoutCloseTag() {
typeTextInFile(parseKeys("dit"), "abcdefg<tag>h<caret>i");
myFixture.checkResult("abcdefg<tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBeforeWrongOrder() {
typeTextInFile(parseKeys("dit"), "abc<caret>de</tag>fg<tag>hi");
myFixture.checkResult("abcde</tag>fg<tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockInOpenWrongOrder() {
typeTextInFile(parseKeys("dit"), "abcde</ta<caret>g>fg<tag>hi");
myFixture.checkResult("abcde</tag>fg<tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBetweenWrongOrder() {
typeTextInFile(parseKeys("dit"), "abcde</tag>f<caret>g<tag>hi");
myFixture.checkResult("abcde</tag>fg<tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockInCloseWrongOrder() {
typeTextInFile(parseKeys("dit"), "abcde</tag>fg<ta<caret>g>hi");
myFixture.checkResult("abcde</tag>fg<tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockTwoTagsWrongOrder() {
typeTextInFile(parseKeys("dit"), "<foo><html>t<caret>ext</foo></html>");
myFixture.checkResult("<foo></foo></html>");
}
//|d| |v_it|
public void testDeleteInnerTagBlockTwoTagsWrongOrderInClosingTag() {
typeTextInFile(parseKeys("dit"), "<foo><html>text</foo></htm<caret>l>");
myFixture.checkResult("<foo><html></html>");
}
//|d| |v_it|
public void testDeleteInnerTagBlockAfterWrongOrder() {
typeTextInFile(parseKeys("dit"), "abcde</tag>fg<tag>h<caret>i");
myFixture.checkResult("abcde</tag>fg<tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBracketInside() {
typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret><>g</tag>hi");
myFixture.checkResult("abcde<tag></tag>hi");
}
//|d| |v_it|
public void testDeleteInnerTagBlockBracketInsideString() {
typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>\"<>\"g</tag>hi");
myFixture.checkResult("abcde<tag></tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockBefore() {
typeTextInFile(parseKeys("dat"), "abc<caret>de<tag>fg</tag>hi");
myFixture.checkResult("abcde<tag>fg</tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockInOpen() {
typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g>fg</tag>hi");
myFixture.checkResult("abcdehi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockInOpenWithArgs() {
typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g name = \"name\">fg</tag>hi");
myFixture.checkResult("abcdehi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockBetween() {
typeTextInFile(parseKeys("dat"), "abcde<tag>f<caret>g</tag>hi");
myFixture.checkResult("abcdehi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockBetweenWithArgs() {
typeTextInFile(parseKeys("dat"), "abcde<tag name = \"name\">f<caret>g</tag>hi");
myFixture.checkResult("abcdehi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockInClose() {
typeTextInFile(parseKeys("dat"), "abcde<tag>fg</ta<caret>g>hi");
myFixture.checkResult("abcdehi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockAfter() {
typeTextInFile(parseKeys("dat"), "abcde<tag>fg</tag>h<caret>i");
myFixture.checkResult("abcde<tag>fg</tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockInAlone() {
typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g>fghi");
myFixture.checkResult("abcde<tag>fghi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockWithoutTags() {
typeTextInFile(parseKeys("dat"), "abc<caret>de");
myFixture.checkResult("abcde");
}
//|d| |v_at|
public void testDeleteOuterTagBlockBeforeWithoutOpenTag() {
typeTextInFile(parseKeys("dat"), "abc<caret>defg</tag>hi");
myFixture.checkResult("abcdefg</tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockInCloseWithoutOpenTag() {
typeTextInFile(parseKeys("dat"), "abcdefg</ta<caret>g>hi");
myFixture.checkResult("abcdefg</tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockAfterWithoutOpenTag() {
typeTextInFile(parseKeys("dat"), "abcdefg</tag>h<caret>i");
myFixture.checkResult("abcdefg</tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockBeforeWithoutCloseTag() {
typeTextInFile(parseKeys("dat"), "abc<caret>defg<tag>hi");
myFixture.checkResult("abcdefg<tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockInOpenWithoutCloseTag() {
typeTextInFile(parseKeys("dat"), "abcdefg<ta<caret>g>hi");
myFixture.checkResult("abcdefg<tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockAfterWithoutCloseTag() {
typeTextInFile(parseKeys("dat"), "abcdefg<tag>h<caret>i");
myFixture.checkResult("abcdefg<tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockBeforeWrongOrder() {
typeTextInFile(parseKeys("dat"), "abc<caret>de</tag>fg<tag>hi");
myFixture.checkResult("abcde</tag>fg<tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockInOpenWrongOrder() {
typeTextInFile(parseKeys("dat"), "abcde</ta<caret>g>fg<tag>hi");
myFixture.checkResult("abcde</tag>fg<tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockBetweenWrongOrder() {
typeTextInFile(parseKeys("dat"), "abcde</tag>f<caret>g<tag>hi");
myFixture.checkResult("abcde</tag>fg<tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockInCloseWrongOrder() {
typeTextInFile(parseKeys("dat"), "abcde</tag>fg<ta<caret>g>hi");
myFixture.checkResult("abcde</tag>fg<tag>hi");
}
//|d| |v_at|
public void testDeleteOuterTagBlockAfterWrongOrder() {
typeTextInFile(parseKeys("dat"), "abcde</tag>fg<tag>h<caret>i");
myFixture.checkResult("abcde</tag>fg<tag>hi");
}
// |v_it|
public void testFileStartsWithSlash() {
configureByText("/*hello\n" +
"<caret>foo\n" +
"bar>baz\n");
typeText(parseKeys("vit"));
assertPluginError(true);
}
// VIM-1427
public void testDeleteOuterTagWithCount() {
typeTextInFile(parseKeys("d2at"),"<a><b><c><caret></c></b></a>");

View File

@ -0,0 +1,374 @@
/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2019 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jetbrains.plugins.ideavim.action.motion.`object`
import com.maddyhome.idea.vim.helper.StringHelper.parseKeys
import org.jetbrains.plugins.ideavim.VimTestCase
class MotionInnerBlockTagActionTest : VimTestCase() {
//|d| |v_it|
fun testDeleteInnerTagBlockCaretInHtml() {
typeTextInFile(parseKeys("dit"), "<template <caret>name=\"hello\">\n" +
" <button>Click Me</button>\n" +
" <p>You've pressed the button {{counter}} times.</p>\n" +
"</template>\n")
myFixture.checkResult("<template name=\"hello\"></template>\n")
}
//|d| |v_it|
fun testDeleteInnerTagBlockCaretInHtmlUnclosedTag() {
typeTextInFile(parseKeys("dit"), "<template <caret>name=\"hello\">\n" +
" <button>Click Me</button>\n" +
" <br>\n" +
" <p>You've pressed the button {{counter}} times.</p>\n" +
"</template>\n")
myFixture.checkResult("<template name=\"hello\"></template>\n")
}
fun testDeleteInnerTagBlockCaretEdgeTag() {
typeTextInFile(parseKeys("dit"), "<template name=\"hello\"<caret>>\n" +
" <button>Click Me</button>\n" +
" <br>\n" +
" <p>You've pressed the button {{counter}} times.</p>\n" +
"</template>\n")
myFixture.checkResult("<template name=\"hello\"></template>\n")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBefore() {
typeTextInFile(parseKeys("dit"), "abc<caret>de<tag>fg</tag>hi")
myFixture.checkResult("abcde<tag>fg</tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockInOpen() {
typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fg</tag>hi")
myFixture.checkResult("abcde<tag></tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockInOpenEndOfLine() {
typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fg</tag>")
myFixture.checkResult("abcde<tag></tag>")
}
//|d| |v_it|
fun testDeleteInnerTagBlockInOpenStartOfLine() {
typeTextInFile(parseKeys("dit"), "<ta<caret>g>fg</tag>hi")
myFixture.checkResult("<tag></tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockInOpenWithArgs() {
typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g name = \"name\">fg</tag>hi")
myFixture.checkResult("abcde<tag name = \"name\"></tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBetween() {
typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>g</tag>hi")
myFixture.checkResult("abcde<tag></tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBetweenTagWithRegex() {
typeTextInFile(parseKeys("dit"), "abcde<[abc]*>af<caret>gbc</[abc]*>hi")
myFixture.checkResult("abcde<[abc]*></[abc]*>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBetweenCamelCase() {
typeTextInFile(parseKeys("dit"), "abcde<tAg>f<caret>g</tag>hi")
myFixture.checkResult("abcde<tAg></tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBetweenCaps() {
typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>g</TAG>hi")
myFixture.checkResult("abcde<tag></TAG>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBetweenWithSpaceBeforeTag() {
typeTextInFile(parseKeys("dit"), "abcde< tag>f<caret>g</ tag>hi")
myFixture.checkResult("abcde< tag>fg</ tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBetweenWithSpaceAfterTag() {
typeTextInFile(parseKeys("dit"), "abcde<tag >f<caret>g</tag>hi")
myFixture.checkResult("abcde<tag ></tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBetweenWithArgs() {
typeTextInFile(parseKeys("dit"), "abcde<tag name = \"name\">f<caret>g</tag>hi")
myFixture.checkResult("abcde<tag name = \"name\"></tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockInClose() {
typeTextInFile(parseKeys("dit"), "abcde<tag>fg</ta<caret>g>hi")
myFixture.checkResult("abcde<tag></tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockAfter() {
typeTextInFile(parseKeys("dit"), "abcde<tag>fg</tag>h<caret>i")
myFixture.checkResult("abcde<tag>fg</tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockInAlone() {
typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fghi")
myFixture.checkResult("abcde<tag>fghi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockWithoutTags() {
typeTextInFile(parseKeys("dit"), "abc<caret>de")
myFixture.checkResult("abcde")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBeforeWithoutOpenTag() {
typeTextInFile(parseKeys("dit"), "abc<caret>defg</tag>hi")
myFixture.checkResult("abcdefg</tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockInCloseWithoutOpenTag() {
typeTextInFile(parseKeys("dit"), "abcdefg</ta<caret>g>hi")
myFixture.checkResult("abcdefg</tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockAfterWithoutOpenTag() {
typeTextInFile(parseKeys("dit"), "abcdefg</tag>h<caret>i")
myFixture.checkResult("abcdefg</tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBeforeWithoutCloseTag() {
typeTextInFile(parseKeys("dit"), "abc<caret>defg<tag>hi")
myFixture.checkResult("abcdefg<tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockInOpenWithoutCloseTag() {
typeTextInFile(parseKeys("dit"), "abcdefg<ta<caret>g>hi")
myFixture.checkResult("abcdefg<tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockAfterWithoutCloseTag() {
typeTextInFile(parseKeys("dit"), "abcdefg<tag>h<caret>i")
myFixture.checkResult("abcdefg<tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBeforeWrongOrder() {
typeTextInFile(parseKeys("dit"), "abc<caret>de</tag>fg<tag>hi")
myFixture.checkResult("abcde</tag>fg<tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockInOpenWrongOrder() {
typeTextInFile(parseKeys("dit"), "abcde</ta<caret>g>fg<tag>hi")
myFixture.checkResult("abcde</tag>fg<tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBetweenWrongOrder() {
typeTextInFile(parseKeys("dit"), "abcde</tag>f<caret>g<tag>hi")
myFixture.checkResult("abcde</tag>fg<tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockInCloseWrongOrder() {
typeTextInFile(parseKeys("dit"), "abcde</tag>fg<ta<caret>g>hi")
myFixture.checkResult("abcde</tag>fg<tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockTwoTagsWrongOrder() {
typeTextInFile(parseKeys("dit"), "<foo><html>t<caret>ext</foo></html>")
myFixture.checkResult("<foo></foo></html>")
}
//|d| |v_it|
fun testDeleteInnerTagBlockTwoTagsWrongOrderInClosingTag() {
typeTextInFile(parseKeys("dit"), "<foo><html>text</foo></htm<caret>l>")
myFixture.checkResult("<foo><html></html>")
}
//|d| |v_it|
fun testDeleteInnerTagBlockAfterWrongOrder() {
typeTextInFile(parseKeys("dit"), "abcde</tag>fg<tag>h<caret>i")
myFixture.checkResult("abcde</tag>fg<tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBracketInside() {
typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret><>g</tag>hi")
myFixture.checkResult("abcde<tag></tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagBlockBracketInsideString() {
typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>\"<>\"g</tag>hi")
myFixture.checkResult("abcde<tag></tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagIsCaseInsensitive() {
typeTextInFile(parseKeys("dit"), "<a> <as<caret>df> </A>")
myFixture.checkResult("<a></A>")
}
//|d| |v_it|
fun testDeleteInnerTagSlashesInAttribute() {
typeTextInFile(parseKeys("dit"), "<a href=\"http://isitchristmas.com\" class=\"button\">Bing <caret>Bing bing</a>")
myFixture.checkResult("<a href=\"http://isitchristmas.com\" class=\"button\"></a>")
}
// VIM-1090 |d| |v_it|
// Adapted from vim source file "test_textobjects.vim"
fun testDeleteInnerTagDuplicateTags() {
typeTextInFile(parseKeys("dit"), "<b>as<caret>d<i>as<b />df</i>asdf</b>")
myFixture.checkResult("<b></b>")
}
// |v_it|
fun testFileStartsWithSlash() {
configureByText("/*hello\n" +
"<caret>foo\n" +
"bar>baz\n")
typeText(parseKeys("vit"))
assertPluginError(true)
}
// |v_it|
fun testSelectInnerTagEmptyTag() {
configureByText("<a><caret></a>")
typeText(parseKeys("vit"))
assertSelection("<a></a>")
}
fun `test single character`() {
// The whole tag block is also selected if there is only a single character inside
configureByText("<a><caret>a</a>")
typeText(parseKeys("vit"))
assertSelection("<a>a</a>")
}
fun `test single character inside tag`() {
configureByText("<a<caret>></a>")
typeText(parseKeys("vit"))
assertSelection("<")
}
// VIM-1633 |v_it|
fun testNestedInTagSelection() {
configureByText("<t>Outer\n" +
" <t><caret>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("vit"))
assertSelection("Inner")
}
fun `test nested tag double motion`() {
configureByText("<o>Outer\n" +
" <caret> <t></t>\n" +
"</o>\n")
typeText(parseKeys("vitit"))
assertSelection("<t></t>")
}
fun `test in inner tag double motion`() {
configureByText("<o><t><caret></t>\n</o>")
typeText(parseKeys("vitit"))
assertSelection("<o><t></t>\n</o>")
}
fun `test nested tags between tags`() {
configureByText("<t>Outer\n" +
" <t>Inner</t> <caret> <t>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("vit"))
assertSelection("Outer\n" + " <t>Inner</t> <t>Inner</t>")
}
fun `test nested tags number motion`() {
configureByText("<t>Outer\n" +
" <t><caret>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("v2it"))
assertSelection("Outer\n" + " <t>Inner</t>")
}
fun `test nested tags double motion`() {
configureByText("<o>Outer\n" +
" <t><caret>Inner</t>\n" +
"</o>\n")
typeText(parseKeys("vitit"))
assertSelection("<t>Inner</t>")
}
fun `test nested tags triple motion`() {
configureByText("<t>Outer\n" +
" <t><caret>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("vititit"))
assertSelection("Outer\n" + " <t>Inner</t>")
}
fun `test nested tags in closing tag`() {
configureByText("<t>Outer\n" +
" <t>Inner</t>\n" +
"</<caret>t>\n")
typeText(parseKeys("vit"))
assertSelection("Outer\n" + " <t>Inner</t>")
}
fun `test nested tags in opening tag`() {
configureByText("<<caret>t>Outer\n" +
" <t>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("vit"))
assertSelection("Outer\n" + " <t>Inner</t>")
}
fun `test nested tags ouside tag`() {
configureByText("<caret><t>Outer\n" +
" <t>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("vit"))
assertSelection("Outer\n" + " <t>Inner</t>")
}
fun `test skip whitespace at start of line`() {
configureByText("<o>Outer\n" +
" <caret> <t></t>\n" +
"</o>\n")
typeText(parseKeys("vit"))
assertSelection("<")
}
}

View File

@ -0,0 +1,254 @@
/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2019 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jetbrains.plugins.ideavim.action.motion.`object`
import com.maddyhome.idea.vim.helper.StringHelper.parseKeys
import org.jetbrains.plugins.ideavim.VimTestCase
class MotionOuterBlockTagActionTest : VimTestCase() {
//|d| |v_at|
fun testDeleteOuterTagBlockBefore() {
typeTextInFile(parseKeys("dat"), "abc<caret>de<tag>fg</tag>hi")
myFixture.checkResult("abcde<tag>fg</tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockInOpen() {
typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g>fg</tag>hi")
myFixture.checkResult("abcdehi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockInOpenWithArgs() {
typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g name = \"name\">fg</tag>hi")
myFixture.checkResult("abcdehi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockBetween() {
typeTextInFile(parseKeys("dat"), "abcde<tag>f<caret>g</tag>hi")
myFixture.checkResult("abcdehi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockBetweenWithArgs() {
typeTextInFile(parseKeys("dat"), "abcde<tag name = \"name\">f<caret>g</tag>hi")
myFixture.checkResult("abcdehi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockInClose() {
typeTextInFile(parseKeys("dat"), "abcde<tag>fg</ta<caret>g>hi")
myFixture.checkResult("abcdehi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockAfter() {
typeTextInFile(parseKeys("dat"), "abcde<tag>fg</tag>h<caret>i")
myFixture.checkResult("abcde<tag>fg</tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockInAlone() {
typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g>fghi")
myFixture.checkResult("abcde<tag>fghi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockWithoutTags() {
typeTextInFile(parseKeys("dat"), "abc<caret>de")
myFixture.checkResult("abcde")
}
//|d| |v_at|
fun testDeleteOuterTagBlockBeforeWithoutOpenTag() {
typeTextInFile(parseKeys("dat"), "abc<caret>defg</tag>hi")
myFixture.checkResult("abcdefg</tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockInCloseWithoutOpenTag() {
typeTextInFile(parseKeys("dat"), "abcdefg</ta<caret>g>hi")
myFixture.checkResult("abcdefg</tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockAfterWithoutOpenTag() {
typeTextInFile(parseKeys("dat"), "abcdefg</tag>h<caret>i")
myFixture.checkResult("abcdefg</tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockBeforeWithoutCloseTag() {
typeTextInFile(parseKeys("dat"), "abc<caret>defg<tag>hi")
myFixture.checkResult("abcdefg<tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockInOpenWithoutCloseTag() {
typeTextInFile(parseKeys("dat"), "abcdefg<ta<caret>g>hi")
myFixture.checkResult("abcdefg<tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockAfterWithoutCloseTag() {
typeTextInFile(parseKeys("dat"), "abcdefg<tag>h<caret>i")
myFixture.checkResult("abcdefg<tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockBeforeWrongOrder() {
typeTextInFile(parseKeys("dat"), "abc<caret>de</tag>fg<tag>hi")
myFixture.checkResult("abcde</tag>fg<tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockInOpenWrongOrder() {
typeTextInFile(parseKeys("dat"), "abcde</ta<caret>g>fg<tag>hi")
myFixture.checkResult("abcde</tag>fg<tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockBetweenWrongOrder() {
typeTextInFile(parseKeys("dat"), "abcde</tag>f<caret>g<tag>hi")
myFixture.checkResult("abcde</tag>fg<tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockInCloseWrongOrder() {
typeTextInFile(parseKeys("dat"), "abcde</tag>fg<ta<caret>g>hi")
myFixture.checkResult("abcde</tag>fg<tag>hi")
}
//|d| |v_at|
fun testDeleteOuterTagBlockAfterWrongOrder() {
typeTextInFile(parseKeys("dat"), "abcde</tag>fg<tag>h<caret>i")
myFixture.checkResult("abcde</tag>fg<tag>hi")
}
//|d| |v_it|
fun testDeleteInnerTagAngleBrackets() {
typeTextInFile(parseKeys("dit"), "<div <caret>hello=\"d > hsj < akl\"></div>")
myFixture.checkResult("<div hello=\"d ></div>")
}
// VIM-1090 |d| |v_at|
fun testDeleteOuterTagDuplicateTags() {
typeTextInFile(parseKeys("dat"), "<a><a></a></a<caret>>")
myFixture.checkResult("")
}
// |v_it| |v_at|
fun testTagSelectionSkipsWhitespaceAtStartOfLine() {
// Also skip tabs
configureByText("<o>Outer\n" +
" <caret> \t <t>Inner</t>\n" +
"</o>\n")
typeText(parseKeys("vat"))
assertSelection("<t>Inner</t>")
}
fun `test skip new line`() {
// Newline must not be skipped
configureByText("<caret>\n" + " <t>asdf</t>")
typeText(parseKeys("vat"))
assertSelection(null)
}
fun `test whitespace skip`() {
// Whitespace is only skipped if there is nothing else at the start of the line
configureByText("<o>Outer\n" +
"a <caret> <t>Inner</t>\n" +
"</o>\n")
typeText(parseKeys("vat"))
assertSelection("<o>Outer\n" +
"a <t>Inner</t>\n" +
"</o>")
}
// |v_at|
fun testNestedTagSelection() {
configureByText("<t>Outer\n" +
" <t><caret>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("vat"))
assertSelection("<t>Inner</t>")
}
fun `test nested tags between tags`() {
configureByText("<t>Outer\n" +
" <t>Inner</t> <caret> <t>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("vat"))
assertSelection("<t>Outer\n" +
" <t>Inner</t> <t>Inner</t>\n" +
"</t>")
}
fun `test nested tags double motion`() {
configureByText("<t>Outer\n" +
" <t><caret>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("vatat"))
assertSelection("<t>Outer\n" +
" <t>Inner</t>\n" +
"</t>")
}
fun `test nested tags number motion`() {
configureByText("<t>Outer\n" +
" <t><caret>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("v2at"))
assertSelection("<t>Outer\n" +
" <t>Inner</t>\n" +
"</t>")
}
fun `test nested tags on outer`() {
configureByText("<t>Outer\n" +
" <t>Inner</t>\n" +
"</<caret>t>\n")
typeText(parseKeys("vat"))
assertSelection("<t>Outer\n" +
" <t>Inner</t>\n" +
"</t>")
}
fun `test nested tags on outer start`() {
configureByText("<<caret>t>Outer\n" +
" <t>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("vat"))
assertSelection("<t>Outer\n" +
" <t>Inner</t>\n" +
"</t>")
}
fun `test nested tags outside outer`() {
configureByText("<caret><t>Outer\n" +
" <t>Inner</t>\n" +
"</t>\n")
typeText(parseKeys("vat"))
assertSelection("<t>Outer\n" +
" <t>Inner</t>\n" +
"</t>")
}
}