1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2026-04-16 05:44:11 +02:00

Compare commits

..

61 Commits

Author SHA1 Message Date
a565dab624 Set plugin version to chylex-57 2026-04-14 19:19:21 +02:00
97bbe7d996 Fix AltGr not triggering Ctrl-Alt bindings on Windows 2026-04-14 19:19:21 +02:00
cdc525d62b Fix argtextobj plugin not working with multiple carets 2026-04-14 19:19:21 +02:00
62cb93b8fd Add 'isactionenabled' function 2026-04-10 17:09:13 +02:00
412dbfffb1 Fix Ex commands not working 2026-04-10 17:09:13 +02:00
fcbd4f3ecd Preserve visual mode after executing IDE action 2026-04-10 17:09:13 +02:00
efea120fc4 Make g0/g^/g$ work with soft wraps 2026-04-10 17:09:13 +02:00
93a3ef99a2 Make gj/gk jump over soft wraps 2026-04-10 17:09:13 +02:00
7e0825250c Make camelCase motions adjust based on direction of visual selection 2026-04-10 17:09:13 +02:00
9b8413a4d4 Make search highlights temporary & use different color for nearby results 2026-04-10 17:09:13 +02:00
5938e20aa9 Do not switch to normal mode after inserting a live template 2026-04-10 16:07:01 +02:00
bf282dbb8b Exit insert mode after refactoring 2026-04-10 16:07:01 +02:00
f729d69ebd Add action to run last macro in all opened files 2026-04-10 16:07:00 +02:00
a6648469b2 Stop macro execution after a failed search 2026-04-10 16:07:00 +02:00
42d5a14b97 Revert per-caret registers 2026-04-10 16:07:00 +02:00
ef9f204069 Apply scrolloff after executing native IDEA actions 2026-04-10 16:07:00 +02:00
dcaa3e081d Automatically add unambiguous imports after running a macro 2026-04-10 16:07:00 +02:00
480c891e0e Fix(VIM-3986): Exception when pasting register contents containing new line 2026-04-10 16:07:00 +02:00
def269e35f Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2026-04-10 16:07:00 +02:00
0fcbbdf0ce Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2026-04-10 16:07:00 +02:00
59f267e723 Update search register when using f/t 2026-04-10 16:07:00 +02:00
e69921d4d3 Add support for count for visual and line motion surround 2026-04-10 16:07:00 +02:00
4ae3a9f426 Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2026-04-10 16:07:00 +02:00
d44afe5284 Fix(VIM-696): Restore visual mode after undo/redo, and disable incompatible actions 2026-04-10 16:07:00 +02:00
4612f5ce68 Respect count with <Action> mappings 2026-04-10 16:07:00 +02:00
3292bc65fd Change matchit plugin to use HTML patterns in unrecognized files 2026-04-10 16:07:00 +02:00
04e67e622a Fix ex command panel causing Undock tool window to hide 2026-04-10 16:07:00 +02:00
b51714e9f9 Reset insert mode when switching active editor 2026-04-10 16:07:00 +02:00
f8d3e9d98e Remove notifications about configuration options 2026-04-10 16:07:00 +02:00
b152819d2b Remove AI 2026-04-10 16:07:00 +02:00
fe90c24a46 Set custom plugin version 2026-04-10 16:06:59 +02:00
8b636f9dde Revert "Fix(VIM-4108): Use default ANTLR output directory for Gradle 9+ compatibility"
This reverts commit a476583ea3.
2026-04-10 16:04:41 +02:00
376bf98dee Revert "Upgrade Gradle wrapper to 9.2.1"
This reverts commit 517bda93
2026-04-10 16:04:41 +02:00
a4c70083aa Revert "Fix(VIM-4109): Configure test source sets for Gradle 9+ compatibility"
This reverts commit 5c0d9569d9.
2026-04-10 16:04:41 +02:00
1grzyb1
9f33320e66 Move split mode tests to team city 2026-04-09 14:21:18 +02:00
1grzyb1
5dc5aaa0d8 VIM-4176 Don't focus single line output
Scroll was causing clearing output panel which resulted in race conditions
2026-04-09 14:06:05 +02:00
1grzyb1
0d8d215946 Clear gradle caches in random order tests
They were keeping old class descriptors
2026-04-09 12:26:32 +02:00
1grzyb1
c9234b82f5 VIM-4175 Clear output panel before showing error
Split showing error message into two separate methods. Once that appends the error to the current output panel and the second that clears the output panel before showing the error. So when no search results are found we don;t show hit enter message,
2026-04-09 11:45:56 +02:00
1grzyb1
b9bd523648 Fix visual selection commands failing off-EDT due to nested write-in-read action
Commands entered from Visual mode (e.g. :'<,'>sort) fail because Command.execute wraps selection cleanup in runReadAction, but exitVisualMode nests a runWriteAction inside it, which deadlocks off-EDT. Remove the unnecessary runWriteAction from exitVisualMode since removeSelection only requires EDT, not a write lock.
2026-04-08 13:48:33 +02:00
1grzyb1
4f5b793642 Bump ideaVersion to 2026.1 2026-04-08 13:48:33 +02:00
1grzyb1
2fdf52d305 Split double undo into two singles once 2026-04-08 09:24:14 +02:00
1grzyb1
56103c990b Increase compatibility pipeline timeout to 180 minutes 2026-04-08 08:50:47 +02:00
1grzyb1
1e489e2c14 Wait for ideavim to attatch to editor 2026-04-08 08:44:16 +02:00
1grzyb1
06d877415d Make go back shortcut platform-specific 2026-04-07 12:52:07 +02:00
claude[bot]
c946ecde86 Fix NPE when using \/, \?, or \& range without previous search pattern
When using \/, \?, or \& in an Ex command range (e.g., :\/ d) without a
previous search or substitute pattern, the code stored null in the
patterns list and then threw NullPointerException via the !! assertion
in calculateLine1.

