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

Compare commits

..

29 Commits

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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-56
version=chylex-53
javaVersion=21
remoteRobotVersion=0.11.23
antlrVersion=4.10.1

2
gradlew vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,9 +49,8 @@ import com.maddyhome.idea.vim.group.NotificationService
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.hasVisualSelection
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
@@ -334,6 +333,23 @@ internal object IdeaSpecifics {
vimEditor.exitMode()
vimEditor.mode = Mode.NORMAL()
}
} else {
// IdeaSelectionControl will not be called if we're moving to a new variable with no change in selection.
// And if we're moving to the end of the template, the change in selection will reset us to Normal because
// IdeaSelectionControl will be called when the template is no longer active.
if ((!editor.selectionModel.hasSelection() && !vimEditor.mode.hasVisualSelection) || newIndex == -1) {
if (vimEditor.isIdeaRefactorModeSelect) {
if (vimEditor.mode !is Mode.INSERT) {
vimEditor.exitMode()
injector.application.runReadAction {
val context = injector.executionContextManager.getEditorExecutionContext(editor.vim)
VimPlugin.getChange().insertBeforeCaret(editor.vim, context)
}
}
} else {
vimEditor.mode = Mode.NORMAL()
}
}
}
}
}
@@ -352,16 +368,6 @@ internal object IdeaSpecifics {
if (newLookup.editor.isIdeaVimDisabledHere) return
(VimPlugin.getKey() as VimKeyGroupBase).registerShortcutsForLookup(newLookup)
// In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
// (due to LookupSummaryInfo popup) before the action system runs, so IdeaVim never sees it.
// Listen for explicit lookup cancellation (Escape) to exit insert mode.
// Note: we check isRider/isClionNova specifically, not !isOctopusEnabled(), because
// JetBrains Client (split mode) also has octopus disabled but doesn't need this workaround,
// and isCanceledExplicitly can be true for non-Escape keys (e.g. space) in that environment.
if (isRider() || isClionNova()) {
newLookup.addLookupListener(RiderEscLookupListener(newLookup.editor))
}
}
// Lookup closed
@@ -373,20 +379,6 @@ internal object IdeaSpecifics {
}
}
}
/**
* In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
* (due to LookupSummaryInfo parameter info popup) before the action system runs, so IdeaVim never sees it.
* This listener exits insert mode when the lookup is explicitly cancelled (Escape).
*/
private class RiderEscLookupListener(private val editor: Editor) : com.intellij.codeInsight.lookup.LookupListener {
override fun lookupCanceled(event: com.intellij.codeInsight.lookup.LookupEvent) {
if (event.isCanceledExplicitly && editor.vim.mode is Mode.INSERT) {
editor.vim.exitInsertMode(injector.executionContextManager.getEditorExecutionContext(editor.vim))
KeyHandler.getInstance().reset(editor.vim)
}
}
}
//endregion
//region Hide Vim search highlights when showing IntelliJ search results

View File

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

View File

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

View File

@@ -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
@@ -9,9 +9,7 @@
package com.maddyhome.idea.vim.newapi
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.wm.WindowManager
import com.maddyhome.idea.vim.api.MessageType
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMessagesBase
import com.maddyhome.idea.vim.api.globalOptions
@@ -25,56 +23,43 @@ internal class IjVimMessages : VimMessagesBase() {
private var message: String? = null
private var error = false
private var lastBeepTimeMillis = 0L
private var allowClearStatusBarMessage = true
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"
}
}
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
val project = editor?.ij?.project
if (project != null) {
setStatusBarMessage(project, message)
} else {
// TODO: We really shouldn't set the status bar text for other projects. That's rude.
ProjectManager.getInstance().openProjects.forEach {
setStatusBarMessage(it, message)
}
if (message.isNullOrBlank()) {
clearStatusBarMessage()
return
}
// 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
val context = injector.executionContextManager.getEditorExecutionContext(editor)
injector.outputPanel.output(editor, context, message, messageType)
}
@Suppress("DEPRECATION")
override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
if (editor != null) {
showMessage(editor, message)
} else {
// Legacy path for when editor is null - just store the message
this.message = 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() {
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 = ""
}
}
}
if (message.isNullOrEmpty()) return
injector.outputPanel.getCurrentOutputPanel()?.close()
message = null
}

View File

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

View File

