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

Compare commits

..

33 Commits

Author SHA1 Message Date
078ddaf3ca Set plugin version to chylex-56 2026-04-09 21:16:06 +02:00
94a7e1d303 Add 'isactionenabled' function 2026-04-09 21:16:06 +02:00
3de7743f56 Fix Ex commands not working 2026-04-09 19:22:04 +02:00
8636717dea Preserve visual mode after executing IDE action 2026-04-09 19:22:04 +02:00
22dfdd8ca6 Make g0/g^/g$ work with soft wraps 2026-04-09 19:22:03 +02:00
49f9f16f0d Make gj/gk jump over soft wraps 2026-04-09 19:22:03 +02:00
9bfc5d72ce Make camelCase motions adjust based on direction of visual selection 2026-04-09 19:22:03 +02:00
84c227122a Make search highlights temporary 2026-04-09 19:22:03 +02:00
1b9ff4c94a Do not switch to normal mode after inserting a live template 2026-04-09 19:22:03 +02:00
bdecbb5ef0 Exit insert mode after refactoring 2026-04-09 19:22:03 +02:00
7dfd8e6cff Add action to run last macro in all opened files 2026-04-09 19:22:03 +02:00
31e76f0fcf Stop macro execution after a failed search 2026-04-09 19:22:03 +02:00
2aadbdc8f0 Revert per-caret registers 2026-04-09 19:22:03 +02:00
627d65e528 Apply scrolloff after executing native IDEA actions 2026-04-09 19:22:03 +02:00
e77871796e Automatically add unambiguous imports after running a macro 2026-04-09 19:22:03 +02:00
c6e993dcbd Fix(VIM-3986): Exception when pasting register contents containing new line 2026-04-09 19:22:03 +02:00
341ba1ba1f Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2026-04-09 19:22:03 +02:00
f3d7ad55f6 Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2026-04-09 19:22:03 +02:00
5480b99898 Update search register when using f/t 2026-04-09 19:22:03 +02:00
5734a13ea0 Add support for count for visual and line motion surround 2026-04-09 19:22:02 +02:00
582e6bdcd8 Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2026-04-09 19:22:02 +02:00
7414c3d3ed Fix(VIM-696): Restore visual mode after undo/redo, and disable incompatible actions 2026-04-09 19:22:02 +02:00
8fa5bec363 Respect count with <Action> mappings 2026-04-09 19:22:02 +02:00
aea54bdf81 Change matchit plugin to use HTML patterns in unrecognized files 2026-04-09 19:22:02 +02:00
79aca4497e Fix ex command panel causing Undock tool window to hide 2026-04-09 19:22:02 +02:00
50976ea9da Revert "VIM-4120 display multiple lines in OutputPanel with different styles"
This reverts commit 5e20bbf1
2026-04-09 19:22:02 +02:00
57d0ef1dd5 Reset insert mode when switching active editor 2026-04-06 20:42:59 +02:00
d2f017887f Remove notifications about configuration options 2026-04-06 20:42:59 +02:00
cfe196ed30 Remove AI 2026-04-06 20:42:59 +02:00
536942f514 Set custom plugin version 2026-04-06 20:42:59 +02:00
36e3cd1adb Revert "Fix(VIM-4108): Use default ANTLR output directory for Gradle 9+ compatibility"
This reverts commit a476583ea3.
2026-04-06 19:58:55 +02:00
7c874f834a Revert "Upgrade Gradle wrapper to 9.2.1"
This reverts commit 517bda93
2026-04-06 19:58:52 +02:00
a4e963c98e Revert "Fix(VIM-4109): Configure test source sets for Gradle 9+ compatibility"
This reverts commit 5c0d9569d9.
2026-04-06 19:58:46 +02:00
59 changed files with 523 additions and 993 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> </option>
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
<option name="parallelModelFetch" value="true" />
</component> </component>
</project> </project>

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 package _Self
import _Self.buildTypes.Compatibility import _Self.buildTypes.Compatibility
@@ -14,7 +6,6 @@ import _Self.buildTypes.Nvim
import _Self.buildTypes.PluginVerifier import _Self.buildTypes.PluginVerifier
import _Self.buildTypes.PropertyBased import _Self.buildTypes.PropertyBased
import _Self.buildTypes.RandomOrderTests import _Self.buildTypes.RandomOrderTests
import _Self.buildTypes.SplitModeTests
import _Self.buildTypes.TestingBuildType import _Self.buildTypes.TestingBuildType
import _Self.buildTypes.TypeScriptTest import _Self.buildTypes.TypeScriptTest
@@ -39,7 +30,6 @@ object Project : Project({
buildType(PropertyBased) buildType(PropertyBased)
buildType(LongRunning) buildType(LongRunning)
buildType(RandomOrderTests) buildType(RandomOrderTests)
buildType(SplitModeTests)
buildType(Nvim) buildType(Nvim)
buildType(PluginVerifier) 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 package _Self.buildTypes
import _Self.AgentSize import _Self.AgentSize
@@ -19,10 +11,6 @@ object Compatibility : IdeaVimBuildType({
id("IdeaVimCompatibility") id("IdeaVimCompatibility")
name = "IdeaVim compatibility with external plugins" name = "IdeaVim compatibility with external plugins"
failureConditions {
executionTimeoutMin = 180
}
vcs { vcs {
root(DslContext.settingsRoot) root(DslContext.settingsRoot)
branchFilter = "+:<default>" 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 package _Self.buildTypes
import _Self.AgentSize import _Self.AgentSize
@@ -34,7 +26,7 @@ object RandomOrderTests : IdeaVimBuildType({
gradle { gradle {
clearConditions() clearConditions()
tasks = """ tasks = """
clean test test
-x :tests:property-tests:test -x :tests:property-tests:test
-x :tests:long-running-tests:test -x :tests:long-running-tests:test
-Djunit.jupiter.execution.order.random.seed=default -Djunit.jupiter.execution.order.random.seed=default
@@ -42,7 +34,7 @@ object RandomOrderTests : IdeaVimBuildType({
""".trimIndent().replace("\n", " ") """.trimIndent().replace("\n", " ")
buildFile = "" buildFile = ""
enableStacktrace = true enableStacktrace = true
gradleParams = "--no-build-cache --configuration-cache" gradleParams = "--build-cache --configuration-cache"
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto" 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") @file:Suppress("ClassName")
package _Self.buildTypes package _Self.buildTypes
@@ -49,10 +41,10 @@ open class TestingBuildType(
steps { steps {
gradle { gradle {
clearConditions() 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 = "" buildFile = ""
enableStacktrace = true enableStacktrace = true
gradleParams = "--no-build-cache --configuration-cache" gradleParams = "--build-cache --configuration-cache"
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto" jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"
} }
} }

View File

@@ -542,10 +542,6 @@ Contributors:
[![icon][github]](https://github.com/1grzyb1) [![icon][github]](https://github.com/1grzyb1)
&nbsp; &nbsp;
1grzyb1 1grzyb1
* [![icon][mail]](mailto:yury@digitalby.me)
[![icon][github]](https://github.com/digitalby)
&nbsp;
digitalby
Contributors with JetBrains IP: 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 * [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `:set foldlevel` option - control fold visibility level
### Fixes: ### 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-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-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-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-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 high CPU usage while showing command line
* Fixed comparison of String and Number in VimScript expressions * Fixed comparison of String and Number in VimScript expressions
### Merged PRs: ### 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 * [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 * [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 package com.intellij.vim.api.scopes
import com.intellij.vim.api.VimApi import com.intellij.vim.api.VimApi
import com.intellij.vim.api.models.CaretId
/** /**
* Represents the range of a text object selection. * 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. * or null if no valid range is found at the current position.
* The function receives the count (e.g., `2iw` passes count=2). * 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( fun register(
keys: String, keys: String,
registerDefaultMapping: Boolean = true, registerDefaultMapping: Boolean = true,
preserveSelectionAnchor: Boolean = true, preserveSelectionAnchor: Boolean = true,
rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?, rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
) { )
register(keys, registerDefaultMapping, preserveSelectionAnchor) { _, count -> rangeProvider(count) }
}
} }

View File

@@ -444,37 +444,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> * <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> <br>
<b>Fixes:</b><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-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-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-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-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 high CPU usage while showing command line<br>
* Fixed comparison of String and Number in VimScript expressions<br> * Fixed comparison of String and Number in VimScript expressions<br>
<br> <br>
<b>Merged PRs:</b><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/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> * <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> <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 # Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
ideaType=IU ideaType=IU
instrumentPluginCode=true instrumentPluginCode=true
version=chylex-57 version=chylex-56
javaVersion=21 javaVersion=21
remoteRobotVersion=0.11.23 remoteRobotVersion=0.11.23
antlrVersion=4.10.1 antlrVersion=4.10.1

View File

@@ -22,7 +22,6 @@ import com.intellij.openapi.util.Disposer;
import com.maddyhome.idea.vim.api.*; import com.maddyhome.idea.vim.api.*;
import com.maddyhome.idea.vim.config.VimState; import com.maddyhome.idea.vim.config.VimState;
import com.maddyhome.idea.vim.config.migration.ApplicationConfigurationMigrator; 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.KeyGroup;
import com.maddyhome.idea.vim.group.VimNotifications; import com.maddyhome.idea.vim.group.VimNotifications;
import com.maddyhome.idea.vim.group.VimWindowGroup; import com.maddyhome.idea.vim.group.VimWindowGroup;
@@ -91,8 +90,8 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
return VimInjectorKt.getInjector().getMotion(); return VimInjectorKt.getInjector().getMotion();
} }
public static @NotNull ChangeGroup getChange() { public static @NotNull VimChangeGroup getChange() {
return ((ChangeGroup)VimInjectorKt.getInjector().getChangeGroup()); return VimInjectorKt.getInjector().getChangeGroup();
} }
public static @NotNull VimCommandGroup getCommand() { public static @NotNull VimCommandGroup getCommand() {

View File

@@ -23,7 +23,6 @@ import com.intellij.openapi.editor.impl.EditorComponentImpl
import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.KeyStrokeAdapter import com.intellij.ui.KeyStrokeAdapter
import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyHandler
@@ -227,9 +226,8 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent) val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent)
val strokeCache = keyStrokeCache val strokeCache = keyStrokeCache
if (defaultKeyStroke != null) { if (defaultKeyStroke != null) {
val fixedKeyStroke = fixKeyStroke(defaultKeyStroke) keyStrokeCache = inputEvent.`when` to defaultKeyStroke
keyStrokeCache = inputEvent.`when` to fixedKeyStroke return defaultKeyStroke
return fixedKeyStroke
} else if (strokeCache.first == inputEvent.`when`) { } else if (strokeCache.first == inputEvent.`when`) {
keyStrokeCache = null to null keyStrokeCache = null to null
return strokeCache.second return strokeCache.second
@@ -239,19 +237,6 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
return null 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? { private fun getEditor(e: AnActionEvent): Editor? {
return e.getData(PlatformDataKeys.EDITOR) return e.getData(PlatformDataKeys.EDITOR)
?: if (e.getData(PlatformDataKeys.CONTEXT_COMPONENT) is ExTextField) { ?: if (e.getData(PlatformDataKeys.CONTEXT_COMPONENT) is ExTextField) {
@@ -332,7 +317,6 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
).build() ).build()
private const val ACTION_ID = "VimShortcutKeyAction" 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>() private val LOG = logger<VimShortcutKeyAction>()

View File

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

View File

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

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.inCommandLineModeWithVisual
import com.maddyhome.idea.vim.state.mode.inVisualMode import com.maddyhome.idea.vim.state.mode.inVisualMode
import org.jetbrains.annotations.Contract import org.jetbrains.annotations.Contract
import java.awt.Color
import java.awt.Font import java.awt.Font
import java.util.* import java.util.*
import javax.swing.Timer import javax.swing.Timer
@@ -88,7 +87,7 @@ fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, end: Int):
} }
val removeHighlightsEditors = mutableListOf<Editor>() val removeHighlightsEditors = mutableListOf<Editor>()
val removeHighlightsTimer = Timer(450) { val removeHighlightsTimer = Timer(400) {
removeHighlightsEditors.forEach(::removeSearchHighlights) removeHighlightsEditors.forEach(::removeSearchHighlights)
removeHighlightsEditors.clear() removeHighlightsEditors.clear()
} }
@@ -162,7 +161,7 @@ private fun updateSearchHighlights(
if (editor === currentEditor?.ij) { if (editor === currentEditor?.ij) {
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards) currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
} }
highlightSearchResults(editor, results, currentMatchOffset) highlightSearchResults(editor, pattern, results, currentMatchOffset)
if (!isSearching) { if (!isSearching) {
removeHighlightsEditors.add(editor) removeHighlightsEditors.add(editor)
removeHighlightsTimer.restart() removeHighlightsTimer.restart()
@@ -185,7 +184,7 @@ private fun updateSearchHighlights(
if (result != null) { if (result != null) {
if (!it.inVisualMode && !it.inCommandLineModeWithVisual) { if (!it.inVisualMode && !it.inCommandLineModeWithVisual) {
val results = listOf(result) val results = listOf(result)
highlightSearchResults(editor, results, result.startOffset) highlightSearchResults(editor, pattern, results, result.startOffset)
} }
currentMatchOffset = result.startOffset currentMatchOffset = result.startOffset
} }
@@ -266,18 +265,9 @@ private fun findClosestMatch(
return sortedResults[nextIndex % results.size].startOffset 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( fun highlightSearchResults(
editor: Editor, editor: Editor,
pattern: String,
results: List<TextRange>, results: List<TextRange>,
currentMatchOffset: Int, currentMatchOffset: Int,
) { ) {
@@ -286,28 +276,38 @@ fun highlightSearchResults(
highlighters = mutableListOf() highlighters = mutableListOf()
editor.vimLastHighlighters = highlighters editor.vimLastHighlighters = highlighters
} }
for (range in results) {
val allCaretOffsets = editor.caretModel.allCarets.map { it.offset } val current = range.startOffset == currentMatchOffset
val highlighter = highlightMatch(editor, range.startOffset, range.endOffset, current, pattern)
for ((index, range) in results.withIndex()) { highlighters.add(highlighter)
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 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 layer = HighlighterLayer.SELECTION - 1
val targetArea = HighlighterTargetArea.EXACT_RANGE 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.AnActionResult
import com.intellij.openapi.actionSystem.AnActionWrapper import com.intellij.openapi.actionSystem.AnActionWrapper
import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.ex.AnActionListener import com.intellij.openapi.actionSystem.ex.AnActionListener
import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.editor.actions.EnterAction import com.intellij.openapi.editor.actions.EnterAction
import com.intellij.openapi.editor.impl.ScrollingModelImpl import com.intellij.openapi.editor.impl.ScrollingModelImpl
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.keymap.KeymapManager import com.intellij.openapi.keymap.KeymapManager
import com.intellij.openapi.project.DumbAwareToggleAction import com.intellij.openapi.project.DumbAwareToggleAction
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
@@ -89,11 +87,6 @@ internal object IdeaSpecifics {
caretOffset = hostEditor.caretModel.offset caretOffset = hostEditor.caretModel.offset
} }
val actionId = ActionManager.getInstance().getId(action)
if (isGotoAction(actionId)) {
saveJumpBeforeGoto(event, editor)
}
val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction
if (!isVimAction && injector.vimState.mode == Mode.INSERT && action !is EnterAction) { if (!isVimAction && injector.vimState.mode == Mode.INSERT && action !is EnterAction) {
val undoService = injector.undo as VimTimestampBasedUndoService val undoService = injector.undo as VimTimestampBasedUndoService
@@ -213,20 +206,6 @@ internal object IdeaSpecifics {
this.completionData = null 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( private data class CompletionData(
val completionStartMarker: RangeMarker, val completionStartMarker: RangeMarker,
val originalStartOffset: Int, val originalStartOffset: Int,

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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -9,7 +9,9 @@
package com.maddyhome.idea.vim.newapi package com.maddyhome.idea.vim.newapi
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.maddyhome.idea.vim.api.MessageType import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.wm.WindowManager
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMessagesBase import com.maddyhome.idea.vim.api.VimMessagesBase
import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.globalOptions
@@ -23,50 +25,56 @@ internal class IjVimMessages : VimMessagesBase() {
private var message: String? = null private var message: String? = null
private var error = false private var error = false
private var lastBeepTimeMillis = 0L private var lastBeepTimeMillis = 0L
private var allowClearStatusBarMessage = true
override fun showMessage(editor: VimEditor, message: String?) { override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
injector.outputPanel.clear(editor, injector.executionContextManager.getEditorExecutionContext(editor)) fun setStatusBarMessage(project: Project, message: String?) {
showMessageInternal(editor, message, MessageType.STANDARD) WindowManager.getInstance().getStatusBar(project)?.let {
} it.info = if (message.isNullOrBlank()) "" else "Vim - $message"
}
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()
}
private fun showMessageInternal(editor: VimEditor, message: String?, messageType: MessageType) {
this.message = message
if (message.isNullOrBlank()) {
clearStatusBarMessage()
return
} }
val context = injector.executionContextManager.getEditorExecutionContext(editor) this.message = message
injector.outputPanel.output(editor, context, message, messageType)
}
@Suppress("DEPRECATION") val project = editor?.ij?.project
override fun showStatusBarMessage(editor: VimEditor?, message: String?) { if (project != null) {
if (editor != null) { setStatusBarMessage(project, message)
showMessage(editor, message)
} else { } else {
// Legacy path for when editor is null - just store the message // TODO: We really shouldn't set the status bar text for other projects. That's rude.
this.message = message ProjectManager.getInstance().openProjects.forEach {
setStatusBarMessage(it, message)
}
}
// Redraw happens automatically based on changes or scrolling. If we've just set the message (e.g., searching for a
// string, hitting the bottom and scrolling to the top), make sure we don't immediately clear it when scrolling.
allowClearStatusBarMessage = false
ApplicationManager.getApplication().invokeLater {
allowClearStatusBarMessage = true
} }
} }
override fun getStatusBarMessage(): String? = message override fun getStatusBarMessage(): String? = message
// Vim doesn't appear to have a policy about clearing the status bar, other than on "redraw". This can be forced with
// <C-L> or the `:redraw` command, but also happens as the screen changes, e.g., when inserting or deleting lines,
// scrolling, entering Command-line mode and probably lots more. We should manually clear the status bar when these
// things happen.
override fun clearStatusBarMessage() { override fun clearStatusBarMessage() {
if (message.isNullOrEmpty()) return val currentMessage = message
injector.outputPanel.getCurrentOutputPanel()?.close() if (currentMessage.isNullOrEmpty()) return
// Don't clear the status bar message if we've only just set it
if (!allowClearStatusBarMessage) return
ProjectManager.getInstance().openProjects.forEach { project ->
WindowManager.getInstance().getStatusBar(project)?.let { statusBar ->
// Only clear the status bar if it's showing our last message
if (statusBar.info?.contains(currentMessage) == true) {
statusBar.info = ""
}
}
}
message = null message = null
} }

View File

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

View File

@@ -11,15 +11,12 @@ import com.intellij.ide.ui.LafManager
import com.intellij.ide.ui.LafManagerListener import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor 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.IdeBackgroundUtil
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
import com.intellij.ui.ClientProperty import com.intellij.ui.ClientProperty
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBPanel import com.intellij.ui.components.JBPanel
import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.JBScrollPane
import com.intellij.util.IJSwingUtilities import com.intellij.util.IJSwingUtilities
import com.intellij.util.messages.MessageBusConnection
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
@@ -27,6 +24,7 @@ import com.maddyhome.idea.vim.api.MessageType
import com.maddyhome.idea.vim.api.VimOutputPanel import com.maddyhome.idea.vim.api.VimOutputPanel
import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.diagnostic.VimLogger
import com.maddyhome.idea.vim.helper.requestFocus import com.maddyhome.idea.vim.helper.requestFocus
import com.maddyhome.idea.vim.helper.selectEditorFont import com.maddyhome.idea.vim.helper.selectEditorFont
import com.maddyhome.idea.vim.helper.vimMorePanel import com.maddyhome.idea.vim.helper.vimMorePanel
@@ -38,167 +36,121 @@ import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent import java.awt.event.ComponentEvent
import java.awt.event.KeyAdapter import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import java.lang.ref.WeakReference
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JLabel import javax.swing.JLabel
import javax.swing.JRootPane
import javax.swing.JScrollPane import javax.swing.JScrollPane
import javax.swing.JTextPane import javax.swing.JTextArea
import javax.swing.KeyStroke import javax.swing.KeyStroke
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import javax.swing.text.DefaultCaret
import javax.swing.text.SimpleAttributeSet
import javax.swing.text.StyleConstants
import javax.swing.text.StyledDocument
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.min import kotlin.math.min
/** /**
* Panel that displays text in a `more` like window overlaid on the editor. * This panel displays text in a `more` like window and implements [VimOutputPanel].
*/ */
class OutputPanel private constructor( class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), VimOutputPanel {
private val editor: Editor, private val myEditorRef: WeakReference<Editor> = editorRef
) : JBPanel<OutputPanel>(), VimOutputPanel { val editor: Editor? get() = myEditorRef.get()
private val textPane = JTextPane() val myLabel: JLabel = JLabel("more")
private val resizeAdapter: ComponentAdapter private val myText = JTextArea()
private var defaultForeground: Color? = null private val myScrollPane: JScrollPane =
JBScrollPane(myText, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
private val myAdapter: ComponentAdapter
private var myLineHeight = 0
private var glassPane: JComponent? = null private var myOldGlass: JComponent? = null
private var originalLayout: LayoutManager? = null private var myOldLayout: LayoutManager? = null
private var wasOpaque = false private var myWasOpaque = false
private var toolWindowListenerConnection: MessageBusConnection? = null
var active: Boolean = false var myActive: Boolean = false
private val segments = mutableListOf<TextLine>()
private val labelComponent: JLabel = JLabel("more") val isActive: Boolean
private val scrollPane: JScrollPane = get() = myActive
JBScrollPane(textPane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
private var cachedLineHeight = 0
private var isSingleLine = false
init { init {
textPane.isEditable = false // Create a text editor for the text and a label for the prompt
textPane.caret = object : DefaultCaret() { val layout = BorderLayout(0, 0)
override fun setVisible(v: Boolean) { setLayout(layout)
super.setVisible(false) add(myScrollPane, BorderLayout.CENTER)
} add(myLabel, BorderLayout.SOUTH)
}
textPane.highlighter = null
resizeAdapter = object : ComponentAdapter() { // Set the text area read only, and support wrap
myText.isEditable = false
myText.setLineWrap(true)
myAdapter = object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent?) { override fun componentResized(e: ComponentEvent?) {
positionPanel() positionPanel()
} }
} }
// Suppress the fancy frame background used in the Islands theme // Setup some listeners to handle keystrokes
ClientProperty.putRecursive(this, IdeBackgroundUtil.NO_BACKGROUND, true) val moreKeyListener = MoreKeyListener()
addKeyListener(moreKeyListener)
myText.addKeyListener(moreKeyListener)
// Initialize panel // Suppress the fancy frame background used in the Islands theme, which comes from a custom Graphics implementation
setLayout(BorderLayout(0, 0)) // applied to the IdeRoot, and used to paint all children, including this panel. This client property is checked by
add(scrollPane, BorderLayout.CENTER) // JBPanel.getComponentGraphics to give us the original Graphics, opting out of the fancy painting.
add(labelComponent, BorderLayout.SOUTH) ClientProperty.putRecursive<Boolean?>(this, IdeBackgroundUtil.NO_BACKGROUND, true)
val keyListener = OutputPanelKeyListener()
addKeyListener(keyListener)
textPane.addKeyListener(keyListener)
editor?.let { putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, it.component) } editor?.let { putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, it.component) }
updateUI() updateUI()
} }
// Called automatically when the LAF is changed and the component is visible, and manually by the LAF listener handler
override fun updateUI() { override fun updateUI() {
super.updateUI() super.updateUI()
setBorder(ExPanelBorder()) setBorder(ExPanelBorder())
// Swing uses a bad pattern of calling updateUI() from the constructor. At this moment, all these variables are null
@Suppress("SENSELESS_COMPARISON") @Suppress("SENSELESS_COMPARISON")
if (textPane != null && labelComponent != null && scrollPane != null) { if (myText != null && myLabel != null && myScrollPane != null) {
setFontForElements() setFontForElements()
textPane.setBorder(null) myText.setBorder(null)
scrollPane.setBorder(null) myScrollPane.setBorder(null)
labelComponent.setForeground(textPane.getForeground()) myLabel.setForeground(myText.getForeground())
// Make sure the panel is positioned correctly in case we're changing font size
positionPanel() positionPanel()
} }
} }
override var text: String override var text: String
get() = textPane.getText() ?: "" get() = myText.text
set(value) { set(value) {
// ExOutputPanel will strip a trailing newline. We'll do it now so that tests have the same behaviour.
val newValue = value.removeSuffix("\n") val newValue = value.removeSuffix("\n")
segments.clear() myText.text = newValue
if (newValue.isEmpty()) return val ed = editor
segments.add(TextLine(newValue, null)) if (ed != null) {
myText.setFont(selectEditorFont(ed, newValue))
}
myText.setCaretPosition(0)
if (newValue.isNotEmpty()) {
activate()
}
} }
override var label: String override var label: String
get() = labelComponent.text get() = myLabel.text ?: ""
set(value) { set(value) {
labelComponent.text = value myLabel.text = value
val ed = editor
if (ed != null) {
myLabel.setFont(selectEditorFont(ed, value))
}
} }
/**
* Sets styled text with multiple segments, each potentially having a different color.
*/
fun setStyledText(lines: List<TextLine>) {
val doc = textPane.styledDocument
doc.remove(0, doc.length)
if (defaultForeground == null) {
defaultForeground = textPane.foreground
}
if (lines.size > 1) {
setMultiLineText(lines, doc)
} else {
doc.insertString(doc.length, lines[0].text.removeSuffix("\n"), getLineColor(lines[0]))
}
val fullText = doc.getText(0, doc.length)
textPane.setFont(selectEditorFont(editor, fullText))
textPane.setCaretPosition(0)
if (fullText.isNotEmpty()) {
activate()
}
}
private fun setMultiLineText(
lines: List<TextLine>,
doc: StyledDocument,
) {
for ((index, line) in lines.withIndex()) {
val text = line.text.removeSuffix("\n")
val attrs = getLineColor(line)
val separator = if (index < lines.size - 1) "\n" else ""
doc.insertString(doc.length, text + separator, attrs)
}
}
private fun getLineColor(segment: TextLine): SimpleAttributeSet {
val attrs = SimpleAttributeSet()
val color = segment.color ?: defaultForeground
if (color != null) {
StyleConstants.setForeground(attrs, color)
}
return attrs
}
override fun addText(text: String, isNewLine: Boolean, messageType: MessageType) { override fun addText(text: String, isNewLine: Boolean, messageType: MessageType) {
val color = when (messageType) { if (this.text.isNotEmpty() && isNewLine) {
MessageType.ERROR -> JBColor.RED this.text += "\n$text"
MessageType.STANDARD -> null } else {
} this.text += text
segments.add(TextLine(text, color))
}
override fun show() {
val currentPanel = injector.outputPanel.getCurrentOutputPanel()
if (currentPanel != null && currentPanel != this) currentPanel.close()
setStyledText(segments)
if (!active) {
activate()
} }
} }
@@ -207,15 +159,20 @@ class OutputPanel private constructor(
} }
override fun clearText() { override fun clearText() {
segments.clear()
}
fun clear() {
text = "" text = ""
} }
override fun handleKey(key: KeyStroke) { override fun show() {
editor ?: return
val currentPanel = injector.outputPanel.getCurrentOutputPanel()
if (currentPanel != null && currentPanel != this) currentPanel.close()
if (!myActive) {
activate()
}
}
override fun handleKey(key: KeyStroke) {
if (isAtEnd) { if (isAtEnd) {
close(key) close(key)
return return
@@ -240,77 +197,183 @@ class OutputPanel private constructor(
override fun getForeground(): Color? { override fun getForeground(): Color? {
@Suppress("SENSELESS_COMPARISON") @Suppress("SENSELESS_COMPARISON")
if (textPane == null) { if (myText == null) {
// Swing uses a bad pattern of calling getForeground() from the constructor. At this moment, `myText` is null.
return super.getForeground() return super.getForeground()
} }
return textPane.getForeground() return myText.getForeground()
} }
override fun getBackground(): Color? { override fun getBackground(): Color? {
@Suppress("SENSELESS_COMPARISON") @Suppress("SENSELESS_COMPARISON")
if (textPane == null) { if (myText == null) {
// Swing uses a bad pattern of calling getBackground() from the constructor. At this moment, `myText` is null.
return super.getBackground() return super.getBackground()
} }
return textPane.getBackground() return myText.getBackground()
} }
/** /**
* Turns off the output panel and optionally puts the focus back to the original component. * Turns off the ex entry field and optionally puts the focus back to the original component
*/ */
fun deactivate(refocusOwningEditor: Boolean) { fun deactivate(refocusOwningEditor: Boolean) {
if (!active) return if (!myActive) return
active = false myActive = false
clearText() myText.text = ""
textPane.text = "" val ed = editor
if (refocusOwningEditor) { if (refocusOwningEditor && ed != null) {
requestFocus(editor.contentComponent) requestFocus(ed.contentComponent)
} }
if (glassPane != null) { if (myOldGlass != null) {
glassPane!!.removeComponentListener(resizeAdapter) myOldGlass!!.removeComponentListener(myAdapter)
toolWindowListenerConnection?.disconnect() myOldGlass!!.isVisible = false
toolWindowListenerConnection = null myOldGlass!!.remove(this)
glassPane!!.isVisible = false myOldGlass!!.setOpaque(myWasOpaque)
glassPane!!.remove(this) myOldGlass!!.setLayout(myOldLayout)
glassPane!!.setOpaque(wasOpaque)
glassPane!!.setLayout(originalLayout)
} }
} }
/** /**
* Turns on the output panel for the given editor. * Turns on the more window for the given editor
*/ */
fun activate() { fun activate() {
disableOldGlass() val ed = editor ?: return
val root = SwingUtilities.getRootPane(ed.contentComponent)
deactivateOldGlass(root)
setFontForElements() setFontForElements()
positionPanel() positionPanel()
if (glassPane != null) { if (myOldGlass != null) {
glassPane!!.isVisible = true myOldGlass!!.isVisible = true
} }
active = true myActive = true
if (isSingleLine) return requestFocus(myText)
requestFocus(textPane)
} }
private fun disableOldGlass() { private fun deactivateOldGlass(root: JRootPane?) {
val root = SwingUtilities.getRootPane(editor.contentComponent) ?: return if (root == null) return
glassPane = root.getGlassPane() as JComponent? myOldGlass = root.getGlassPane() as JComponent?
if (glassPane == null) { if (myOldGlass != null) {
myOldLayout = myOldGlass!!.layout
myWasOpaque = myOldGlass!!.isOpaque
myOldGlass!!.setLayout(null)
myOldGlass!!.setOpaque(false)
myOldGlass!!.add(this)
myOldGlass!!.addComponentListener(myAdapter)
}
}
private fun setFontForElements() {
val ed = editor ?: return
myText.setFont(selectEditorFont(ed, myText.getText()))
myLabel.setFont(selectEditorFont(ed, myLabel.text))
}
override fun scrollLine() {
scrollOffset(myLineHeight)
}
override fun scrollPage() {
scrollOffset(myScrollPane.getVerticalScrollBar().visibleAmount)
}
override fun scrollHalfPage() {
val sa = myScrollPane.getVerticalScrollBar().visibleAmount / 2.0
val offset = ceil(sa / myLineHeight) * myLineHeight
scrollOffset(offset.toInt())
}
fun onBadKey() {
val ed = editor ?: return
myLabel.setText(injector.messages.message("message.ex.output.more.prompt.full"))
myLabel.setFont(selectEditorFont(ed, myLabel.text))
}
private fun scrollOffset(more: Int) {
val ed = editor ?: return
val `val` = myScrollPane.getVerticalScrollBar().value
myScrollPane.getVerticalScrollBar().setValue(`val` + more)
myScrollPane.getHorizontalScrollBar().setValue(0)
if (isAtEnd) {
myLabel.setText(injector.messages.message("message.ex.output.end.prompt"))
} else {
myLabel.setText(injector.messages.message("message.ex.output.more.prompt"))
}
myLabel.setFont(selectEditorFont(ed, myLabel.text))
}
val isAtEnd: Boolean
get() {
val isSingleLine = myText.getLineCount() == 1
if (isSingleLine) return true
val scrollBar = myScrollPane.getVerticalScrollBar()
val value = scrollBar.value
if (!scrollBar.isVisible) {
return true
}
return value >= scrollBar.maximum - scrollBar.visibleAmount ||
scrollBar.maximum <= scrollBar.visibleAmount
}
private fun positionPanel() {
val ed = editor ?: return
val contentComponent = ed.contentComponent
val scroll = SwingUtilities.getAncestorOfClass(JScrollPane::class.java, contentComponent)
val rootPane = SwingUtilities.getRootPane(contentComponent)
if (scroll == null || rootPane == null) {
// These might be null if we're invoked during component initialisation and before it's been added to the tree
return return
} }
originalLayout = glassPane!!.layout
wasOpaque = glassPane!!.isOpaque size = scroll.size
glassPane!!.setLayout(null)
glassPane!!.setOpaque(false) myLineHeight = myText.getFontMetrics(myText.getFont()).height
glassPane!!.add(this) val count: Int = countLines(myText.getText())
glassPane!!.addComponentListener(resizeAdapter) val visLines = size.height / myLineHeight - 1
val project = editor.project val lines = min(count, visLines)
if (project != null) { setSize(
toolWindowListenerConnection = project.messageBus.connect() size.width,
toolWindowListenerConnection!!.subscribe(ToolWindowManagerListener.TOPIC, ToolWindowPositioningListener { positionPanel() }) lines * myLineHeight + myLabel.getPreferredSize().height + border.getBorderInsets(this).top * 2
)
val height = size.height
val bounds = scroll.bounds
bounds.translate(0, scroll.getHeight() - height)
bounds.height = height
val pos = SwingUtilities.convertPoint(scroll.getParent(), bounds.location, rootPane.getGlassPane())
bounds.location = pos
setBounds(bounds)
myScrollPane.getVerticalScrollBar().setValue(0)
if (!injector.globalOptions().more) {
// FIX
scrollOffset(100000)
} else {
scrollOffset(0)
}
}
fun close(key: KeyStroke? = null) {
val ed = editor ?: return
ApplicationManager.getApplication().invokeLater {
deactivate(true)
val project = ed.project
if (project != null && key != null && key.keyChar != '\n') {
val keys: MutableList<KeyStroke> = ArrayList(1)
keys.add(key)
if (LOG.isTrace()) {
LOG.trace(
"Adding new keys to keyStack as part of playback. State before adding keys: " +
getInstance().keyStack.dump()
)
}
getInstance().keyStack.addKeys(keys)
val context: ExecutionContext =
injector.executionContextManager.getEditorExecutionContext(IjVimEditor(ed))
VimPlugin.getMacro().playbackKeys(IjVimEditor(ed), context, 1)
}
} }
} }
@@ -318,193 +381,30 @@ class OutputPanel private constructor(
close(null) close(null)
} }
fun close(key: KeyStroke?) { private class MoreKeyListener : KeyAdapter() {
val passKeyBack = isSingleLine /**
ApplicationManager.getApplication().invokeLater { * Invoked when a key has been pressed.
deactivate(true) */
val project = editor.project
// For single line messages, pass any key back to the editor (including Enter)
// For multi-line messages, don't pass Enter back (it was used to dismiss)
if (project != null && key != null && (passKeyBack || key.keyChar != '\n')) {
val keys: MutableList<KeyStroke> = ArrayList(1)
keys.add(key)
getInstance().keyStack.addKeys(keys)
val context: ExecutionContext =
injector.executionContextManager.getEditorExecutionContext(IjVimEditor(editor))
VimPlugin.getMacro().playbackKeys(IjVimEditor(editor), context, 1)
}
}
}
private fun setFontForElements() {
textPane.setFont(selectEditorFont(editor, textPane.getText()))
labelComponent.setFont(selectEditorFont(editor, labelComponent.text))
}
private fun positionPanel() {
val scroll = positionPanelStart() ?: return
val lineHeight = textPane.getFontMetrics(textPane.getFont()).height
val count = countLines(textPane.getText())
val visLines = size.height / lineHeight - 1
val lines = min(count, visLines)
// Simple output: single line that fits entirely - no label needed
isSingleLine = count == 1 && count <= visLines
labelComponent.isVisible = !isSingleLine
val extraHeight = if (isSingleLine) 0 else labelComponent.getPreferredSize().height
setSize(
size.width,
lines * lineHeight + extraHeight + border.getBorderInsets(this).top * 2
)
finishPositioning(scroll)
// Force layout so that viewport sizes are valid before checking scroll state
validate()
// onPositioned
cachedLineHeight = lineHeight
scrollPane.getVerticalScrollBar().setValue(0)
if (!isSingleLine) {
if (!injector.globalOptions().more) {
scrollOffset(100000)
} else {
scrollOffset(0)
}
}
}
private fun positionPanelStart(): JScrollPane? {
val contentComponent = editor.contentComponent
val scroll = SwingUtilities.getAncestorOfClass(JScrollPane::class.java, contentComponent) as? JScrollPane
val rootPane = SwingUtilities.getRootPane(contentComponent)
if (scroll == null || rootPane == null) {
return null
}
size = scroll.size
return scroll
}
private fun finishPositioning(scroll: JScrollPane) {
val rootPane = SwingUtilities.getRootPane(editor.contentComponent)
val bounds = scroll.bounds
bounds.translate(0, scroll.getHeight() - size.height)
bounds.height = size.height
val pos = SwingUtilities.convertPoint(scroll.getParent(), bounds.location, rootPane.getGlassPane())
bounds.location = pos
setBounds(bounds)
}
private fun countLines(text: String): Int {
if (text.isEmpty()) {
return 1
}
var count = 0
var pos = -1
while ((text.indexOf('\n', pos + 1).also { pos = it }) != -1) {
count++
}
if (text[text.length - 1] != '\n') {
count++
}
return count
}
override fun scrollLine() {
scrollOffset(cachedLineHeight)
}
override fun scrollPage() {
scrollOffset(scrollPane.getVerticalScrollBar().visibleAmount)
}
override fun scrollHalfPage() {
val sa = scrollPane.getVerticalScrollBar().visibleAmount / 2.0
val offset = ceil(sa / cachedLineHeight) * cachedLineHeight
scrollOffset(offset.toInt())
}
fun onBadKey() {
labelComponent.setText(injector.messages.message("message.ex.output.more.prompt.full"))
labelComponent.setFont(selectEditorFont(editor, labelComponent.text))
}
private fun scrollOffset(more: Int) {
scrollPane.validate()
val scrollBar = scrollPane.getVerticalScrollBar()
val value = scrollBar.value
scrollBar.setValue(value + more)
scrollPane.getHorizontalScrollBar().setValue(0)
// Check if we're at the end or if content fits entirely (nothing to scroll)
if (isAtEnd) {
labelComponent.setText(injector.messages.message("message.ex.output.end.prompt"))
} else {
labelComponent.setText(injector.messages.message("message.ex.output.more.prompt"))
}
labelComponent.setFont(selectEditorFont(editor, labelComponent.text))
}
val isAtEnd: Boolean
get() {
if (isSingleLine) return true
val contentHeight = textPane.preferredSize.height
val viewportHeight = scrollPane.viewport.height
if (contentHeight <= viewportHeight) return true
val scrollBar = scrollPane.getVerticalScrollBar()
return scrollBar.value >= scrollBar.maximum - scrollBar.visibleAmount
}
private inner class OutputPanelKeyListener : KeyAdapter() {
override fun keyTyped(e: KeyEvent) { override fun keyTyped(e: KeyEvent) {
val currentPanel: VimOutputPanel = injector.outputPanel.getCurrentOutputPanel() ?: return val currentPanel: VimOutputPanel = injector.outputPanel.getCurrentOutputPanel() ?: return
val keyChar = e.keyChar val keyCode = e.getKeyCode()
val keyChar = e.getKeyChar()
val modifiers = e.modifiersEx val modifiers = e.modifiersEx
val keyStroke = KeyStroke.getKeyStroke(keyChar, modifiers) val keyStroke = if (keyChar == KeyEvent.CHAR_UNDEFINED)
KeyStroke.getKeyStroke(keyCode, modifiers)
else
KeyStroke.getKeyStroke(keyChar, modifiers)
currentPanel.handleKey(keyStroke) currentPanel.handleKey(keyStroke)
} }
override fun keyPressed(e: KeyEvent) {
if (!e.isActionKey && e.keyCode != KeyEvent.VK_ENTER) return
val currentPanel = injector.outputPanel.getCurrentOutputPanel() as? OutputPanel ?: return
val keyCode = e.keyCode
val modifiers = e.modifiersEx
val keyStroke = KeyStroke.getKeyStroke(keyCode, modifiers)
if (isSingleLine) {
currentPanel.close(keyStroke)
e.consume()
return
}
// Multi-line mode: arrow keys scroll, down/right at end closes
when (keyCode) {
KeyEvent.VK_ENTER -> {
if (currentPanel.isAtEnd) currentPanel.close() else currentPanel.scrollLine()
e.consume()
}
KeyEvent.VK_DOWN -> if (currentPanel.isAtEnd) currentPanel.close(keyStroke) else currentPanel.scrollLine()
KeyEvent.VK_RIGHT -> if (currentPanel.isAtEnd) currentPanel.close(keyStroke) else currentPanel.scrollLine()
KeyEvent.VK_UP -> currentPanel.scrollOffset(-cachedLineHeight)
KeyEvent.VK_LEFT -> currentPanel.scrollOffset(-cachedLineHeight)
KeyEvent.VK_PAGE_DOWN -> if (currentPanel.isAtEnd) currentPanel.close(keyStroke) else currentPanel.scrollPage()
KeyEvent.VK_PAGE_UP -> currentPanel.scrollOffset(-scrollPane.verticalScrollBar.visibleAmount)
}
}
} }
class LafListener : LafManagerListener { class LafListener : LafManagerListener {
override fun lookAndFeelChanged(source: LafManager) { override fun lookAndFeelChanged(source: LafManager) {
if (VimPlugin.isNotEnabled()) return if (VimPlugin.isNotEnabled()) return
// This listener is only invoked for local scenarios, and we only need to update local editor UI. This will invoke
// updateUI on the output pane and it's child components
for (vimEditor in injector.editorGroup.getEditors()) { for (vimEditor in injector.editorGroup.getEditors()) {
val editor = (vimEditor as IjVimEditor).editor val editor = (vimEditor as IjVimEditor).editor
if (!isPanelActive(editor)) continue if (!isPanelActive(editor)) continue
@@ -514,24 +414,41 @@ class OutputPanel private constructor(
} }
companion object { companion object {
private val LOG: VimLogger = injector.getLogger<OutputPanel>(OutputPanel::class.java)
fun getNullablePanel(editor: Editor): OutputPanel? { fun getNullablePanel(editor: Editor): OutputPanel? {
return editor.vimMorePanel as OutputPanel? return editor.vimMorePanel as? OutputPanel
} }
fun isPanelActive(editor: Editor): Boolean { fun isPanelActive(editor: Editor): Boolean {
return getNullablePanel(editor) != null return getNullablePanel(editor)?.myActive ?: false
} }
fun getInstance(editor: Editor): OutputPanel { fun getInstance(editor: Editor): OutputPanel {
var panel: OutputPanel? = getNullablePanel(editor) var panel: OutputPanel? = getNullablePanel(editor)
if (panel == null) { if (panel == null) {
panel = OutputPanel(editor) panel = OutputPanel(WeakReference(editor))
editor.vimMorePanel = panel editor.vimMorePanel = panel
} }
return panel return panel
} }
private fun countLines(text: String): Int {
if (text.isEmpty()) {
return 0
}
var count = 0
var pos = -1
while ((text.indexOf('\n', pos + 1).also { pos = it }) != -1) {
count++
}
if (text[text.length - 1] != '\n') {
count++
}
return count
}
} }
} }
data class TextLine(val text: String, val color: Color?)

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

View File

@@ -22,11 +22,11 @@ class IjOutputPanelService : VimOutputPanelServiceBase() {
private var activeOutputPanel: WeakReference<VimOutputPanel>? = null private var activeOutputPanel: WeakReference<VimOutputPanel>? = null
override fun getCurrentOutputPanel(): VimOutputPanel? { override fun getCurrentOutputPanel(): VimOutputPanel? {
return activeOutputPanel?.get()?.takeIf { (it as OutputPanel).active } return activeOutputPanel?.get()?.takeIf { (it as OutputPanel).isActive }
} }
override fun create(editor: VimEditor, context: ExecutionContext): VimOutputPanel { override fun create(editor: VimEditor, context: ExecutionContext): VimOutputPanel {
val panel = OutputPanel.getInstance(editor.ij) val panel = OutputPanel(WeakReference(editor.ij))
activeOutputPanel = WeakReference(panel) activeOutputPanel = WeakReference(panel)
return panel return panel
} }

View File

@@ -8,14 +8,12 @@
package org.jetbrains.plugins.ideavim.action.motion.search 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.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -29,14 +27,6 @@ class SearchEntryFwdActionTest : VimTestCase() {
assertStatusLineCleared() 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 @Test
fun `search in visual mode`() { fun `search in visual mode`() {
doTest( doTest(

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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * 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 @Test
fun `test last word`() { fun `test last word`() {
doTest( doTest(

View File

@@ -169,31 +169,4 @@ class AddressTest : VimTestCase() {
typeText(commandToKeys("/bar//foo/d")) typeText(commandToKeys("/bar//foo/d"))
assertState("a\nfoo\nbar\nbar\nbaz\n") 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

@@ -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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -134,7 +134,7 @@ class CmdCommandTest : VimTestCase() {
VimPlugin.getCommand().resetAliases() VimPlugin.getCommand().resetAliases()
configureByText("\n") configureByText("\n")
typeText(commandToKeys("command! -range Error echo <args>")) typeText(commandToKeys("command! -range Error echo <args>"))
assertPluginError(true) assertPluginError(false)
kotlin.test.assertEquals("'-range' is not supported by `command`", injector.messages.getStatusBarMessage()) kotlin.test.assertEquals("'-range' is not supported by `command`", injector.messages.getStatusBarMessage())
} }
@@ -143,7 +143,7 @@ class CmdCommandTest : VimTestCase() {
VimPlugin.getCommand().resetAliases() VimPlugin.getCommand().resetAliases()
configureByText("\n") configureByText("\n")
typeText(commandToKeys("command! -complete=color Error echo <args>")) typeText(commandToKeys("command! -complete=color Error echo <args>"))
assertPluginError(true) assertPluginError(false)
kotlin.test.assertEquals("'-complete' is not supported by `command`", injector.messages.getStatusBarMessage()) kotlin.test.assertEquals("'-complete' is not supported by `command`", injector.messages.getStatusBarMessage())
} }

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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -24,6 +24,7 @@ class ExecuteCommandTest : VimTestCase() {
fun `test execute with range`() { fun `test execute with range`() {
configureByText("\n") configureByText("\n")
typeText(commandToKeys("1,2execute 'echo 42'")) typeText(commandToKeys("1,2execute 'echo 42'"))
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
} }

View File

@@ -72,6 +72,7 @@ class HistoryCommandTest : VimTestCase() {
fun `test history with 'history' option set to 0 shows nothing`() { fun `test history with 'history' option set to 0 shows nothing`() {
enterCommand("set history=0") enterCommand("set history=0")
enterCommand("history") enterCommand("history")
assertNoExOutput()
assertPluginError(false) assertPluginError(false)
assertPluginErrorMessage("'history' option is zero") assertPluginErrorMessage("'history' option is zero")
} }
@@ -224,6 +225,17 @@ class HistoryCommandTest : VimTestCase() {
) )
} }
@Test
fun `test history cmd lists empty command history`() {
assertCommandOutput(
"history cmd",
"""
| # cmd history
|> 1 history cmd
""".trimMargin()
)
}
@Test @Test
fun `test history cmd lists current cmd in history`() { fun `test history cmd lists current cmd in history`() {
assertCommandOutput( assertCommandOutput(
@@ -488,7 +500,7 @@ class HistoryCommandTest : VimTestCase() {
@Test @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}") } repeat(10) { i -> enterSearch("foo${i + 1}") }
injector.outputPanel.getCurrentOutputPanel()?.clearText() injector.outputPanel.getCurrentOutputPanel()?.clearText()
assertCommandOutput( assertCommandOutput(

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2026 The IdeaVim authors * Copyright 2003-2025 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -43,6 +43,7 @@ class AndFunctionTest : VimTestCase() {
@Test @Test
fun `test and function with list causes error`() { fun `test and function with list causes error`() {
enterCommand("echo and([1, 2, 3], [2, 3, 4])") enterCommand("echo and([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number") assertPluginErrorMessage("E745: Using a List as a Number")
} }
@@ -50,6 +51,7 @@ class AndFunctionTest : VimTestCase() {
@Test @Test
fun `test and function with dict causes error`() { fun `test and function with dict causes error`() {
enterCommand("echo and({1: 2, 3: 4}, {3: 4, 5: 6})") enterCommand("echo and({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number") assertPluginErrorMessage("E728: Using a Dictionary as a Number")
} }
@@ -57,6 +59,7 @@ class AndFunctionTest : VimTestCase() {
@Test @Test
fun `test and function with float causes error`() { fun `test and function with float causes error`() {
enterCommand("echo and(1.5, 2.5)") enterCommand("echo and(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number") assertPluginErrorMessage("E805: Using a Float as a Number")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2026 The IdeaVim authors * Copyright 2003-2025 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -28,6 +28,7 @@ class InvertFunctionTest : VimTestCase() {
@Test @Test
fun `test invert function with list causes error`() { fun `test invert function with list causes error`() {
enterCommand("echo invert([1, 2, 3])") enterCommand("echo invert([1, 2, 3])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number") assertPluginErrorMessage("E745: Using a List as a Number")
} }
@@ -35,6 +36,7 @@ class InvertFunctionTest : VimTestCase() {
@Test @Test
fun `test invert function with dict causes error`() { fun `test invert function with dict causes error`() {
enterCommand("echo invert({1: 2, 3: 4})") enterCommand("echo invert({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number") assertPluginErrorMessage("E728: Using a Dictionary as a Number")
} }
@@ -42,6 +44,7 @@ class InvertFunctionTest : VimTestCase() {
@Test @Test
fun `test invert function with float causes error`() { fun `test invert function with float causes error`() {
enterCommand("echo invert(1.5)") enterCommand("echo invert(1.5)")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number") assertPluginErrorMessage("E805: Using a Float as a Number")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2026 The IdeaVim authors * Copyright 2003-2025 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -43,6 +43,7 @@ class OrFunctionTest : VimTestCase() {
@Test @Test
fun `test or function with list causes error`() { fun `test or function with list causes error`() {
enterCommand("echo or([1, 2, 3], [2, 3, 4])") enterCommand("echo or([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number") assertPluginErrorMessage("E745: Using a List as a Number")
} }
@@ -50,6 +51,7 @@ class OrFunctionTest : VimTestCase() {
@Test @Test
fun `test or function with dict causes error`() { fun `test or function with dict causes error`() {
enterCommand("echo or({1: 2, 3: 4}, {3: 4, 5: 6})") enterCommand("echo or({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number") assertPluginErrorMessage("E728: Using a Dictionary as a Number")
} }
@@ -57,6 +59,7 @@ class OrFunctionTest : VimTestCase() {
@Test @Test
fun `test or function with float causes error`() { fun `test or function with float causes error`() {
enterCommand("echo or(1.5, 2.5)") enterCommand("echo or(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number") assertPluginErrorMessage("E805: Using a Float as a Number")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2026 The IdeaVim authors * Copyright 2003-2025 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -43,6 +43,7 @@ class XorFunctionTest : VimTestCase() {
@Test @Test
fun `test xor function with list causes error`() { fun `test xor function with list causes error`() {
enterCommand("echo xor([1, 2, 3], [2, 3, 4])") enterCommand("echo xor([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number") assertPluginErrorMessage("E745: Using a List as a Number")
} }
@@ -50,6 +51,7 @@ class XorFunctionTest : VimTestCase() {
@Test @Test
fun `test xor function with dict causes error`() { fun `test xor function with dict causes error`() {
enterCommand("echo xor({1: 2, 3: 4}, {3: 4, 5: 6})") enterCommand("echo xor({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number") assertPluginErrorMessage("E728: Using a Dictionary as a Number")
} }
@@ -57,6 +59,7 @@ class XorFunctionTest : VimTestCase() {
@Test @Test
fun `test xor function with float causes error`() { fun `test xor function with float causes error`() {
enterCommand("echo xor(1.5, 2.5)") enterCommand("echo xor(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number") assertPluginErrorMessage("E805: Using a Float as a Number")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2026 The IdeaVim authors * Copyright 2003-2025 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -33,6 +33,7 @@ class ToLowerFunctionTest : VimTestCase() {
@Test @Test
fun `test tolower with list causes error`() { fun `test tolower with list causes error`() {
enterCommand("echo tolower([1, 2, 3])") enterCommand("echo tolower([1, 2, 3])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E730: Using a List as a String") assertPluginErrorMessage("E730: Using a List as a String")
} }
@@ -40,6 +41,7 @@ class ToLowerFunctionTest : VimTestCase() {
@Test @Test
fun `test tolower with dict causes error`() { fun `test tolower with dict causes error`() {
enterCommand("echo tolower({1: 2, 3: 4})") enterCommand("echo tolower({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E731: Using a Dictionary as a String") assertPluginErrorMessage("E731: Using a Dictionary as a String")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2026 The IdeaVim authors * Copyright 2003-2025 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -33,6 +33,7 @@ class ToUpperFunctionTest : VimTestCase() {
@Test @Test
fun `test toupper with list causes error`() { fun `test toupper with list causes error`() {
enterCommand("echo toupper([1, 2, 3])") enterCommand("echo toupper([1, 2, 3])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E730: Using a List as a String") assertPluginErrorMessage("E730: Using a List as a String")
} }
@@ -40,6 +41,7 @@ class ToUpperFunctionTest : VimTestCase() {
@Test @Test
fun `test toupper with dict causes error`() { fun `test toupper with dict causes error`() {
enterCommand("echo toupper({1: 2, 3: 4})") enterCommand("echo toupper({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E731: Using a Dictionary as a String") assertPluginErrorMessage("E731: Using a Dictionary as a String")
} }

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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -207,12 +207,7 @@ class FunctionDeclarationTest : VimTestCase() {
typeText(commandToKeys("echo F1()")) typeText(commandToKeys("echo F1()"))
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E121: Undefined variable: x") assertPluginErrorMessage("E121: Undefined variable: x")
assertExOutput( assertExOutput("0")
"""
E121: Undefined variable: x
0
""".trimIndent()
)
typeText(commandToKeys("delf! F1")) typeText(commandToKeys("delf! F1"))
typeText(commandToKeys("delf! F2")) typeText(commandToKeys("delf! F2"))

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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -154,12 +154,7 @@ class TryCatchTest : VimTestCase() {
), ),
) )
assertPluginError(true) assertPluginError(true)
assertExOutput( assertExOutput("finally block")
"""
finally block
my exception
""".trimIndent()
)
} }
@Test @Test

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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -223,7 +223,7 @@ class SearchGroupTest : VimTestCase() {
) { ) {
enterCommand("set nowrapscan") enterCommand("set nowrapscan")
} }
assertPluginError(true) assertPluginError(false)
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one") assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
} }
@@ -242,7 +242,7 @@ class SearchGroupTest : VimTestCase() {
three three
""".trimIndent() """.trimIndent()
) )
assertPluginError(true) assertPluginError(false)
assertPluginErrorMessage("E486: Pattern not found: banana") assertPluginErrorMessage("E486: Pattern not found: banana")
} }
@@ -282,7 +282,7 @@ class SearchGroupTest : VimTestCase() {
) { ) {
enterCommand("set nowrapscan") enterCommand("set nowrapscan")
} }
assertPluginError(true) assertPluginError(false)
assertPluginErrorMessage("E384: Search hit TOP without match for: three") assertPluginErrorMessage("E384: Search hit TOP without match for: three")
} }
@@ -301,7 +301,7 @@ class SearchGroupTest : VimTestCase() {
three three
""".trimIndent() """.trimIndent()
) )
assertPluginError(true) assertPluginError(false)
assertPluginErrorMessage("E486: Pattern not found: banana") assertPluginErrorMessage("E486: Pattern not found: banana")
} }
@@ -615,7 +615,7 @@ class SearchGroupTest : VimTestCase() {
) )
enterCommand("set nowrapscan") enterCommand("set nowrapscan")
typeText("10", "/", searchCommand("one")) typeText("10", "/", searchCommand("one"))
assertPluginError(true) assertPluginError(false)
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one") assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
assertPosition(2, 0) assertPosition(2, 0)
} }
@@ -679,7 +679,7 @@ class SearchGroupTest : VimTestCase() {
) )
enterCommand("set nowrapscan") enterCommand("set nowrapscan")
typeText("12", "?one<CR>") typeText("12", "?one<CR>")
assertPluginError(true) assertPluginError(false)
assertPluginErrorMessage("E384: Search hit TOP without match for: one") assertPluginErrorMessage("E384: Search hit TOP without match for: one")
assertPosition(8, 0) assertPosition(8, 0)
} }

View File

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

View File

@@ -17,6 +17,7 @@ class FileOpsSplitTest : IdeaVimStarterTestBase() {
@Test @Test
fun `save file with write command`() { fun `save file with write command`() {
openFile(createFile("src/Save1.txt", "Line 1\nLine 2\nLine 3\n")) openFile(createFile("src/Save1.txt", "Line 1\nLine 2\nLine 3\n"))
clickEditor()
typeVimAndEscape("0iSAVED ") typeVimAndEscape("0iSAVED ")
pause() pause()
exCommand("w") 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.project.LocalProjectInfo
import com.intellij.ide.starter.runner.Starter import com.intellij.ide.starter.runner.Starter
import org.junit.jupiter.api.AfterAll 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.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
@@ -124,45 +125,9 @@ abstract class IdeaVimStarterTestBase {
// ── IDE interaction helpers ───────────────────────────────── // ── 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) { protected fun openFile(relativePath: String) {
driver.withContext { openFile(relativePath) } 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. */ /** 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). */ /** Presses Ctrl-G (file info). */
protected fun ctrlG() { protected fun ctrlG() {
driver.withContext { driver.withContext {
@@ -263,23 +207,6 @@ abstract class IdeaVimStarterTestBase {
Thread.sleep(ms) 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 ──────────────────────────────────── // ── Editor state helpers ────────────────────────────────────
/** Reads the current editor text. */ /** Reads the current editor text. */
@@ -302,48 +229,42 @@ abstract class IdeaVimStarterTestBase {
// ── Assertions ────────────────────────────────────────────── // ── 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) { protected fun assertEditorContains(expected: String, message: String? = null) {
var text = "" val text = editorText()
val found = waitUntil { text = editorText(); text.contains(expected) } assertTrue(text.contains(expected)) {
assertTrue(found) {
(message ?: "Editor should contain '$expected'") + ". Actual: $text" (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) { protected fun assertEditorNotContains(unexpected: String, message: String? = null) {
// Give operations time to settle, then check
pause(1000)
val text = editorText() val text = editorText()
assertFalse(text.contains(unexpected)) { assertFalse(text.contains(unexpected)) {
(message ?: "Editor should not contain '$unexpected'") + ". Actual: $text" (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) { protected fun assertCaretAtLine(expected: Int, message: String? = null) {
var actual = 0 val actual = caretLine()
val found = waitUntil { actual = caretLine(); actual == expected } assertEquals(expected, actual) {
assertTrue(found) {
(message ?: "Caret should be at line $expected") + ". Actual line: $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) { protected fun assertCaretBefore(line: Int, message: String? = null) {
var actual = 0 val actual = caretLine()
val found = waitUntil { actual = caretLine(); actual < line } assertTrue(actual < line) {
assertTrue(found) {
(message ?: "Caret should be before line $line") + ". Actual line: $actual" (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) { protected fun assertCaretAfter(line: Int, message: String? = null) {
var actual = 0 val actual = caretLine()
val found = waitUntil { actual = caretLine(); actual > line } assertTrue(actual > line) {
assertTrue(found) {
(message ?: "Caret should be after line $line") + ". Actual line: $actual" (message ?: "Caret should be after line $line") + ". Actual line: $actual"
} }
} }

View File

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

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.SelectionType.CHARACTER_WISE
import com.maddyhome.idea.vim.state.mode.inVisualMode import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.state.mode.selectionType import com.maddyhome.idea.vim.state.mode.selectionType
import java.lang.Long.toHexString
abstract class VimFileBase : VimFile { abstract class VimFileBase : VimFile {
override fun displayHexInfo(editor: VimEditor) { override fun displayHexInfo(editor: VimEditor) {
val offset = editor.currentCaret().offset val offset = editor.currentCaret().offset
val ch = editor.text()[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) { override fun displayLocationInfo(editor: VimEditor) {

View File

@@ -150,13 +150,6 @@ interface VimMarkService {
*/ */
fun loadLegacyState(element: Any) {} 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 fun isValidMark(char: Char, operation: Operation, isCaretPrimary: Boolean): Boolean
enum class Operation { enum class Operation {

View File

@@ -248,10 +248,6 @@ abstract class VimMarkServiceBase : VimMarkService {
return setMark(caret, mark) 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? { protected open fun createGlobalMark(editor: VimEditor, char: Char, offset: Int): Mark? {
val markChar = char.normalizeMarkChar() val markChar = char.normalizeMarkChar()
if (!markChar.isGlobalMark()) return null if (!markChar.isGlobalMark()) return null

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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -12,33 +12,7 @@ import com.maddyhome.idea.vim.helper.EngineMessageHelper
import org.jetbrains.annotations.PropertyKey import org.jetbrains.annotations.PropertyKey
interface VimMessages { interface VimMessages {
/**
* Displays an informational message to the user.
* The message panel closes on any keystroke and passes the key through to the editor.
*/
fun showMessage(editor: VimEditor, message: String?)
/**
* 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.
*/
@Deprecated("Use showMessage or showErrorMessage instead", ReplaceWith("showMessage(editor, message)"))
fun showStatusBarMessage(editor: VimEditor?, message: String?) fun showStatusBarMessage(editor: VimEditor?, message: String?)
fun getStatusBarMessage(): String? fun getStatusBarMessage(): String?
fun clearStatusBarMessage() fun clearStatusBarMessage()
fun indicateError() fun indicateError()
@@ -54,4 +28,13 @@ interface VimMessages {
fun message(@PropertyKey(resourceBundle = EngineMessageHelper.BUNDLE) key: String, vararg params: Any): String fun message(@PropertyKey(resourceBundle = EngineMessageHelper.BUNDLE) key: String, vararg params: Any): String
fun updateStatusBar(editor: VimEditor) fun updateStatusBar(editor: VimEditor)
fun showMessage(editor: VimEditor, message: String) {
showStatusBarMessage(editor, message)
}
fun showErrorMessage(editor: VimEditor, message: String?) {
showStatusBarMessage(editor, message)
indicateError()
}
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2026 The IdeaVim authors * Copyright 2003-2024 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -29,8 +29,7 @@ interface VimOutputPanel {
* Note: The full text content is not updated in the display until [show] is invoked. * Note: The full text content is not updated in the display until [show] is invoked.
* *
* @param text The text to append. * @param text The text to append.
* @param isNewLine Whether to start the appended text on a new line. * @param isNewLine Whether to start the appended text on a new line. Defaults to true.
* @param messageType The type of message, used to determine text styling.
*/ */
fun addText(text: String, isNewLine: Boolean = true, messageType: MessageType = MessageType.STANDARD) fun addText(text: String, isNewLine: Boolean = true, messageType: MessageType = MessageType.STANDARD)
@@ -52,4 +51,4 @@ interface VimOutputPanel {
fun setContent(text: String) fun setContent(text: String)
fun clearText() fun clearText()
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2026 The IdeaVim authors * Copyright 2003-2024 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -26,17 +26,8 @@ interface VimOutputPanelService {
fun getCurrentOutputPanel(): VimOutputPanel? fun getCurrentOutputPanel(): VimOutputPanel?
/** /**
* Appends text to the existing output panel or creates a new one with the given text and message type. * Appends text to the existing output panel or creates a new one with the given text.
* Basic method that should be sufficient in most cases.
*/ */
fun output( fun output(editor: VimEditor, context: ExecutionContext, text: String)
editor: VimEditor, }
context: ExecutionContext,
text: String,
messageType: MessageType = MessageType.STANDARD,
)
fun clear(
editor: VimEditor,
context: ExecutionContext,
)
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2026 The IdeaVim authors * Copyright 2003-2024 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -13,17 +13,9 @@ abstract class VimOutputPanelServiceBase : VimOutputPanelService {
return getCurrentOutputPanel() ?: create(editor, context) return getCurrentOutputPanel() ?: create(editor, context)
} }
override fun output(editor: VimEditor, context: ExecutionContext, text: String, messageType: MessageType) { override fun output(editor: VimEditor, context: ExecutionContext, text: String) {
val panel = getOrCreate(editor, context) val panel = getOrCreate(editor, context)
panel.addText(text, true, messageType) panel.addText(text)
panel.show() 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 finalResult = ExecutionResult.Error
if (indicateErrors) { if (indicateErrors) {
injector.messages.appendErrorMessage(editor, e.message) injector.messages.showErrorMessage(editor, e.message)
} else { } else {
logger.warn("Failed while executing $unit. " + e.message) logger.warn("Failed while executing $unit. " + e.message)
} }
} catch (e: NotImplementedError) { } catch (e: NotImplementedError) {
if (indicateErrors) { if (indicateErrors) {
injector.messages.appendErrorMessage(editor, "Not implemented yet :(") injector.messages.showErrorMessage(editor, "Not implemented yet :(")
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.warn(e.toString()) logger.warn(e.toString())

View File

@@ -653,9 +653,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
caret.moveToOffset(matchRange.startOffset) caret.moveToOffset(matchRange.startOffset)
val highlight = addSubstitutionConfirmationHighlight(editor, matchRange.startOffset, matchRange.endOffset) val highlight = addSubstitutionConfirmationHighlight(editor, matchRange.startOffset, matchRange.endOffset)
injector.modalInput.create( injector.modalInput.create(
editor, editor, context, injector.messages.message("command.substitute.replace.with.prompt", lineToNextSubstitute.second.second),
context,
injector.messages.message("command.substitute.replace.with.prompt", lineToNextSubstitute.second.second),
SubstituteWithAskInputInterceptor( SubstituteWithAskInputInterceptor(
editor, caret, nextSubstitute, highlight, line, 0, parent, pattern, regex, editor, caret, nextSubstitute, highlight, line, 0, parent, pattern, regex,
oldLastSubstituteString, line2, hasExpression, substituteString, options, oldLastSubstituteString, line2, hasExpression, substituteString, options,
@@ -856,7 +854,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
if (lastMatchLine != -1) { if (lastMatchLine != -1) {
caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, lastMatchLine)) caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, lastMatchLine))
} else { } 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 // todo throw multiple exceptions at once
if (exceptions.isNotEmpty()) { if (exceptions.isNotEmpty()) {
injector.messages.indicateError() injector.messages.indicateError()
injector.messages.showErrorMessage(editor, exceptions[0].message) injector.messages.showStatusBarMessage(null, 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 logger = vimLogger<SearchAddress>()
} }
private val patterns: MutableList<String> = mutableListOf() private val patterns: MutableList<String?> = mutableListOf()
private val directions: MutableList<Direction> = mutableListOf() private val directions: MutableList<Direction> = mutableListOf()
init { init {
@@ -216,17 +216,17 @@ private class SearchAddress(pattern: String, offset: Int, move: Boolean) : Addre
var pat = tok.nextToken() var pat = tok.nextToken()
when (pat) { when (pat) {
"\\/" -> { "\\/" -> {
patterns.add(injector.searchGroup.lastSearchPattern ?: throw exExceptionMessage("E35")) patterns.add(injector.searchGroup.lastSearchPattern)
directions.add(Direction.FORWARDS) directions.add(Direction.FORWARDS)
} }
"\\?" -> { "\\?" -> {
patterns.add(injector.searchGroup.lastSearchPattern ?: throw exExceptionMessage("E35")) patterns.add(injector.searchGroup.lastSearchPattern)
directions.add(Direction.BACKWARDS) directions.add(Direction.BACKWARDS)
} }
"\\&" -> { "\\&" -> {
patterns.add(injector.searchGroup.lastSubstitutePattern ?: throw exExceptionMessage("E33")) patterns.add(injector.searchGroup.lastSubstitutePattern)
directions.add(Direction.FORWARDS) 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 // Note that wrapscan, ignorecase, etc. all come from current option values, as expected
searchOffset = getSearchOffset(editor, line0, direction) 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 (searchOffset == -1) {
if (injector.options(editor).wrapscan) { 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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -25,7 +25,9 @@ fun VimEditor.exitVisualMode() {
if (inBlockSelection) { if (inBlockSelection) {
removeSecondaryCarets() removeSecondaryCarets()
} }
nativeCarets().forEach(VimCaret::removeSelection) injector.application.runWriteAction {
nativeCarets().forEach(VimCaret::removeSelection)
}
} }
if (inVisualMode || inCommandLineModeWithVisual) { if (inVisualMode || inCommandLineModeWithVisual) {
vimLastSelectionType = selectionType vimLastSelectionType = selectionType

View File

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

View File

@@ -41,7 +41,9 @@ data class DelfunctionCommand(
try { try {
injector.functionService.deleteFunction(name, scope, this) injector.functionService.deleteFunction(name, scope, this)
} catch (e: ExException) { } 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 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.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ranges.Range import com.maddyhome.idea.vim.ex.ranges.Range
import com.maddyhome.idea.vim.vimscript.model.ExecutionResult import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
import kotlin.math.min import java.lang.Integer.min
/** /**
* see "h :[range]" * see "h :[range]"

View File

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