1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2026-04-10 07:13:05 +02:00

Compare commits

..

50 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
1grzyb1
46823abcda Fix timing in jump navigation split tests 2026-04-03 12:25:19 +02:00
d85e7dba19 Fix pumvisible returning opposite result
The implementation was broken in ed50fa28f5, which inverted the result but did not invert the condition.
2026-04-03 12:17:47 +02:00
1grzyb1
a9c3277a51 Reset KeyHandler in rider esc lookup 2026-04-03 11:28:24 +02:00
1grzyb1
6e6039c22a Enable lookup listener only in rider/clion 2026-04-03 11:28:24 +02:00
1grzyb1
b49e896b41 Return VimCaret fields back to IjVimCaret
There was compatibility issue with multicursor due to change of return type
2026-04-03 11:14:54 +02:00
1grzyb1
122b066b75 Return KeyGroup from getKey
There was compatibility issue with multicursor due to change of return type
2026-04-03 11:14:54 +02:00
1grzyb1
cb24ac2bfa Restore public fields in IjVimEditor
Some fields where moved to factory and it resulted in compatybility issues with multicursor plugin
2026-04-03 11:14:54 +02:00
1grzyb1
b14324a3e6 Catching initialization exceptions
When external plugin couldn't be initilized and throw exception it resulted in broken ideavim state
2026-04-03 08:37:20 +02:00
1grzyb1
e40a839f52 Fix Escape not exiting insert mode after Ctrl+Space completion in Rider
Octopus is disabled for Rider (VIM-3815), and Rider's LookupSummaryInfo popup causes the popup manager to consume Escape before IdeaVim's action handlers can process it, so we now listen for explicit lookup cancellation via LookupListener to exit insert mode.
2026-04-02 12:27:25 +02:00
1grzyb1
a45cc0891b Don't extend octopus handler in VimEscForRiderHandler
Octopus is disabled for Rider so VimEscForRiderHandler couldn't properly handle esc
2026-04-02 11:02:08 +02:00
1grzyb1
89bad651c0 Add missing frontend module decriptor 2026-04-02 11:02:04 +02:00
dependabot[bot]
5150dc0c9e Bump io.ktor:ktor-client-content-negotiation from 3.4.1 to 3.4.2
Bumps [io.ktor:ktor-client-content-negotiation](https://github.com/ktorio/ktor) from 3.4.1 to 3.4.2.
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.4.1...3.4.2)

---
updated-dependencies:
- dependency-name: io.ktor:ktor-client-content-negotiation
  dependency-version: 3.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 19:54:28 +00:00