@@ -14,6 +14,7 @@ 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
@@ -24,7 +25,6 @@ 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,121 +36,166 @@ 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.JTextArea
import javax.swing.JTextPane
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
/**
* This panel displays text in a `more` like window and implements [VimOutputPanel].
* Panel that displays text in a `more` like window overlaid on the editor.
*/
class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), VimOutputPanel {
private val myEditorRef: WeakReference<Editor> = editorRef
val editor: Editor? get() = myEditorRef.get()
class OutputPanel private constructor(
private val editor: Editor,
) : JBPanel<OutputPanel>(), VimOutputPanel {
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 val textPane = JTextPane()
private val resizeAdapter: ComponentAdapter
private var defaultForeground: Color? = null
private var myOldGlass: JComponent? = null
private var myOldLayout: LayoutManager? = null
private var myWasOpaque = false
private var glassPane: JComponent? = null
private var originalLayout: LayoutManager? = null
private var wasOpaque = false
var myActive: Boolean = false
var active: Boolean = false
private val segments = mutableListOf<TextLine>()
val isActive: Boolean
get() = myActive
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
init {
// 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)
textPane.isEditable = false
textPane.caret = object : DefaultCaret() {
override fun setVisible(v: Boolean) {
super.setVisible(false)
}
}
textPane.highlighter = null
// Set the text area read only, and support wrap
myText.isEditable = false
myText.setLineWrap(true)
myAdapter = object : ComponentAdapter() {
resizeAdapter = object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent?) {
positionPanel()
}
}
// Setup some listeners to handle keystrokes
val moreKeyListener = MoreKeyListener()
addKeyListener(moreKeyListener)
myText.addKeyListener(moreKeyListener)
// Suppress the fancy frame background used in the Islands theme
ClientProperty.putRecursive(this, IdeBackgroundUtil.NO_BACKGROUND, true)
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, editor.component)
// 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) }
// Initialize panel
setLayout(BorderLayout(0, 0))
add(scrollPane, BorderLayout.CENTER)
add(labelComponent, BorderLayout.SOUTH)
val keyListener = OutputPanelKeyListener()
addKeyListener(keyListener)
textPane.addKeyListener(keyListener)
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 (myText != null && myLabel != null && myScrollPane != null) {
if (textPane != null && labelComponent != null && scrollPane != null) {
setFontForElements()
myText.setBorder(null)
myScrollPane.setBorder(null)
myLabel.setForeground(myText.getForeground())
// Make sure the panel is positioned correctly in case we're changing font size
textPane.setBorder(null)
scrollPane.setBorder(null)
labelComponent.setForeground(textPane.getForeground())
positionPanel()
}
}
override var text: String
get() = myText.text
get() = textPane.getText() ?: ""
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")
myText.text = newValue
val ed = editor
if (ed != null) {
myText.setFont(selectEditorFont(ed, newValue))
}
myText.setCaretPosition(0)
if (newValue.isNotEmpty()) {
activate()
}
segments.clear()
if (newValue.isEmpty()) return
segments.add(TextLine(newValue, null))
}
override var label: String
get() = myLabel.text ?: ""
get() = labelComponent.text
set(value) {
myLabel.text = value
val ed = editor
if (ed != null) {
myLabel.setFont(selectEditorFont(ed, value))
}
labelComponent.text = value
}
override fun addText(text: String, isNewLine: Boolean, messageType: MessageType) {
if (this.text.isNotEmpty() && isNewLine) {
this.text += "\n$text"
/**
* 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 {
this.text += text
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()
}
}
@@ -159,20 +204,15 @@ class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), V
}
override fun clearText() {
segments.clear()
}
fun clear() {
text = ""
}
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
@@ -197,214 +237,262 @@ class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), V
override fun getForeground(): Color? {
@Suppress("SENSELESS_COMPARISON")
if (myText == null) {
// Swing uses a bad pattern of calling getForeground() from the constructor. At this moment, `myText` is null.
if (textPane == null) {
return super.getForeground()
}
return myText.getForeground()
return textPane.getForeground()
}
override fun getBackground(): Color? {
@Suppress("SENSELESS_COMPARISON")
if (myText == null) {
// Swing uses a bad pattern of calling getBackground() from the constructor. At this moment, `myText` is null.
if (textPane == null) {
return super.getBackground()
}
return myText.getBackground()
return textPane.getBackground()
}
/**
* Turns off the ex entry field and optionally puts the focus back to the original component
* Turns off the output panel and optionally puts the focus back to the original component.
*/
fun deactivate(refocusOwningEditor: Boolean) {
if (!myActive) return
myActive = false
myText.text = ""
val ed = editor
if (refocusOwningEditor && ed != null) {
requestFocus(ed.contentComponent)
if (!active) return
active = false
clearText()
textPane.text = ""
if (refocusOwningEditor) {
requestFocus(editor.contentComponent)
}
if (myOldGlass != null) {
myOldGlass!!.removeComponentListener(myAdapter)
myOldGlass!!.isVisible = false
myOldGlass!!.remove(this)
myOldGlass!!.setOpaque(myWasOpaque)
myOldGlass!!.setLayout(myOldLayout)
if (glassPane != null) {
glassPane!!.removeComponentListener(resizeAdapter)
glassPane!!.isVisible = false
glassPane!!.remove(this)
glassPane!!.setOpaque(wasOpaque)
glassPane!!.setLayout(originalLayout)
}
}
/**
* Turns on the more window for the given editor
* Turns on the output panel for the given editor.
*/
fun activate() {
val ed = editor ?: return
val root = SwingUtilities.getRootPane(ed.contentComponent)
deactivateOldGlass(root)
disableOldGlass()
setFontForElements()
positionPanel()
if (myOldGlass != null) {
myOldGlass!!.isVisible = true
if (glassPane != null) {
glassPane!!.isVisible = true
}
myActive = true
requestFocus(myText)
active = true
requestFocus(textPane)
}
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
private fun disableOldGlass() {
val root = SwingUtilities.getRootPane(editor.contentComponent) ?: return
glassPane = root.getGlassPane() as JComponent?
if (glassPane == null) {
return
}
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)
}
}
originalLayout = glassPane!!.layout
wasOpaque = glassPane!!.isOpaque
glassPane!!.setLayout(null)
glassPane!!.setOpaque(false)
glassPane!!.add(this)
glassPane!!.addComponentListener(resizeAdapter)
}
override fun close() {
close(null)
}
private class MoreKeyListener : KeyAdapter() {
/**
* Invoked when a key has been pressed.
*/
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() {
override fun keyTyped(e: KeyEvent) {
val currentPanel: VimOutputPanel = injector.outputPanel.getCurrentOutputPanel() ?: return
val keyCode = e.getKeyCode()
val keyChar = e.getKeyChar()
val keyChar = e.keyChar
val modifiers = e.modifiersEx
val keyStroke = if (keyChar == KeyEvent.CHAR_UNDEFINED)
KeyStroke.getKeyStroke(keyCode, modifiers)
else
KeyStroke.getKeyStroke(keyChar, modifiers)
val keyStroke = 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
@@ -414,41 +502,24 @@ class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), V
}
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)?.myActive ?: false
return getNullablePanel(editor) != null
}
fun getInstance(editor: Editor): OutputPanel {
var panel: OutputPanel? = getNullablePanel(editor)
if (panel == null) {
panel = OutputPanel(WeakReference(editor))
panel = OutputPanel(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 && !isKeyCharEnterOrEscape(stroke.keyChar)) {
if (stroke.keyChar != KeyEvent.CHAR_UNDEFINED) {
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).isActive }
return activeOutputPanel?.get()?.takeIf { (it as OutputPanel).active }
}
override fun create(editor: VimEditor, context: ExecutionContext): VimOutputPanel {
val panel = OutputPanel(WeakReference(editor.ij))
val panel = OutputPanel.getInstance(editor.ij)
activeOutputPanel = WeakReference(panel)
return panel
}

View File

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

View File

@@ -20,5 +20,5 @@ import com.maddyhome.idea.vim.vimscript.model.functions.BuiltinFunctionHandler
@VimscriptFunction(name = "pumvisible")
internal class PopupMenuVisibleFunctionHandler : BuiltinFunctionHandler<VimInt>() {
override fun doFunction(arguments: Arguments, editor: VimEditor, context: ExecutionContext, vimContext: VimLContext) =
(CompletionService.getCompletionService().currentCompletion != null).asVimInt()
(CompletionService.getCompletionService().currentCompletion == null).asVimInt()
}

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-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
@@ -134,7 +134,7 @@ class CmdCommandTest : VimTestCase() {
VimPlugin.getCommand().resetAliases()
configureByText("\n")
typeText(commandToKeys("command! -range Error echo <args>"))
assertPluginError(false)
assertPluginError(true)
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(false)
assertPluginError(true)
kotlin.test.assertEquals("'-complete' is not supported by `command`", injector.messages.getStatusBarMessage())
}

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
@@ -24,7 +24,6 @@ class ExecuteCommandTest : VimTestCase() {
fun `test execute with range`() {
configureByText("\n")
typeText(commandToKeys("1,2execute 'echo 42'"))
assertNoExOutput()
assertPluginError(true)
}

View File

@@ -72,7 +72,6 @@ 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-2025 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
@@ -43,7 +43,6 @@ 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")
}
@@ -51,7 +50,6 @@ 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")
}
@@ -59,7 +57,6 @@ 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-2025 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
@@ -28,7 +28,6 @@ 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")
}
@@ -36,7 +35,6 @@ 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")
}
@@ -44,7 +42,6 @@ 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-2025 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
@@ -43,7 +43,6 @@ 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")
}
@@ -51,7 +50,6 @@ 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")
}
@@ -59,7 +57,6 @@ 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-2025 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
@@ -43,7 +43,6 @@ 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")
}
@@ -51,7 +50,6 @@ 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")
}
@@ -59,7 +57,6 @@ 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-2025 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
@@ -33,7 +33,6 @@ 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")
}
@@ -41,7 +40,6 @@ 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-2025 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
@@ -33,7 +33,6 @@ 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")
}
@@ -41,7 +40,6 @@ 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-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
@@ -207,7 +207,12 @@ class FunctionDeclarationTest : VimTestCase() {
typeText(commandToKeys("echo F1()"))
assertPluginError(true)
assertPluginErrorMessage("E121: Undefined variable: x")
assertExOutput("0")
assertExOutput(
"""
E121: Undefined variable: x
0
""".trimIndent()
)
typeText(commandToKeys("delf! F1"))
typeText(commandToKeys("delf! F2"))

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
@@ -154,7 +154,12 @@ class TryCatchTest : VimTestCase() {
),
)
assertPluginError(true)
assertExOutput("finally block")
assertExOutput(
"""
finally block
my exception
""".trimIndent()
)
}
@Test

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
@@ -223,7 +223,7 @@ class SearchGroupTest : VimTestCase() {
) {
enterCommand("set nowrapscan")
}
assertPluginError(false)
assertPluginError(true)
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
}
@@ -242,7 +242,7 @@ class SearchGroupTest : VimTestCase() {
three
""".trimIndent()
)
assertPluginError(false)
assertPluginError(true)
assertPluginErrorMessage("E486: Pattern not found: banana")
}
@@ -282,7 +282,7 @@ class SearchGroupTest : VimTestCase() {
) {
enterCommand("set nowrapscan")
}
assertPluginError(false)
assertPluginError(true)
assertPluginErrorMessage("E384: Search hit TOP without match for: three")
}
@@ -301,7 +301,7 @@ class SearchGroupTest : VimTestCase() {
three
""".trimIndent()
)
assertPluginError(false)
assertPluginError(true)
assertPluginErrorMessage("E486: Pattern not found: banana")
}
@@ -615,7 +615,7 @@ class SearchGroupTest : VimTestCase() {
)
enterCommand("set nowrapscan")
typeText("10", "/", searchCommand("one"))
assertPluginError(false)
assertPluginError(true)
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(false)
assertPluginError(true)
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(500)
pause(300)
assertCaretAfter(40, "G should go to end of file")
ctrlO()
pause(500)
pause(300)
assertCaretBefore(10, "Ctrl-O should jump back to start")
}
@@ -35,7 +35,7 @@ class JumpNavigationSplitTest : IdeaVimStarterTestBase() {
openFile(longFile("Jump2"))
typeVim("gg")
pause(500)
pause(300)
typeVim("/Line 30\n")
pause()

View File

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

View File

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

View File

@@ -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
@@ -12,7 +12,25 @@ 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()
@@ -28,13 +46,4 @@ 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-2024 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
@@ -29,7 +29,8 @@ 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. Defaults to true.
* @param isNewLine Whether to start the appended text on a new line.
* @param messageType The type of message, used to determine text styling.
*/
fun addText(text: String, isNewLine: Boolean = true, messageType: MessageType = MessageType.STANDARD)
@@ -51,4 +52,4 @@ interface VimOutputPanel {
fun setContent(text: String)
fun clearText()
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2024 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
@@ -26,8 +26,12 @@ interface VimOutputPanelService {
fun getCurrentOutputPanel(): VimOutputPanel?
/**
* 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.
* Appends text to the existing output panel or creates a new one with the given text and message type.
*/
fun output(editor: VimEditor, context: ExecutionContext, text: String)
}
fun output(
editor: VimEditor,
context: ExecutionContext,
text: String,
messageType: MessageType = MessageType.STANDARD,
)
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2024 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,9 +13,9 @@ abstract class VimOutputPanelServiceBase : VimOutputPanelService {
return getCurrentOutputPanel() ?: create(editor, context)
}
override fun output(editor: VimEditor, context: ExecutionContext, text: String) {
override fun output(editor: VimEditor, context: ExecutionContext, text: String, messageType: MessageType) {
val panel = getOrCreate(editor, context)
panel.addText(text)
panel.addText(text, true, messageType)
panel.show()
}
}
}

View File

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

View File

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

View File

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

View File

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