diff --git a/ThirdPartyLicenses.md b/ThirdPartyLicenses.md index 112cd06cf..bf1784094 100644 --- a/ThirdPartyLicenses.md +++ b/ThirdPartyLicenses.md @@ -84,3 +84,8 @@ IV) It is not allowed to remove this license from the distribution of the Vim license for previous Vim releases instead of the license that they came with, at your option. ``` + +--- + +File [sneakIcon.png](doc/images/sneakIcon.svg), which is originally an icon of the ideavim-sneak plugin, +is merged icons of IdeaVim plugin and a random sneaker by FreePic from flaticon.com. diff --git a/doc/IdeaVim Plugins.md b/doc/IdeaVim Plugins.md index f2478e027..8e1d6b546 100644 --- a/doc/IdeaVim Plugins.md +++ b/doc/IdeaVim Plugins.md @@ -44,16 +44,21 @@ All commands with the mappings are supported. See the [full list of supported co <details> <summary><h2>sneak</h2></summary> - + +<img src="images/sneakIcon.svg" width="80" height="80" alt="icon"/> + +By [Mikhail Levchenko](https://github.com/Mishkun) +Original repository with the plugin: https://github.com/Mishkun/ideavim-sneak Original plugin: [vim-sneak](https://github.com/justinmk/vim-sneak). ### Setup: -- Install [IdeaVim-sneak](https://plugins.jetbrains.com/plugin/15348-ideavim-sneak) plugin. -- Add the following command to `~/.ideavimrc`: `set sneak` +- Add the following command to `~/.ideavimrc`: `Plug 'justinmk/vim-sneak'` ### Instructions - -See the [docs](https://github.com/Mishkun/ideavim-sneak#usage) + +* Type `s` and two chars to start sneaking in forward direction +* Type `S` and two chars to start sneaking in backward direction +* Type `;` or `,` to proceed with sneaking just as if you were using `f` or `t` commands </details> diff --git a/doc/images/sneakIcon.svg b/doc/images/sneakIcon.svg new file mode 100644 index 000000000..79dc30b65 --- /dev/null +++ b/doc/images/sneakIcon.svg @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg viewBox="386.498 234 32 32" xmlns="http://www.w3.org/2000/svg"> + <defs> + <linearGradient id="ideavim_plugin-a" x1="-6.748%" x2="47.286%" y1="33.61%" y2="85.907%"> + <stop offset="0" stop-color="#3BEA62"/> + <stop offset="1" stop-color="#087CFA"/> + </linearGradient> + </defs> + <g transform="matrix(1.238978, 0.90017, -0.90017, 1.238978, 131.776901, -422.953003)" style=""> + <path d="M 399.962 247.648 C 399.207 246.894 399.147 246.318 399.692 245.453 C 400.237 244.588 401.955 245.886 401.955 245.886 L 401.955 250.737" style="fill: rgb(248, 245, 231);"/> + <path d="M 413.846 253.602 C 413.846 255.077 411.827 256.134 409.392 256.134 L 396.381 256.134 C 395.211 256.134 394.232 256.003 393.433 255.817 C 391.496 255.367 390.613 254.596 390.613 254.596 L 391.478 252.513 L 393.607 252.617 Z M 413.846 253.602" fill="#cce6f6" style=""/> + <path d="M 413.846 253.602 C 413.846 253.602 411.475 254.468 408.27 254.468 C 405.94 254.468 398.116 253.433 394.023 252.869 C 392.488 252.658 391.478 252.512 391.478 252.512 C 390.864 251.662 392.078 248.741 392.758 247.263 C 393.118 246.484 393.85 245.929 394.703 245.83 C 394.782 245.82 394.861 245.815 394.939 245.815 C 395.369 245.815 395.645 246.532 396.059 247.225 C 396.446 247.877 396.955 248.507 397.823 248.507 C 399.617 248.507 401.955 245.886 401.955 245.886 C 406.544 249.03 410.097 250.43 410.097 250.43 C 410.097 250.43 413.061 250.446 413.782 251.167 C 414.503 251.888 414.422 253.218 413.846 253.602 Z M 413.846 253.602" style="fill: rgb(8, 124, 250);"/> + <path d="M 394.023 252.869 L 393.433 255.817 C 391.496 255.367 390.613 254.596 390.613 254.596 L 391.478 252.513 L 393.607 252.617 Z M 394.023 252.869" fill="#9dcae0" style=""/> + <path d="M 396.059 247.225 C 395.073 245.986 393.193 250.255 394.023 252.869 C 392.488 252.658 391.478 252.513 391.478 252.513 C 390.864 251.662 392.078 248.741 392.758 247.263 C 393.118 246.484 393.85 245.929 394.703 245.83 C 394.782 245.82 394.861 245.815 394.939 245.815 C 395.369 245.815 395.645 246.532 396.059 247.225 Z M 396.059 247.225" style="fill: rgb(14, 112, 142);"/> + <path d="M 403.527 246.924 L 399.174 251.768 C 397.7 250.892 395.578 250.174 394.016 249.717 C 392.843 249.372 391.985 249.174 391.959 249.168 C 392.108 248.769 392.268 248.379 392.421 248.022 L 394.341 248.65 L 394.884 248.827 C 395.509 249.031 396.192 248.95 396.753 248.608 L 397.153 248.363 C 397.35 248.454 397.572 248.507 397.823 248.507 C 399.617 248.507 401.955 245.886 401.955 245.886 C 402.494 246.256 403.02 246.601 403.527 246.924 Z M 403.527 246.924" style="fill: rgb(59, 234, 98);"/> + <path d="M 413.847 253.602 C 413.847 253.602 411.475 254.468 408.27 254.468 C 407.586 254.468 406.426 254.378 405.025 254.238 L 405.025 254.237 C 405.025 253.495 406.924 251.743 408.366 251.616 C 408.623 252.865 410.097 252.512 411.219 252.128 C 412.341 251.743 413.783 251.167 413.783 251.167 C 414.503 251.888 414.422 253.218 413.847 253.602 Z M 413.847 253.602" style="fill: rgb(8, 124, 250);"/> + <path d="M 394.341 248.65 C 394.214 248.978 394.103 249.339 394.016 249.717 C 392.843 249.372 391.985 249.174 391.959 249.168 C 392.108 248.769 392.268 248.379 392.421 248.022 Z M 394.341 248.65" style="fill: rgb(37, 187, 163);"/> + <path d="M 408.366 251.616 C 406.924 251.743 405.025 253.495 405.025 254.237 L 405.025 254.238 C 403.784 254.113 402.355 253.948 400.899 253.77 C 400.899 253.051 400.191 252.372 399.174 251.768 L 399.174 251.768 L 403.528 246.924 C 407.331 249.34 410.097 250.43 410.097 250.43 C 410.77 251.102 409.809 251.487 408.366 251.616 Z M 408.366 251.616" style="fill: rgb(248, 245, 231);"/> + <polygon fill="url(#ideavim_plugin-a)" fill-rule="evenodd" points="406.356 248.463 403.261 252.616 402.183 248.212 400.984 249.899 401.999 254.236 403.927 254.176 407.639 249.062" style="" transform="matrix(0.994522, 0.104529, -0.104529, 0.994522, 28.475005, -40.88594)"/> + <g fill="#fb6572" transform="matrix(0.046265, 0, 0, 0.046265, 390.612823, 245.155533)" style=""> + <path d="m288.839844 72.65625c-2.007813 0-4.011719-.761719-5.542969-2.292969-3.058594-3.0625-3.058594-8.023437 0-11.082031l20.089844-20.089844c3.0625-3.058594 8.023437-3.058594 11.082031 0 3.058594 3.0625 3.0625 8.023438 0 11.082032l-20.089844 20.089843c-1.527344 1.53125-3.535156 2.292969-5.539062 2.292969zm0 0" style="fill: rgb(56, 228, 105);"/> + <path d="m314.589844 87.082031c-2.007813 0-4.011719-.765625-5.542969-2.296875-3.0625-3.058594-3.0625-8.019531 0-11.082031l20.089844-20.085937c3.0625-3.058594 8.023437-3.058594 11.082031 0 3.058594 3.0625 3.058594 8.023437 0 11.082031l-20.089844 20.085937c-1.527344 1.53125-3.535156 2.296875-5.539062 2.296875zm0 0" style="fill: rgb(59, 233, 100);"/> + <path d="m340.339844 101.507812c-2.007813 0-4.011719-.765624-5.542969-2.296874-3.058594-3.058594-3.058594-8.023438 0-11.082032l20.089844-20.085937c3.0625-3.0625 8.023437-3.0625 11.082031 0 3.058594 3.058593 3.0625 8.023437 0 11.082031l-20.089844 20.085938c-1.527344 1.53125-3.535156 2.296874-5.539062 2.296874zm0 0" style="fill: rgb(59, 233, 100);"/> + <path d="m366.089844 115.929688c-2.003906 0-4.011719-.761719-5.539063-2.292969-3.0625-3.0625-3.0625-8.023438 0-11.082031l20.085938-20.089844c3.0625-3.058594 8.023437-3.058594 11.082031 0 3.0625 3.0625 3.0625 8.023437 0 11.082031l-20.085938 20.089844c-1.53125 1.53125-3.535156 2.292969-5.542968 2.292969zm0 0" style="fill: rgb(59, 233, 100);"/> + </g> + <path d="M 401.925 247.748 C 401.761 247.748 401.611 247.629 401.573 247.469 C 401.536 247.312 401.611 247.144 401.753 247.067 C 402 246.933 402.318 247.147 402.286 247.426 C 402.265 247.606 402.107 247.748 401.925 247.748 Z M 401.925 247.748" fill="#1e2628" style=""/> + </g> +</svg> \ No newline at end of file diff --git a/src/main/java/com/maddyhome/idea/vim/extension/VimExtensionRegistrar.kt b/src/main/java/com/maddyhome/idea/vim/extension/VimExtensionRegistrar.kt index 3b42bdc0d..445c6a36a 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/VimExtensionRegistrar.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/VimExtensionRegistrar.kt @@ -53,6 +53,11 @@ internal object VimExtensionRegistrar : VimExtensionRegistrator { @Synchronized private fun registerExtension(extensionBean: ExtensionBeanClass) { val name = extensionBean.name ?: extensionBean.instance.name + if (name == "sneak" && extensionBean.name == null) { + // Filter out the old ideavim-sneak extension that used to be a separate plugin + // https://github.com/Mishkun/ideavim-sneak + return + } if (name in registeredExtensions) return registeredExtensions.add(name) diff --git a/src/main/java/com/maddyhome/idea/vim/extension/sneak/IdeaVimSneakExtension.kt b/src/main/java/com/maddyhome/idea/vim/extension/sneak/IdeaVimSneakExtension.kt new file mode 100644 index 000000000..2bde8d17b --- /dev/null +++ b/src/main/java/com/maddyhome/idea/vim/extension/sneak/IdeaVimSneakExtension.kt @@ -0,0 +1,291 @@ +/* + * 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.extension.sneak + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.ScrollType +import com.intellij.openapi.editor.colors.EditorColors +import com.intellij.openapi.editor.markup.EffectType +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea +import com.intellij.openapi.editor.markup.RangeHighlighter +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.util.Disposer +import com.maddyhome.idea.vim.VimProjectService +import com.maddyhome.idea.vim.api.ExecutionContext +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.command.MappingMode +import com.maddyhome.idea.vim.command.OperatorArguments +import com.maddyhome.idea.vim.common.TextRange +import com.maddyhome.idea.vim.extension.ExtensionHandler +import com.maddyhome.idea.vim.extension.VimExtension +import com.maddyhome.idea.vim.extension.VimExtensionFacade +import com.maddyhome.idea.vim.extension.VimExtensionHandler +import com.maddyhome.idea.vim.helper.StrictMode +import com.maddyhome.idea.vim.newapi.ij +import java.awt.Font +import java.awt.event.KeyEvent +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + + +private const val DEFAULT_HIGHLIGHT_DURATION_SNEAK: Long = 300 + +// By [Mikhail Levchenko](https://github.com/Mishkun) +// Original repository with the plugin: https://github.com/Mishkun/ideavim-sneak +internal class IdeaVimSneakExtension : VimExtension { + override fun getName(): String = "sneak" + + override fun init() { + val highlightHandler = HighlightHandler() + mapToFunctionAndProvideKeys("s", SneakHandler(highlightHandler, Direction.FORWARD)) + mapToFunctionAndProvideKeys("S", SneakHandler(highlightHandler, Direction.BACKWARD)) + + // workaround to support ; and , commands + mapToFunctionAndProvideKeys("f", SneakMemoryHandler("f")) + mapToFunctionAndProvideKeys("F", SneakMemoryHandler("F")) + mapToFunctionAndProvideKeys("t", SneakMemoryHandler("t")) + mapToFunctionAndProvideKeys("T", SneakMemoryHandler("T")) + + mapToFunctionAndProvideKeys(";", SneakRepeatHandler(highlightHandler, RepeatDirection.IDENTICAL)) + mapToFunctionAndProvideKeys(",", SneakRepeatHandler(highlightHandler, RepeatDirection.REVERSE)) + } + + private class SneakHandler( + private val highlightHandler: HighlightHandler, + private val direction: Direction, + ) : ExtensionHandler { + override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { + val charone = getChar(editor) ?: return + val chartwo = getChar(editor) ?: return + val range = Util.jumpTo(editor, charone, chartwo, direction) + range?.let { highlightHandler.highlightSneakRange(editor.ij, range) } + Util.lastSymbols = "${charone}${chartwo}" + Util.lastSDirection = direction + } + + private fun getChar(editor: VimEditor): Char? { + val key = VimExtensionFacade.inputKeyStroke(editor.ij) + return when { + key.keyChar == KeyEvent.CHAR_UNDEFINED || key.keyCode == KeyEvent.VK_ESCAPE -> null + else -> key.keyChar + } + } + } + + /** + * This class acts as proxy for normal find commands because we need to update [Util.lastSDirection] + */ + private class SneakMemoryHandler(private val char: String) : VimExtensionHandler { + override fun execute(editor: Editor, context: DataContext) { + Util.lastSDirection = null + VimExtensionFacade.executeNormalWithoutMapping(injector.parser.parseKeys(char), editor) + } + } + + private class SneakRepeatHandler( + private val highlightHandler: HighlightHandler, + private val direction: RepeatDirection, + ) : ExtensionHandler { + override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { + val lastSDirection = Util.lastSDirection + if (lastSDirection != null) { + val (charone, chartwo) = Util.lastSymbols.toList() + val jumpRange = Util.jumpTo(editor, charone, chartwo, direction.map(lastSDirection)) + jumpRange?.let { highlightHandler.highlightSneakRange(editor.ij, jumpRange) } + } else { + VimExtensionFacade.executeNormalWithoutMapping(injector.parser.parseKeys(direction.symb), editor.ij) + } + } + } + + private object Util { + var lastSDirection: Direction? = null + var lastSymbols: String = "" + fun jumpTo(editor: VimEditor, charone: Char, chartwo: Char, sneakDirection: Direction): TextRange? { + val caret = editor.primaryCaret() + val position = caret.offset.point + val chars = editor.text() + val foundPosition = sneakDirection.findBiChar(editor, chars, position, charone, chartwo) + if (foundPosition != null) { + editor.primaryCaret().moveToOffset(foundPosition) + } + editor.ij.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE) + return foundPosition?.let { TextRange(foundPosition, foundPosition + 2) } + } + } + + private enum class Direction(val offset: Int) { + FORWARD(1) { + override fun findBiChar( + editor: VimEditor, + charSequence: CharSequence, + position: Int, + charone: Char, + chartwo: Char + ): Int? { + for (i in (position + offset) until charSequence.length - 1) { + if (matches(editor, charSequence, i, charone, chartwo)) { + return i + } + } + return null + } + }, + BACKWARD(-1) { + override fun findBiChar( + editor: VimEditor, + charSequence: CharSequence, + position: Int, + charone: Char, + chartwo: Char + ): Int? { + for (i in (position + offset) downTo 0) { + if (matches(editor, charSequence, i, charone, chartwo)) { + return i + } + } + return null + } + + }; + + abstract fun findBiChar( + editor: VimEditor, + charSequence: CharSequence, + position: Int, + charone: Char, + chartwo: Char, + ): Int? + + fun matches( + editor: VimEditor, + charSequence: CharSequence, + charPosition: Int, + charOne: Char, + charTwo: Char, + ): Boolean { + var match = charSequence[charPosition].equals(charOne, ignoreCase = injector.options(editor).ignorecase) && + charSequence[charPosition + 1].equals(charTwo, ignoreCase = injector.options(editor).ignorecase) + + if (injector.options(editor).ignorecase && injector.options(editor).smartcase) { + if (charOne.isUpperCase() || charTwo.isUpperCase()) { + match = charSequence[charPosition].equals(charOne, ignoreCase = false) && + charSequence[charPosition + 1].equals(charTwo, ignoreCase = false) + } + } + return match + } + } + + private enum class RepeatDirection(val symb: String) { + IDENTICAL(";") { + override fun map(direction: Direction): Direction = direction + }, + REVERSE(",") { + override fun map(direction: Direction): Direction = when (direction) { + Direction.FORWARD -> Direction.BACKWARD + Direction.BACKWARD -> Direction.FORWARD + } + }; + + abstract fun map(direction: Direction): Direction + } + + private class HighlightHandler { + private var editor: Editor? = null + private val sneakHighlighters: MutableSet<RangeHighlighter> = mutableSetOf() + + fun highlightSneakRange(editor: Editor, range: TextRange) { + clearAllSneakHighlighters() + + this.editor = editor + val project = editor.project + if (project != null) { + Disposer.register(VimProjectService.getInstance(project)) { + this.editor = null + sneakHighlighters.clear() + } + } + + if (range.isMultiple) { + for (i in 0 until range.size()) { + highlightSingleRange(editor, range.startOffsets[i]..range.endOffsets[i]) + } + } else { + highlightSingleRange(editor, range.startOffset..range.endOffset) + } + } + + fun clearAllSneakHighlighters() { + sneakHighlighters.forEach { highlighter -> + editor?.markupModel?.removeHighlighter(highlighter) ?: StrictMode.fail("Highlighters without an editor") + } + + sneakHighlighters.clear() + } + + private fun highlightSingleRange(editor: Editor, range: ClosedRange<Int>) { + val highlighter = editor.markupModel.addRangeHighlighter( + range.start, + range.endInclusive, + HighlighterLayer.SELECTION, + getHighlightTextAttributes(), + HighlighterTargetArea.EXACT_RANGE + ) + + sneakHighlighters.add(highlighter) + + setClearHighlightRangeTimer(highlighter) + } + + private fun setClearHighlightRangeTimer(highlighter: RangeHighlighter) { + Executors.newSingleThreadScheduledExecutor().schedule({ + ApplicationManager.getApplication().invokeLater { + editor?.markupModel?.removeHighlighter(highlighter) ?: StrictMode.fail("Highlighters without an editor") + } + }, DEFAULT_HIGHLIGHT_DURATION_SNEAK, TimeUnit.MILLISECONDS) + } + + private fun getHighlightTextAttributes() = TextAttributes( + null, + EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES.defaultAttributes.backgroundColor, + editor?.colorsScheme?.getColor(EditorColors.CARET_COLOR), + EffectType.SEARCH_MATCH, + Font.PLAIN + ) + } +} + +/** + * Map some <Plug>(keys) command to given handler + * and create mapping to <Plug>(prefix)[keys] + */ +private fun VimExtension.mapToFunctionAndProvideKeys(keys: String, handler: ExtensionHandler) { + VimExtensionFacade.putExtensionHandlerMapping( + MappingMode.NXO, + injector.parser.parseKeys(command(keys)), + owner, + handler, + false + ) + VimExtensionFacade.putKeyMapping( + MappingMode.NXO, + injector.parser.parseKeys(keys), + owner, + injector.parser.parseKeys(command(keys)), + true + ) +} + +private fun command(keys: String) = "<Plug>(sneak-$keys)" diff --git a/src/main/resources/META-INF/includes/VimExtensions.xml b/src/main/resources/META-INF/includes/VimExtensions.xml index 63d1e30d1..e2006e5a2 100644 --- a/src/main/resources/META-INF/includes/VimExtensions.xml +++ b/src/main/resources/META-INF/includes/VimExtensions.xml @@ -124,6 +124,14 @@ <alias name="chrisbra/matchit"/> </aliases> </vimExtension> + + <vimExtension implementation="com.maddyhome.idea.vim.extension.sneak.IdeaVimSneakExtension" name="sneak"> + <aliases> + <alias name="https://github.com/justinmk/vim-sneak"/> + <alias name="justinmk/vim-sneak"/> + <alias name="vim-sneak"/> + </aliases> + </vimExtension> </extensions> <!-- IdeaVim extensions--> diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt index a3d8381d2..3b81e5aa7 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt @@ -164,19 +164,20 @@ class SetCommandTest : VimTestCase() { assertCommandOutput("set all", """ |--- Options --- - |noargtextobj noideatracetime scroll=0 nosurround - | closenotebooks ideawrite=all scrolljump=1 notextobj-entire - |nocommentary noignorecase scrolloff=0 notextobj-indent - |nodigraph noincsearch selectmode= timeout - |noexchange nomatchit shellcmdflag=-x timeoutlen=1000 - |nogdefault maxmapdepth=20 shellxescape=@ notrackactionids - |nohighlightedyank more shellxquote={ undolevels=1000 - | history=50 nomultiple-cursors showcmd unifyjumps - |nohlsearch noNERDTree showmode virtualedit= - |noideaglobalmode nrformats=hex sidescroll=0 novisualbell - |noideajoin nonumber sidescrolloff=0 visualdelay=100 - | ideamarks octopushandler nosmartcase whichwrap=b,s - | ideastrictmode norelativenumber startofline wrapscan + |noargtextobj ideawrite=all scrolloff=0 notextobj-indent + | closenotebooks noignorecase selectmode= timeout + |nocommentary noincsearch shellcmdflag=-x timeoutlen=1000 + |nodigraph nomatchit shellxescape=@ notrackactionids + |noexchange maxmapdepth=20 shellxquote={ undolevels=1000 + |nogdefault more showcmd unifyjumps + |nohighlightedyank nomultiple-cursors showmode virtualedit= + | history=50 noNERDTree sidescroll=0 novisualbell + |nohlsearch nrformats=hex sidescrolloff=0 visualdelay=100 + |noideaglobalmode nonumber nosmartcase whichwrap=b,s + |noideajoin octopushandler nosneak wrapscan + | ideamarks norelativenumber startofline + | ideastrictmode scroll=0 nosurround + |noideatracetime scrolljump=1 notextobj-entire | clipboard=ideaput,autoselect,exclude:cons\|linux | excommandannotation | guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175 @@ -277,6 +278,7 @@ class SetCommandTest : VimTestCase() { | sidescroll=0 | sidescrolloff=0 |nosmartcase + |nosneak | startofline |nosurround |notextobj-entire diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt index 4bd1a3108..cf18809a5 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt @@ -350,19 +350,20 @@ class SetglobalCommandTest : VimTestCase() { setOsSpecificOptionsToSafeValues() assertCommandOutput("setglobal all", """ |--- Global option values --- - |noargtextobj noideatracetime scroll=0 nosurround - | closenotebooks ideawrite=all scrolljump=1 notextobj-entire - |nocommentary noignorecase scrolloff=0 notextobj-indent - |nodigraph noincsearch selectmode= timeout - |noexchange nomatchit shellcmdflag=-x timeoutlen=1000 - |nogdefault maxmapdepth=20 shellxescape=@ notrackactionids - |nohighlightedyank more shellxquote={ undolevels=1000 - | history=50 nomultiple-cursors showcmd unifyjumps - |nohlsearch noNERDTree showmode virtualedit= - |noideaglobalmode nrformats=hex sidescroll=0 novisualbell - |noideajoin nonumber sidescrolloff=0 visualdelay=100 - | ideamarks octopushandler nosmartcase whichwrap=b,s - | ideastrictmode norelativenumber startofline wrapscan + |noargtextobj ideawrite=all scrolloff=0 notextobj-indent + | closenotebooks noignorecase selectmode= timeout + |nocommentary noincsearch shellcmdflag=-x timeoutlen=1000 + |nodigraph nomatchit shellxescape=@ notrackactionids + |noexchange maxmapdepth=20 shellxquote={ undolevels=1000 + |nogdefault more showcmd unifyjumps + |nohighlightedyank nomultiple-cursors showmode virtualedit= + | history=50 noNERDTree sidescroll=0 novisualbell + |nohlsearch nrformats=hex sidescrolloff=0 visualdelay=100 + |noideaglobalmode nonumber nosmartcase whichwrap=b,s + |noideajoin octopushandler nosneak wrapscan + | ideamarks norelativenumber startofline + | ideastrictmode scroll=0 nosurround + |noideatracetime scrolljump=1 notextobj-entire | clipboard=ideaput,autoselect,exclude:cons\|linux | excommandannotation | guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175 @@ -459,6 +460,7 @@ class SetglobalCommandTest : VimTestCase() { | sidescroll=0 | sidescrolloff=0 |nosmartcase + |nosneak | startofline |nosurround |notextobj-entire diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt index 8beecd4d4..9a24456c2 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt @@ -382,19 +382,20 @@ class SetlocalCommandTest : VimTestCase() { setOsSpecificOptionsToSafeValues() assertCommandOutput("setlocal all", """ |--- Local option values --- - |noargtextobj ideastrictmode norelativenumber startofline - | closenotebooks noideatracetime scroll=0 nosurround - |nocommentary ideawrite=all scrolljump=1 notextobj-entire - |nodigraph noignorecase scrolloff=-1 notextobj-indent - |noexchange noincsearch selectmode= timeout - |nogdefault nomatchit shellcmdflag=-x timeoutlen=1000 - |nohighlightedyank maxmapdepth=20 shellxescape=@ notrackactionids - | history=50 more shellxquote={ unifyjumps - |nohlsearch nomultiple-cursors showcmd virtualedit= - |noideaglobalmode noNERDTree showmode novisualbell - |--ideajoin nrformats=hex sidescroll=0 visualdelay=100 - | ideamarks nonumber sidescrolloff=-1 whichwrap=b,s - | idearefactormode= octopushandler nosmartcase wrapscan + |noargtextobj noideatracetime scrolljump=1 notextobj-entire + | closenotebooks ideawrite=all scrolloff=-1 notextobj-indent + |nocommentary noignorecase selectmode= timeout + |nodigraph noincsearch shellcmdflag=-x timeoutlen=1000 + |noexchange nomatchit shellxescape=@ notrackactionids + |nogdefault maxmapdepth=20 shellxquote={ unifyjumps + |nohighlightedyank more showcmd virtualedit= + | history=50 nomultiple-cursors showmode novisualbell + |nohlsearch noNERDTree sidescroll=0 visualdelay=100 + |noideaglobalmode nrformats=hex sidescrolloff=-1 whichwrap=b,s + |--ideajoin nonumber nosmartcase wrapscan + | ideamarks octopushandler nosneak + | idearefactormode= norelativenumber startofline + | ideastrictmode scroll=0 nosurround | clipboard=ideaput,autoselect,exclude:cons\|linux | excommandannotation | guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175 @@ -497,6 +498,7 @@ class SetlocalCommandTest : VimTestCase() { | sidescroll=0 | sidescrolloff=-1 |nosmartcase + |nosneak | startofline |nosurround |notextobj-entire diff --git a/src/test/java/org/jetbrains/plugins/ideavim/extension/sneak/IdeaVimSneakTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/extension/sneak/IdeaVimSneakTest.kt new file mode 100644 index 000000000..947f9e66b --- /dev/null +++ b/src/test/java/org/jetbrains/plugins/ideavim/extension/sneak/IdeaVimSneakTest.kt @@ -0,0 +1,165 @@ +/* + * 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 org.jetbrains.plugins.ideavim.extension.sneak + +import com.maddyhome.idea.vim.state.mode.Mode +import org.jetbrains.plugins.ideavim.VimTestCase +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInfo + +class IdeaVimSneakTest : VimTestCase() { + @Throws(Exception::class) + override fun setUp(testInfo: TestInfo) { + super.setUp(testInfo) + enableExtensions("sneak") + } + + @Test + fun testSneakForward() { + val before = "som${c}e text" + val after = "some te${c}xt" + + doTest("sxt", before, after, Mode.NORMAL()) + } + + @Test + fun testSneakForwardVertical() { + val before = """som${c}e text + another line + third line""" + val after = """some text + another line + thi${c}rd line""" + + doTest("srd", before, after, Mode.NORMAL()) + } + + @Test + fun testSneakForwardDoNotIgnoreCase() { + val before = "som${c}e teXt" + val after = "som${c}e teXt" + + doTest("sxt", before, after, Mode.NORMAL()) + } + + @Test + fun testSneakForwardIgnoreCase() { + val before = "som${c}e teXt" + val after = "some te${c}Xt" + + enableExtensions("ignorecase") + + doTest("sxt", before, after, Mode.NORMAL()) + doTest("sXt", before, after, Mode.NORMAL()) + doTest("sXT", before, after, Mode.NORMAL()) + doTest("sxT", before, after, Mode.NORMAL()) + } + + @Test + fun testSneakForwardSmartIgnoreCase() { + val before = "som${c}e teXt" + val after = "some te${c}Xt" + + enableExtensions("ignorecase", "smartcase") + + doTest("sxt", before, after, Mode.NORMAL()) + doTest("sXt", before, after, Mode.NORMAL()) + doTest("sXT", before, before, Mode.NORMAL()) + doTest("sxT", before, before, Mode.NORMAL()) + } + + @Test + fun testSneakForwardAndFindAgain() { + val before = "som${c}e text text" + val after = "some text te${c}xt" + + doTest("sxt;", before, after, Mode.NORMAL()) + } + + @Test + fun testSneakForwardAndFindReverseAgain() { + val before = "some tex${c}t text" + val after = "some ${c}text text" + + doTest("ste,", before, after, Mode.NORMAL()) + } + + @Test + fun testSneakBackward() { + val before = "some tex${c}t" + val after = "so${c}me text" + + doTest("Sme", before, after, Mode.NORMAL()) + } + + @Test + fun testSneakBackwardVertical() { + val before = """some text + another line + thi${c}rd line""" + val after = """so${c}me text + another line + third line""" + + doTest("Sme", before, after, Mode.NORMAL()) + } + + @Test + fun testSneakBackwardAndFindAgain() { + // caret has to be before another character (space here) + val before = "some text text${c} " + val after = "some ${c}text text " + + doTest("Ste;", before, after, Mode.NORMAL()) + } + + @Test + fun testSneakBackwardAndFindReverseAgain() { + val before = "some tex${c}t text" + val after = "some text ${c}text" + + doTest("Ste,", before, after, Mode.NORMAL()) + } + + @Test + fun testEndOfFile() { + val before = """first line + some te${c}xt + another line + last line.""" + val after = """first line + some text + another line + last lin${c}e.""" + + doTest("se.", before, after, Mode.NORMAL()) + } + + @Test + fun testStartOfFile() { + val before = """first line + some text + another${c} line + last line.""" + val after = """${c}first line + some text + another line + last line.""" + + doTest("Sfi", before, after, Mode.NORMAL()) + } + + @Test + fun testEscapeFirstChar() { + val before = "so${c}me dwarf" + val after = "some ${c}dwarf" + + doTest("sa<ESC>sdw", before, after, Mode.NORMAL()) + } +}