dependabot[bot]
c6c7d68876 Bump gradle-wrapper from 9.4.0 to 9.4.1
Bumps [gradle-wrapper](https://github.com/gradle/gradle) from 9.4.0 to 9.4.1.
- [Release notes](https://github.com/gradle/gradle/releases)
- [Commits](https://github.com/gradle/gradle/compare/v9.4.0...v9.4.1)

---
updated-dependencies:
- dependency-name: gradle-wrapper
  dependency-version: 9.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 19:51:49 +00:00
1grzyb1
02130a87c9 Exit search with proper defocus and handle escape 2026-04-01 11:48:43 +02:00
1grzyb1
40ba977e58 Add run configurations for all platforms
To simplify running ideavim with different platforms, this commit introduce run configurations for each platform both in monolith and split mode
2026-04-01 09:43:37 +02:00
1grzyb1
21f304a560 VIM-4135 fix loading rider module 2026-04-01 09:32:32 +02:00
1grzyb1
36e8bd4663 VIM-4016 Fix :edit when project has no source roots
When project couldn't properlly indexed and didn't have source roots it couldn't find file using edit command. So I've modified it to search using absolute paths in project
2026-03-31 09:48:34 +02:00
54 changed files with 978 additions and 536 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@
* https://opensource.org/licenses/MIT.
*/
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
@@ -26,11 +27,11 @@ buildscript {
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.6.0.202603022253-r")
classpath("org.kohsuke:github-api:1.305")
classpath("io.ktor:ktor-client-core:3.4.1")
classpath("io.ktor:ktor-client-cio:3.4.1")
classpath("io.ktor:ktor-client-auth:3.4.1")
classpath("io.ktor:ktor-client-content-negotiation:3.4.1")
classpath("io.ktor:ktor-serialization-kotlinx-json:3.4.1")
classpath("io.ktor:ktor-client-core:3.4.2")
classpath("io.ktor:ktor-client-cio:3.4.2")
classpath("io.ktor:ktor-client-auth:3.4.2")
classpath("io.ktor:ktor-client-content-negotiation:3.4.2")
classpath("io.ktor:ktor-serialization-kotlinx-json:3.4.2")
// This comes from the changelog plugin
// classpath("org.jetbrains:markdown:0.3.1")
@@ -112,7 +113,7 @@ dependencies {
testFramework(TestFrameworkType.Platform)
testFramework(TestFrameworkType.JUnit5)
plugin("com.intellij.classic.ui", "261.22158.185")
compatiblePlugin("com.intellij.classic.ui")
pluginModule(runtimeOnly(project(":modules:ideavim-common")))
pluginModule(runtimeOnly(project(":modules:ideavim-frontend")))
@@ -225,6 +226,30 @@ tasks {
// localPath = file("/Users/{user}/Applications/WebStorm.app")
// }
val runPycharm by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.PyCharmProfessional
version = "2025.3.2"
task {
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
}
}
val runWebstorm by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.WebStorm
version = "2025.3.2"
task {
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
}
}
val runClion by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.CLion
version = "2025.3.2"
task {
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
}
}
val runIdeForUiTests by intellijPlatformTesting.runIde.registering {
task {
jvmArgumentProviders += CommandLineArgumentProvider {
@@ -247,6 +272,55 @@ tasks {
val runIdeSplitMode by intellijPlatformTesting.runIde.registering {
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
}
val runWebstormSplitMode by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.WebStorm
version = "2025.3.2"
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
}
val runRider by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.Rider
version = "2026.1"
task {
systemProperty("idea.log.debug.categories", "com.maddyhome.idea.vim.handler.EditorHandlersChainLogger")
}
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
}
val runCLionSplitMode by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.CLion
version = "2025.3.2"
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
}
val runPycharmSplitMode by intellijPlatformTesting.runIde.registering {
type = IntelliJPlatformType.PyCharmProfessional
version = "2025.3.2"
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
}
// Run split mode with a JDWP debug agent on the frontend (JetBrains Client) process.
@@ -255,6 +329,11 @@ tasks {
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
prepareSandboxTask {
val sandboxDir = project.layout.buildDirectory.dir("idea-sandbox").map { it.asFile }
doLast {
@@ -286,6 +365,12 @@ tasks {
val testIdeSplitMode by intellijPlatformTesting.testIde.registering {
splitMode = true
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
plugins {
plugin("AceJump", "3.8.22")
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
}
task {
useJUnitPlatform()
}

View File

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

2
gradlew vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,6 +50,8 @@ import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl
import com.maddyhome.idea.vim.helper.exitSelectMode
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
import com.maddyhome.idea.vim.ide.isClionNova
import com.maddyhome.idea.vim.ide.isRider
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.initInjector
import com.maddyhome.idea.vim.newapi.vim
@@ -350,6 +352,16 @@ internal object IdeaSpecifics {
if (newLookup.editor.isIdeaVimDisabledHere) return
(VimPlugin.getKey() as VimKeyGroupBase).registerShortcutsForLookup(newLookup)
// In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
// (due to LookupSummaryInfo popup) before the action system runs, so IdeaVim never sees it.
// Listen for explicit lookup cancellation (Escape) to exit insert mode.
// Note: we check isRider/isClionNova specifically, not !isOctopusEnabled(), because
// JetBrains Client (split mode) also has octopus disabled but doesn't need this workaround,
// and isCanceledExplicitly can be true for non-Escape keys (e.g. space) in that environment.
if (isRider() || isClionNova()) {
newLookup.addLookupListener(RiderEscLookupListener(newLookup.editor))
}
}
// Lookup closed
@@ -361,6 +373,20 @@ internal object IdeaSpecifics {
}
}
}
/**
* In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
* (due to LookupSummaryInfo parameter info popup) before the action system runs, so IdeaVim never sees it.
* This listener exits insert mode when the lookup is explicitly cancelled (Escape).
*/
private class RiderEscLookupListener(private val editor: Editor) : com.intellij.codeInsight.lookup.LookupListener {
override fun lookupCanceled(event: com.intellij.codeInsight.lookup.LookupEvent) {
if (event.isCanceledExplicitly && editor.vim.mode is Mode.INSERT) {
editor.vim.exitInsertMode(injector.executionContextManager.getEditorExecutionContext(editor.vim))
KeyHandler.getInstance().reset(editor.vim)
}
}
}
//endregion
//region Hide Vim search highlights when showing IntelliJ search results

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -9,7 +9,9 @@
package com.maddyhome.idea.vim.newapi
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.VimMessagesBase
import com.maddyhome.idea.vim.api.globalOptions
@@ -23,43 +25,56 @@ internal class IjVimMessages : VimMessagesBase() {
private var message: String? = null
private var error = false
private var lastBeepTimeMillis = 0L
private var allowClearStatusBarMessage = true
override fun showMessage(editor: VimEditor, message: String?) {
showMessageInternal(editor, message, MessageType.STANDARD)
}
override fun showErrorMessage(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
override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
fun setStatusBarMessage(project: Project, message: String?) {
WindowManager.getInstance().getStatusBar(project)?.let {
it.info = if (message.isNullOrBlank()) "" else "Vim - $message"
}
}
val context = injector.executionContextManager.getEditorExecutionContext(editor)
injector.outputPanel.output(editor, context, message, messageType)
}
this.message = message
@Suppress("DEPRECATION")
override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
if (editor != null) {
showMessage(editor, message)
val project = editor?.ij?.project
if (project != null) {
setStatusBarMessage(project, message)
} else {
// Legacy path for when editor is null - just store the message
this.message = message
// TODO: We really shouldn't set the status bar text for other projects. That's rude.
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
// 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() {
if (message.isNullOrEmpty()) return
injector.outputPanel.getCurrentOutputPanel()?.close()
val currentMessage = message
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
}

View File

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

View File

@@ -14,7 +14,6 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.wm.impl.IdeBackgroundUtil
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
import com.intellij.ui.ClientProperty
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBPanel
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.IJSwingUtilities
@@ -25,6 +24,7 @@ import com.maddyhome.idea.vim.api.MessageType
import com.maddyhome.idea.vim.api.VimOutputPanel
import com.maddyhome.idea.vim.api.globalOptions
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.selectEditorFont
import com.maddyhome.idea.vim.helper.vimMorePanel
@@ -36,166 +36,121 @@ import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.lang.ref.WeakReference
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JRootPane
import javax.swing.JScrollPane
import javax.swing.JTextPane
import javax.swing.JTextArea
import javax.swing.KeyStroke
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.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(
private val editor: Editor,
) : JBPanel<OutputPanel>(), VimOutputPanel {
class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), VimOutputPanel {
private val myEditorRef: WeakReference<Editor> = editorRef
val editor: Editor? get() = myEditorRef.get()
private val textPane = JTextPane()
private val resizeAdapter: ComponentAdapter
private var defaultForeground: Color? = null
val myLabel: JLabel = JLabel("more")
private val myText = JTextArea()
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 originalLayout: LayoutManager? = null
private var wasOpaque = false
private var myOldGlass: JComponent? = null
private var myOldLayout: LayoutManager? = null
private var myWasOpaque = false
var active: Boolean = false
private val segments = mutableListOf<TextLine>()
var myActive: Boolean = false
private val labelComponent: JLabel = JLabel("more")
private val scrollPane: JScrollPane =
JBScrollPane(textPane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
private var cachedLineHeight = 0
private var isSingleLine = false
val isActive: Boolean
get() = myActive
init {
textPane.isEditable = false
textPane.caret = object : DefaultCaret() {
override fun setVisible(v: Boolean) {
super.setVisible(false)
}
}
textPane.highlighter = null
// Create a text editor for the text and a label for the prompt
val layout = BorderLayout(0, 0)
setLayout(layout)
add(myScrollPane, BorderLayout.CENTER)
add(myLabel, BorderLayout.SOUTH)
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?) {
positionPanel()
}
}
// Suppress the fancy frame background used in the Islands theme
ClientProperty.putRecursive(this, IdeBackgroundUtil.NO_BACKGROUND, true)
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, editor.component)
// Setup some listeners to handle keystrokes
val moreKeyListener = MoreKeyListener()
addKeyListener(moreKeyListener)
myText.addKeyListener(moreKeyListener)
// Initialize panel
setLayout(BorderLayout(0, 0))
add(scrollPane, BorderLayout.CENTER)
add(labelComponent, BorderLayout.SOUTH)
val keyListener = OutputPanelKeyListener()
addKeyListener(keyListener)
textPane.addKeyListener(keyListener)
// Suppress the fancy frame background used in the Islands theme, which comes from a custom Graphics implementation
// applied to the IdeRoot, and used to paint all children, including this panel. This client property is checked by
// JBPanel.getComponentGraphics to give us the original Graphics, opting out of the fancy painting.
ClientProperty.putRecursive<Boolean?>(this, IdeBackgroundUtil.NO_BACKGROUND, true)
editor?.let { putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, it.component) }
updateUI()
}
// Called automatically when the LAF is changed and the component is visible, and manually by the LAF listener handler
override fun updateUI() {
super.updateUI()
setBorder(ExPanelBorder())
// Swing uses a bad pattern of calling updateUI() from the constructor. At this moment, all these variables are null
@Suppress("SENSELESS_COMPARISON")
if (textPane != null && labelComponent != null && scrollPane != null) {
if (myText != null && myLabel != null && myScrollPane != null) {
setFontForElements()
textPane.setBorder(null)
scrollPane.setBorder(null)
labelComponent.setForeground(textPane.getForeground())
myText.setBorder(null)
myScrollPane.setBorder(null)
myLabel.setForeground(myText.getForeground())
// Make sure the panel is positioned correctly in case we're changing font size
positionPanel()
}
}
override var text: String
get() = textPane.getText() ?: ""
get() = myText.text
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")
segments.clear()
if (newValue.isEmpty()) return
segments.add(TextLine(newValue, null))
myText.text = newValue
val ed = editor
if (ed != null) {
myText.setFont(selectEditorFont(ed, newValue))
}
myText.setCaretPosition(0)
if (newValue.isNotEmpty()) {
activate()
}
}
override var label: String
get() = labelComponent.text
get() = myLabel.text ?: ""
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) {
val color = when (messageType) {
MessageType.ERROR -> JBColor.RED
MessageType.STANDARD -> null
}
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()
if (this.text.isNotEmpty() && isNewLine) {
this.text += "\n$text"
} else {
this.text += text
}
}
@@ -204,15 +159,20 @@ class OutputPanel private constructor(
}
override fun clearText() {
segments.clear()
}
fun clear() {
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) {
close(key)
return
@@ -237,262 +197,214 @@ class OutputPanel private constructor(
override fun getForeground(): Color? {
@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 textPane.getForeground()
return myText.getForeground()
}
override fun getBackground(): Color? {
@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 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) {
if (!active) return
active = false
clearText()
textPane.text = ""
if (refocusOwningEditor) {
requestFocus(editor.contentComponent)
if (!myActive) return
myActive = false
myText.text = ""
val ed = editor
if (refocusOwningEditor && ed != null) {
requestFocus(ed.contentComponent)
}
if (glassPane != null) {
glassPane!!.removeComponentListener(resizeAdapter)
glassPane!!.isVisible = false
glassPane!!.remove(this)
glassPane!!.setOpaque(wasOpaque)
glassPane!!.setLayout(originalLayout)
if (myOldGlass != null) {
myOldGlass!!.removeComponentListener(myAdapter)
myOldGlass!!.isVisible = false
myOldGlass!!.remove(this)
myOldGlass!!.setOpaque(myWasOpaque)
myOldGlass!!.setLayout(myOldLayout)
}
}
/**
* Turns on the output panel for the given editor.
* Turns on the more window for the given editor
*/
fun activate() {
disableOldGlass()
val ed = editor ?: return
val root = SwingUtilities.getRootPane(ed.contentComponent)
deactivateOldGlass(root)
setFontForElements()
positionPanel()
if (glassPane != null) {
glassPane!!.isVisible = true
if (myOldGlass != null) {
myOldGlass!!.isVisible = true
}
active = true
requestFocus(textPane)
myActive = true
requestFocus(myText)
}
private fun disableOldGlass() {
val root = SwingUtilities.getRootPane(editor.contentComponent) ?: return
glassPane = root.getGlassPane() as JComponent?
if (glassPane == null) {
private fun deactivateOldGlass(root: JRootPane?) {
if (root == null) return
myOldGlass = root.getGlassPane() as JComponent?
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
}
originalLayout = glassPane!!.layout
wasOpaque = glassPane!!.isOpaque
glassPane!!.setLayout(null)
glassPane!!.setOpaque(false)
glassPane!!.add(this)
glassPane!!.addComponentListener(resizeAdapter)
size = scroll.size
myLineHeight = myText.getFontMetrics(myText.getFont()).height
val count: Int = countLines(myText.getText())
val visLines = size.height / myLineHeight - 1
val lines = min(count, visLines)
setSize(
size.width,
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)
}
}
}
override fun close() {
close(null)
}
fun close(key: KeyStroke?) {
val passKeyBack = isSingleLine
ApplicationManager.getApplication().invokeLater {
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() {
private class MoreKeyListener : KeyAdapter() {
/**
* Invoked when a key has been pressed.
*/
override fun keyTyped(e: KeyEvent) {
val currentPanel: VimOutputPanel = injector.outputPanel.getCurrentOutputPanel() ?: return
val keyChar = e.keyChar
val keyCode = e.getKeyCode()
val keyChar = e.getKeyChar()
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)
}
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 {
override fun lookAndFeelChanged(source: LafManager) {
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()) {
val editor = (vimEditor as IjVimEditor).editor
if (!isPanelActive(editor)) continue
@@ -502,24 +414,41 @@ class OutputPanel private constructor(
}
companion object {
private val LOG: VimLogger = injector.getLogger<OutputPanel>(OutputPanel::class.java)
fun getNullablePanel(editor: Editor): OutputPanel? {
return editor.vimMorePanel as OutputPanel?
return editor.vimMorePanel as? OutputPanel
}
fun isPanelActive(editor: Editor): Boolean {
return getNullablePanel(editor) != null
return getNullablePanel(editor)?.myActive ?: false
}
fun getInstance(editor: Editor): OutputPanel {
var panel: OutputPanel? = getNullablePanel(editor)
if (panel == null) {
panel = OutputPanel(editor)
panel = OutputPanel(WeakReference(editor))
editor.vimMorePanel = 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

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

View File

@@ -22,11 +22,11 @@ class IjOutputPanelService : VimOutputPanelServiceBase() {
private var activeOutputPanel: WeakReference<VimOutputPanel>? = null
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 {
val panel = OutputPanel.getInstance(editor.ij)
val panel = OutputPanel(WeakReference(editor.ij))
activeOutputPanel = WeakReference(panel)
return panel
}

View File

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

View File

@@ -0,0 +1,17 @@
<!--
~ Copyright 2003-2026 The IdeaVim authors
~
~ Use of this source code is governed by an MIT-style
~ license that can be found in the LICENSE.txt file or at
~ https://opensource.org/licenses/MIT.
-->
<!-- Split-mode (Remote Dev) specific registrations.
This module only loads when intellij.platform.frontend.split is available,
which provides access to intellij.rd.client and its extension points. -->
<idea-plugin package="com.maddyhome.idea.vim">
<dependencies>
<module name="intellij.platform.frontend.split"/>
<module name="IdeaVIM.ideavim-frontend"/>
</dependencies>
</idea-plugin>

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2023 The IdeaVim authors
* Copyright 2003-2026 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -10,9 +10,12 @@ package org.jetbrains.plugins.ideavim.action.motion.search
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class SearchEntryFwdActionTest : VimTestCase() {
@Test
@@ -110,6 +113,23 @@ class SearchEntryFwdActionTest : VimTestCase() {
)
}
@Test
fun `test escape after search not found closes panel without inserting escape char`() {
configureByText("lorem ipsum dolor sit amet")
typeText("/notfound")
val panel = ExEntryPanel.getOrCreatePanelInstance()
assertTrue(panel.isActive)
typeText("<Esc>")
assertFalse(panel.isActive)
assertMode(Mode.NORMAL())
// The panel text should not contain ^[ (escape character written as text)
assertFalse(panel.text.contains("\u001B"), "Panel text should not contain escape character")
assertFalse(panel.text.contains("^["), "Panel text should not contain ^[ literal")
}
@Disabled("Ctrl-o doesn't work yet in select mode")
@Test
fun `search in one time from select mode`() {

View File

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

View File

@@ -72,6 +72,7 @@ class HistoryCommandTest : VimTestCase() {
fun `test history with 'history' option set to 0 shows nothing`() {
enterCommand("set history=0")
enterCommand("history")
assertNoExOutput()
assertPluginError(false)
assertPluginErrorMessage("'history' option is zero")
}

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
* license that can be found in the LICENSE.txt file or at
@@ -43,6 +43,7 @@ class AndFunctionTest : VimTestCase() {
@Test
fun `test and function with list causes error`() {
enterCommand("echo and([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number")
}
@@ -50,6 +51,7 @@ class AndFunctionTest : VimTestCase() {
@Test
fun `test and function with dict causes error`() {
enterCommand("echo and({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
}
@@ -57,6 +59,7 @@ class AndFunctionTest : VimTestCase() {
@Test
fun `test and function with float causes error`() {
enterCommand("echo and(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true)
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
* license that can be found in the LICENSE.txt file or at
@@ -28,6 +28,7 @@ class InvertFunctionTest : VimTestCase() {
@Test
fun `test invert function with list causes error`() {
enterCommand("echo invert([1, 2, 3])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number")
}
@@ -35,6 +36,7 @@ class InvertFunctionTest : VimTestCase() {
@Test
fun `test invert function with dict causes error`() {
enterCommand("echo invert({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
}
@@ -42,6 +44,7 @@ class InvertFunctionTest : VimTestCase() {
@Test
fun `test invert function with float causes error`() {
enterCommand("echo invert(1.5)")
assertNoExOutput()
assertPluginError(true)
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
* license that can be found in the LICENSE.txt file or at
@@ -43,6 +43,7 @@ class OrFunctionTest : VimTestCase() {
@Test
fun `test or function with list causes error`() {
enterCommand("echo or([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number")
}
@@ -50,6 +51,7 @@ class OrFunctionTest : VimTestCase() {
@Test
fun `test or function with dict causes error`() {
enterCommand("echo or({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
}
@@ -57,6 +59,7 @@ class OrFunctionTest : VimTestCase() {
@Test
fun `test or function with float causes error`() {
enterCommand("echo or(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true)
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
* license that can be found in the LICENSE.txt file or at
@@ -43,6 +43,7 @@ class XorFunctionTest : VimTestCase() {
@Test
fun `test xor function with list causes error`() {
enterCommand("echo xor([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number")
}
@@ -50,6 +51,7 @@ class XorFunctionTest : VimTestCase() {
@Test
fun `test xor function with dict causes error`() {
enterCommand("echo xor({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
}
@@ -57,6 +59,7 @@ class XorFunctionTest : VimTestCase() {
@Test
fun `test xor function with float causes error`() {
enterCommand("echo xor(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true)
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
* license that can be found in the LICENSE.txt file or at
@@ -33,6 +33,7 @@ class ToLowerFunctionTest : VimTestCase() {
@Test
fun `test tolower with list causes error`() {
enterCommand("echo tolower([1, 2, 3])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E730: Using a List as a String")
}
@@ -40,6 +41,7 @@ class ToLowerFunctionTest : VimTestCase() {
@Test
fun `test tolower with dict causes error`() {
enterCommand("echo tolower({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true)
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
* license that can be found in the LICENSE.txt file or at
@@ -33,6 +33,7 @@ class ToUpperFunctionTest : VimTestCase() {
@Test
fun `test toupper with list causes error`() {
enterCommand("echo toupper([1, 2, 3])")
assertNoExOutput()
assertPluginError(true)
assertPluginErrorMessage("E730: Using a List as a String")
}
@@ -40,6 +41,7 @@ class ToUpperFunctionTest : VimTestCase() {
@Test
fun `test toupper with dict causes error`() {
enterCommand("echo toupper({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true)
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
* license that can be found in the LICENSE.txt file or at
@@ -207,12 +207,7 @@ class FunctionDeclarationTest : VimTestCase() {
typeText(commandToKeys("echo F1()"))
assertPluginError(true)
assertPluginErrorMessage("E121: Undefined variable: x")
assertExOutput(
"""
E121: Undefined variable: x
0
""".trimIndent()
)
assertExOutput("0")
typeText(commandToKeys("delf! F1"))
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
* license that can be found in the LICENSE.txt file or at
@@ -154,12 +154,7 @@ class TryCatchTest : VimTestCase() {
),
)
assertPluginError(true)
assertExOutput(
"""
finally block
my exception
""".trimIndent()
)
assertExOutput("finally block")
}
@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
* license that can be found in the LICENSE.txt file or at
@@ -223,7 +223,7 @@ class SearchGroupTest : VimTestCase() {
) {
enterCommand("set nowrapscan")
}
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
}
@@ -242,7 +242,7 @@ class SearchGroupTest : VimTestCase() {
three
""".trimIndent()
)
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E486: Pattern not found: banana")
}
@@ -282,7 +282,7 @@ class SearchGroupTest : VimTestCase() {
) {
enterCommand("set nowrapscan")
}
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E384: Search hit TOP without match for: three")
}
@@ -301,7 +301,7 @@ class SearchGroupTest : VimTestCase() {
three
""".trimIndent()
)
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E486: Pattern not found: banana")
}
@@ -615,7 +615,7 @@ class SearchGroupTest : VimTestCase() {
)
enterCommand("set nowrapscan")
typeText("10", "/", searchCommand("one"))
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
assertPosition(2, 0)
}
@@ -679,7 +679,7 @@ class SearchGroupTest : VimTestCase() {
)
enterCommand("set nowrapscan")
typeText("12", "?one<CR>")
assertPluginError(true)
assertPluginError(false)
assertPluginErrorMessage("E384: Search hit TOP without match for: one")
assertPosition(8, 0)
}

View File

@@ -22,11 +22,11 @@ class JumpNavigationSplitTest : IdeaVimStarterTestBase() {
openFile(longFile("Jump1"))
typeVim("G")
pause(300)
pause(500)
assertCaretAfter(40, "G should go to end of file")
ctrlO()
pause(300)
pause(500)
assertCaretBefore(10, "Ctrl-O should jump back to start")
}
@@ -35,7 +35,7 @@ class JumpNavigationSplitTest : IdeaVimStarterTestBase() {
openFile(longFile("Jump2"))
typeVim("gg")
pause(300)
pause(500)
typeVim("/Line 30\n")
pause()

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2026 The IdeaVim authors
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -12,25 +12,7 @@ import com.maddyhome.idea.vim.helper.EngineMessageHelper
import org.jetbrains.annotations.PropertyKey
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).
* The message panel closes on any keystroke and passes the key through to the editor.
*/
fun showErrorMessage(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 getStatusBarMessage(): String?
fun clearStatusBarMessage()
fun indicateError()
@@ -46,4 +28,13 @@ interface VimMessages {
fun message(@PropertyKey(resourceBundle = EngineMessageHelper.BUNDLE) key: String, vararg params: Any): String
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
* 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.
*
* @param text The text to append.
* @param isNewLine Whether to start the appended text on a new line.
* @param messageType The type of message, used to determine text styling.
* @param isNewLine Whether to start the appended text on a new line. Defaults to true.
*/
fun addText(text: String, isNewLine: Boolean = true, messageType: MessageType = MessageType.STANDARD)
@@ -52,4 +51,4 @@ interface VimOutputPanel {
fun setContent(text: String)
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
* license that can be found in the LICENSE.txt file or at
@@ -26,12 +26,8 @@ interface VimOutputPanelService {
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(
editor: VimEditor,
context: ExecutionContext,
text: String,
messageType: MessageType = MessageType.STANDARD,
)
}
fun output(editor: VimEditor, context: ExecutionContext, text: String)
}

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
* license that can be found in the LICENSE.txt file or at
@@ -13,9 +13,9 @@ abstract class VimOutputPanelServiceBase : VimOutputPanelService {
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)
panel.addText(text, true, messageType)
panel.addText(text)
panel.show()
}
}
}

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2023 The IdeaVim authors
* Copyright 2003-2026 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
@@ -18,3 +18,13 @@ fun KeyStroke.isCloseKeyStroke(): Boolean {
keyCode == KeyEvent.VK_C && modifiers and InputEvent.CTRL_DOWN_MASK != 0 ||
keyCode == '['.code && modifiers and InputEvent.CTRL_DOWN_MASK != 0
}
/**
* Returns true if this character would be matched as a command-line action (close or execute) rather than text input
* when re-injected through the key handler in CMD_LINE mode.
*
* Escape closes the command line, Enter/CR executes it.
*/
fun Char.isCommandLineActionChar(): Boolean {
return this == '\u001B' || this == '\n' || this == '\r'
}

View File

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

View File

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