1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2026-05-01 02:50:39 +02:00

Compare commits

..

31 Commits

Author SHA1 Message Date
7c4efe496d Set plugin version to chylex-54 2026-03-30 07:58:58 +02:00
19aa1f928c Fix pumvisible returning opposite result 2026-03-30 07:58:49 +02:00
b7d17bbb6e Preserve visual mode after executing IDE action 2026-03-30 07:58:49 +02:00
91546dd0d7 Make g0/g^/g$ work with soft wraps 2026-03-30 07:58:49 +02:00
89e1511860 Make gj/gk jump over soft wraps 2026-03-30 07:58:49 +02:00
507bbff1c3 Make camelCase motions adjust based on direction of visual selection 2026-03-30 07:58:49 +02:00
716956a30f Make search highlights temporary 2026-03-30 07:58:49 +02:00
dd33e39850 Do not switch to normal mode after inserting a live template 2026-03-30 07:58:49 +02:00
ebc77454ab Exit insert mode after refactoring 2026-03-29 19:45:22 +02:00
c9193cb6d4 Add action to run last macro in all opened files 2026-03-29 19:45:22 +02:00
13246c0a80 Stop macro execution after a failed search 2026-03-29 19:45:22 +02:00
b0ff57a4f5 Revert per-caret registers 2026-03-29 19:45:22 +02:00
f4e0684ca8 Apply scrolloff after executing native IDEA actions 2026-03-29 19:45:22 +02:00
3a3e7952b1 Automatically add unambiguous imports after running a macro 2026-03-29 19:45:22 +02:00
1ff6066e33 Fix(VIM-3986): Exception when pasting register contents containing new line 2026-03-29 19:45:21 +02:00
3a9abba410 Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2026-03-29 19:45:21 +02:00
510f8f948e Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2026-03-29 19:45:21 +02:00
b623bf739c Update search register when using f/t 2026-03-29 19:45:21 +02:00
c99d97b3bc Add support for count for visual and line motion surround 2026-03-29 19:45:21 +02:00
6b8eb8952f Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2026-03-29 19:45:21 +02:00
25d70ee975 Fix(VIM-696): Restore visual mode after undo/redo, and disable incompatible actions 2026-03-29 19:45:21 +02:00
cbc9637d17 Respect count with <Action> mappings 2026-03-29 19:45:21 +02:00
0d893d9961 Change matchit plugin to use HTML patterns in unrecognized files 2026-03-29 19:45:21 +02:00
4ac3a1eaaa Fix ex command panel causing Undock tool window to hide 2026-03-29 19:45:21 +02:00
86a6e9643f Reset insert mode when switching active editor 2026-03-29 19:45:21 +02:00
8b06078607 Remove notifications about configuration options 2026-03-29 19:45:20 +02:00
924455907a Remove AI 2026-03-29 19:45:20 +02:00
40367859b8 Set custom plugin version 2026-03-29 19:45:20 +02:00
45f7934d71 Revert "Fix(VIM-4108): Use default ANTLR output directory for Gradle 9+ compatibility"
This reverts commit a476583ea3.
2026-03-27 21:40:07 +01:00
0880e5f935 Revert "Upgrade Gradle wrapper to 9.2.1"
This reverts commit 517bda93
2026-03-27 21:40:07 +01:00
8af3788379 Revert "Fix(VIM-4109): Configure test source sets for Gradle 9+ compatibility"
This reverts commit 5c0d9569d9.
2026-03-27 21:40:07 +01:00
75 changed files with 265 additions and 1177 deletions

50
.github/workflows/runSplitModeTests.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Run Split Mode Tests
on:
workflow_dispatch:
push:
branches:
- master
jobs:
test-linux:
if: github.repository == 'JetBrains/ideavim'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Free up disk space
run: |
echo "Disk space before cleanup:"
df -h
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
echo "Disk space after cleanup:"
df -h
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1920x1080x24 &
echo "DISPLAY=:99" >> $GITHUB_ENV
- name: Run split mode tests
run: gradle :tests:split-mode-tests:testSplitMode --console=plain
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: split-mode-reports
path: |
tests/split-mode-tests/build/reports
out/ide-tests/tests/**/log
out/ide-tests/tests/**/frontend/log

1
.idea/gradle.xml generated
View File

@@ -33,6 +33,5 @@
</option>
</GradleProjectSettings>
</option>
<option name="parallelModelFetch" value="true" />
</component>
</project>

View File

@@ -1,16 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Split Frontend Debugger" type="Remote" folderName="Split Mode">
<module name="ideavim" />
<option name="USE_SOCKET_TRANSPORT" value="true" />
<option name="SERVER_MODE" value="false" />
<option name="SHMEM_ADDRESS" />
<option name="HOST" value="localhost" />
<option name="PORT" value="5006" />
<option name="AUTO_RESTART" value="false" />
<RunnerSettings RunnerId="Debug">
<option name="DEBUG_PORT" value="5006" />
<option name="LOCAL" value="false" />
</RunnerSettings>
<method v="2" />
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start CLion with IdeaVim" type="GradleRunConfiguration" factoryName="Gradle" folderName="Platforms">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runClion" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,25 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start CLion with IdeaVim (Split Mode)" type="GradleRunConfiguration" factoryName="Gradle" folderName="Platforms (Split)">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runCLionSplitMode" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start IJ with IdeaVim (Split Mode)" type="GradleRunConfiguration" factoryName="Gradle" folderName="Split Mode">
<configuration default="false" name="Start IJ with IdeaVim (Split Mode)" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />

View File

