mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-06-01 19:34:05 +02:00
parent
bfcf706ca7
commit
3f65d1d99a
src
main/java/com/maddyhome/idea/vim
group
helper
newapi
test/java/org/jetbrains/plugins/ideavim/action/motion
testFixtures/kotlin/org/jetbrains/plugins/ideavim
tests/java-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/action/motion/updown
vim-engine/src
main/kotlin/com/maddyhome/idea/vim
action/motion/text
api
group
test/kotlin/com/maddyhome/idea/vim/groups
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2003-2024 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
|
||||
package com.maddyhome.idea.vim.group
|
||||
|
||||
import com.intellij.lang.CodeDocumentationAwareCommenter
|
||||
import com.intellij.lang.LanguageCommenters
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.psi.PsiComment
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.api.VimPsiService
|
||||
import com.maddyhome.idea.vim.common.TextRange
|
||||
import com.maddyhome.idea.vim.helper.PsiHelper
|
||||
import com.maddyhome.idea.vim.newapi.ij
|
||||
import com.maddyhome.idea.vim.newapi.vim
|
||||
|
||||
@Service
|
||||
public class IjVimPsiService: VimPsiService {
|
||||
override fun getCommentAtPos(editor: VimEditor, pos: Int): Pair<TextRange, Pair<String, String>?>? {
|
||||
val psiFile = PsiHelper.getFile(editor.ij) ?: return null
|
||||
val psiElement = psiFile.findElementAt(pos) ?: return null
|
||||
val language = psiElement.language
|
||||
val commenter = LanguageCommenters.INSTANCE.forLanguage(language)
|
||||
val psiComment = PsiTreeUtil.getParentOfType(psiElement, PsiComment::class.java, false) ?: return null
|
||||
val commentText = psiComment.text
|
||||
|
||||
val blockCommentPrefix = commenter.blockCommentPrefix
|
||||
val blockCommentSuffix = commenter.blockCommentSuffix
|
||||
|
||||
val docCommentPrefix = (commenter as? CodeDocumentationAwareCommenter)?.documentationCommentPrefix
|
||||
val docCommentSuffix = (commenter as? CodeDocumentationAwareCommenter)?.documentationCommentSuffix
|
||||
|
||||
val prefixToSuffix: Pair<String, String>? =
|
||||
if (docCommentPrefix != null && docCommentSuffix != null && commentText.startsWith(docCommentPrefix) && commentText.endsWith(docCommentSuffix)) {
|
||||
docCommentPrefix to docCommentSuffix
|
||||
}
|
||||
else if (blockCommentPrefix != null && blockCommentSuffix != null && commentText.startsWith(blockCommentPrefix) && commentText.endsWith(blockCommentSuffix)) {
|
||||
blockCommentPrefix to blockCommentSuffix
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
return Pair(psiComment.textRange.vim, prefixToSuffix)
|
||||
}
|
||||
|
||||
override fun getDoubleQuotedString(editor: VimEditor, pos: Int, isInner: Boolean): TextRange? {
|
||||
// TODO[ideavim] It wasn't implemented before, but implementing it will significantly improve % motion
|
||||
return getDoubleQuotesRangeNoPSI(editor.text(), pos, isInner)
|
||||
}
|
||||
|
||||
override fun getSingleQuotedString(editor: VimEditor, pos: Int, isInner: Boolean): TextRange? {
|
||||
// TODO[ideavim] It wasn't implemented before, but implementing it will significantly improve % motion
|
||||
return getSingleQuotesRangeNoPSI(editor.text(), pos, isInner)
|
||||
}
|
||||
}
|
@ -105,7 +105,7 @@ internal class MotionGroup : VimMotionGroupBase() {
|
||||
VimPlugin.getFile().selectEditor(project, file)
|
||||
|
||||
override fun moveCaretToMatchingPair(editor: VimEditor, caret: ImmutableVimCaret): Motion {
|
||||
return SearchHelper.findMatchingPairOnCurrentLine(editor.ij, caret.ij).toMotionOrError()
|
||||
return findMatchingPairOnCurrentLine(editor, caret)?.toMotionOrError() ?: Motion.Error
|
||||
}
|
||||
|
||||
override fun moveCaretToFirstDisplayLine(
|
||||
|
@ -632,113 +632,6 @@ public class SearchHelper {
|
||||
return new TextRange(bstart, bend + 1);
|
||||
}
|
||||
|
||||
private static int findMatchingBlockCommentPair(@NotNull PsiComment comment,
|
||||
int pos,
|
||||
@Nullable String prefix,
|
||||
@Nullable String suffix) {
|
||||
if (prefix != null && suffix != null) {
|
||||
// TODO: Try to get rid of `getText()` because it takes a lot of time to calculate the string
|
||||
final String commentText = comment.getText();
|
||||
if (commentText.startsWith(prefix) && commentText.endsWith(suffix)) {
|
||||
final int endOffset = comment.getTextOffset() + comment.getTextLength();
|
||||
if (pos < comment.getTextOffset() + prefix.length()) {
|
||||
return endOffset;
|
||||
}
|
||||
else if (pos >= endOffset - suffix.length()) {
|
||||
return comment.getTextOffset();
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int findMatchingBlockCommentPair(@NotNull PsiElement element, int pos) {
|
||||
final Language language = element.getLanguage();
|
||||
final Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(language);
|
||||
final PsiComment comment = PsiTreeUtil.getParentOfType(element, PsiComment.class, false);
|
||||
if (comment != null) {
|
||||
final int ret = findMatchingBlockCommentPair(comment, pos, commenter.getBlockCommentPrefix(),
|
||||
commenter.getBlockCommentSuffix());
|
||||
if (ret >= 0) {
|
||||
return ret;
|
||||
}
|
||||
if (commenter instanceof CodeDocumentationAwareCommenter docCommenter) {
|
||||
return findMatchingBlockCommentPair(comment, pos, docCommenter.getDocumentationCommentPrefix(),
|
||||
docCommenter.getDocumentationCommentSuffix());
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* This looks on the current line, starting at the cursor position for one of {, }, (, ), [, or ]. It then searches
|
||||
* forward or backward, as appropriate for the associated match pair. String in double quotes are skipped over.
|
||||
* Single characters in single quotes are skipped too.
|
||||
*
|
||||
* @param editor The editor to search in
|
||||
* @return The offset within the editor of the found character or -1 if no match was found or none of the characters
|
||||
* were found on the remainder of the current line.
|
||||
*/
|
||||
public static int findMatchingPairOnCurrentLine(@NotNull Editor editor, @NotNull Caret caret) {
|
||||
int pos = caret.getOffset();
|
||||
|
||||
final int commentPos = findMatchingComment(editor, pos);
|
||||
if (commentPos >= 0) {
|
||||
return commentPos;
|
||||
}
|
||||
|
||||
int line = caret.getLogicalPosition().line;
|
||||
final IjVimEditor vimEditor = new IjVimEditor(editor);
|
||||
int end = EngineEditorHelperKt.getLineEndOffset(vimEditor, line, true);
|
||||
|
||||
// To handle the case where visual mode allows the user to go past the end of the line,
|
||||
// which will prevent loc from finding a pairable character below
|
||||
if (pos > 0 && pos == end) {
|
||||
pos = end - 1;
|
||||
}
|
||||
|
||||
final String pairChars = parseMatchPairsOption(vimEditor);
|
||||
|
||||
CharSequence chars = editor.getDocument().getCharsSequence();
|
||||
int loc = -1;
|
||||
// Search the remainder of the current line for one of the candidate characters
|
||||
while (pos < end) {
|
||||
loc = pairChars.indexOf(chars.charAt(pos));
|
||||
if (loc >= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
int res = -1;
|
||||
// If we found one ...
|
||||
if (loc >= 0) {
|
||||
// What direction should we go now (-1 is backward, 1 is forward)
|
||||
Direction dir = loc % 2 == 0 ? Direction.FORWARDS : Direction.BACKWARDS;
|
||||
// Which character did we find and which should we now search for
|
||||
char found = pairChars.charAt(loc);
|
||||
char match = pairChars.charAt(loc + dir.toInt());
|
||||
res = findBlockLocation(chars, found, match, dir, pos, 1, true);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* If on the start/end of a block comment, jump to the matching of that comment, or vice versa.
|
||||
*/
|
||||
private static int findMatchingComment(@NotNull Editor editor, int pos) {
|
||||
final PsiFile psiFile = PsiHelper.getFile(editor);
|
||||
if (psiFile != null) {
|
||||
final PsiElement element = psiFile.findElementAt(pos);
|
||||
if (element != null) {
|
||||
return findMatchingBlockCommentPair(element, pos);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int findBlockLocation(@NotNull CharSequence chars,
|
||||
char found,
|
||||
char match,
|
||||
|
@ -500,3 +500,6 @@ public val Editor.vim: VimEditor
|
||||
get() = IjVimEditor(this)
|
||||
public val VimEditor.ij: Editor
|
||||
get() = (this as IjVimEditor).editor
|
||||
|
||||
public val com.intellij.openapi.util.TextRange.vim: TextRange
|
||||
get() = TextRange(this.startOffset, this.endOffset)
|
||||
|
@ -42,6 +42,7 @@ import com.maddyhome.idea.vim.api.VimMessages
|
||||
import com.maddyhome.idea.vim.api.VimMotionGroup
|
||||
import com.maddyhome.idea.vim.api.VimOptionGroup
|
||||
import com.maddyhome.idea.vim.api.VimProcessGroup
|
||||
import com.maddyhome.idea.vim.api.VimPsiService
|
||||
import com.maddyhome.idea.vim.api.VimRegexpService
|
||||
import com.maddyhome.idea.vim.api.VimScrollGroup
|
||||
import com.maddyhome.idea.vim.api.VimSearchGroup
|
||||
@ -65,6 +66,7 @@ import com.maddyhome.idea.vim.group.FileGroup
|
||||
import com.maddyhome.idea.vim.group.GlobalIjOptions
|
||||
import com.maddyhome.idea.vim.group.HistoryGroup
|
||||
import com.maddyhome.idea.vim.group.IjVimOptionGroup
|
||||
import com.maddyhome.idea.vim.group.IjVimPsiService
|
||||
import com.maddyhome.idea.vim.group.MacroGroup
|
||||
import com.maddyhome.idea.vim.group.MotionGroup
|
||||
import com.maddyhome.idea.vim.group.SearchGroup
|
||||
@ -147,6 +149,8 @@ internal class IjVimInjector : VimInjectorBase() {
|
||||
get() = service<MacroGroup>()
|
||||
override val undo: VimUndoRedo
|
||||
get() = service<UndoRedoHelper>()
|
||||
override val psiService: VimPsiService
|
||||
get() = service<IjVimPsiService>()
|
||||
override val commandLineHelper: VimCommandLineHelper
|
||||
get() = service<CommandLineHelper>()
|
||||
override val nativeActionManager: NativeActionManager
|
||||
|
@ -84,22 +84,6 @@ internal class IjVimSearchHelper : VimSearchHelperBase() {
|
||||
return PsiHelper.findMethodStart(editor.ij, caret.ij.offset, count)
|
||||
}
|
||||
|
||||
override fun findUnmatchedBlock(editor: VimEditor, caret: ImmutableVimCaret, type: Char, count: Int): Int {
|
||||
val chars: CharSequence = editor.ij.document.charsSequence
|
||||
var pos: Int = caret.ij.offset
|
||||
val loc = BLOCK_CHARS.indexOf(type)
|
||||
// What direction should we go now (-1 is backward, 1 is forward)
|
||||
val dir = if (loc % 2 == 0) Direction.BACKWARDS else Direction.FORWARDS
|
||||
// Which character did we find and which should we now search for
|
||||
val match = BLOCK_CHARS[loc]
|
||||
val found = BLOCK_CHARS[loc - dir.toInt()]
|
||||
|
||||
if (pos < chars.length && chars[pos] == type) {
|
||||
pos += dir.toInt()
|
||||
}
|
||||
return findBlockLocation(chars, found, match, dir, pos, count)
|
||||
}
|
||||
|
||||
private fun findBlockLocation(
|
||||
chars: CharSequence,
|
||||
found: Char,
|
||||
|
@ -98,15 +98,6 @@ class MotionUnmatchedBraceOpenActionTest : VimTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
@VimBehaviorDiffers(
|
||||
originalVimAfter = """
|
||||
class Xxx $c{
|
||||
int main() {
|
||||
|
||||
}
|
||||
}
|
||||
""",
|
||||
)
|
||||
@Test
|
||||
fun `test go to next next bracket with great count`() {
|
||||
doTest(
|
||||
@ -119,9 +110,9 @@ class MotionUnmatchedBraceOpenActionTest : VimTestCase() {
|
||||
}
|
||||
""".trimIndent(),
|
||||
"""
|
||||
class Xxx {
|
||||
class Xxx $c{
|
||||
int main() {
|
||||
$c
|
||||
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
|
@ -8,10 +8,12 @@
|
||||
|
||||
package org.jetbrains.plugins.ideavim.action.motion.updown
|
||||
|
||||
import com.intellij.idea.TestFor
|
||||
import com.maddyhome.idea.vim.state.mode.Mode
|
||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
@ -143,6 +145,7 @@ class MotionPercentOrMatchActionTest : VimTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("It will work after implementing all of the methods in VimPsiService")
|
||||
fun `test motion outside text`() {
|
||||
doTest(
|
||||
"%",
|
||||
@ -207,41 +210,45 @@ class MotionPercentOrMatchActionTest : VimTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(SkipNeovimReason.BUG_IN_NEOVIM)
|
||||
fun `test motion in text with escape (outer forward)`() {
|
||||
doTest(
|
||||
"%",
|
||||
""" debugPrint$c(\(var)) """,
|
||||
""" debugPrint(\(var)$c) """,
|
||||
""" debugPrint(\(var$c)) """,
|
||||
Mode.NORMAL(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(SkipNeovimReason.BUG_IN_NEOVIM)
|
||||
fun `test motion in text with escape (outer backward)`() {
|
||||
doTest(
|
||||
"%",
|
||||
""" debugPrint(\(var)$c) """,
|
||||
""" debugPrint$c(\(var)) """,
|
||||
""" debugPrint(\(var)$c) """,
|
||||
Mode.NORMAL(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(SkipNeovimReason.BUG_IN_NEOVIM)
|
||||
fun `test motion in text with escape (inner forward)`() {
|
||||
doTest(
|
||||
"%",
|
||||
""" debugPrint(\$c(var)) """,
|
||||
""" debugPrint(\(var$c)) """,
|
||||
""" debugPrint(\$c(var)) """,
|
||||
Mode.NORMAL(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(SkipNeovimReason.BUG_IN_NEOVIM)
|
||||
fun `test motion in text with escape (inner backward)`() {
|
||||
doTest(
|
||||
"%",
|
||||
""" debugPrint(\$c(var)) """,
|
||||
""" debugPrint(\(var$c)) """,
|
||||
""" debugPrint(\$c(var)) """,
|
||||
Mode.NORMAL(),
|
||||
)
|
||||
}
|
||||
@ -332,4 +339,28 @@ class MotionPercentOrMatchActionTest : VimTestCase() {
|
||||
)
|
||||
assertOffset(10)
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestFor(issues = ["VIM-3294"])
|
||||
fun `test matching with braces inside of string`() {
|
||||
configureByText("""
|
||||
$c("("")")
|
||||
""".trimIndent())
|
||||
typeText("%")
|
||||
assertState("""
|
||||
("("")"$c)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestFor(issues = ["VIM-3294"])
|
||||
fun `test matching with braces inside of string 2`() {
|
||||
configureByText("""
|
||||
("("")"$c)
|
||||
""".trimIndent())
|
||||
typeText("%")
|
||||
assertState("""
|
||||
$c("("")")
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
@ -245,6 +245,9 @@ enum class SkipNeovimReason {
|
||||
|
||||
GUARDED_BLOCKS,
|
||||
CTRL_CODES,
|
||||
|
||||
BUG_IN_NEOVIM,
|
||||
PSI,
|
||||
}
|
||||
|
||||
fun LogicalPosition.toVimCoords(): VimCoords {
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
package org.jetbrains.plugins.ideavim.action.motion.updown
|
||||
|
||||
import com.intellij.idea.TestFor
|
||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
||||
import org.jetbrains.plugins.ideavim.VimJavaTestCase
|
||||
@ -66,4 +67,98 @@ class MotionPercentOrMatchActionJavaTest : VimJavaTestCase() {
|
||||
typeText("%")
|
||||
assertState("/* foo $c */")
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestFor(issues = ["VIM-1399"])
|
||||
@TestWithoutNeovim(SkipNeovimReason.PSI)
|
||||
fun `test percent ignores brace inside comment`() {
|
||||
configureByJavaText("""
|
||||
protected TokenStream normalize(String fieldName, TokenStream in) {
|
||||
TokenStream result = new EmptyTokenFilter(in); /* $c{
|
||||
* some text
|
||||
*/
|
||||
result = new LowerCaseFilter(result);
|
||||
return result;
|
||||
}
|
||||
""".trimIndent())
|
||||
typeText("%")
|
||||
assertState("""
|
||||
protected TokenStream normalize(String fieldName, TokenStream in) {
|
||||
TokenStream result = new EmptyTokenFilter(in); /* $c{
|
||||
* some text
|
||||
*/
|
||||
result = new LowerCaseFilter(result);
|
||||
return result;
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestFor(issues = ["VIM-1399"])
|
||||
@TestWithoutNeovim(SkipNeovimReason.PSI)
|
||||
fun `test percent doesnt match brace inside comment`() {
|
||||
configureByJavaText("""
|
||||
protected TokenStream normalize(String fieldName, TokenStream in) $c{
|
||||
TokenStream result = new EmptyTokenFilter(in); /* {
|
||||
* some text
|
||||
*/
|
||||
result = new LowerCaseFilter(result);
|
||||
return result;
|
||||
}
|
||||
""".trimIndent())
|
||||
typeText("%")
|
||||
assertState("""
|
||||
protected TokenStream normalize(String fieldName, TokenStream in) {
|
||||
TokenStream result = new EmptyTokenFilter(in); /* {
|
||||
* some text
|
||||
*/
|
||||
result = new LowerCaseFilter(result);
|
||||
return result;
|
||||
$c}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(SkipNeovimReason.PSI)
|
||||
fun `test matching works with a sequence of single-line comments`() {
|
||||
configureByJavaText("""
|
||||
protected TokenStream normalize(String fieldName, TokenStream in) {
|
||||
// $c{
|
||||
// result = new LowerCaseFilter(result);
|
||||
// }
|
||||
return result;
|
||||
}
|
||||
""".trimIndent())
|
||||
typeText("%")
|
||||
assertState("""
|
||||
protected TokenStream normalize(String fieldName, TokenStream in) {
|
||||
// {
|
||||
// result = new LowerCaseFilter(result);
|
||||
// $c}
|
||||
return result;
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(SkipNeovimReason.PSI)
|
||||
fun `test matching doesn't work if a sequence of single-line comments is broken`() {
|
||||
configureByJavaText("""
|
||||
protected TokenStream normalize(String fieldName, TokenStream in) {
|
||||
// $c{
|
||||
result = new LowerCaseFilter(result);
|
||||
// }
|
||||
return result;
|
||||
}
|
||||
""".trimIndent())
|
||||
typeText("%")
|
||||
assertState("""
|
||||
protected TokenStream normalize(String fieldName, TokenStream in) {
|
||||
// $c{
|
||||
result = new LowerCaseFilter(result);
|
||||
// }
|
||||
return result;
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
@ -12,12 +12,12 @@ import com.intellij.vim.annotations.Mode
|
||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||
import com.maddyhome.idea.vim.api.ImmutableVimCaret
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.api.normalizeOffset
|
||||
import com.maddyhome.idea.vim.command.Argument
|
||||
import com.maddyhome.idea.vim.command.CommandFlags
|
||||
import com.maddyhome.idea.vim.command.MotionType
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.group.findUnmatchedBlock
|
||||
import com.maddyhome.idea.vim.handler.Motion
|
||||
import com.maddyhome.idea.vim.handler.MotionActionHandler
|
||||
import com.maddyhome.idea.vim.handler.toMotionOrError
|
||||
@ -36,8 +36,7 @@ public sealed class MotionUnmatchedAction(private val motionChar: Char) : Motion
|
||||
argument: Argument?,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Motion {
|
||||
return moveCaretToUnmatchedBlock(editor, caret, operatorArguments.count1, motionChar)
|
||||
.toMotionOrError()
|
||||
return moveCaretToUnmatchedBlock(editor, caret, operatorArguments.count1, motionChar)?.toMotionOrError() ?: Motion.Error
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,14 +52,11 @@ public class MotionUnmatchedParenCloseAction : MotionUnmatchedAction(')')
|
||||
@CommandOrMotion(keys = ["[("], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
|
||||
public class MotionUnmatchedParenOpenAction : MotionUnmatchedAction('(')
|
||||
|
||||
private fun moveCaretToUnmatchedBlock(editor: VimEditor, caret: ImmutableVimCaret, count: Int, type: Char): Int {
|
||||
private fun moveCaretToUnmatchedBlock(editor: VimEditor, caret: ImmutableVimCaret, count: Int, type: Char): Int? {
|
||||
return if (editor.currentCaret().offset.point == 0 && count < 0 || editor.currentCaret().offset.point >= editor.fileSize() - 1 && count > 0) {
|
||||
-1
|
||||
null
|
||||
} else {
|
||||
var res = injector.searchHelper.findUnmatchedBlock(editor, caret, type, count)
|
||||
if (res != -1) {
|
||||
res = editor.normalizeOffset(res, false)
|
||||
}
|
||||
res
|
||||
val res = findUnmatchedBlock(editor, caret.offset.point, type, count) ?: return null
|
||||
return editor.normalizeOffset(res, false)
|
||||
}
|
||||
}
|
||||
|
@ -167,6 +167,8 @@ public interface VimInjector {
|
||||
// !! in progress
|
||||
public val undo: VimUndoRedo
|
||||
|
||||
public val psiService: VimPsiService
|
||||
|
||||
// !! in progress
|
||||
public val commandLineHelper: VimCommandLineHelper
|
||||
|
||||
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2003-2024 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
|
||||
package com.maddyhome.idea.vim.api
|
||||
|
||||
import com.maddyhome.idea.vim.common.TextRange
|
||||
|
||||
public interface VimPsiService {
|
||||
/**
|
||||
* @return triple of comment range, comment prefix, comment suffix or null if there is no comment at the given position
|
||||
*/
|
||||
public fun getCommentAtPos(editor: VimEditor, pos: Int): Pair<TextRange, Pair<String, String>?>?
|
||||
|
||||
/**
|
||||
* @param isInner A flag indicating whether the start and end quote characters should be considered part of the string:
|
||||
* - If set to true, only the text between the quote characters is included in the range.
|
||||
* - If set to false, the quote characters at the boundaries are included as part of the string range.
|
||||
*
|
||||
* NOTE: Regardless of the [isInner] value, a TextRange will be returned if the caret is positioned on a quote character.
|
||||
*/
|
||||
public fun getDoubleQuotedString(editor: VimEditor, pos: Int, isInner: Boolean): TextRange?
|
||||
|
||||
/**
|
||||
* @param isInner A flag indicating whether the start and end quote characters should be considered part of the string:
|
||||
* - If set to true, only the text between the quote characters is included in the range.
|
||||
* - If set to false, the quote characters at the boundaries are included as part of the string range.
|
||||
*
|
||||
* NOTE: Regardless of the [isInner] value, a TextRange will be returned if the caret is positioned on a quote character.
|
||||
*/
|
||||
public fun getSingleQuotedString(editor: VimEditor, pos: Int, isInner: Boolean): TextRange?
|
||||
}
|
@ -97,13 +97,6 @@ public interface VimSearchHelper {
|
||||
count: Int,
|
||||
): Int
|
||||
|
||||
public fun findUnmatchedBlock(
|
||||
editor: VimEditor,
|
||||
caret: ImmutableVimCaret,
|
||||
type: Char,
|
||||
count: Int,
|
||||
): Int
|
||||
|
||||
/**
|
||||
* Find the next word in the editor's document, from the given starting point
|
||||
*
|
||||
|
@ -0,0 +1,381 @@
|
||||
/*
|
||||
* Copyright 2003-2024 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
|
||||
package com.maddyhome.idea.vim.group
|
||||
|
||||
import com.maddyhome.idea.vim.api.ImmutableVimCaret
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.api.options
|
||||
import com.maddyhome.idea.vim.common.Direction
|
||||
import com.maddyhome.idea.vim.common.TextRange
|
||||
|
||||
public const val BLOCK_CHARS: String = "{}()[]<>"
|
||||
|
||||
public fun isInsideComment(editor: VimEditor, pos: Int): Boolean {
|
||||
return injector.psiService.getCommentAtPos(editor, pos) != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the specified caret position is within the bounds of a single-quoted string in the editor
|
||||
* (uses PSI when possible)
|
||||
*
|
||||
* @param isInner A flag indicating the behavior regarding quote characters themselves:
|
||||
* - If set to true, the positions of the quote characters are not considered part of the string.
|
||||
* In this case, the caret must be between the quotes to be considered inside the string.
|
||||
* - If set to false, the positions of the quote characters enclosing a string are considered part of the string.
|
||||
* This means the caret is considered inside the string even if it's directly on one of the quotes.
|
||||
*/
|
||||
public fun isInsideSingleQuotes(editor: VimEditor, pos: Int, isInner: Boolean): Boolean {
|
||||
val range = injector.psiService.getSingleQuotedString(editor, pos, isInner) ?: return false
|
||||
return pos in range
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the specified caret position is within the bounds of a double-quoted string in the editor
|
||||
* (uses PSI when possible)
|
||||
*
|
||||
* @param isInner A flag indicating the behavior regarding quote characters themselves:
|
||||
* - If set to true, the positions of the quote characters are not considered part of the string.
|
||||
* In this case, the caret must be between the quotes to be considered inside the string.
|
||||
* - If set to false, the positions of the quote characters enclosing a string are considered part of the string.
|
||||
* This means the caret is considered inside the string even if it's directly on one of the quotes.
|
||||
*/
|
||||
public fun isInsideDoubleQuotes(editor: VimEditor, pos: Int, isInner: Boolean): Boolean {
|
||||
val range = injector.psiService.getDoubleQuotedString(editor, pos, isInner) ?: return false
|
||||
return pos in range
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the specified caret position is located within the bounds of a string in the document.
|
||||
* A "string" is defined as any sequence of text enclosed in single or double quotes.
|
||||
* (uses PSI when possible)
|
||||
*
|
||||
* @param isInner Flag indicating if the quotes themselves are treated as inside the string:
|
||||
* - If true, the caret needs to be between the quote marks to be within the string.
|
||||
* - If false, caret positions on the quote marks are counted as within the string.
|
||||
*/
|
||||
public fun isInsideString(editor: VimEditor, pos: Int, isInner: Boolean): Boolean {
|
||||
val range = getStringAtPos(editor, pos, isInner) ?: return false
|
||||
return pos in range
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the range of text that represents a "string" at a given caret position within the editor's document.
|
||||
* A "string" is defined as any sequence of text enclosed in single or double quotes.
|
||||
* (uses PSI when possible)
|
||||
*
|
||||
* @param isInner A flag indicating whether the start and end quote characters should be considered part of the string:
|
||||
* - If set to true, only the text between the quote characters is included in the range.
|
||||
* - If set to false, the quote characters at the boundaries are included as part of the string range.
|
||||
*
|
||||
* NOTE: Regardless of the [isInner] value, a TextRange will be returned if the caret is positioned on a quote character.
|
||||
*/
|
||||
public fun getStringAtPos(editor: VimEditor, pos: Int, isInner: Boolean): TextRange? {
|
||||
return injector.psiService.getDoubleQuotedString(editor, pos, isInner) ?: injector.psiService.getSingleQuotedString(editor, pos, isInner)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method emulates the Vim '%' command for comments.
|
||||
* If the caret is positioned over a comment boundary, this method returns the position of the opposing boundary.
|
||||
*/
|
||||
public fun getCommentsOppositeBoundary(editor: VimEditor, pos: Int): Int? {
|
||||
val (range, prefixToSuffix) = injector.psiService.getCommentAtPos(editor, pos) ?: return null
|
||||
if (prefixToSuffix == null) return null
|
||||
|
||||
return if (pos < range.startOffset + prefixToSuffix.first.length) {
|
||||
range.endOffset - 1
|
||||
} else if (pos >= range.endOffset - prefixToSuffix.second.length) {
|
||||
range.startOffset
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This looks on the current line, starting at the cursor position for one of {, }, (, ), [, or ]. It then searches
|
||||
* forward or backward, as appropriate for the associated match pair. String in double quotes are skipped over.
|
||||
* Single characters in single quotes are skipped too.
|
||||
*
|
||||
* @param editor The editor to search in
|
||||
* @return The offset within the editor of the found character or -1 if no match was found or none of the characters
|
||||
* were found on the remainder of the current line.
|
||||
*/
|
||||
// TODO [vakhitov] it would be better to make this search PSI-aware and skip chars in strings and comments
|
||||
public fun findMatchingPairOnCurrentLine(editor: VimEditor, caret: ImmutableVimCaret): Int? {
|
||||
var pos = caret.offset.point
|
||||
|
||||
val commentPos = getCommentsOppositeBoundary(editor, pos)
|
||||
if (commentPos != null) {
|
||||
return commentPos
|
||||
}
|
||||
|
||||
val lineEnd = editor.getLineEndOffset(caret.getBufferPosition().line)
|
||||
|
||||
// To handle the case where visual mode allows the user to go past the end of the line
|
||||
if (pos > 0 && pos >= lineEnd) {
|
||||
pos = lineEnd - 1
|
||||
}
|
||||
|
||||
val chars = editor.text()
|
||||
val charPairs = parsMatchPairsOption(editor)
|
||||
|
||||
val pairChars = charPairs.keys + charPairs.values
|
||||
if (!pairChars.contains(chars[pos])) {
|
||||
pos = chars.indexOfAnyOrNull(pairChars.toCharArray(), pos, lineEnd, null) ?: return null
|
||||
}
|
||||
|
||||
val charToMatch = chars[pos]
|
||||
// TODO [vakhitov] should I implement BiMap for IdeaVim?
|
||||
val pairChar = charPairs[charToMatch] ?: charPairs.entries.first { it.value == charToMatch }.key
|
||||
val direction = if (charPairs.contains(charToMatch)) Direction.FORWARDS else Direction.BACKWARDS
|
||||
|
||||
return findMatchingChar(editor, pos, charToMatch, pairChar, direction)
|
||||
}
|
||||
|
||||
private fun parsMatchPairsOption(editor: VimEditor): Map<Char, Char> {
|
||||
return injector.options(editor).matchpairs
|
||||
.filter { it.length == 3 }
|
||||
.associate { it[0] to it[2] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Our implementation differs from the Vim one, but it is more consistent and uses the power of IDE.
|
||||
* We don't just count for opening and closing braces till their number will be equal, but keep context in mind.
|
||||
* If the first brace is inside string or comment, then the second one should be also in the same string or comment; otherwise there is no match.
|
||||
*/
|
||||
public fun findMatchingChar(editor: VimEditor, start: Int, charToMatch: Char, pairChar: Char, direction: Direction): Int? {
|
||||
// If we are inside string, we search for the pair inside the string only
|
||||
val stringRange = getStringAtPos(editor, start, true)
|
||||
if (stringRange != null && start in stringRange) {
|
||||
return findBlockLocation(editor, stringRange, start, charToMatch, pairChar, direction)
|
||||
}
|
||||
|
||||
val comment = injector.psiService.getCommentAtPos(editor, start)
|
||||
if (comment != null && start in comment.first) {
|
||||
val prefixToSuffix = comment.second
|
||||
return if (prefixToSuffix != null) {
|
||||
// If it is a block comment (has prefix & suffix), we search for the pair inside the block only
|
||||
findBlockLocation(editor, comment.first, start, charToMatch, pairChar, direction)
|
||||
} else {
|
||||
// If it is not a block comment, that there may be a sequence of single line comments, and we want to iterate over
|
||||
// all of them in an attempt to find a matching char
|
||||
val commentRange = getRangeOfNonBlockComments(editor, comment.first, direction)
|
||||
findBlockLocation(editor, commentRange, start, charToMatch, pairChar, direction)
|
||||
}
|
||||
}
|
||||
|
||||
return findBlockLocation(editor, start, charToMatch, pairChar, direction, 0)
|
||||
}
|
||||
|
||||
private fun getRangeOfNonBlockComments(editor: VimEditor, startComment: TextRange, direction: Direction): TextRange {
|
||||
var lastComment: TextRange = startComment
|
||||
|
||||
while (true) {
|
||||
val nextNonWhitespaceChar = if (direction == Direction.FORWARDS) {
|
||||
findNextNonWhitespaceChar(editor.text(), lastComment.endOffset)
|
||||
} else {
|
||||
findPreviousNonWhitespaceChar(editor.text(), lastComment.startOffset - 1)
|
||||
} ?: break
|
||||
|
||||
val nextComment = injector.psiService.getCommentAtPos(editor, nextNonWhitespaceChar)
|
||||
if (nextComment != null && nextComment.second == null) {
|
||||
lastComment = nextComment.first
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return if (direction == Direction.FORWARDS) {
|
||||
TextRange(startComment.startOffset, lastComment.endOffset)
|
||||
} else {
|
||||
TextRange(lastComment.startOffset, startComment.endOffset)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findNextNonWhitespaceChar(chars: CharSequence, startIndex: Int): Int? {
|
||||
for (i in startIndex .. chars.lastIndex) {
|
||||
if (!chars[i].isWhitespace()) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findPreviousNonWhitespaceChar(chars: CharSequence, startIndex: Int): Int? {
|
||||
for (i in startIndex downTo 0) {
|
||||
if (!chars[i].isWhitespace()) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @see `[{`, `]}` and similar Vim commands.
|
||||
* @return position of [count] unmatched [type]
|
||||
*/
|
||||
public fun findUnmatchedBlock(editor: VimEditor, pos: Int, type: Char, count: Int): Int? {
|
||||
val chars: CharSequence = editor.text()
|
||||
|
||||
val loc = BLOCK_CHARS.indexOf(type)
|
||||
val direction = if (loc % 2 == 0) Direction.BACKWARDS else Direction.FORWARDS
|
||||
|
||||
val charToMatch = BLOCK_CHARS[loc]
|
||||
val pairChar = BLOCK_CHARS[loc - direction.toInt()]
|
||||
|
||||
val start = if (pos < chars.length && (chars[pos] == type || chars[pos] == pairChar)) pos + direction.toInt() else pos
|
||||
return findBlockLocation(editor, start, charToMatch, pairChar, direction, count)
|
||||
}
|
||||
|
||||
private fun findBlockLocation(editor: VimEditor, range: TextRange, start: Int, charToMatch: Char, pairChar: Char, direction: Direction): Int? {
|
||||
val strictEscapeMatching = true // Vim's default behavior, see `help cpoptions`
|
||||
val chars = editor.text()
|
||||
|
||||
var depth = 0
|
||||
val escapedRestriction = if (strictEscapeMatching) isEscaped(chars, start) else null
|
||||
var i: Int? = start
|
||||
while (i != null && i in range) {
|
||||
val c = chars[i]
|
||||
when (c) {
|
||||
charToMatch -> depth++
|
||||
pairChar -> depth--
|
||||
}
|
||||
if (depth == 0) return i
|
||||
// TODO what should we do inside strings?
|
||||
i = chars.indexOfAnyOrNullInDirection(charArrayOf(charToMatch, pairChar), i + direction.toInt(), escapedRestriction, direction)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findBlockLocation(editor: VimEditor, start: Int, charToMatch: Char, pairChar: Char, direction: Direction, delta: Int): Int? {
|
||||
val strictEscapeMatching = true // Vim's default behavior, see `help cpoptions`
|
||||
val chars = editor.text()
|
||||
|
||||
var result: Int? = null
|
||||
|
||||
var depth = 0
|
||||
val escapedRestriction = if (strictEscapeMatching) isEscaped(chars, start) else null
|
||||
var i: Int? = start
|
||||
while (i != null) {
|
||||
val rangeToSkip = getStringAtPos(editor, i, false) ?: injector.psiService.getCommentAtPos(editor, i)?.first
|
||||
if (rangeToSkip != null) {
|
||||
val searchStart = if (direction == Direction.FORWARDS) rangeToSkip.endOffset else rangeToSkip.startOffset - 1
|
||||
i = chars.indexOfAnyOrNullInDirection(charArrayOf(charToMatch, pairChar), searchStart, escapedRestriction, direction)
|
||||
} else {
|
||||
when (chars[i]) {
|
||||
charToMatch -> {
|
||||
depth++
|
||||
if (delta > 0) result = i // For `]}` and similar commands we should return the result even if [delta] is unachievable
|
||||
}
|
||||
pairChar -> {
|
||||
depth--
|
||||
if (delta < 0) result = i // For `]}` and similar commands we should return the result even if [delta] is unachievable
|
||||
}
|
||||
}
|
||||
if (depth == delta) {
|
||||
result = i // For standard `%` motion, which should return [result] only if we succeeded to match
|
||||
break
|
||||
}
|
||||
i = chars.indexOfAnyOrNullInDirection(charArrayOf(charToMatch, pairChar), i + direction.toInt(), escapedRestriction, direction)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun CharSequence.indexOfAnyOrNullInDirection(chars: CharArray, startIndex: Int, escaped: Boolean?, direction: Direction): Int? {
|
||||
return if (direction == Direction.FORWARDS) {
|
||||
this.indexOfAnyOrNull(chars, startIndex, length, escaped)
|
||||
} else {
|
||||
this.lastIndexOfAnyOrNull(chars, startIndex, -1, escaped)
|
||||
}
|
||||
}
|
||||
|
||||
public fun getDoubleQuotesRangeNoPSI(chars: CharSequence, currentPos: Int, isInner: Boolean): TextRange? =
|
||||
getQuoteRangeNoPSI(chars, currentPos, isInner, false)
|
||||
|
||||
public fun getSingleQuotesRangeNoPSI(chars: CharSequence, currentPos: Int, isInner: Boolean): TextRange? =
|
||||
getQuoteRangeNoPSI(chars, currentPos, isInner, true)
|
||||
|
||||
private fun getQuoteRangeNoPSI(chars: CharSequence, currentPos: Int, isInner: Boolean, isSingleQuotes: Boolean): TextRange? {
|
||||
require(currentPos in 0 .. chars.lastIndex) // We can't use StrictMode here because I would like to test it without an injector initialized
|
||||
|
||||
val start = chars.lastIndexOf('\n', currentPos) + 1
|
||||
val changes = quotesChanges(chars, start).takeWhileInclusive { it.position <= currentPos }
|
||||
|
||||
val beforePos = changes.lastOrNull { it.position < currentPos }
|
||||
val atPos = changes.firstOrNull { it.position == currentPos }
|
||||
val afterPos = changes.firstOrNull { it.position > currentPos }
|
||||
|
||||
val inQuoteCheck: (State) -> Boolean = if (isSingleQuotes) State::isInSingleQuotes else State::isInDoubleQuotes
|
||||
|
||||
val openToClose: Pair<State, State> = if (beforePos != null && atPos != null && inQuoteCheck.invoke(beforePos)) {
|
||||
beforePos to atPos
|
||||
} else if (beforePos != null && afterPos != null && inQuoteCheck.invoke(beforePos)) {
|
||||
beforePos to afterPos
|
||||
} else if (atPos != null && afterPos != null && inQuoteCheck.invoke(atPos)) {
|
||||
atPos to afterPos
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
return if (isInner) {
|
||||
TextRange(openToClose.first.position + 1, openToClose.second.position)
|
||||
} else {
|
||||
TextRange(openToClose.first.position, openToClose.second.position + 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isEscaped(chars: CharSequence, pos: Int): Boolean {
|
||||
var backslashCounter = 0
|
||||
|
||||
var i = pos
|
||||
while (i-- > 0 && chars[i] == '\\') {
|
||||
backslashCounter++
|
||||
}
|
||||
return backslashCounter % 2 != 0
|
||||
}
|
||||
|
||||
private data class State(val position: Int, val isInSingleQuotes: Boolean, val isInDoubleQuotes: Boolean)
|
||||
|
||||
private fun quotesChanges(chars: CharSequence, startIndex: Int) = sequence {
|
||||
var isInDoubleQuotes = false
|
||||
var isInSingleQuotes = false
|
||||
|
||||
val eolIndex = chars.indexOfOrNull('\n', startIndex) ?: chars.length
|
||||
var nextQuoteIndex = chars.indexOfAnyOrNull(charArrayOf('"', '\''), startIndex, eolIndex, escaped = false)
|
||||
|
||||
while (nextQuoteIndex != null) {
|
||||
val quotes = chars[nextQuoteIndex]
|
||||
when (quotes) {
|
||||
'"' -> {
|
||||
if (!isInSingleQuotes) {
|
||||
isInDoubleQuotes = !isInDoubleQuotes
|
||||
yield(State(nextQuoteIndex, false, isInDoubleQuotes))
|
||||
}
|
||||
}
|
||||
'\'' -> {
|
||||
if (!isInDoubleQuotes) {
|
||||
isInSingleQuotes = !isInSingleQuotes
|
||||
yield(State(nextQuoteIndex, isInSingleQuotes, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
nextQuoteIndex = chars.indexOfAnyOrNull(charArrayOf('"', '\''), nextQuoteIndex + 1, eolIndex, escaped = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Sequence<T>.takeWhileInclusive(predicate: (T) -> Boolean) = sequence {
|
||||
with(iterator()) {
|
||||
while (hasNext()) {
|
||||
val next = next()
|
||||
yield(next)
|
||||
if (!predicate(next)) break
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2003-2024 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
|
||||
package com.maddyhome.idea.vim.group
|
||||
|
||||
/**
|
||||
* Searches for a single character and returns its index. Supports optional escaping.
|
||||
*
|
||||
* @param char The character to look for.
|
||||
* @param startIndex The index to start the search from (inclusive).
|
||||
* @param endIndex The index to end the search (exclusive).
|
||||
* @param escaped If true, the method returns the index of char only if it's escaped. If false, it returns the index only if it's not escaped. If null, escaping is not considered.
|
||||
* @return The index of the character, or null if it could not be located within the specified range.
|
||||
*/
|
||||
public fun CharSequence.indexOfOrNull(char: Char, startIndex: Int = 0, endIndex: Int = length, escaped: Boolean? = null): Int? {
|
||||
return indexOfAnyOrNull(charArrayOf(char), startIndex, endIndex, escaped)
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for any character from the specified array and returns its index. Supports optional escaping.
|
||||
*
|
||||
* @param chars An array of characters to search for.
|
||||
* @param startIndex The index to start the search from (inclusive).
|
||||
* @param endIndex The index to end the search (exclusive).
|
||||
* @param escaped If true, the method returns the index of char only if it's escaped. If false, it returns the index only if it's not escaped. If null, escaping is not considered.
|
||||
* @return The first index of any character from the array, or null if none could be found within the specified range.
|
||||
*/
|
||||
public fun CharSequence.indexOfAnyOrNull(chars: CharArray, startIndex: Int = 0, endIndex: Int = length, escaped: Boolean? = null): Int? {
|
||||
for (i in startIndex until kotlin.math.min(endIndex, length)) {
|
||||
if (chars.contains(get(i)) &&
|
||||
(
|
||||
escaped == null ||
|
||||
(escaped == true && isEscaped(this, i)) ||
|
||||
(escaped == false && !isEscaped(this, i))
|
||||
)
|
||||
) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
/**
|
||||
* Searches for any character from the specified array in reverse and returns its index. Supports optional escaping.
|
||||
*
|
||||
* @param chars An array of characters to search for.
|
||||
* @param startIndex The index to start the search from (in reverse). The default is the end of the string.
|
||||
* @param endIndex The index to finish the search, exclusive. If the index is -1, the last checked index will be 0. By default, it's the start of the string.
|
||||
* @param escaped If true, the method returns the index of char only if it's escaped. If false, it returns the index only if it's not escaped. If null, escaping is not considered.
|
||||
* @return The last index of any character from the array, or null if none could be found within the specified range.
|
||||
*/
|
||||
public fun CharSequence.lastIndexOfAnyOrNull(chars: CharArray, startIndex: Int = lastIndex, endIndex: Int = -1, escaped: Boolean? = null): Int? {
|
||||
for (i in startIndex downTo kotlin.math.max(endIndex + 1, 0)) {
|
||||
if (chars.contains(get(i)) &&
|
||||
(
|
||||
escaped == null ||
|
||||
(escaped == true && isEscaped(this, i)) ||
|
||||
(escaped == false && !isEscaped(this, i))
|
||||
)
|
||||
) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a character at the specified position is escaped (preceded by an odd number of backslashes).
|
||||
*
|
||||
* @param chars The character sequence.
|
||||
* @param pos The position of the character to check.
|
||||
* @return true if the character is escaped, and false otherwise.
|
||||
*/
|
||||
private fun isEscaped(chars: CharSequence, pos: Int): Boolean {
|
||||
var backslashCounter = 0
|
||||
|
||||
var i = pos
|
||||
while (i-- > 0 && chars[i] == '\\') {
|
||||
backslashCounter++
|
||||
}
|
||||
return backslashCounter % 2 != 0
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2003-2024 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
|
||||
package com.maddyhome.idea.vim.groups
|
||||
|
||||
import com.maddyhome.idea.vim.common.TextRange
|
||||
import com.maddyhome.idea.vim.group.getDoubleQuotesRangeNoPSI
|
||||
import com.maddyhome.idea.vim.group.getSingleQuotesRangeNoPSI
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class SearchGroupNoPSITests {
|
||||
@Test
|
||||
fun `test outer single quotes range for position outside string`() {
|
||||
val text = "let myVar = 'Oh, hi Mark'"
|
||||
assertNull(getSingleQuotesRangeNoPSI(text, 1, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test inner single quotes range for position outside string`() {
|
||||
val text = "let myVar = 'Oh, hi Mark'"
|
||||
assertNull(getSingleQuotesRangeNoPSI(text, 1, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test outer single quotes range for position inside string`() {
|
||||
val text = "let myVar = 'Oh, hi Mark'"
|
||||
assertEquals(TextRange(12, 25), getSingleQuotesRangeNoPSI(text, 15, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test inner single quotes range for position inside string`() {
|
||||
val text = "let myVar = 'Oh, hi Mark'"
|
||||
assertEquals(TextRange(13, 24), getSingleQuotesRangeNoPSI(text, 15, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test outer single quotes range for position at opening quote`() {
|
||||
val text = "let myVar = 'Oh, hi Mark'"
|
||||
assertEquals(TextRange(12, 25), getSingleQuotesRangeNoPSI(text, 12, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test inner single quotes range for position at opening quote`() {
|
||||
val text = "let myVar = 'Oh, hi Mark'"
|
||||
assertEquals(TextRange(13, 24), getSingleQuotesRangeNoPSI(text, 12, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test outer single quotes range for position at closing quote`() {
|
||||
val text = "let myVar = 'Oh, hi Mark'"
|
||||
assertEquals(TextRange(12, 25), getSingleQuotesRangeNoPSI(text, 24, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test inner single quotes range for position at closing quote`() {
|
||||
val text = "let myVar = 'Oh, hi Mark'"
|
||||
assertEquals(TextRange(13, 24), getSingleQuotesRangeNoPSI(text, 24, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test outer single quotes range for position after closing quote`() {
|
||||
val text = "let myVar = 'Oh, hi Mark' // comment"
|
||||
assertNull(getSingleQuotesRangeNoPSI(text, 25, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test inner single quotes range for position after closing quote`() {
|
||||
val text = "let myVar = 'Oh, hi Mark' // comment"
|
||||
assertNull(getSingleQuotesRangeNoPSI(text, 25, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test outer double quotes range for position outside string`() {
|
||||
val text = "let myVar = \"Oh, hi Mark\""
|
||||
assertNull(getDoubleQuotesRangeNoPSI(text, 1, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test inner double quotes range for position outside string`() {
|
||||
val text = "let myVar = \"Oh, hi Mark\""
|
||||
assertNull(getDoubleQuotesRangeNoPSI(text, 1, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test outer double quotes range for position inside string`() {
|
||||
val text = "let myVar = \"Oh, hi Mark\""
|
||||
assertEquals(TextRange(12, 25), getDoubleQuotesRangeNoPSI(text, 15, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test inner double quotes range for position inside string`() {
|
||||
val text = "let myVar = \"Oh, hi Mark\""
|
||||
assertEquals(TextRange(13, 24), getDoubleQuotesRangeNoPSI(text, 15, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test outer double quotes range for position at opening quote`() {
|
||||
val text = "let myVar = \"Oh, hi Mark\""
|
||||
assertEquals(TextRange(12, 25), getDoubleQuotesRangeNoPSI(text, 12, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test inner double quotes range for position at opening quote`() {
|
||||
val text = "let myVar = \"Oh, hi Mark\""
|
||||
assertEquals(TextRange(13, 24), getDoubleQuotesRangeNoPSI(text, 12, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test outer double quotes range for position at closing quote`() {
|
||||
val text = "let myVar = \"Oh, hi Mark\""
|
||||
assertEquals(TextRange(12, 25), getDoubleQuotesRangeNoPSI(text, 24, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test inner double quotes range for position at closing quote`() {
|
||||
val text = "let myVar = \"Oh, hi Mark\""
|
||||
assertEquals(TextRange(13, 24), getDoubleQuotesRangeNoPSI(text, 24, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test outer double quotes range for position after closing quote`() {
|
||||
val text = "let myVar = \"Oh, hi Mark\" // comment"
|
||||
assertNull(getDoubleQuotesRangeNoPSI(text, 25, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test inner double quotes range for position after closing quote`() {
|
||||
val text = "let myVar = \"Oh, hi Mark\" // comment"
|
||||
assertNull(getDoubleQuotesRangeNoPSI(text, 25, true))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user