Instead, throw the appropriate Vim error eagerly when building the
SearchAddress: E35 for \/ and \? (no previous search), E33 for \& (no
previous substitute). The patterns list is now non-nullable, eliminating
the !! assertion.

Add regression tests that would have caught this NPE.
2026-04-07 12:51:40 +02:00
claude[bot]
00e1d8173e Remove duplicate test and fix typo in HistoryCommandTest
- Remove `test history cmd lists empty command history` which was an
  exact duplicate of `test history cmd lists current cmd in history`
- Fix typo "saerch" -> "search" in test name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:50:20 +02:00
IdeaVim Bot
cff4afa050 Add digitalby to contributors list 2026-04-07 09:20:20 +00:00
1grzyb1
706ac76b5e Update changelog: aggregate all pending changelog entries
Combines entries from 10 individual changelog PRs (#1593, #1604, #1607,
#1612, #1614, #1618, #1625, #1637, #1644, #1648) into a single update.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:13:16 +02:00
1grzyb1
98934ff7bf Clean Gradle before running tests
It should ensure no stale classes survive between builds
2026-04-07 11:09:05 +02:00
1grzyb1
c26658d27d Ensure editor is ready before proceeding with split tests 2026-04-07 10:52:41 +02:00
1grzyb1
f6fb0fbea6 Disable gradle cache for testing build type
It was preserving old class descriptors
2026-04-07 10:36:31 +02:00
digitalby
74cf6fbee8 refactor: Extract isGotoAction and saveJumpBeforeGoto helpers, add split-mode test for IDE Back jump 2026-04-07 09:47:42 +02:00
digitalby
b22089f50f fix: Fix `` and \'\' jump commands not working after the IJ Meta+B shortcut 2026-04-07 09:47:42 +02:00
digitalby
dcb15c826c Save jump location before IDE Back/Forward navigation 2026-04-07 09:47:42 +02:00
digitalby
4ddc30055c refactor: Extract ToolWindowPositioningListener to DRY up duplicated subscriptions in ExEntryPanel and OutputPanel 2026-04-07 09:37:19 +02:00
digitalby
20b46279ad Reposition command and output panels on tool window state change 2026-04-07 09:37:19 +02:00
claude[bot]
3653e7f193 Update changelog: Fix pumvisible, Rider/CLion ESC, extension init
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 09:23:15 +02:00
claude[bot]
fd3222dd76 Replace java.lang.Long.toHexString with Kotlin's Int.toString(16) in VimFileBase 2026-04-07 09:12:01 +02:00
claude[bot]
11ca10d10a Replace java.lang.Integer.min with kotlin.math.min in GoToLineCommand 2026-04-07 09:12:01 +02:00
claude[bot]
fa33b264ba Use ExException.code to identify E130 in DelfunctionCommand
Instead of checking e.message.startsWith("E130"), use the dedicated code
field on ExException, which is set by exExceptionMessage() when the exception
is created. This is more robust since it doesn't depend on message formatting.
2026-04-07 09:12:01 +02:00
1grzyb1
46a48b03a1 Return ChangeGroup from VimPLugin to make it compatibile 2026-04-07 08:55:25 +02:00
48 changed files with 611 additions and 159 deletions

View File

@@ -1,50 +0,0 @@
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,5 +33,6 @@
</option>
</GradleProjectSettings>
</option>
<option name="parallelModelFetch" value="true" />
</component>
</project>

View File

@@ -1,3 +1,11 @@
/*
* 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
@@ -6,6 +14,7 @@ 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
@@ -30,6 +39,7 @@ object Project : Project({
buildType(PropertyBased)
buildType(LongRunning)
buildType(RandomOrderTests)
buildType(SplitModeTests)
buildType(Nvim)
buildType(PluginVerifier)

View File

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

View File

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

View File

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

View File

@@ -542,6 +542,10 @@ 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,14 +36,37 @@ 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,6 +9,7 @@
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.
@@ -113,6 +114,15 @@ interface TextObjectScope {
keys: String,
registerDefaultMapping: Boolean = true,
preserveSelectionAnchor: Boolean = true,
rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
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

@@ -444,14 +444,37 @@ 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-55
version=chylex-57
javaVersion=21
remoteRobotVersion=0.11.23
antlrVersion=4.10.1

View File

@@ -22,6 +22,7 @@ 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;
@@ -90,8 +91,8 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
return VimInjectorKt.getInjector().getMotion();
}
public static @NotNull VimChangeGroup getChange() {
return VimInjectorKt.getInjector().getChangeGroup();
public static @NotNull ChangeGroup getChange() {
return ((ChangeGroup)VimInjectorKt.getInjector().getChangeGroup());
}
public static @NotNull VimCommandGroup getCommand() {

View File

@@ -23,6 +23,7 @@ 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
@@ -226,8 +227,9 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent)
val strokeCache = keyStrokeCache
if (defaultKeyStroke != null) {
keyStrokeCache = inputEvent.`when` to defaultKeyStroke
return defaultKeyStroke
val fixedKeyStroke = fixKeyStroke(defaultKeyStroke)
keyStrokeCache = inputEvent.`when` to fixedKeyStroke
return fixedKeyStroke
} else if (strokeCache.first == inputEvent.`when`) {
keyStrokeCache = null to null
return strokeCache.second
@@ -237,6 +239,19 @@ 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) {
@@ -317,6 +332,7 @@ 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

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

View File

@@ -42,6 +42,7 @@ 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

@@ -29,6 +29,7 @@ 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
@@ -87,7 +88,7 @@ fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, end: Int):
}
val removeHighlightsEditors = mutableListOf<Editor>()
val removeHighlightsTimer = Timer(400) {
val removeHighlightsTimer = Timer(450) {
removeHighlightsEditors.forEach(::removeSearchHighlights)
removeHighlightsEditors.clear()
}
@@ -161,7 +162,7 @@ private fun updateSearchHighlights(
if (editor === currentEditor?.ij) {
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
}
highlightSearchResults(editor, pattern, results, currentMatchOffset)
highlightSearchResults(editor, results, currentMatchOffset)
if (!isSearching) {
removeHighlightsEditors.add(editor)
removeHighlightsTimer.restart()
@@ -184,7 +185,7 @@ private fun updateSearchHighlights(
if (result != null) {
if (!it.inVisualMode && !it.inCommandLineModeWithVisual) {
val results = listOf(result)
highlightSearchResults(editor, pattern, results, result.startOffset)
highlightSearchResults(editor, results, result.startOffset)
}
currentMatchOffset = result.startOffset
}
@@ -265,9 +266,18 @@ 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,
) {
@@ -276,38 +286,28 @@ fun highlightSearchResults(
highlighters = mutableListOf()
editor.vimLastHighlighters = highlighters
}
for (range in results) {
val current = range.startOffset == currentMatchOffset
val highlighter = highlightMatch(editor, range.startOffset, range.endOffset, current, pattern)
highlighters.add(highlighter)
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))
}
editor.vimIncsearchCurrentMatchOffset = currentMatchOffset
}
private fun highlightMatch(editor: Editor, start: Int, end: Int, current: Boolean, tooltip: String): RangeHighlighter {
private fun highlightMatch(editor: Editor, start: Int, end: Int, attributes: TextAttributes): RangeHighlighter {
val layer = HighlighterLayer.SELECTION - 1
val targetArea = HighlighterTargetArea.EXACT_RANGE
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
}
return editor.markupModel.addRangeHighlighter(start, end, layer, attributes, targetArea)
}
/**

View File

@@ -29,12 +29,14 @@ 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
@@ -87,6 +89,11 @@ 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
@@ -206,6 +213,20 @@ 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,

View File

@@ -25,10 +25,17 @@ 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, pattern, results, -1)
highlightSearchResults(editor.ij, results, -1)
}
}

View File

@@ -11,6 +11,7 @@ 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
@@ -18,6 +19,7 @@ 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
@@ -64,6 +66,7 @@ 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>()
@@ -91,7 +94,6 @@ 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))
@@ -101,6 +103,7 @@ class OutputPanel private constructor(
val keyListener = OutputPanelKeyListener()
addKeyListener(keyListener)
textPane.addKeyListener(keyListener)
editor?.let { putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, it.component) }
updateUI()
}
@@ -264,6 +267,8 @@ class OutputPanel private constructor(
}
if (glassPane != null) {
glassPane!!.removeComponentListener(resizeAdapter)
toolWindowListenerConnection?.disconnect()
toolWindowListenerConnection = null
glassPane!!.isVisible = false
glassPane!!.remove(this)
glassPane!!.setOpaque(wasOpaque)
@@ -285,6 +290,8 @@ class OutputPanel private constructor(
}
active = true
if (isSingleLine) return
requestFocus(textPane)
}
@@ -300,6 +307,11 @@ 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

@@ -0,0 +1,23 @@
/*
* 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,9 +15,11 @@ 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
@@ -39,6 +41,7 @@ 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
@@ -143,6 +146,11 @@ 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)
@@ -195,6 +203,8 @@ 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)
@@ -510,6 +520,8 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
}
}
private var toolWindowListenerConnection: MessageBusConnection? = null
init {
val layout = GridBagLayout()

View File

@@ -0,0 +1,43 @@
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,4 +1,5 @@
{
"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

@@ -8,12 +8,14 @@
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
@@ -27,6 +29,14 @@ 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(

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2023 The IdeaVim authors
* 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
@@ -65,6 +65,49 @@ 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,4 +169,31 @@ 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,17 +224,6 @@ 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(
@@ -499,7 +488,7 @@ class HistoryCommandTest : VimTestCase() {
@Test
fun `test history search with first number lists single entry from saerch history`() {
fun `test history search with first number lists single entry from search history`() {
repeat(10) { i -> enterSearch("foo${i + 1}") }
injector.outputPanel.getCurrentOutputPanel()?.clearText()
assertCommandOutput(

View File

@@ -26,6 +26,7 @@ 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,7 +17,6 @@ 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,7 +29,6 @@ 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
@@ -125,9 +124,45 @@ abstract class IdeaVimStarterTestBase {
// ── IDE interaction helpers ─────────────────────────────────
/** Opens a file in the editor by relative path. */
/** Opens a file in the editor by relative path and waits until the editor is ready for input. */
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. */
@@ -177,6 +212,27 @@ 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 {
@@ -207,6 +263,23 @@ 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. */
@@ -229,42 +302,48 @@ abstract class IdeaVimStarterTestBase {
// ── Assertions ──────────────────────────────────────────────
/** Asserts the editor contains the given text. */
/** Asserts the editor contains the given text, polling until timeout. */
protected fun assertEditorContains(expected: String, message: String? = null) {
val text = editorText()
assertTrue(text.contains(expected)) {
var text = ""
val found = waitUntil { text = editorText(); text.contains(expected) }
assertTrue(found) {
(message ?: "Editor should contain '$expected'") + ". Actual: $text"
}
}
/** Asserts the editor does NOT contain the given text. */
/** Asserts the editor does NOT contain the given text, polling until timeout. */
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). */
/** Asserts the caret is at the given line (1-based), polling until timeout. */
protected fun assertCaretAtLine(expected: Int, message: String? = null) {
val actual = caretLine()
assertEquals(expected, actual) {
var actual = 0
val found = waitUntil { actual = caretLine(); actual == expected }
assertTrue(found) {
(message ?: "Caret should be at line $expected") + ". Actual line: $actual"
}
}
/** Asserts the caret is before the given line. */
/** Asserts the caret is before the given line, polling until timeout. */
protected fun assertCaretBefore(line: Int, message: String? = null) {
val actual = caretLine()
assertTrue(actual < line) {
var actual = 0
val found = waitUntil { actual = caretLine(); actual < line }
assertTrue(found) {
(message ?: "Caret should be before line $line") + ". Actual line: $actual"
}
}
/** Asserts the caret is past the given line. */
/** Asserts the caret is past the given line, polling until timeout. */
protected fun assertCaretAfter(line: Int, message: String? = null) {
val actual = caretLine()
assertTrue(actual > line) {
var actual = 0
val found = waitUntil { actual = caretLine(); actual > line }
assertTrue(found) {
(message ?: "Caret should be after line $line") + ". Actual line: $actual"
}
}

View File

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

View File

@@ -15,14 +15,12 @@ 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, toHexString(ch.code.toLong()))
injector.messages.showMessage(editor, ch.code.toString(16))
}
override fun displayLocationInfo(editor: VimEditor) {

View File

@@ -150,6 +150,13 @@ 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,6 +248,10 @@ 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,10 +20,18 @@ 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,4 +34,9 @@ interface VimOutputPanelService {
text: String,
messageType: MessageType = MessageType.STANDARD,
)
fun clear(
editor: VimEditor,
context: ExecutionContext,
)
}

View File

@@ -18,4 +18,12 @@ 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.showErrorMessage(editor, e.message)
injector.messages.appendErrorMessage(editor, e.message)
} else {
logger.warn("Failed while executing $unit. " + e.message)
}
} catch (e: NotImplementedError) {
if (indicateErrors) {
injector.messages.showErrorMessage(editor, "Not implemented yet :(")
injector.messages.appendErrorMessage(editor, "Not implemented yet :(")
}
} catch (e: Exception) {
logger.warn(e.toString())

View File

@@ -653,7 +653,9 @@ 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,
@@ -854,7 +856,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
if (lastMatchLine != -1) {
caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, lastMatchLine))
} else {
injector.messages.showStatusBarMessage(null, "E486: Pattern not found: $pattern")
injector.messages.showErrorMessage(editor, "E486: Pattern not found: $pattern")
}
}
@@ -863,7 +865,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
// todo throw multiple exceptions at once
if (exceptions.isNotEmpty()) {
injector.messages.indicateError()
injector.messages.showStatusBarMessage(null, exceptions[0].message)
injector.messages.showErrorMessage(editor, exceptions[0].message)
}
}

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)
patterns.add(injector.searchGroup.lastSearchPattern ?: throw exExceptionMessage("E35"))
directions.add(Direction.FORWARDS)
}
"\\?" -> {
patterns.add(injector.searchGroup.lastSearchPattern)
patterns.add(injector.searchGroup.lastSearchPattern ?: throw exExceptionMessage("E35"))
directions.add(Direction.BACKWARDS)
}
"\\&" -> {
patterns.add(injector.searchGroup.lastSubstitutePattern)
patterns.add(injector.searchGroup.lastSubstitutePattern ?: throw exExceptionMessage("E33"))
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-2023 The IdeaVim authors
* 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
@@ -25,9 +25,7 @@ fun VimEditor.exitVisualMode() {
if (inBlockSelection) {
removeSecondaryCarets()
}
injector.application.runWriteAction {
nativeCarets().forEach(VimCaret::removeSelection)
}
nativeCarets().forEach(VimCaret::removeSelection)
}
if (inVisualMode || inCommandLineModeWithVisual) {
vimLastSelectionType = selectionType

View File

@@ -9,6 +9,7 @@
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
@@ -41,7 +42,7 @@ internal class TextObjectScopeImpl(
keys: String,
registerDefaultMapping: Boolean,
preserveSelectionAnchor: Boolean,
rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
) {
val plugKeys = "<Plug>($pluginName-$keys)"
@@ -88,7 +89,7 @@ private class TextObjectExtensionHandler(
private val listenerOwner: ListenerOwner,
private val mappingOwner: MappingOwner,
private val preserveSelectionAnchor: Boolean,
private val rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
private val rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
) : ExtensionHandler {
override val isRepeatable: Boolean = false
@@ -130,7 +131,7 @@ private class ApiTextObjectActionHandler(
private val listenerOwner: ListenerOwner,
private val mappingOwner: MappingOwner,
override val preserveSelectionAnchor: Boolean,
private val rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
private val rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
) : TextObjectActionHandler() {
// Will be set based on the result of rangeProvider
@@ -149,7 +150,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(count) } ?: return null
val apiRange = kotlinx.coroutines.runBlocking { vimApi.rangeProvider(CaretId(caret.id), count) } ?: return null
// Convert API range to internal TextRange and set visual type
return when (apiRange) {

View File

@@ -41,9 +41,7 @@ data class DelfunctionCommand(
try {
injector.functionService.deleteFunction(name, scope, this)
} catch (e: ExException) {
if (e.message != null && e.message!!.startsWith("E130")) {
// "ignoreIfMissing" flag handles the "E130: Unknown function" exception
} else {
if (e.code != "E130") {
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 java.lang.Integer.min
import kotlin.math.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.showErrorMessage(editor, exceptionsCaught.last().message)
injector.messages.appendErrorMessage(editor, exceptionsCaught.last().message)
}
return returnValue ?: VimInt.ZERO
}