@@ -1,25 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start IJ with IdeaVim (Split Mode Debug Frontend)" type="GradleRunConfiguration" factoryName="Gradle" folderName="Split Mode">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runIdeSplitModeDebugFrontend" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start PyCharm with IdeaVim" type="GradleRunConfiguration" factoryName="Gradle" folderName="Platforms">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runPycharm" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,25 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start PyCharm with IdeaVim (Split Mode)" type="GradleRunConfiguration" factoryName="Gradle" folderName="Platforms (Split)">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runPycharmSplitMode" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start Rider with IdeaVim" type="GradleRunConfiguration" factoryName="Gradle" folderName="Platforms">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runRider" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start WebStorm with IdeaVim" type="GradleRunConfiguration" factoryName="Gradle" folderName="Platforms">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runWebstorm" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -1,25 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start WebStorm with IdeaVim (Split Mode)" type="GradleRunConfiguration" factoryName="Gradle" folderName="Platforms (Split)">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runWebstormSplitMode" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -1,11 +1,3 @@
/*
* Copyright 2003-2026 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 _Self
import _Self.buildTypes.Compatibility
@@ -14,7 +6,6 @@ import _Self.buildTypes.Nvim
import _Self.buildTypes.PluginVerifier
import _Self.buildTypes.PropertyBased
import _Self.buildTypes.RandomOrderTests
import _Self.buildTypes.SplitModeTests
import _Self.buildTypes.TestingBuildType
import _Self.buildTypes.TypeScriptTest
@@ -39,7 +30,6 @@ object Project : Project({
buildType(PropertyBased)
buildType(LongRunning)
buildType(RandomOrderTests)
buildType(SplitModeTests)
buildType(Nvim)
buildType(PluginVerifier)

View File

@@ -1,11 +1,3 @@
/*
* Copyright 2003-2026 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 _Self.buildTypes
import _Self.AgentSize
@@ -19,10 +11,6 @@ object Compatibility : IdeaVimBuildType({
id("IdeaVimCompatibility")
name = "IdeaVim compatibility with external plugins"
failureConditions {
executionTimeoutMin = 180
}
vcs {
root(DslContext.settingsRoot)
branchFilter = "+:<default>"

View File

@@ -1,11 +1,3 @@
/*
* Copyright 2003-2026 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 _Self.buildTypes
import _Self.AgentSize
@@ -34,7 +26,7 @@ object RandomOrderTests : IdeaVimBuildType({
gradle {
clearConditions()
tasks = """
clean test
test
-x :tests:property-tests:test
-x :tests:long-running-tests:test
-Djunit.jupiter.execution.order.random.seed=default
@@ -42,7 +34,7 @@ object RandomOrderTests : IdeaVimBuildType({
""".trimIndent().replace("\n", " ")
buildFile = ""
enableStacktrace = true
gradleParams = "--no-build-cache --configuration-cache"
gradleParams = "--build-cache --configuration-cache"
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2003-2026 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 _Self.buildTypes
import _Self.AgentSize
import _Self.IdeaVimBuildType
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script
import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs
object SplitModeTests : IdeaVimBuildType({
name = "Split mode tests"
description = "Tests for IdeaVim in Remote Development split mode (backend + frontend)"
artifactRules = """
+:tests/split-mode-tests/build/reports => split-mode-tests/build/reports
+:out/ide-tests/tests/**/log => out/ide-tests/log
+:out/ide-tests/tests/**/frontend/log => out/ide-tests/frontend-log
""".trimIndent()
params {
param("env.ORG_GRADLE_PROJECT_downloadIdeaSources", "false")
param("env.ORG_GRADLE_PROJECT_instrumentPluginCode", "false")
param("env.DISPLAY", ":99")
}
vcs {
root(DslContext.settingsRoot)
branchFilter = "+:<default>"
checkoutMode = CheckoutMode.AUTO
}
steps {
script {
name = "Start Xvfb and run split mode tests"
scriptContent = """
Xvfb :99 -screen 0 1920x1080x24 &
sleep 2
./gradlew :tests:split-mode-tests:testSplitMode --console=plain --build-cache --configuration-cache --stacktrace
""".trimIndent()
}
}
triggers {
vcs {
branchFilter = "+:<default>"
}
}
requirements {
// Use a larger agent for split-mode tests — they launch two full IDE instances
equals("teamcity.agent.hardware.cpuCount", AgentSize.XLARGE)
equals("teamcity.agent.os.family", "Linux")
}
})

View File

@@ -1,11 +1,3 @@
/*
* Copyright 2003-2026 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.
*/
@file:Suppress("ClassName")
package _Self.buildTypes
@@ -49,10 +41,10 @@ open class TestingBuildType(
steps {
gradle {
clearConditions()
tasks = "clean test -x :tests:property-tests:test -x :tests:long-running-tests:test"
tasks = "test -x :tests:property-tests:test -x :tests:long-running-tests:test"
buildFile = ""
enableStacktrace = true
gradleParams = "--no-build-cache --configuration-cache"
gradleParams = "--build-cache --configuration-cache"
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"
}
}

View File

@@ -542,10 +542,6 @@ Contributors:
[![icon][github]](https://github.com/1grzyb1)
&nbsp;
1grzyb1
* [![icon][mail]](mailto:yury@digitalby.me)
[![icon][github]](https://github.com/digitalby)
&nbsp;
digitalby
Contributors with JetBrains IP:

View File

@@ -36,37 +36,14 @@ usual beta standards.
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `:set foldlevel` option - control fold visibility level
### Fixes:
* [VIM-4135](https://youtrack.jetbrains.com/issue/VIM-4135) Fixed IdeaVim not loading in Rider
* [VIM-4134](https://youtrack.jetbrains.com/issue/VIM-4134) Fixed undo in commentary - `gcc`/`gc{motion}` changes are now properly grouped as a single undo step
* [VIM-4134](https://youtrack.jetbrains.com/issue/VIM-4134) Fixed `=` (format/auto-indent) action in split mode
* [VIM-4134](https://youtrack.jetbrains.com/issue/VIM-4134) Fixed global marks causing errors when used inside write actions (e.g., during document modifications)
* [VIM-4105](https://youtrack.jetbrains.com/issue/VIM-4105) Fixed `a"` `a'` `a\`` text objects to include surrounding whitespace per Vim spec
* [VIM-4097](https://youtrack.jetbrains.com/issue/VIM-4097) Fixed `<A-n>` (NextOccurrence) with text containing backslashes - e.g., selecting `\IntegerField` now works correctly
* [VIM-4094](https://youtrack.jetbrains.com/issue/VIM-4094) Fixed UninitializedPropertyAccessException when loading history
* [VIM-4016](https://youtrack.jetbrains.com/issue/VIM-4016) Fixed `:edit` command when project has no source roots
* [VIM-3948](https://youtrack.jetbrains.com/issue/VIM-3948) Improved hint generation visibility checks for better UI component detection
* [VIM-3473](https://youtrack.jetbrains.com/issue/VIM-3473) Fixed "Reload .ideavimrc" action in remote development (split) mode - no longer causes File Cache Conflict dialogs
* [VIM-2821](https://youtrack.jetbrains.com/issue/VIM-2821) Fixed undo grouping when repeating text insertion with `.` in remote development (split mode)
* [VIM-1705](https://youtrack.jetbrains.com/issue/VIM-1705) Fixed window-switching commands (e.g., `<C-w>h`) during macro playback
* Fixed `pumvisible()` function returning incorrect result (was inverted)
* Fixed `<Esc>` not properly exiting insert mode in Rider/CLion when canceling a completion lookup
* Fixed `<Esc>` not exiting insert mode after `<C-Space>` completion in Rider
* Fixed `<Esc>` in search bar no longer inserts `^[` literal text when search is not found - panel is now properly closed
* Fixed IdeaVim entering broken state when a VimScript extension plugin fails to initialize
* Fixed compatibility issues with external plugins (e.g., IdeaVim-EasyMotion, multicursor)
* Fixed recursive key mappings (e.g., `map b wbb`) causing an apparent infinite loop - `maxmapdepth` limit now properly terminates the entire mapping chain
* Fixed NERDTree `gs`/`gi` preview split commands to keep focus on the tree
* Fixed visual marks (`<` and `>`) position tracking after text deletion - `gv` now re-selects correctly
* Fixed `IndexOutOfBoundsException` when using text objects like `a)` at end of file
* Fixed high CPU usage while showing command line
* Fixed comparison of String and Number in VimScript expressions
### Merged PRs:
* [1632](https://github.com/JetBrains/ideavim/pull/1632) by [chylex](https://github.com/chylex): Fix pumvisible returning opposite result
* [1615](https://github.com/JetBrains/ideavim/pull/1615) by [1grzyb1](https://github.com/1grzyb1): Fix IndexOutOfBoundsException in findBlock when caret is at end of file
* [1613](https://github.com/JetBrains/ideavim/pull/1613) by [1grzyb1](https://github.com/1grzyb1): VIM-3473 Sync ideavim in remdev
* [1608](https://github.com/JetBrains/ideavim/pull/1608) by [1grzyb1](https://github.com/1grzyb1): VIM-4134 format using = action in split mode
* [1585](https://github.com/JetBrains/ideavim/pull/1585) by [1grzyb1](https://github.com/1grzyb1): Break in case of maximum recursion depth
* [1414](https://github.com/JetBrains/ideavim/pull/1414) by [Matt Ellis](https://github.com/citizenmatt): Refactor/functions
* [1442](https://github.com/JetBrains/ideavim/pull/1442) by [Matt Ellis](https://github.com/citizenmatt): Fix high CPU usage while showing command line

View File

@@ -9,7 +9,6 @@
package com.intellij.vim.api.scopes
import com.intellij.vim.api.VimApi
import com.intellij.vim.api.models.CaretId
/**
* Represents the range of a text object selection.
@@ -110,19 +109,10 @@ interface TextObjectScope {
* or null if no valid range is found at the current position.
* The function receives the count (e.g., `2iw` passes count=2).
*/
fun register(
keys: String,
registerDefaultMapping: Boolean = true,
preserveSelectionAnchor: Boolean = true,
rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
)
fun register(
keys: String,
registerDefaultMapping: Boolean = true,
preserveSelectionAnchor: Boolean = true,
rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
) {
register(keys, registerDefaultMapping, preserveSelectionAnchor) { _, count -> rangeProvider(count) }
}
)
}

View File

@@ -6,7 +6,6 @@
* https://opensource.org/licenses/MIT.
*/
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
@@ -27,11 +26,11 @@ buildscript {
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.6.0.202603022253-r")
classpath("org.kohsuke:github-api:1.305")
classpath("io.ktor:ktor-client-core:3.4.2")
classpath("io.ktor:ktor-client-cio:3.4.2")
classpath("io.ktor:ktor-client-auth:3.4.2")
classpath("io.ktor:ktor-client-content-negotiation:3.4.2")
classpath("io.ktor:ktor-serialization-kotlinx-json:3.4.2")
classpath("io.ktor:ktor-client-core:3.4.1")
classpath("io.ktor:ktor-client-cio:3.4.1")
classpath("io.ktor:ktor-client-auth:3.4.1")
classpath("io.ktor:ktor-client-content-negotiation:3.4.1")
classpath("io.ktor:ktor-serialization-kotlinx-json:3.4.1")
// This comes from the changelog plugin
// classpath("org.jetbrains:markdown:0.3.1")
@@ -113,7 +112,7 @@ dependencies {
testFramework(TestFrameworkType.Platform)
testFramework(TestFrameworkType.JUnit5)
compatiblePlugin("com.intellij.classic.ui")
plugin("com.intellij.classic.ui", "261.22158.185")
pluginModule(runtimeOnly(project(":modules:ideavim-common")))
pluginModule(runtimeOnly(project(":modules:ideavim-frontend")))
@@ -226,30 +225,6 @@ tasks {
// localPath = file("/Users/{user}/Applications/WebStorm.app")
// }
val runPycharm by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.PyCharmProfessional
version = "2025.3.2"
task {
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
}
}
val runWebstorm by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.WebStorm
version = "2025.3.2"
task {
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
}
}
val runClion by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.CLion
version = "2025.3.2"
task {
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
}
}
val runIdeForUiTests by intellijPlatformTesting.runIde.registering {
task {
jvmArgumentProviders += CommandLineArgumentProvider {
@@ -272,55 +247,6 @@ tasks {
val runIdeSplitMode by intellijPlatformTesting.runIde.registering {
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
}
val runWebstormSplitMode by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.WebStorm
version = "2025.3.2"
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
}
val runRider by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.Rider
version = "2026.1"
task {
systemProperty("idea.log.debug.categories", "com.maddyhome.idea.vim.handler.EditorHandlersChainLogger")
}
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
}
val runCLionSplitMode by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.CLion
version = "2025.3.2"
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
}
val runPycharmSplitMode by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.PyCharmProfessional
version = "2025.3.2"
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
}
// Run split mode with a JDWP debug agent on the frontend (JetBrains Client) process.
@@ -329,11 +255,6 @@ tasks {
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
prepareSandboxTask {
val sandboxDir = project.layout.buildDirectory.dir("idea-sandbox").map { it.asFile }
doLast {
@@ -365,12 +286,6 @@ tasks {
val testIdeSplitMode by intellijPlatformTesting.testIde.registering {
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
task {
useJUnitPlatform()
}
@@ -444,37 +359,14 @@ intellijPlatform {
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>:set foldlevel</code> option - control fold visibility level<br>
<br>
<b>Fixes:</b><br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-4135">VIM-4135</a> Fixed IdeaVim not loading in Rider<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-4134">VIM-4134</a> Fixed undo in commentary - <code>gcc</code>/<code>gc{motion}</code> changes are now properly grouped as a single undo step<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-4134">VIM-4134</a> Fixed <code>=</code> (format/auto-indent) action in split mode<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-4134">VIM-4134</a> Fixed global marks causing errors when used inside write actions (e.g., during document modifications)<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-4105">VIM-4105</a> Fixed <code>a"</code> <code>a'</code> <code>a`</code> text objects to include surrounding whitespace per Vim spec<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-4097">VIM-4097</a> Fixed <code>&lt;A-n&gt;</code> (NextOccurrence) with text containing backslashes - e.g., selecting <code>\IntegerField</code> now works correctly<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-4094">VIM-4094</a> Fixed UninitializedPropertyAccessException when loading history<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-4016">VIM-4016</a> Fixed <code>:edit</code> command when project has no source roots<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-3948">VIM-3948</a> Improved hint generation visibility checks for better UI component detection<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-3473">VIM-3473</a> Fixed "Reload .ideavimrc" action in remote development (split) mode - no longer causes File Cache Conflict dialogs<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-2821">VIM-2821</a> Fixed undo grouping when repeating text insertion with <code>.</code> in remote development (split mode)<br>
* <a href="https://youtrack.jetbrains.com/issue/VIM-1705">VIM-1705</a> Fixed window-switching commands (e.g., <code>&lt;C-w&gt;h</code>) during macro playback<br>
* Fixed <code>pumvisible()</code> function returning incorrect result (was inverted)<br>
* Fixed <code>&lt;Esc&gt;</code> not properly exiting insert mode in Rider/CLion when canceling a completion lookup<br>
* Fixed <code>&lt;Esc&gt;</code> not exiting insert mode after <code>&lt;C-Space&gt;</code> completion in Rider<br>
* Fixed <code>&lt;Esc&gt;</code> in search bar no longer inserts <code>^[</code> literal text when search is not found - panel is now properly closed<br>
* Fixed IdeaVim entering broken state when a VimScript extension plugin fails to initialize<br>
* Fixed compatibility issues with external plugins (e.g., IdeaVim-EasyMotion, multicursor)<br>
* Fixed recursive key mappings (e.g., <code>map b wbb</code>) causing an apparent infinite loop - <code>maxmapdepth</code> limit now properly terminates the entire mapping chain<br>
* Fixed NERDTree <code>gs</code>/<code>gi</code> preview split commands to keep focus on the tree<br>
* Fixed visual marks (<code>&lt;</code> and <code>&gt;</code>) position tracking after text deletion - <code>gv</code> now re-selects correctly<br>
* Fixed <code>IndexOutOfBoundsException</code> when using text objects like <code>a)</code> at end of file<br>
* Fixed high CPU usage while showing command line<br>
* Fixed comparison of String and Number in VimScript expressions<br>
<br>
<b>Merged PRs:</b><br>
* <a href="https://github.com/JetBrains/ideavim/pull/1632">1632</a> by <a href="https://github.com/chylex">chylex</a>: Fix pumvisible returning opposite result<br>
* <a href="https://github.com/JetBrains/ideavim/pull/1615">1615</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Fix IndexOutOfBoundsException in findBlock when caret is at end of file<br>
* <a href="https://github.com/JetBrains/ideavim/pull/1613">1613</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-3473 Sync ideavim in remdev<br>
* <a href="https://github.com/JetBrains/ideavim/pull/1608">1608</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4134 format using = action in split mode<br>
* <a href="https://github.com/JetBrains/ideavim/pull/1585">1585</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Break in case of maximum recursion depth<br>
* <a href="https://github.com/JetBrains/ideavim/pull/1414">1414</a> by <a href="https://github.com/citizenmatt">Matt Ellis</a>: Refactor/functions<br>
* <a href="https://github.com/JetBrains/ideavim/pull/1442">1442</a> by <a href="https://github.com/citizenmatt">Matt Ellis</a>: Fix high CPU usage while showing command line<br>
<br>

View File

@@ -20,7 +20,7 @@ ideaVersion=2026.1
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
ideaType=IU
instrumentPluginCode=true
version=chylex-57
version=chylex-54
javaVersion=21
remoteRobotVersion=0.11.23
antlrVersion=4.10.1

2
gradlew vendored
View File

@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.

View File

@@ -165,27 +165,25 @@ internal class FileRemoteApiImpl : FileRemoteApi {
// ======================== Private helpers ========================
private fun findFile(filename: String, project: Project): VirtualFile? {
var found: VirtualFile?
if (filename.startsWith("~/") || filename.startsWith("~\\")) {
val relativePath = filename.substring(2)
val dir = System.getProperty("user.home")
logger.debug { "home dir file" }
logger.debug { "looking for $relativePath in $dir" }
return LocalFileSystem.getInstance().refreshAndFindFileByNioFile(Path(dir, relativePath))
found = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(Path(dir, relativePath))
} else {
found = VirtualFileManager.getInstance().findFileByNioPath(Path(filename))
if (found == null) {
found = findByNameInContentRoots(filename, project)
if (found == null) {
found = findByNameInProject(filename, project)
}
}
}
val basePath = project.basePath
if (basePath != null) {
val baseDir = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(Path(basePath))
baseDir?.findFileByRelativePath(filename)?.let { return it }
}
VirtualFileManager.getInstance().findFileByNioPath(Path(filename))?.let { return it }
findByNameInContentRoots(filename, project)?.let { return it }
findByNameInProject(filename, project)?.let { return it }
return null
return found
}
private fun buildFileInfoMessage(editor: Editor, project: Project, fullPath: Boolean): String {

View File

@@ -8,7 +8,7 @@
<idea-plugin>
<dependencies>
<plugin id="com.intellij.modules.rider"/>
<module name="com.intellij.modules.rider"/>
</dependencies>
<projectListeners>
<listener class="com.maddyhome.idea.vim.listener.RiderActionListener"

View File

@@ -26,11 +26,11 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("io.ktor:ktor-client-core:3.4.2")
implementation("io.ktor:ktor-client-cio:3.4.2")
implementation("io.ktor:ktor-client-content-negotiation:3.4.2")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.2")
implementation("io.ktor:ktor-client-auth:3.4.2")
implementation("io.ktor:ktor-client-core:3.4.1")
implementation("io.ktor:ktor-client-cio:3.4.1")
implementation("io.ktor:ktor-client-content-negotiation:3.4.1")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.1")
implementation("io.ktor:ktor-client-auth:3.4.1")
implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
// This is needed for jgit to connect to ssh

View File

@@ -22,8 +22,6 @@ import com.intellij.openapi.util.Disposer;
import com.maddyhome.idea.vim.api.*;
import com.maddyhome.idea.vim.config.VimState;
import com.maddyhome.idea.vim.config.migration.ApplicationConfigurationMigrator;
import com.maddyhome.idea.vim.group.ChangeGroup;
import com.maddyhome.idea.vim.group.KeyGroup;
import com.maddyhome.idea.vim.group.VimNotifications;
import com.maddyhome.idea.vim.group.VimWindowGroup;
import com.maddyhome.idea.vim.history.VimHistory;
@@ -91,8 +89,8 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
return VimInjectorKt.getInjector().getMotion();
}
public static @NotNull ChangeGroup getChange() {
return ((ChangeGroup)VimInjectorKt.getInjector().getChangeGroup());
public static @NotNull VimChangeGroup getChange() {
return VimInjectorKt.getInjector().getChangeGroup();
}
public static @NotNull VimCommandGroup getCommand() {
@@ -132,12 +130,12 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
return VimInjectorKt.getInjector().getHistoryGroup();
}
public static @NotNull KeyGroup getKey() {
return ((KeyGroup)VimInjectorKt.getInjector().getKeyGroup());
public static @NotNull VimKeyGroup getKey() {
return VimInjectorKt.getInjector().getKeyGroup();
}
public static @Nullable KeyGroup getKeyIfCreated() {
return ApplicationManager.getApplication().getServiceIfCreated(KeyGroup.class);
public static @Nullable VimKeyGroup getKeyIfCreated() {
return ApplicationManager.getApplication().getServiceIfCreated(VimKeyGroup.class);
}
public static @NotNull VimWindowGroup getWindow() {
@@ -339,7 +337,7 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
}
}
if (element.getChild("shortcut-conflicts") != null) {
getKey().loadShortcutConflictsData(element);
((VimKeyGroupBase)getKey()).loadShortcutConflictsData(element);
}
if (element.getChild("editor") != null) {
getEditor().loadEditorStateData(element);

View File

@@ -23,7 +23,6 @@ import com.intellij.openapi.editor.impl.EditorComponentImpl
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.KeyStrokeAdapter
import com.maddyhome.idea.vim.KeyHandler
@@ -227,9 +226,8 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent)
val strokeCache = keyStrokeCache
if (defaultKeyStroke != null) {
val fixedKeyStroke = fixKeyStroke(defaultKeyStroke)
keyStrokeCache = inputEvent.`when` to fixedKeyStroke
return fixedKeyStroke
keyStrokeCache = inputEvent.`when` to defaultKeyStroke
return defaultKeyStroke
} else if (strokeCache.first == inputEvent.`when`) {
keyStrokeCache = null to null
return strokeCache.second
@@ -239,19 +237,6 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
return null
}
private fun fixKeyStroke(key: KeyStroke): KeyStroke {
return if (
key.modifiers and CTRL_ALT_MASK != 0 &&
key.isOnKeyRelease &&
SystemInfoRt.isWindows &&
Registry.`is`("actionSystem.fix.alt.gr", true)
) {
KeyStroke.getKeyStroke(key.keyCode, key.modifiers)
} else {
key
}
}
private fun getEditor(e: AnActionEvent): Editor? {
return e.getData(PlatformDataKeys.EDITOR)
?: if (e.getData(PlatformDataKeys.CONTEXT_COMPONENT) is ExTextField) {
@@ -332,7 +317,6 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
).build()
private const val ACTION_ID = "VimShortcutKeyAction"
private const val CTRL_ALT_MASK = InputEvent.CTRL_DOWN_MASK or InputEvent.ALT_DOWN_MASK
private val LOG = logger<VimShortcutKeyAction>()

View File

@@ -11,7 +11,6 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.extensions.ExtensionPointListener
import com.intellij.openapi.extensions.PluginDescriptor
import com.intellij.vim.api.VimInitApi
import com.maddyhome.idea.vim.api.VimExtensionRegistrator
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.setToggleOption
@@ -21,6 +20,7 @@ import com.maddyhome.idea.vim.key.MappingOwner.Plugin.Companion.remove
import com.maddyhome.idea.vim.options.OptionAccessScope
import com.maddyhome.idea.vim.options.OptionDeclaredScope
import com.maddyhome.idea.vim.options.ToggleOption
import com.intellij.vim.api.VimInitApi
import com.maddyhome.idea.vim.statistic.ExtensionTracking
import com.maddyhome.idea.vim.thinapi.VimApiImpl
@@ -106,13 +106,9 @@ class VimExtensionRegistrar : VimExtensionRegistrator {
override fun enableDelayedExtensions() {
delayedExtensionEnabling.forEach {
val name = it.name ?: it.instance.name
try {
val initApi = createVimApi(name)
it.instance.init(initApi)
logger.info("IdeaVim extension '$name' initialized")
} catch (e: Throwable) {
logger.error("Failed to initialize IdeaVim extension '$name'", e)
}
val initApi = createVimApi(name)
it.instance.init(initApi)
logger.info("IdeaVim extension '$name' initialized")
}
delayedExtensionEnabling.clear()
}

View File

@@ -9,7 +9,6 @@ package com.maddyhome.idea.vim.extension.argtextobj
import com.intellij.vim.api.VimApi
import com.intellij.vim.api.VimInitApi
import com.intellij.vim.api.models.CaretId
import com.intellij.vim.api.scopes.TextObjectRange
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.extension.VimExtension
@@ -122,11 +121,11 @@ class VimArgTextObjExtension : VimExtension {
override fun init(initApi: VimInitApi) {
initApi.textObjects {
register("ia", preserveSelectionAnchor = false) { caret, count ->
findArgumentRange(isInner = true, caret, count)
register("ia", preserveSelectionAnchor = false) { count ->
findArgumentRange(isInner = true, count)
}
register("aa", preserveSelectionAnchor = false) { caret, count ->
findArgumentRange(isInner = false, caret, count)
register("aa", preserveSelectionAnchor = false) { count ->
findArgumentRange(isInner = false, count)
}
}
}
@@ -611,7 +610,7 @@ private object ArgTextObjUtil {
/**
* Find argument range using the new VimApi.
*/
private suspend fun VimApi.findArgumentRange(isInner: Boolean, caret: CaretId, count: Int): TextObjectRange? {
private suspend fun VimApi.findArgumentRange(isInner: Boolean, count: Int): TextObjectRange? {
var bracketPairs: BracketPairs = ArgTextObjUtil.DEFAULT_BRACKET_PAIRS
val bracketPairsVar: String? = ArgTextObjUtil.bracketPairsVariable()
if (bracketPairsVar != null) {
@@ -626,7 +625,7 @@ private suspend fun VimApi.findArgumentRange(isInner: Boolean, caret: CaretId, c
}
}
val (text, caretOffset) = editor { read { text to with(caret) { offset } } }
val (text, caretOffset) = editor { read { text to withPrimaryCaret { offset } } }
val finder = ArgBoundsFinder(text, this, bracketPairs)
var pos = caretOffset

View File

@@ -42,7 +42,6 @@ internal class JumpRemoteTopicListener : ProjectRemoteTopicListener<JumpInfo> {
if (event.added) {
jumpService.addJump(projectId, jump, true)
injector.markService.setJumpMark(event.filepath, event.protocol, event.line, event.col)
} else {
jumpService.removeJump(projectId, jump)
}

View File

@@ -241,17 +241,13 @@ internal class VimEscHandler(nextHandler: EditorActionHandler) : VimKeyHandler(n
/**
* Rider (and CLion Nova) uses a separate handler for esc to close the completion. IdeaOnlyEscapeHandlerAction is especially
* designed to get all the esc presses, and if there is a completion close it and do not pass the execution further.
* designer to get all the esc presses, and if there is a completion close it and do not pass the execution further.
* This doesn't work the same as in IJ.
* In IdeaVim, we'd like to exit insert mode on closing completion. This is a requirement as the change of this
* behaviour causes a lot of complaining from users. Since the rider handler gets execution control, we don't
* receive an event and don't exit the insert mode.
* To fix it, this special handler exists only for rider and stands before the rider's handler. We don't execute the
* handler from rider because the autocompletion is closed automatically anyway.
*
* NOTE: This handler only works when octopus is enabled (non-Rider IDEs). For Rider, where octopus is disabled
* (VIM-3815) and Escape is consumed by the popup manager before the EditorEscape chain fires, the fix is in
* [com.maddyhome.idea.vim.listener.IdeaSpecifics.LookupTopicListener] via a LookupListener.
*/
internal class VimEscForRiderHandler(nextHandler: EditorActionHandler) : VimKeyHandler(nextHandler) {
override val key: String = "<Esc>"

View File

@@ -29,7 +29,6 @@ import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual
import com.maddyhome.idea.vim.state.mode.inVisualMode
import org.jetbrains.annotations.Contract
import java.awt.Color
import java.awt.Font
import java.util.*
import javax.swing.Timer
@@ -88,7 +87,7 @@ fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, end: Int):
}
val removeHighlightsEditors = mutableListOf<Editor>()
val removeHighlightsTimer = Timer(450) {
val removeHighlightsTimer = Timer(400) {
removeHighlightsEditors.forEach(::removeSearchHighlights)
removeHighlightsEditors.clear()
}
@@ -162,7 +161,7 @@ private fun updateSearchHighlights(
if (editor === currentEditor?.ij) {
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
}
highlightSearchResults(editor, results, currentMatchOffset)
highlightSearchResults(editor, pattern, results, currentMatchOffset)
if (!isSearching) {
removeHighlightsEditors.add(editor)
removeHighlightsTimer.restart()
@@ -185,7 +184,7 @@ private fun updateSearchHighlights(
if (result != null) {
if (!it.inVisualMode && !it.inCommandLineModeWithVisual) {
val results = listOf(result)
highlightSearchResults(editor, results, result.startOffset)
highlightSearchResults(editor, pattern, results, result.startOffset)
}
currentMatchOffset = result.startOffset
}
@@ -266,18 +265,9 @@ private fun findClosestMatch(
return sortedResults[nextIndex % results.size].startOffset
}
@Suppress("UseJBColor")
private val DEFAULT_RESULT_ATTRIBUTES = TextAttributes().apply {
backgroundColor = Color(50, 81, 61)
}
@Suppress("UseJBColor")
private val NEARBY_RESULT_ATTRIBUTES = TextAttributes().apply {
backgroundColor = Color(89, 80, 50)
}
fun highlightSearchResults(
editor: Editor,
pattern: String,
results: List<TextRange>,
currentMatchOffset: Int,
) {
@@ -286,28 +276,38 @@ fun highlightSearchResults(
highlighters = mutableListOf()
editor.vimLastHighlighters = highlighters
}
val allCaretOffsets = editor.caretModel.allCarets.map { it.offset }
for ((index, range) in results.withIndex()) {
if (allCaretOffsets.any { range.startOffset == it }) {
continue
}
val attributes = if (allCaretOffsets.any { (index > 0 && results[index - 1].startOffset == it) || (index < results.lastIndex && results[index + 1].startOffset == it) })
NEARBY_RESULT_ATTRIBUTES
else
DEFAULT_RESULT_ATTRIBUTES
highlighters.add(highlightMatch(editor, range.startOffset, range.endOffset, attributes))
for (range in results) {
val current = range.startOffset == currentMatchOffset
val highlighter = highlightMatch(editor, range.startOffset, range.endOffset, current, pattern)
highlighters.add(highlighter)
}
editor.vimIncsearchCurrentMatchOffset = currentMatchOffset
}
private fun highlightMatch(editor: Editor, start: Int, end: Int, attributes: TextAttributes): RangeHighlighter {
private fun highlightMatch(editor: Editor, start: Int, end: Int, current: Boolean, tooltip: String): RangeHighlighter {
val layer = HighlighterLayer.SELECTION - 1
val targetArea = HighlighterTargetArea.EXACT_RANGE
return editor.markupModel.addRangeHighlighter(start, end, layer, attributes, targetArea)
if (!current) {
// If we use a text attribute key, it will update automatically when the editor's colour scheme changes
val highlighter =
editor.markupModel.addRangeHighlighter(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES, start, end, layer, targetArea)
highlighter.errorStripeTooltip = tooltip
return highlighter
}
// There isn't a text attribute key for current selection. This means we won't update automatically when the editor's
// colour scheme changes. However, this is only used during incsearch, so it should be replaced pretty quickly. It's a
// small visual glitch that will fix itself quickly. Let's not bother implementing an editor colour scheme listener
// just for this.
// These are the same modifications that the Find live preview does. We could look at using LivePreviewPresentation,
// which might also be useful for text attributes in selection (if we supported that)
val attributes = editor.colorsScheme.getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES).clone().apply {
effectType = EffectType.ROUNDED_BOX
effectColor = editor.colorsScheme.getColor(EditorColors.CARET_COLOR)
}
return editor.markupModel.addRangeHighlighter(start, end, layer, attributes, targetArea).apply {
errorStripeTooltip = tooltip
}
}
/**

View File

@@ -29,14 +29,12 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.AnActionResult
import com.intellij.openapi.actionSystem.AnActionWrapper
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.ex.AnActionListener
import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.editor.actions.EnterAction
import com.intellij.openapi.editor.impl.ScrollingModelImpl
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.keymap.KeymapManager
import com.intellij.openapi.project.DumbAwareToggleAction
import com.intellij.openapi.util.TextRange
@@ -52,8 +50,6 @@ import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl
import com.maddyhome.idea.vim.helper.exitSelectMode
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
import com.maddyhome.idea.vim.ide.isClionNova
import com.maddyhome.idea.vim.ide.isRider
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.initInjector
import com.maddyhome.idea.vim.newapi.vim
@@ -89,11 +85,6 @@ internal object IdeaSpecifics {
caretOffset = hostEditor.caretModel.offset
}
val actionId = ActionManager.getInstance().getId(action)
if (isGotoAction(actionId)) {
saveJumpBeforeGoto(event, editor)
}
val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction
if (!isVimAction && injector.vimState.mode == Mode.INSERT && action !is EnterAction) {
val undoService = injector.undo as VimTimestampBasedUndoService
@@ -213,20 +204,6 @@ internal object IdeaSpecifics {
this.completionData = null
}
private fun isGotoAction(actionId: String?): Boolean =
actionId == IdeActions.ACTION_GOTO_BACK || actionId == IdeActions.ACTION_GOTO_FORWARD
private fun saveJumpBeforeGoto(event: AnActionEvent, editor: Editor?) {
val project = event.dataContext.getData(CommonDataKeys.PROJECT)
val currentEditor = editor
?: event.dataContext.getData(CommonDataKeys.EDITOR)
?: project?.let { VimListenerManager.VimLastSelectedEditorTracker.getLastSelectedEditor(it) }
?: project?.let { FileEditorManager.getInstance(it).selectedTextEditor }
if (currentEditor != null && !currentEditor.isIdeaVimDisabledHere) {
injector.jumpService.saveJumpLocation(currentEditor.vim)
}
}
private data class CompletionData(
val completionStartMarker: RangeMarker,
val originalStartOffset: Int,
@@ -373,16 +350,6 @@ internal object IdeaSpecifics {
if (newLookup.editor.isIdeaVimDisabledHere) return
(VimPlugin.getKey() as VimKeyGroupBase).registerShortcutsForLookup(newLookup)
// In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
// (due to LookupSummaryInfo popup) before the action system runs, so IdeaVim never sees it.
// Listen for explicit lookup cancellation (Escape) to exit insert mode.
// Note: we check isRider/isClionNova specifically, not !isOctopusEnabled(), because
// JetBrains Client (split mode) also has octopus disabled but doesn't need this workaround,
// and isCanceledExplicitly can be true for non-Escape keys (e.g. space) in that environment.
if (isRider() || isClionNova()) {
newLookup.addLookupListener(RiderEscLookupListener(newLookup.editor))
}
}
// Lookup closed
@@ -394,20 +361,6 @@ internal object IdeaSpecifics {
}
}
}
/**
* In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
* (due to LookupSummaryInfo parameter info popup) before the action system runs, so IdeaVim never sees it.
* This listener exits insert mode when the lookup is explicitly cancelled (Escape).
*/
private class RiderEscLookupListener(private val editor: Editor) : com.intellij.codeInsight.lookup.LookupListener {
override fun lookupCanceled(event: com.intellij.codeInsight.lookup.LookupEvent) {
if (event.isCanceledExplicitly && editor.vim.mode is Mode.INSERT) {
editor.vim.exitInsertMode(injector.executionContextManager.getEditorExecutionContext(editor.vim))
KeyHandler.getInstance().reset(editor.vim)
}
}
}
//endregion
//region Hide Vim search highlights when showing IntelliJ search results

View File

@@ -12,7 +12,6 @@ import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.VisualPosition
import com.maddyhome.idea.vim.api.BufferPosition
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.LocalMarkStorage
import com.maddyhome.idea.vim.api.SelectionInfo
import com.maddyhome.idea.vim.api.VimCaret
@@ -199,12 +198,3 @@ class IjVimCaret(val caret: Caret) : VimCaretBase() {
override fun hashCode(): Int = this.caret.hashCode()
}
val Caret.vim: VimCaret
get() = VimEditorFactory.getInstance().createVimCaret(this)
val VimCaret.ij: Caret
get() = VimEditorFactory.getInstance().extractCaret(this)
val ImmutableVimCaret.ij: Caret
get() = VimEditorFactory.getInstance().extractCaret(this)

View File

@@ -671,12 +671,3 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
}
}
val Editor.vim: VimEditor
get() = VimEditorFactory.getInstance().createVimEditor(this)
val VimEditor.ij: Editor
get() = VimEditorFactory.getInstance().extractEditor(this)
val com.intellij.openapi.util.TextRange.vim: TextRange
get() = TextRange(this.startOffset, this.endOffset)

View File

@@ -25,17 +25,10 @@ internal class IjVimMessages : VimMessagesBase() {
private var lastBeepTimeMillis = 0L
override fun showMessage(editor: VimEditor, message: String?) {
injector.outputPanel.clear(editor, injector.executionContextManager.getEditorExecutionContext(editor))
showMessageInternal(editor, message, MessageType.STANDARD)
}
override fun showErrorMessage(editor: VimEditor, message: String?) {
injector.outputPanel.clear(editor, injector.executionContextManager.getEditorExecutionContext(editor))
showMessageInternal(editor, message, MessageType.ERROR)
indicateError()
}
override fun appendErrorMessage(editor: VimEditor, message: String?) {
showMessageInternal(editor, message, MessageType.ERROR)
indicateError()
}

View File

@@ -78,7 +78,7 @@ open class IjVimSearchGroup : VimSearchGroupBase(), PersistentStateComponent<Ele
editor, pattern, startLine, endLine,
shouldIgnoreCase(pattern, lastIgnoreSmartCase)
)
highlightSearchResults(editor.ij, results, -1)
highlightSearchResults(editor.ij, pattern, results, -1)
}
}

View File

@@ -14,6 +14,7 @@ import com.intellij.openapi.editor.Editor
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.common.TextRange
interface VimEditorFactory {
fun createVimEditor(editor: Editor): VimEditor
@@ -26,3 +27,21 @@ interface VimEditorFactory {
fun getInstance(): VimEditorFactory = service()
}
}
val Editor.vim: VimEditor
get() = VimEditorFactory.getInstance().createVimEditor(this)
val VimEditor.ij: Editor
get() = VimEditorFactory.getInstance().extractEditor(this)
val Caret.vim: VimCaret
get() = VimEditorFactory.getInstance().createVimCaret(this)
val VimCaret.ij: Caret
get() = VimEditorFactory.getInstance().extractCaret(this)
val ImmutableVimCaret.ij: Caret
get() = VimEditorFactory.getInstance().extractCaret(this)
val com.intellij.openapi.util.TextRange.vim: TextRange
get() = TextRange(this.startOffset, this.endOffset)

View File

@@ -11,7 +11,6 @@ import com.intellij.ide.ui.LafManager
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.wm.ex.ToolWindowManagerListener
import com.intellij.openapi.wm.impl.IdeBackgroundUtil
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
import com.intellij.ui.ClientProperty
@@ -19,7 +18,6 @@ import com.intellij.ui.JBColor
import com.intellij.ui.components.JBPanel
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.IJSwingUtilities
import com.intellij.util.messages.MessageBusConnection
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
@@ -66,7 +64,6 @@ class OutputPanel private constructor(
private var glassPane: JComponent? = null
private var originalLayout: LayoutManager? = null
private var wasOpaque = false
private var toolWindowListenerConnection: MessageBusConnection? = null
var active: Boolean = false
private val segments = mutableListOf<TextLine>()
@@ -94,6 +91,7 @@ class OutputPanel private constructor(
// Suppress the fancy frame background used in the Islands theme
ClientProperty.putRecursive(this, IdeBackgroundUtil.NO_BACKGROUND, true)
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, editor.component)
// Initialize panel
setLayout(BorderLayout(0, 0))
@@ -103,7 +101,6 @@ class OutputPanel private constructor(
val keyListener = OutputPanelKeyListener()
addKeyListener(keyListener)
textPane.addKeyListener(keyListener)
editor?.let { putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, it.component) }
updateUI()
}
@@ -267,8 +264,6 @@ class OutputPanel private constructor(
}
if (glassPane != null) {
glassPane!!.removeComponentListener(resizeAdapter)
toolWindowListenerConnection?.disconnect()
toolWindowListenerConnection = null
glassPane!!.isVisible = false
glassPane!!.remove(this)
glassPane!!.setOpaque(wasOpaque)
@@ -290,8 +285,6 @@ class OutputPanel private constructor(
}
active = true
if (isSingleLine) return
requestFocus(textPane)
}
@@ -307,11 +300,6 @@ class OutputPanel private constructor(
glassPane!!.setOpaque(false)
glassPane!!.add(this)
glassPane!!.addComponentListener(resizeAdapter)
val project = editor.project
if (project != null) {
toolWindowListenerConnection = project.messageBus.connect()
toolWindowListenerConnection!!.subscribe(ToolWindowManagerListener.TOPIC, ToolWindowPositioningListener { positionPanel() })
}
}
override fun close() {

View File

@@ -1,23 +0,0 @@
/*
* Copyright 2003-2026 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.ui
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.openapi.wm.ex.ToolWindowManagerListener
import javax.swing.SwingUtilities
/**
* Repositions a panel whenever a tool window visibility state changes.
* Shared between [com.maddyhome.idea.vim.ui.ex.ExEntryPanel] and [OutputPanel].
*/
internal class ToolWindowPositioningListener(private val reposition: () -> Unit) : ToolWindowManagerListener {
override fun stateChanged(toolWindowManager: ToolWindowManager) {
SwingUtilities.invokeLater(reposition)
}
}

View File

@@ -15,11 +15,9 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.openapi.wm.ex.ToolWindowManagerListener
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
import com.intellij.ui.DocumentAdapter
import com.intellij.util.IJSwingUtilities
import com.intellij.util.messages.MessageBusConnection
import com.maddyhome.idea.vim.EventFacade
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
import com.maddyhome.idea.vim.VimPlugin
@@ -41,7 +39,6 @@ import com.maddyhome.idea.vim.key.interceptors.VimInputInterceptor
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.ui.ExPanelBorder
import com.maddyhome.idea.vim.ui.ToolWindowPositioningListener
import com.maddyhome.idea.vim.vimscript.model.commands.Command
import com.maddyhome.idea.vim.vimscript.model.commands.GlobalCommand
import com.maddyhome.idea.vim.vimscript.model.commands.SubstituteCommand
@@ -146,11 +143,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
glassPane.setOpaque(false)
glassPane.add(this)
glassPane.addComponentListener(resizePanelListener)
val project = editor.project
if (project != null) {
toolWindowListenerConnection = project.messageBus.connect()
toolWindowListenerConnection!!.subscribe(ToolWindowManagerListener.TOPIC, ToolWindowPositioningListener { positionPanel() })
}
positionPanel()
glassPane.isVisible = true
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, parent)
@@ -203,8 +195,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, null)
oldGlass!!.removeComponentListener(resizePanelListener)
toolWindowListenerConnection?.disconnect()
toolWindowListenerConnection = null
oldGlass!!.isVisible = false
oldGlass!!.remove(this)
oldGlass!!.setOpaque(wasOpaque)
@@ -520,8 +510,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
}
}
private var toolWindowListenerConnection: MessageBusConnection? = null
init {
val layout = GridBagLayout()

View File

@@ -194,7 +194,7 @@ class ExTextField internal constructor(private val myParentPanel: ExEntryPanel)
// handler adds all non-control characters to the text field. We want to add all characters, so if we have an
// actual character, just add it. Anything else, we'll pass to the super class like before (even though it's unclear
// what it will do with the keystroke)
if (stroke.keyChar != KeyEvent.CHAR_UNDEFINED && !isKeyCharEnterOrEscape(stroke.keyChar)) {
if (stroke.keyChar != KeyEvent.CHAR_UNDEFINED) {
replaceSelection(stroke.keyChar.toString())
} else {
val event = KeyEvent(

View File

@@ -1,43 +0,0 @@
package com.maddyhome.idea.vim.vimscript.model.functions.handlers
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.impl.PresentationFactory
import com.intellij.openapi.actionSystem.impl.Utils
import com.intellij.openapi.keymap.impl.ActionProcessor
import com.intellij.vim.annotations.VimscriptFunction
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.vimscript.model.VimLContext
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimInt
import com.maddyhome.idea.vim.vimscript.model.datatypes.asVimInt
import com.maddyhome.idea.vim.vimscript.model.functions.BuiltinFunctionHandler
import java.awt.event.KeyEvent
@VimscriptFunction(name = "isactionenabled")
internal class IsActionEnabled : BuiltinFunctionHandler<VimInt>() {
override fun doFunction(
arguments: Arguments,
editor: VimEditor,
context: ExecutionContext,
vimContext: VimLContext,
): VimInt {
val action = ActionManager.getInstance().getAction(arguments.getString(0).value)
if (action == null) {
return false.asVimInt()
}
val presentationFactory = PresentationFactory()
val wrappedContext = Utils.createAsyncDataContext(context.ij)
val actionProcessor = object : ActionProcessor() {}
val inputEventAdjusted = KeyEvent(editor.ij.contentComponent, KeyEvent.KEY_PRESSED, 0L, 0, KeyEvent.VK_UNDEFINED, '\u0000')
val updateEvent = Utils.runUpdateSessionForInputEvent(listOf(action), inputEventAdjusted, wrappedContext, "IdeaVim", actionProcessor, presentationFactory) { _, updater, events ->
val presentation = updater(action)
events[presentation]
}
val result = updateEvent != null && updateEvent.presentation.isEnabled
return result.asVimInt()
}
}

View File

@@ -1,17 +0,0 @@
<!--
~ Copyright 2003-2026 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.
-->
<!-- Split-mode (Remote Dev) specific registrations.
This module only loads when intellij.platform.frontend.split is available,
which provides access to intellij.rd.client and its extension points. -->
<idea-plugin package="com.maddyhome.idea.vim">
<dependencies>
<module name="intellij.platform.frontend.split"/>
<module name="IdeaVIM.ideavim-frontend"/>
</dependencies>
</idea-plugin>

View File

@@ -1,5 +1,4 @@
{
"has": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.HasFunctionHandler",
"isactionenabled": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.IsActionEnabled",
"pumvisible": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.PopupMenuVisibleFunctionHandler"
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 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
@@ -8,16 +8,11 @@
package org.jetbrains.plugins.ideavim.action.motion.search
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class SearchEntryFwdActionTest : VimTestCase() {
@Test
@@ -29,14 +24,6 @@ class SearchEntryFwdActionTest : VimTestCase() {
assertStatusLineCleared()
}
@Test
fun `test search not found shows only error message on output panel`() {
configureByText("lorem ipsum dolor sit amet")
enterSearch("nonexistent")
val panelText = injector.outputPanel.getCurrentOutputPanel()?.text ?: ""
assertEquals("E486: Pattern not found: nonexistent", panelText)
}
@Test
fun `search in visual mode`() {
doTest(
@@ -123,23 +110,6 @@ class SearchEntryFwdActionTest : VimTestCase() {
)
}
@Test
fun `test escape after search not found closes panel without inserting escape char`() {
configureByText("lorem ipsum dolor sit amet")
typeText("/notfound")
val panel = ExEntryPanel.getOrCreatePanelInstance()
assertTrue(panel.isActive)
typeText("<Esc>")
assertFalse(panel.isActive)
assertMode(Mode.NORMAL())
// The panel text should not contain ^[ (escape character written as text)
assertFalse(panel.text.contains("\u001B"), "Panel text should not contain escape character")
assertFalse(panel.text.contains("^["), "Panel text should not contain ^[ literal")
}
@Disabled("Ctrl-o doesn't work yet in select mode")
@Test
fun `search in one time from select mode`() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 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
@@ -65,49 +65,6 @@ class SearchWholeWordForwardActionTest : VimTestCase() {
)
}
@Test
fun `test repeated star search wraps around`() {
configureByText(
"""
aaa
abc
def
abc
dfg
abc
agg
abc
xyz
""".trimIndent(),
)
typeText("5j") // move to line 5, "abc"
assertPosition(5, 0)
typeText("*")
assertPosition(7, 0) // next "abc" forward
typeText("*")
assertPosition(1, 0) // wraps to first "abc"
typeText("*")
assertPosition(3, 0)
typeText("*")
assertPosition(5, 0)
typeText("*")
assertPosition(7, 0)
typeText("*")
assertPosition(1, 0) // wraps again
typeText("*")
assertPosition(3, 0)
typeText("*")
assertPosition(5, 0)
}
@Test
fun `test last word`() {
doTest(

View File

@@ -169,31 +169,4 @@ class AddressTest : VimTestCase() {
typeText(commandToKeys("/bar//foo/d"))
assertState("a\nfoo\nbar\nbar\nbaz\n")
}
@Test
fun `test backslash-slash range without previous search reports E35`() {
// Before this fix, using \/ with no previous search caused a NullPointerException instead of E35
configureByText("1\n2\n3\n")
typeText(commandToKeys("\\/d"))
assertPluginError(true)
assertPluginErrorMessage("E35: No previous regular expression")
}
@Test
fun `test backslash-question range without previous search reports E35`() {
// Before this fix, using \? with no previous search caused a NullPointerException instead of E35
configureByText("1\n2\n3\n")
typeText(commandToKeys("\\?d"))
assertPluginError(true)
assertPluginErrorMessage("E35: No previous regular expression")
}
@Test
fun `test backslash-ampersand range without previous substitute reports E33`() {
// Before this fix, using \& with no previous substitute caused a NullPointerException instead of E33
configureByText("1\n2\n3\n")
typeText(commandToKeys("\\&d"))
assertPluginError(true)
assertPluginErrorMessage("E33: No previous substitute regular expression")
}
}

View File

@@ -224,6 +224,17 @@ class HistoryCommandTest : VimTestCase() {
)
}
@Test
fun `test history cmd lists empty command history`() {
assertCommandOutput(
"history cmd",
"""
| # cmd history
|> 1 history cmd
""".trimMargin()
)
}
@Test
fun `test history cmd lists current cmd in history`() {
assertCommandOutput(
@@ -488,7 +499,7 @@ class HistoryCommandTest : VimTestCase() {
@Test
fun `test history search with first number lists single entry from search history`() {
fun `test history search with first number lists single entry from saerch history`() {
repeat(10) { i -> enterSearch("foo${i + 1}") }
injector.outputPanel.getCurrentOutputPanel()?.clearText()
assertCommandOutput(

View File

@@ -26,7 +26,6 @@ dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
testImplementation(testFixtures(project(":"))) // The root project
testImplementation("org.junit.vintage:junit-vintage-engine:6.0.3")
testImplementation("org.jetbrains:jetCheck:0.2.3")
intellijPlatform {
// Snapshots don't use installers

View File

@@ -17,6 +17,7 @@ class FileOpsSplitTest : IdeaVimStarterTestBase() {
@Test
fun `save file with write command`() {
openFile(createFile("src/Save1.txt", "Line 1\nLine 2\nLine 3\n"))
clickEditor()
typeVimAndEscape("0iSAVED ")
pause()
exCommand("w")

View File

@@ -29,6 +29,7 @@ import com.intellij.ide.starter.plugins.PluginConfigurator
import com.intellij.ide.starter.project.LocalProjectInfo
import com.intellij.ide.starter.runner.Starter
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeAll
@@ -124,45 +125,9 @@ abstract class IdeaVimStarterTestBase {
// ── IDE interaction helpers ─────────────────────────────────
/** Opens a file in the editor by relative path and waits until the editor is ready for input. */
/** Opens a file in the editor by relative path. */
protected fun openFile(relativePath: String) {
driver.withContext { openFile(relativePath) }
ensureEditorReady(relativePath)
}
private fun ensureEditorReady(relativePath: String, timeoutMs: Long = 10_000) {
val deadline = System.currentTimeMillis() + timeoutMs
var lastText = ""
while (System.currentTimeMillis() < deadline) {
try {
lastText = editorText()
if (lastText.isNotBlank()) break
} catch (_: Exception) {
// Editor component not yet available — retry
}
Thread.sleep(200)
}
check(lastText.isNotBlank()) {
"Editor for '$relativePath' did not become ready within ${timeoutMs}ms"
}
clickEditor()
// Verify IdeaVim's key handler is actually attached by sending `gg` (go to first line)
// and checking the caret moves to line 1. This confirms vim is processing keystrokes.
val vimReady = waitUntil(timeoutMs = 10_000, pollMs = 500) {
try {
driver.withContext {
ideFrame { codeEditor().apply { waitFound(); keyboard { typeText("gg") } } }
}
Thread.sleep(300)
caretLine() <= 1
} catch (_: Exception) {
false
}
}
check(vimReady) {
"IdeaVim key handler did not attach for '$relativePath' within timeout"
}
}
/** Types vim keys in the active editor. */
@@ -212,27 +177,6 @@ abstract class IdeaVimStarterTestBase {
}
}
/** Triggers the IDE "Navigate > Back" action (Cmd+[ on macOS, Ctrl+Alt+Left on Linux). */
protected fun ideaGoBack() {
driver.withContext {
ideFrame {
codeEditor().apply {
if (System.getProperty("os.name").lowercase().contains("mac")) {
keyboard { hotKey(java.awt.event.KeyEvent.VK_META, java.awt.event.KeyEvent.VK_OPEN_BRACKET) }
} else {
keyboard {
hotKey(
java.awt.event.KeyEvent.VK_CONTROL,
java.awt.event.KeyEvent.VK_ALT,
java.awt.event.KeyEvent.VK_LEFT
)
}
}
}
}
}
}
/** Presses Ctrl-G (file info). */
protected fun ctrlG() {
driver.withContext {
@@ -263,23 +207,6 @@ abstract class IdeaVimStarterTestBase {
Thread.sleep(ms)
}
/**
* Retries [check] every [pollMs] until it returns true or [timeoutMs] elapses.
* Use instead of fixed pauses to handle variable split-mode RPC latency on CI.
*/
protected fun waitUntil(
timeoutMs: Long = 5000,
pollMs: Long = 200,
check: () -> Boolean,
): Boolean {
val deadline = System.currentTimeMillis() + timeoutMs
while (System.currentTimeMillis() < deadline) {
if (check()) return true
Thread.sleep(pollMs)
}
return false
}
// ── Editor state helpers ────────────────────────────────────
/** Reads the current editor text. */
@@ -302,48 +229,42 @@ abstract class IdeaVimStarterTestBase {
// ── Assertions ──────────────────────────────────────────────
/** Asserts the editor contains the given text, polling until timeout. */
/** Asserts the editor contains the given text. */
protected fun assertEditorContains(expected: String, message: String? = null) {
var text = ""
val found = waitUntil { text = editorText(); text.contains(expected) }
assertTrue(found) {
val text = editorText()
assertTrue(text.contains(expected)) {
(message ?: "Editor should contain '$expected'") + ". Actual: $text"
}
}
/** Asserts the editor does NOT contain the given text, polling until timeout. */
/** Asserts the editor does NOT contain the given text. */
protected fun assertEditorNotContains(unexpected: String, message: String? = null) {
// Give operations time to settle, then check
pause(1000)
val text = editorText()
assertFalse(text.contains(unexpected)) {
(message ?: "Editor should not contain '$unexpected'") + ". Actual: $text"
}
}
/** Asserts the caret is at the given line (1-based), polling until timeout. */
/** Asserts the caret is at the given line (1-based). */
protected fun assertCaretAtLine(expected: Int, message: String? = null) {
var actual = 0
val found = waitUntil { actual = caretLine(); actual == expected }
assertTrue(found) {
val actual = caretLine()
assertEquals(expected, actual) {
(message ?: "Caret should be at line $expected") + ". Actual line: $actual"
}
}
/** Asserts the caret is before the given line, polling until timeout. */
/** Asserts the caret is before the given line. */
protected fun assertCaretBefore(line: Int, message: String? = null) {
var actual = 0
val found = waitUntil { actual = caretLine(); actual < line }
assertTrue(found) {
val actual = caretLine()
assertTrue(actual < line) {
(message ?: "Caret should be before line $line") + ". Actual line: $actual"
}
}
/** Asserts the caret is past the given line, polling until timeout. */
/** Asserts the caret is past the given line. */
protected fun assertCaretAfter(line: Int, message: String? = null) {
var actual = 0
val found = waitUntil { actual = caretLine(); actual > line }
assertTrue(found) {
val actual = caretLine()
assertTrue(actual > line) {
(message ?: "Caret should be after line $line") + ". Actual line: $actual"
}
}

View File

@@ -22,11 +22,11 @@ class JumpNavigationSplitTest : IdeaVimStarterTestBase() {
openFile(longFile("Jump1"))
typeVim("G")
pause(500)
pause(300)
assertCaretAfter(40, "G should go to end of file")
ctrlO()
pause(500)
pause(300)
assertCaretBefore(10, "Ctrl-O should jump back to start")
}
@@ -35,7 +35,7 @@ class JumpNavigationSplitTest : IdeaVimStarterTestBase() {
openFile(longFile("Jump2"))
typeVim("gg")
pause(500)
pause(300)
typeVim("/Line 30\n")
pause()
@@ -46,21 +46,4 @@ class JumpNavigationSplitTest : IdeaVimStarterTestBase() {
pause()
assertCaretBefore(10, "Ctrl-O should jump back near start after search")
}
@Test
fun `IDE Back navigation records jump so apostrophe mark navigates back`() {
openFile(longFile("Jump3"))
typeVim("G")
pause(300)
assertCaretAfter(40, "G should move to end of file")
ideaGoBack()
pause(500)
assertCaretBefore(10, "IDE Back should return to start of file")
typeVim("''")
pause(300)
assertCaretAfter(40, "'' should return to position before IDE Back (end of file)")
}
}

View File

@@ -20,14 +20,10 @@ class RepeatUndoSplitTest : IdeaVimStarterTestBase() {
typeVimAndEscape("0sX")
assertEditorContains("Xbcdef", "First char should be X")
typeVim("l")
pause()
typeVim(".")
typeVim("l.")
assertEditorContains("XXcdef", "Second char should also be X")
typeVim("u")
pause()
typeVim("u")
typeVim("uu")
assertEditorContains("Xbcdef", "First undo should revert dot-repeat only")
}
@@ -42,9 +38,8 @@ class RepeatUndoSplitTest : IdeaVimStarterTestBase() {
assertEditorContains("HiHi", "Dot repeat should insert again")
typeVim("u")
var text = ""
val found = waitUntil { text = editorText(); text.contains("Hi ") && !text.contains("HiHi") }
assertTrue(found) {
val text = editorText()
assertTrue(text.contains("Hi ") && !text.contains("HiHi")) {
"Undo should revert dot repeat only. Actual: $text"
}
}
@@ -56,11 +51,8 @@ class RepeatUndoSplitTest : IdeaVimStarterTestBase() {
typeVimAndEscape("0cwHELLO")
assertEditorContains("HELLO", "Should have changed word")
typeVim("w")
pause()
typeVim(".")
var helloCount = 0
waitUntil { helloCount = editorText().lines().first().split("HELLO").size - 1; helloCount >= 2 }
typeVim("w.")
val helloCount = editorText().lines().first().split("HELLO").size - 1
assertTrue(helloCount >= 2) { "Should have two HELLOs. Actual: ${editorText()}" }
typeVim("u")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 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
@@ -13,22 +13,14 @@ import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.KeyHandler
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.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.helper.isCommandLineActionChar
import com.maddyhome.idea.vim.state.KeyHandlerState
import javax.swing.KeyStroke
/**
* Insert mode: insert a digraph character via `<C-K>`
*
* The converted digraph character is re-injected through the key handler so that it is processed as typed input in
* Insert mode (handled by the change group).
*/
@CommandOrMotion(keys = ["<C-K>"], modes = [Mode.INSERT])
@CommandOrMotion(keys = ["<C-K>"], modes = [Mode.INSERT, Mode.CMD_LINE])
class InsertCompletedDigraphAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.INSERT
@@ -37,6 +29,22 @@ class InsertCompletedDigraphAction : VimActionHandler.SingleExecution() {
// We're waiting for it to complete and give us a CHARACTER
override val argumentType: Argument.Type = Argument.Type.DIGRAPH
/**
* Perform additional initialisation when starting to wait for an argument
*
* IdeaVim has two ways of handling digraphs/literals. Actions such as `r` or `f` can accept a digraph, which really
* means it accepts a character, but the user can use `<C-K>`/`<C-V>` to type a digraph or literal and convert it into
* a character. Unfortunately, there is no mode that can be used to register an "insert digraph/literal" action for
* these keys while replace or find is active. So the key handler hard codes these keys and will check for them when
* an action expects a digraph (and like Vim, these keys cannot be mapped). Once the state machine has matched a
* character, the expected argument is reset to [Argument.Type.CHARACTER] and the character is passed through the key
* handler again, potentially mapped, and then attached as an argument to the current command, which is now complete
* and executed.
*
* In Insert and Command-line mode, the `<C-K>` and `<C-V>` keys are actions that will wait for a character argument,
* and then insert it. Commands are only executed once complete, so we use [onStartWaitingForArgument] to start the
* digraph state machine. This also gives us a repeatable command and captures the keys for `'showcmd'`.
*/
override fun onStartWaitingForArgument(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
val result = keyState.digraphSequence.startDigraphSequence()
KeyHandler.getInstance().setPromptCharacterEx(result.promptCharacter)
@@ -48,6 +56,7 @@ class InsertCompletedDigraphAction : VimActionHandler.SingleExecution() {
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
// The converted digraph character has been captured as an argument, push it back through key handler
val argument = cmd.argument as? Argument.Character ?: return false
val keyStroke = KeyStroke.getKeyStroke(argument.character)
val keyHandler = KeyHandler.getInstance()
@@ -55,37 +64,3 @@ class InsertCompletedDigraphAction : VimActionHandler.SingleExecution() {
return true
}
}
/**
* Command-line mode: insert a digraph character via `<C-K>`
*
* Control characters like Escape or Enter are inserted directly into the command line to avoid being matched as
* commands. Other characters use [VimCommandLine.handleKey] so that overwrite mode is handled correctly.
*/
@CommandOrMotion(keys = ["<C-K>"], modes = [Mode.CMD_LINE])
class CmdLineCompletedDigraphAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.INSERT
override val argumentType: Argument.Type = Argument.Type.DIGRAPH
override fun onStartWaitingForArgument(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
val result = keyState.digraphSequence.startDigraphSequence()
KeyHandler.getInstance().setPromptCharacterEx(result.promptCharacter)
}
override fun execute(
editor: VimEditor,
context: ExecutionContext,
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
val argument = cmd.argument as? Argument.Character ?: return false
val commandLine = injector.commandLine.getActiveCommandLine() ?: return false
val ch = argument.character
if (ch.isCommandLineActionChar()) {
commandLine.insertText(commandLine.caret.offset, ch.toString())
} else {
commandLine.handleKey(KeyStroke.getKeyStroke(ch))
}
return true
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 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
@@ -13,22 +13,14 @@ import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.KeyHandler
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.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.helper.isCommandLineActionChar
import com.maddyhome.idea.vim.state.KeyHandlerState
import javax.swing.KeyStroke
/**
* Insert mode: insert a literal character via `<C-V>` / `<C-Q>`
*
* The converted literal character is re-injected through the key handler so that it is processed as typed input in
* Insert mode (handled by the change group).
*/
@CommandOrMotion(keys = ["<C-V>", "<C-Q>"], modes = [Mode.INSERT])
@CommandOrMotion(keys = ["<C-V>", "<C-Q>"], modes = [Mode.INSERT, Mode.CMD_LINE])
class InsertCompletedLiteralAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.INSERT
@@ -37,6 +29,22 @@ class InsertCompletedLiteralAction : VimActionHandler.SingleExecution() {
// We're waiting for it to complete and give us a CHARACTER
override val argumentType: Argument.Type = Argument.Type.DIGRAPH
/**
* Perform additional initialisation when starting to wait for an argument
*
* IdeaVim has two ways of handling digraphs/literals. Actions such as `r` or `f` can accept a digraph, which really
* means it accepts a character, but the user can use `<C-K>`/`<C-V>` to type a digraph or literal and convert it into
* a character. Unfortunately, there is no mode that can be used to register an "insert digraph/literal" action for
* these keys while replace or find is active. So the key handler hard codes these keys and will check for them when
* an action expects a digraph (and like Vim, these keys cannot be mapped). Once the state machine has matched a
* character, the expected argument is reset to [Argument.Type.CHARACTER] and the character is passed through the key
* handler again, potentially mapped, and then attached as an argument to the current command, which is now complete
* and executed.
*
* In Insert and Command-line mode, the `<C-K>` and `<C-V>` keys are actions that will wait for a character argument,
* and then insert it. Commands are only executed once complete, so we use [onStartWaitingForArgument] to start the
* digraph state machine. This also gives us a repeatable command and captures the keys for `'showcmd'`.
*/
override fun onStartWaitingForArgument(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
val result = keyState.digraphSequence.startLiteralSequence()
KeyHandler.getInstance().setPromptCharacterEx(result.promptCharacter)
@@ -48,6 +56,7 @@ class InsertCompletedLiteralAction : VimActionHandler.SingleExecution() {
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
// The converted literal character has been captured as an argument, push it back through key handler
val argument = cmd.argument as? Argument.Character ?: return false
val keyStroke = KeyStroke.getKeyStroke(argument.character)
val keyHandler = KeyHandler.getInstance()
@@ -55,39 +64,3 @@ class InsertCompletedLiteralAction : VimActionHandler.SingleExecution() {
return true
}
}
/**
* Command-line mode: insert a literal character via `<C-V>` / `<C-Q>`
*
* Control characters like Escape or Enter are inserted directly into the command line to avoid being matched as
* commands (e.g., LeaveCommandLineAction). Other characters use [VimCommandLine.handleKey] so that overwrite mode
* is handled correctly.
*/
@CommandOrMotion(keys = ["<C-V>", "<C-Q>"], modes = [Mode.CMD_LINE])
class CmdLineCompletedLiteralAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.INSERT
override val argumentType: Argument.Type = Argument.Type.DIGRAPH
override fun onStartWaitingForArgument(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
val result = keyState.digraphSequence.startLiteralSequence()
KeyHandler.getInstance().setPromptCharacterEx(result.promptCharacter)
}
override fun execute(
editor: VimEditor,
context: ExecutionContext,
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
val argument = cmd.argument as? Argument.Character ?: return false
val commandLine = injector.commandLine.getActiveCommandLine() ?: return false
val ch = argument.character
if (ch.isCommandLineActionChar()) {
// Insert directly to avoid these being matched as commands by the key handler
commandLine.insertText(commandLine.caret.offset, ch.toString())
} else {
commandLine.handleKey(KeyStroke.getKeyStroke(ch))
}
return true
}
}

View File

@@ -15,12 +15,14 @@ import com.maddyhome.idea.vim.helper.endOffsetInclusive
import com.maddyhome.idea.vim.state.mode.SelectionType.CHARACTER_WISE
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.state.mode.selectionType
import java.lang.Long.toHexString
abstract class VimFileBase : VimFile {
override fun displayHexInfo(editor: VimEditor) {
val offset = editor.currentCaret().offset
val ch = editor.text()[offset]
injector.messages.showMessage(editor, ch.code.toString(16))
injector.messages.showMessage(editor, toHexString(ch.code.toLong()))
}
override fun displayLocationInfo(editor: VimEditor) {

View File

@@ -150,13 +150,6 @@ interface VimMarkService {
*/
fun loadLegacyState(element: Any) {}
/**
* Sets the BEFORE_JUMP_MARK directly from a position, without requiring an open editor or caret.
* Used by jump listeners that receive position data from the IDE navigation history.
* Default no-op; implementations that maintain local mark state should override this.
*/
fun setJumpMark(filepath: String, protocol: String, line: Int, col: Int) {}
fun isValidMark(char: Char, operation: Operation, isCaretPrimary: Boolean): Boolean
enum class Operation {

View File

@@ -248,10 +248,6 @@ abstract class VimMarkServiceBase : VimMarkService {
return setMark(caret, mark)
}
override fun setJumpMark(filepath: String, protocol: String, line: Int, col: Int) {
getLocalMarks(filepath)[BEFORE_JUMP_MARK] = VimMark(BEFORE_JUMP_MARK, line, col, filepath, protocol)
}
protected open fun createGlobalMark(editor: VimEditor, char: Char, offset: Int): Mark? {
val markChar = char.normalizeMarkChar()
if (!markChar.isGlobalMark()) return null

View File

@@ -20,18 +20,10 @@ interface VimMessages {
/**
* Displays an error message to the user (typically in red).
* Clears any existing output panel content before showing the message.
* The message panel closes on any keystroke and passes the key through to the editor.
*/
fun showErrorMessage(editor: VimEditor, message: String?)
/**
* Appends an error message to the existing output panel content (typically in red).
* Unlike [showErrorMessage], this does not clear prior output.
* Use this when reporting errors during script execution where earlier output should be preserved.
*/
fun appendErrorMessage(editor: VimEditor, message: String?)
/**
* Legacy method for displaying messages.
* @deprecated Use [showMessage] or [showErrorMessage] instead.

View File

@@ -34,9 +34,4 @@ interface VimOutputPanelService {
text: String,
messageType: MessageType = MessageType.STANDARD,
)
fun clear(
editor: VimEditor,
context: ExecutionContext,
)
}

View File

@@ -18,12 +18,4 @@ abstract class VimOutputPanelServiceBase : VimOutputPanelService {
panel.addText(text, true, messageType)
panel.show()
}
override fun clear(
editor: VimEditor,
context: ExecutionContext,
) {
val panel = getOrCreate(editor, context)
panel.clearText()
}
}

View File

@@ -65,13 +65,13 @@ abstract class VimScriptExecutorBase : VimscriptExecutor {
}
finalResult = ExecutionResult.Error
if (indicateErrors) {
injector.messages.appendErrorMessage(editor, e.message)
injector.messages.showErrorMessage(editor, e.message)
} else {
logger.warn("Failed while executing $unit. " + e.message)
}
} catch (e: NotImplementedError) {
if (indicateErrors) {
injector.messages.appendErrorMessage(editor, "Not implemented yet :(")
injector.messages.showErrorMessage(editor, "Not implemented yet :(")
}
} catch (e: Exception) {
logger.warn(e.toString())

View File

@@ -653,9 +653,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
caret.moveToOffset(matchRange.startOffset)
val highlight = addSubstitutionConfirmationHighlight(editor, matchRange.startOffset, matchRange.endOffset)
injector.modalInput.create(
editor,
context,
injector.messages.message("command.substitute.replace.with.prompt", lineToNextSubstitute.second.second),
editor, context, injector.messages.message("command.substitute.replace.with.prompt", lineToNextSubstitute.second.second),
SubstituteWithAskInputInterceptor(
editor, caret, nextSubstitute, highlight, line, 0, parent, pattern, regex,
oldLastSubstituteString, line2, hasExpression, substituteString, options,
@@ -856,7 +854,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
if (lastMatchLine != -1) {
caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, lastMatchLine))
} else {
injector.messages.showErrorMessage(editor, "E486: Pattern not found: $pattern")
injector.messages.showStatusBarMessage(null, "E486: Pattern not found: $pattern")
}
}
@@ -865,7 +863,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
// todo throw multiple exceptions at once
if (exceptions.isNotEmpty()) {
injector.messages.indicateError()
injector.messages.showErrorMessage(editor, exceptions[0].message)
injector.messages.showStatusBarMessage(null, exceptions[0].message)
}
}

View File

@@ -160,11 +160,7 @@ abstract class VimSearchHelperBase : VimSearchHelper {
return TextRange(start, end)
}
override fun findWordAtOrFollowingCursor(
editor: VimEditor,
caret: ImmutableVimCaret,
isBigWord: Boolean,
): TextRange? {
override fun findWordAtOrFollowingCursor(editor: VimEditor, caret: ImmutableVimCaret, isBigWord: Boolean): TextRange? {
val offset = caret.offset
return findWordAtOrFollowingCursor(editor, offset, isBigWord)
}
@@ -172,7 +168,6 @@ abstract class VimSearchHelperBase : VimSearchHelper {
override fun findFilenameAtOrFollowingCursor(editor: VimEditor, caret: ImmutableVimCaret): TextRange? {
return findFilenameAtOrFollowingCursor(editor, caret.offset)
}
override fun findFilenameAtOrFollowingCursor(editor: VimEditor, offset: Int): TextRange? {
val text = editor.text()
if (text.isEmpty()) return null
@@ -305,9 +300,6 @@ abstract class VimSearchHelperBase : VimSearchHelper {
}
if (result is VimMatchResult.Failure) {
if (!showMessages) {
return null
}
if (wrap) {
injector.messages.showErrorMessage(editor, injector.messages.message("E486", pattern))
} else if (dir === Direction.FORWARDS) {
@@ -315,6 +307,7 @@ abstract class VimSearchHelperBase : VimSearchHelper {
} else {
injector.messages.showErrorMessage(editor, injector.messages.message("E384", pattern))
}
return null
}
// When trying to find the end position for a match, we're allowed to match the current position. But if we do that
@@ -341,10 +334,6 @@ abstract class VimSearchHelperBase : VimSearchHelper {
}
}
if (result is VimMatchResult.Failure) {
return null
}
return (result as VimMatchResult.Success).range
}
@@ -1779,8 +1768,7 @@ abstract class VimSearchHelperBase : VimSearchHelper {
if (isOuter && shouldEndOnWhitespace && start > 0
&& !isWhitespace(editor, chars[end], isBig)
&& !isWhitespace(editor, chars[start], isBig)
) {
&& !isWhitespace(editor, chars[start], isBig)) {
// Outer word objects normally include following whitespace. But if there's no following whitespace to include,
// we should extend the range to include preceding whitespace. However, Vim doesn't select whitespace at the

View File

@@ -204,7 +204,7 @@ private class SearchAddress(pattern: String, offset: Int, move: Boolean) : Addre
private val logger = vimLogger<SearchAddress>()
}
private val patterns: MutableList<String> = mutableListOf()
private val patterns: MutableList<String?> = mutableListOf()
private val directions: MutableList<Direction> = mutableListOf()
init {
@@ -216,17 +216,17 @@ private class SearchAddress(pattern: String, offset: Int, move: Boolean) : Addre
var pat = tok.nextToken()
when (pat) {
"\\/" -> {
patterns.add(injector.searchGroup.lastSearchPattern ?: throw exExceptionMessage("E35"))
patterns.add(injector.searchGroup.lastSearchPattern)
directions.add(Direction.FORWARDS)
}
"\\?" -> {
patterns.add(injector.searchGroup.lastSearchPattern ?: throw exExceptionMessage("E35"))
patterns.add(injector.searchGroup.lastSearchPattern)
directions.add(Direction.BACKWARDS)
}
"\\&" -> {
patterns.add(injector.searchGroup.lastSubstitutePattern ?: throw exExceptionMessage("E33"))
patterns.add(injector.searchGroup.lastSubstitutePattern)
directions.add(Direction.FORWARDS)
}
@@ -266,7 +266,7 @@ private class SearchAddress(pattern: String, offset: Int, move: Boolean) : Addre
// Note that wrapscan, ignorecase, etc. all come from current option values, as expected
searchOffset = getSearchOffset(editor, line0, direction)
searchOffset = injector.searchGroup.processSearchRange(editor, pattern, patternOffset, searchOffset, direction)
searchOffset = injector.searchGroup.processSearchRange(editor, pattern!!, patternOffset, searchOffset, direction)
if (searchOffset == -1) {
if (injector.options(editor).wrapscan) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 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
@@ -25,7 +25,9 @@ fun VimEditor.exitVisualMode() {
if (inBlockSelection) {
removeSecondaryCarets()
}
nativeCarets().forEach(VimCaret::removeSelection)
injector.application.runWriteAction {
nativeCarets().forEach(VimCaret::removeSelection)
}
}
if (inVisualMode || inCommandLineModeWithVisual) {
vimLastSelectionType = selectionType

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 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
@@ -18,13 +18,3 @@ fun KeyStroke.isCloseKeyStroke(): Boolean {
keyCode == KeyEvent.VK_C && modifiers and InputEvent.CTRL_DOWN_MASK != 0 ||
keyCode == '['.code && modifiers and InputEvent.CTRL_DOWN_MASK != 0
}
/**
* Returns true if this character would be matched as a command-line action (close or execute) rather than text input
* when re-injected through the key handler in CMD_LINE mode.
*
* Escape closes the command line, Enter/CR executes it.
*/
fun Char.isCommandLineActionChar(): Boolean {
return this == '\u001B' || this == '\n' || this == '\r'
}

View File

@@ -9,7 +9,6 @@
package com.maddyhome.idea.vim.thinapi
import com.intellij.vim.api.VimApi
import com.intellij.vim.api.models.CaretId
import com.intellij.vim.api.scopes.TextObjectRange
import com.intellij.vim.api.scopes.TextObjectScope
import com.maddyhome.idea.vim.KeyHandler
@@ -42,7 +41,7 @@ internal class TextObjectScopeImpl(
keys: String,
registerDefaultMapping: Boolean,
preserveSelectionAnchor: Boolean,
rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
) {
val plugKeys = "<Plug>($pluginName-$keys)"
@@ -89,7 +88,7 @@ private class TextObjectExtensionHandler(
private val listenerOwner: ListenerOwner,
private val mappingOwner: MappingOwner,
private val preserveSelectionAnchor: Boolean,
private val rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
private val rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
) : ExtensionHandler {
override val isRepeatable: Boolean = false
@@ -131,7 +130,7 @@ private class ApiTextObjectActionHandler(
private val listenerOwner: ListenerOwner,
private val mappingOwner: MappingOwner,
override val preserveSelectionAnchor: Boolean,
private val rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
private val rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
) : TextObjectActionHandler() {
// Will be set based on the result of rangeProvider
@@ -150,7 +149,7 @@ private class ApiTextObjectActionHandler(
val vimApi = VimApiImpl(listenerOwner, mappingOwner, editor.projectId)
// Execute the range provider (suspend lambda bridged via runBlocking for now)
val apiRange = kotlinx.coroutines.runBlocking { vimApi.rangeProvider(CaretId(caret.id), count) } ?: return null
val apiRange = kotlinx.coroutines.runBlocking { vimApi.rangeProvider(count) } ?: return null
// Convert API range to internal TextRange and set visual type
return when (apiRange) {

View File

@@ -83,7 +83,7 @@ sealed class Command(
if (Flag.SAVE_SELECTION !in argFlags.flags) {
// Editor.inBlockSelection is not available, because we're not in Visual mode anymore. Check if the primary caret
// currently has a selection and if (when we still in Visual) it was a block selection.
injector.application.runWriteAction {
injector.application.runReadAction {
if (editor.primaryCaret().hasSelection() && editor.primaryCaret().lastSelectionInfo.selectionType.isBlock) {
editor.removeSecondaryCarets()
}

View File

@@ -41,7 +41,9 @@ data class DelfunctionCommand(
try {
injector.functionService.deleteFunction(name, scope, this)
} catch (e: ExException) {
if (e.code != "E130") {
if (e.message != null && e.message!!.startsWith("E130")) {
// "ignoreIfMissing" flag handles the "E130: Unknown function" exception
} else {
throw e
}
}

View File

@@ -15,7 +15,7 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ranges.Range
import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
import kotlin.math.min
import java.lang.Integer.min
/**
* see "h :[range]"

View File

@@ -65,7 +65,7 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) :
val returnValue = executeFunctionBody(exceptionsCaught, editor, context)
if (exceptionsCaught.isNotEmpty()) {
injector.messages.appendErrorMessage(editor, exceptionsCaught.last().message)
injector.messages.showErrorMessage(editor, exceptionsCaught.last().message)
}
return returnValue ?: VimInt.ZERO
}

View File

@@ -299,15 +299,10 @@
"class": "com.maddyhome.idea.vim.action.motion.updown.MotionDownAction",
"modes": "NXO"
},
{
"keys": "<C-K>",
"class": "com.maddyhome.idea.vim.action.change.insert.CmdLineCompletedDigraphAction",
"modes": "C"
},
{
"keys": "<C-K>",
"class": "com.maddyhome.idea.vim.action.change.insert.InsertCompletedDigraphAction",
"modes": "I"
"modes": "IC"
},
{
"keys": "<C-Left>",
@@ -414,15 +409,10 @@
"class": "com.maddyhome.idea.vim.action.window.tabs.PreviousTabAction",
"modes": "NXO"
},
{
"keys": "<C-Q>",
"class": "com.maddyhome.idea.vim.action.change.insert.CmdLineCompletedLiteralAction",
"modes": "C"
},
{
"keys": "<C-Q>",
"class": "com.maddyhome.idea.vim.action.change.insert.InsertCompletedLiteralAction",
"modes": "I"
"modes": "IC"
},
{
"keys": "<C-R>",
@@ -569,15 +559,10 @@
"class": "com.maddyhome.idea.vim.action.motion.scroll.CtrlUpAction",
"modes": "N"
},
{
"keys": "<C-V>",
"class": "com.maddyhome.idea.vim.action.change.insert.CmdLineCompletedLiteralAction",
"modes": "C"
},
{
"keys": "<C-V>",
"class": "com.maddyhome.idea.vim.action.change.insert.InsertCompletedLiteralAction",
"modes": "I"
"modes": "IC"
},
{
"keys": "<C-W>",