mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2026-05-01 11:50:37 +02:00
Compare commits
33 Commits
customized
...
customized
| Author | SHA1 | Date | |
|---|---|---|---|
|
078ddaf3ca
|
|||
|
94a7e1d303
|
|||
|
3de7743f56
|
|||
|
8636717dea
|
|||
|
22dfdd8ca6
|
|||
|
49f9f16f0d
|
|||
|
9bfc5d72ce
|
|||
|
84c227122a
|
|||
|
1b9ff4c94a
|
|||
|
bdecbb5ef0
|
|||
|
7dfd8e6cff
|
|||
|
31e76f0fcf
|
|||
|
2aadbdc8f0
|
|||
|
627d65e528
|
|||
|
e77871796e
|
|||
|
c6e993dcbd
|
|||
|
341ba1ba1f
|
|||
|
f3d7ad55f6
|
|||
|
5480b99898
|
|||
|
5734a13ea0
|
|||
|
582e6bdcd8
|
|||
|
7414c3d3ed
|
|||
|
8fa5bec363
|
|||
|
aea54bdf81
|
|||
|
79aca4497e
|
|||
|
50976ea9da
|
|||
|
57d0ef1dd5
|
|||
|
d2f017887f
|
|||
|
cfe196ed30
|
|||
|
536942f514
|
|||
|
36e3cd1adb
|
|||
|
7c874f834a
|
|||
|
a4e963c98e
|
50
.github/workflows/runSplitModeTests.yml
vendored
Normal file
50
.github/workflows/runSplitModeTests.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Run Split Mode Tests
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
test-linux:
|
||||||
|
if: github.repository == 'JetBrains/ideavim'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Free up disk space
|
||||||
|
run: |
|
||||||
|
echo "Disk space before cleanup:"
|
||||||
|
df -h
|
||||||
|
sudo rm -rf /usr/share/dotnet
|
||||||
|
sudo rm -rf /usr/local/lib/android
|
||||||
|
sudo rm -rf /opt/ghc
|
||||||
|
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||||
|
sudo docker image prune --all --force
|
||||||
|
echo "Disk space after cleanup:"
|
||||||
|
df -h
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: zulu
|
||||||
|
java-version: 21
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
with:
|
||||||
|
cache-read-only: false
|
||||||
|
- name: Start Xvfb
|
||||||
|
run: |
|
||||||
|
Xvfb :99 -screen 0 1920x1080x24 &
|
||||||
|
echo "DISPLAY=:99" >> $GITHUB_ENV
|
||||||
|
- name: Run split mode tests
|
||||||
|
run: gradle :tests:split-mode-tests:testSplitMode --console=plain
|
||||||
|
- name: Upload reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: split-mode-reports
|
||||||
|
path: |
|
||||||
|
tests/split-mode-tests/build/reports
|
||||||
|
out/ide-tests/tests/**/log
|
||||||
|
out/ide-tests/tests/**/frontend/log
|
||||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -33,6 +33,5 @@
|
|||||||
</option>
|
</option>
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
<option name="parallelModelFetch" value="true" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
10
.teamcity/_Self/Project.kt
vendored
10
.teamcity/_Self/Project.kt
vendored
@@ -1,11 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by an MIT-style
|
|
||||||
* license that can be found in the LICENSE.txt file or at
|
|
||||||
* https://opensource.org/licenses/MIT.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package _Self
|
package _Self
|
||||||
|
|
||||||
import _Self.buildTypes.Compatibility
|
import _Self.buildTypes.Compatibility
|
||||||
@@ -14,7 +6,6 @@ import _Self.buildTypes.Nvim
|
|||||||
import _Self.buildTypes.PluginVerifier
|
import _Self.buildTypes.PluginVerifier
|
||||||
import _Self.buildTypes.PropertyBased
|
import _Self.buildTypes.PropertyBased
|
||||||
import _Self.buildTypes.RandomOrderTests
|
import _Self.buildTypes.RandomOrderTests
|
||||||
import _Self.buildTypes.SplitModeTests
|
|
||||||
|
|
||||||
import _Self.buildTypes.TestingBuildType
|
import _Self.buildTypes.TestingBuildType
|
||||||
import _Self.buildTypes.TypeScriptTest
|
import _Self.buildTypes.TypeScriptTest
|
||||||
@@ -39,7 +30,6 @@ object Project : Project({
|
|||||||
buildType(PropertyBased)
|
buildType(PropertyBased)
|
||||||
buildType(LongRunning)
|
buildType(LongRunning)
|
||||||
buildType(RandomOrderTests)
|
buildType(RandomOrderTests)
|
||||||
buildType(SplitModeTests)
|
|
||||||
|
|
||||||
buildType(Nvim)
|
buildType(Nvim)
|
||||||
buildType(PluginVerifier)
|
buildType(PluginVerifier)
|
||||||
|
|||||||
12
.teamcity/_Self/buildTypes/Compatibility.kt
vendored
12
.teamcity/_Self/buildTypes/Compatibility.kt
vendored
@@ -1,11 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by an MIT-style
|
|
||||||
* license that can be found in the LICENSE.txt file or at
|
|
||||||
* https://opensource.org/licenses/MIT.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package _Self.buildTypes
|
package _Self.buildTypes
|
||||||
|
|
||||||
import _Self.AgentSize
|
import _Self.AgentSize
|
||||||
@@ -19,10 +11,6 @@ object Compatibility : IdeaVimBuildType({
|
|||||||
id("IdeaVimCompatibility")
|
id("IdeaVimCompatibility")
|
||||||
name = "IdeaVim compatibility with external plugins"
|
name = "IdeaVim compatibility with external plugins"
|
||||||
|
|
||||||
failureConditions {
|
|
||||||
executionTimeoutMin = 180
|
|
||||||
}
|
|
||||||
|
|
||||||
vcs {
|
vcs {
|
||||||
root(DslContext.settingsRoot)
|
root(DslContext.settingsRoot)
|
||||||
branchFilter = "+:<default>"
|
branchFilter = "+:<default>"
|
||||||
|
|||||||
12
.teamcity/_Self/buildTypes/RandomOrderTests.kt
vendored
12
.teamcity/_Self/buildTypes/RandomOrderTests.kt
vendored
@@ -1,11 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by an MIT-style
|
|
||||||
* license that can be found in the LICENSE.txt file or at
|
|
||||||
* https://opensource.org/licenses/MIT.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package _Self.buildTypes
|
package _Self.buildTypes
|
||||||
|
|
||||||
import _Self.AgentSize
|
import _Self.AgentSize
|
||||||
@@ -34,7 +26,7 @@ object RandomOrderTests : IdeaVimBuildType({
|
|||||||
gradle {
|
gradle {
|
||||||
clearConditions()
|
clearConditions()
|
||||||
tasks = """
|
tasks = """
|
||||||
clean test
|
test
|
||||||
-x :tests:property-tests:test
|
-x :tests:property-tests:test
|
||||||
-x :tests:long-running-tests:test
|
-x :tests:long-running-tests:test
|
||||||
-Djunit.jupiter.execution.order.random.seed=default
|
-Djunit.jupiter.execution.order.random.seed=default
|
||||||
@@ -42,7 +34,7 @@ object RandomOrderTests : IdeaVimBuildType({
|
|||||||
""".trimIndent().replace("\n", " ")
|
""".trimIndent().replace("\n", " ")
|
||||||
buildFile = ""
|
buildFile = ""
|
||||||
enableStacktrace = true
|
enableStacktrace = true
|
||||||
gradleParams = "--no-build-cache --configuration-cache"
|
gradleParams = "--build-cache --configuration-cache"
|
||||||
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"
|
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
.teamcity/_Self/buildTypes/SplitModeTests.kt
vendored
63
.teamcity/_Self/buildTypes/SplitModeTests.kt
vendored
@@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by an MIT-style
|
|
||||||
* license that can be found in the LICENSE.txt file or at
|
|
||||||
* https://opensource.org/licenses/MIT.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package _Self.buildTypes
|
|
||||||
|
|
||||||
import _Self.AgentSize
|
|
||||||
import _Self.IdeaVimBuildType
|
|
||||||
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
|
|
||||||
import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext
|
|
||||||
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script
|
|
||||||
import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs
|
|
||||||
|
|
||||||
object SplitModeTests : IdeaVimBuildType({
|
|
||||||
name = "Split mode tests"
|
|
||||||
description = "Tests for IdeaVim in Remote Development split mode (backend + frontend)"
|
|
||||||
|
|
||||||
artifactRules = """
|
|
||||||
+:tests/split-mode-tests/build/reports => split-mode-tests/build/reports
|
|
||||||
+:out/ide-tests/tests/**/log => out/ide-tests/log
|
|
||||||
+:out/ide-tests/tests/**/frontend/log => out/ide-tests/frontend-log
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
params {
|
|
||||||
param("env.ORG_GRADLE_PROJECT_downloadIdeaSources", "false")
|
|
||||||
param("env.ORG_GRADLE_PROJECT_instrumentPluginCode", "false")
|
|
||||||
param("env.DISPLAY", ":99")
|
|
||||||
}
|
|
||||||
|
|
||||||
vcs {
|
|
||||||
root(DslContext.settingsRoot)
|
|
||||||
branchFilter = "+:<default>"
|
|
||||||
|
|
||||||
checkoutMode = CheckoutMode.AUTO
|
|
||||||
}
|
|
||||||
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
name = "Start Xvfb and run split mode tests"
|
|
||||||
scriptContent = """
|
|
||||||
Xvfb :99 -screen 0 1920x1080x24 &
|
|
||||||
sleep 2
|
|
||||||
./gradlew :tests:split-mode-tests:testSplitMode --console=plain --build-cache --configuration-cache --stacktrace
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
triggers {
|
|
||||||
vcs {
|
|
||||||
branchFilter = "+:<default>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requirements {
|
|
||||||
// Use a larger agent for split-mode tests — they launch two full IDE instances
|
|
||||||
equals("teamcity.agent.hardware.cpuCount", AgentSize.XLARGE)
|
|
||||||
equals("teamcity.agent.os.family", "Linux")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
12
.teamcity/_Self/buildTypes/TestingBuildType.kt
vendored
12
.teamcity/_Self/buildTypes/TestingBuildType.kt
vendored
@@ -1,11 +1,3 @@
|
|||||||
/*
|
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by an MIT-style
|
|
||||||
* license that can be found in the LICENSE.txt file or at
|
|
||||||
* https://opensource.org/licenses/MIT.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:Suppress("ClassName")
|
@file:Suppress("ClassName")
|
||||||
|
|
||||||
package _Self.buildTypes
|
package _Self.buildTypes
|
||||||
@@ -49,10 +41,10 @@ open class TestingBuildType(
|
|||||||
steps {
|
steps {
|
||||||
gradle {
|
gradle {
|
||||||
clearConditions()
|
clearConditions()
|
||||||
tasks = "clean test -x :tests:property-tests:test -x :tests:long-running-tests:test"
|
tasks = "test -x :tests:property-tests:test -x :tests:long-running-tests:test"
|
||||||
buildFile = ""
|
buildFile = ""
|
||||||
enableStacktrace = true
|
enableStacktrace = true
|
||||||
gradleParams = "--no-build-cache --configuration-cache"
|
gradleParams = "--build-cache --configuration-cache"
|
||||||
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"
|
jdkHome = "/usr/lib/jvm/java-21-amazon-corretto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -542,10 +542,6 @@ Contributors:
|
|||||||
[![icon][github]](https://github.com/1grzyb1)
|
[![icon][github]](https://github.com/1grzyb1)
|
||||||
|
|
||||||
1grzyb1
|
1grzyb1
|
||||||
* [![icon][mail]](mailto:yury@digitalby.me)
|
|
||||||
[![icon][github]](https://github.com/digitalby)
|
|
||||||
|
|
||||||
digitalby
|
|
||||||
|
|
||||||
Contributors with JetBrains IP:
|
Contributors with JetBrains IP:
|
||||||
|
|
||||||
|
|||||||
23
CHANGES.md
23
CHANGES.md
@@ -36,37 +36,14 @@ usual beta standards.
|
|||||||
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `:set foldlevel` option - control fold visibility level
|
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `:set foldlevel` option - control fold visibility level
|
||||||
|
|
||||||
### Fixes:
|
### Fixes:
|
||||||
* [VIM-4135](https://youtrack.jetbrains.com/issue/VIM-4135) Fixed IdeaVim not loading in Rider
|
|
||||||
* [VIM-4134](https://youtrack.jetbrains.com/issue/VIM-4134) Fixed undo in commentary - `gcc`/`gc{motion}` changes are now properly grouped as a single undo step
|
|
||||||
* [VIM-4134](https://youtrack.jetbrains.com/issue/VIM-4134) Fixed `=` (format/auto-indent) action in split mode
|
|
||||||
* [VIM-4134](https://youtrack.jetbrains.com/issue/VIM-4134) Fixed global marks causing errors when used inside write actions (e.g., during document modifications)
|
|
||||||
* [VIM-4105](https://youtrack.jetbrains.com/issue/VIM-4105) Fixed `a"` `a'` `a\`` text objects to include surrounding whitespace per Vim spec
|
* [VIM-4105](https://youtrack.jetbrains.com/issue/VIM-4105) Fixed `a"` `a'` `a\`` text objects to include surrounding whitespace per Vim spec
|
||||||
* [VIM-4097](https://youtrack.jetbrains.com/issue/VIM-4097) Fixed `<A-n>` (NextOccurrence) with text containing backslashes - e.g., selecting `\IntegerField` now works correctly
|
* [VIM-4097](https://youtrack.jetbrains.com/issue/VIM-4097) Fixed `<A-n>` (NextOccurrence) with text containing backslashes - e.g., selecting `\IntegerField` now works correctly
|
||||||
* [VIM-4094](https://youtrack.jetbrains.com/issue/VIM-4094) Fixed UninitializedPropertyAccessException when loading history
|
* [VIM-4094](https://youtrack.jetbrains.com/issue/VIM-4094) Fixed UninitializedPropertyAccessException when loading history
|
||||||
* [VIM-4016](https://youtrack.jetbrains.com/issue/VIM-4016) Fixed `:edit` command when project has no source roots
|
|
||||||
* [VIM-3948](https://youtrack.jetbrains.com/issue/VIM-3948) Improved hint generation visibility checks for better UI component detection
|
* [VIM-3948](https://youtrack.jetbrains.com/issue/VIM-3948) Improved hint generation visibility checks for better UI component detection
|
||||||
* [VIM-3473](https://youtrack.jetbrains.com/issue/VIM-3473) Fixed "Reload .ideavimrc" action in remote development (split) mode - no longer causes File Cache Conflict dialogs
|
|
||||||
* [VIM-2821](https://youtrack.jetbrains.com/issue/VIM-2821) Fixed undo grouping when repeating text insertion with `.` in remote development (split mode)
|
|
||||||
* [VIM-1705](https://youtrack.jetbrains.com/issue/VIM-1705) Fixed window-switching commands (e.g., `<C-w>h`) during macro playback
|
|
||||||
* Fixed `pumvisible()` function returning incorrect result (was inverted)
|
|
||||||
* Fixed `<Esc>` not properly exiting insert mode in Rider/CLion when canceling a completion lookup
|
|
||||||
* Fixed `<Esc>` not exiting insert mode after `<C-Space>` completion in Rider
|
|
||||||
* Fixed `<Esc>` in search bar no longer inserts `^[` literal text when search is not found - panel is now properly closed
|
|
||||||
* Fixed IdeaVim entering broken state when a VimScript extension plugin fails to initialize
|
|
||||||
* Fixed compatibility issues with external plugins (e.g., IdeaVim-EasyMotion, multicursor)
|
|
||||||
* Fixed recursive key mappings (e.g., `map b wbb`) causing an apparent infinite loop - `maxmapdepth` limit now properly terminates the entire mapping chain
|
|
||||||
* Fixed NERDTree `gs`/`gi` preview split commands to keep focus on the tree
|
|
||||||
* Fixed visual marks (`<` and `>`) position tracking after text deletion - `gv` now re-selects correctly
|
|
||||||
* Fixed `IndexOutOfBoundsException` when using text objects like `a)` at end of file
|
|
||||||
* Fixed high CPU usage while showing command line
|
* Fixed high CPU usage while showing command line
|
||||||
* Fixed comparison of String and Number in VimScript expressions
|
* Fixed comparison of String and Number in VimScript expressions
|
||||||
|
|
||||||
### Merged PRs:
|
### Merged PRs:
|
||||||
* [1632](https://github.com/JetBrains/ideavim/pull/1632) by [chylex](https://github.com/chylex): Fix pumvisible returning opposite result
|
|
||||||
* [1615](https://github.com/JetBrains/ideavim/pull/1615) by [1grzyb1](https://github.com/1grzyb1): Fix IndexOutOfBoundsException in findBlock when caret is at end of file
|
|
||||||
* [1613](https://github.com/JetBrains/ideavim/pull/1613) by [1grzyb1](https://github.com/1grzyb1): VIM-3473 Sync ideavim in remdev
|
|
||||||
* [1608](https://github.com/JetBrains/ideavim/pull/1608) by [1grzyb1](https://github.com/1grzyb1): VIM-4134 format using = action in split mode
|
|
||||||
* [1585](https://github.com/JetBrains/ideavim/pull/1585) by [1grzyb1](https://github.com/1grzyb1): Break in case of maximum recursion depth
|
|
||||||
* [1414](https://github.com/JetBrains/ideavim/pull/1414) by [Matt Ellis](https://github.com/citizenmatt): Refactor/functions
|
* [1414](https://github.com/JetBrains/ideavim/pull/1414) by [Matt Ellis](https://github.com/citizenmatt): Refactor/functions
|
||||||
* [1442](https://github.com/JetBrains/ideavim/pull/1442) by [Matt Ellis](https://github.com/citizenmatt): Fix high CPU usage while showing command line
|
* [1442](https://github.com/JetBrains/ideavim/pull/1442) by [Matt Ellis](https://github.com/citizenmatt): Fix high CPU usage while showing command line
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
package com.intellij.vim.api.scopes
|
package com.intellij.vim.api.scopes
|
||||||
|
|
||||||
import com.intellij.vim.api.VimApi
|
import com.intellij.vim.api.VimApi
|
||||||
import com.intellij.vim.api.models.CaretId
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the range of a text object selection.
|
* Represents the range of a text object selection.
|
||||||
@@ -110,19 +109,10 @@ interface TextObjectScope {
|
|||||||
* or null if no valid range is found at the current position.
|
* or null if no valid range is found at the current position.
|
||||||
* The function receives the count (e.g., `2iw` passes count=2).
|
* The function receives the count (e.g., `2iw` passes count=2).
|
||||||
*/
|
*/
|
||||||
fun register(
|
|
||||||
keys: String,
|
|
||||||
registerDefaultMapping: Boolean = true,
|
|
||||||
preserveSelectionAnchor: Boolean = true,
|
|
||||||
rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun register(
|
fun register(
|
||||||
keys: String,
|
keys: String,
|
||||||
registerDefaultMapping: Boolean = true,
|
registerDefaultMapping: Boolean = true,
|
||||||
preserveSelectionAnchor: Boolean = true,
|
preserveSelectionAnchor: Boolean = true,
|
||||||
rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
|
rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
|
||||||
) {
|
)
|
||||||
register(keys, registerDefaultMapping, preserveSelectionAnchor) { _, count -> rangeProvider(count) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -444,37 +444,14 @@ intellijPlatform {
|
|||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>:set foldlevel</code> option - control fold visibility level<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>:set foldlevel</code> option - control fold visibility level<br>
|
||||||
<br>
|
<br>
|
||||||
<b>Fixes:</b><br>
|
<b>Fixes:</b><br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4135">VIM-4135</a> Fixed IdeaVim not loading in Rider<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4134">VIM-4134</a> Fixed undo in commentary - <code>gcc</code>/<code>gc{motion}</code> changes are now properly grouped as a single undo step<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4134">VIM-4134</a> Fixed <code>=</code> (format/auto-indent) action in split mode<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4134">VIM-4134</a> Fixed global marks causing errors when used inside write actions (e.g., during document modifications)<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4105">VIM-4105</a> Fixed <code>a"</code> <code>a'</code> <code>a`</code> text objects to include surrounding whitespace per Vim spec<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4105">VIM-4105</a> Fixed <code>a"</code> <code>a'</code> <code>a`</code> text objects to include surrounding whitespace per Vim spec<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4097">VIM-4097</a> Fixed <code><A-n></code> (NextOccurrence) with text containing backslashes - e.g., selecting <code>\IntegerField</code> now works correctly<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4097">VIM-4097</a> Fixed <code><A-n></code> (NextOccurrence) with text containing backslashes - e.g., selecting <code>\IntegerField</code> now works correctly<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4094">VIM-4094</a> Fixed UninitializedPropertyAccessException when loading history<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-4094">VIM-4094</a> Fixed UninitializedPropertyAccessException when loading history<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4016">VIM-4016</a> Fixed <code>:edit</code> command when project has no source roots<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-3948">VIM-3948</a> Improved hint generation visibility checks for better UI component detection<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-3948">VIM-3948</a> Improved hint generation visibility checks for better UI component detection<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-3473">VIM-3473</a> Fixed "Reload .ideavimrc" action in remote development (split) mode - no longer causes File Cache Conflict dialogs<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-2821">VIM-2821</a> Fixed undo grouping when repeating text insertion with <code>.</code> in remote development (split mode)<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-1705">VIM-1705</a> Fixed window-switching commands (e.g., <code><C-w>h</code>) during macro playback<br>
|
|
||||||
* Fixed <code>pumvisible()</code> function returning incorrect result (was inverted)<br>
|
|
||||||
* Fixed <code><Esc></code> not properly exiting insert mode in Rider/CLion when canceling a completion lookup<br>
|
|
||||||
* Fixed <code><Esc></code> not exiting insert mode after <code><C-Space></code> completion in Rider<br>
|
|
||||||
* Fixed <code><Esc></code> in search bar no longer inserts <code>^[</code> literal text when search is not found - panel is now properly closed<br>
|
|
||||||
* Fixed IdeaVim entering broken state when a VimScript extension plugin fails to initialize<br>
|
|
||||||
* Fixed compatibility issues with external plugins (e.g., IdeaVim-EasyMotion, multicursor)<br>
|
|
||||||
* Fixed recursive key mappings (e.g., <code>map b wbb</code>) causing an apparent infinite loop - <code>maxmapdepth</code> limit now properly terminates the entire mapping chain<br>
|
|
||||||
* Fixed NERDTree <code>gs</code>/<code>gi</code> preview split commands to keep focus on the tree<br>
|
|
||||||
* Fixed visual marks (<code><</code> and <code>></code>) position tracking after text deletion - <code>gv</code> now re-selects correctly<br>
|
|
||||||
* Fixed <code>IndexOutOfBoundsException</code> when using text objects like <code>a)</code> at end of file<br>
|
|
||||||
* Fixed high CPU usage while showing command line<br>
|
* Fixed high CPU usage while showing command line<br>
|
||||||
* Fixed comparison of String and Number in VimScript expressions<br>
|
* Fixed comparison of String and Number in VimScript expressions<br>
|
||||||
<br>
|
<br>
|
||||||
<b>Merged PRs:</b><br>
|
<b>Merged PRs:</b><br>
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1632">1632</a> by <a href="https://github.com/chylex">chylex</a>: Fix pumvisible returning opposite result<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1615">1615</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Fix IndexOutOfBoundsException in findBlock when caret is at end of file<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1613">1613</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-3473 Sync ideavim in remdev<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1608">1608</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4134 format using = action in split mode<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1585">1585</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Break in case of maximum recursion depth<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1414">1414</a> by <a href="https://github.com/citizenmatt">Matt Ellis</a>: Refactor/functions<br>
|
* <a href="https://github.com/JetBrains/ideavim/pull/1414">1414</a> by <a href="https://github.com/citizenmatt">Matt Ellis</a>: Refactor/functions<br>
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1442">1442</a> by <a href="https://github.com/citizenmatt">Matt Ellis</a>: Fix high CPU usage while showing command line<br>
|
* <a href="https://github.com/JetBrains/ideavim/pull/1442">1442</a> by <a href="https://github.com/citizenmatt">Matt Ellis</a>: Fix high CPU usage while showing command line<br>
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ ideaVersion=2026.1
|
|||||||
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
|
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
|
||||||
ideaType=IU
|
ideaType=IU
|
||||||
instrumentPluginCode=true
|
instrumentPluginCode=true
|
||||||
version=chylex-57
|
version=chylex-56
|
||||||
javaVersion=21
|
javaVersion=21
|
||||||
remoteRobotVersion=0.11.23
|
remoteRobotVersion=0.11.23
|
||||||
antlrVersion=4.10.1
|
antlrVersion=4.10.1
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import com.intellij.openapi.util.Disposer;
|
|||||||
import com.maddyhome.idea.vim.api.*;
|
import com.maddyhome.idea.vim.api.*;
|
||||||
import com.maddyhome.idea.vim.config.VimState;
|
import com.maddyhome.idea.vim.config.VimState;
|
||||||
import com.maddyhome.idea.vim.config.migration.ApplicationConfigurationMigrator;
|
import com.maddyhome.idea.vim.config.migration.ApplicationConfigurationMigrator;
|
||||||
import com.maddyhome.idea.vim.group.ChangeGroup;
|
|
||||||
import com.maddyhome.idea.vim.group.KeyGroup;
|
import com.maddyhome.idea.vim.group.KeyGroup;
|
||||||
import com.maddyhome.idea.vim.group.VimNotifications;
|
import com.maddyhome.idea.vim.group.VimNotifications;
|
||||||
import com.maddyhome.idea.vim.group.VimWindowGroup;
|
import com.maddyhome.idea.vim.group.VimWindowGroup;
|
||||||
@@ -91,8 +90,8 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
|
|||||||
return VimInjectorKt.getInjector().getMotion();
|
return VimInjectorKt.getInjector().getMotion();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull ChangeGroup getChange() {
|
public static @NotNull VimChangeGroup getChange() {
|
||||||
return ((ChangeGroup)VimInjectorKt.getInjector().getChangeGroup());
|
return VimInjectorKt.getInjector().getChangeGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimCommandGroup getCommand() {
|
public static @NotNull VimCommandGroup getCommand() {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import com.intellij.openapi.editor.impl.EditorComponentImpl
|
|||||||
import com.intellij.openapi.progress.ProcessCanceledException
|
import com.intellij.openapi.progress.ProcessCanceledException
|
||||||
import com.intellij.openapi.project.DumbAware
|
import com.intellij.openapi.project.DumbAware
|
||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
import com.intellij.openapi.util.SystemInfoRt
|
|
||||||
import com.intellij.openapi.util.registry.Registry
|
import com.intellij.openapi.util.registry.Registry
|
||||||
import com.intellij.ui.KeyStrokeAdapter
|
import com.intellij.ui.KeyStrokeAdapter
|
||||||
import com.maddyhome.idea.vim.KeyHandler
|
import com.maddyhome.idea.vim.KeyHandler
|
||||||
@@ -227,9 +226,8 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
|||||||
val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent)
|
val defaultKeyStroke = KeyStrokeAdapter.getDefaultKeyStroke(inputEvent)
|
||||||
val strokeCache = keyStrokeCache
|
val strokeCache = keyStrokeCache
|
||||||
if (defaultKeyStroke != null) {
|
if (defaultKeyStroke != null) {
|
||||||
val fixedKeyStroke = fixKeyStroke(defaultKeyStroke)
|
keyStrokeCache = inputEvent.`when` to defaultKeyStroke
|
||||||
keyStrokeCache = inputEvent.`when` to fixedKeyStroke
|
return defaultKeyStroke
|
||||||
return fixedKeyStroke
|
|
||||||
} else if (strokeCache.first == inputEvent.`when`) {
|
} else if (strokeCache.first == inputEvent.`when`) {
|
||||||
keyStrokeCache = null to null
|
keyStrokeCache = null to null
|
||||||
return strokeCache.second
|
return strokeCache.second
|
||||||
@@ -239,19 +237,6 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fixKeyStroke(key: KeyStroke): KeyStroke {
|
|
||||||
return if (
|
|
||||||
key.modifiers and CTRL_ALT_MASK != 0 &&
|
|
||||||
key.isOnKeyRelease &&
|
|
||||||
SystemInfoRt.isWindows &&
|
|
||||||
Registry.`is`("actionSystem.fix.alt.gr", true)
|
|
||||||
) {
|
|
||||||
KeyStroke.getKeyStroke(key.keyCode, key.modifiers)
|
|
||||||
} else {
|
|
||||||
key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getEditor(e: AnActionEvent): Editor? {
|
private fun getEditor(e: AnActionEvent): Editor? {
|
||||||
return e.getData(PlatformDataKeys.EDITOR)
|
return e.getData(PlatformDataKeys.EDITOR)
|
||||||
?: if (e.getData(PlatformDataKeys.CONTEXT_COMPONENT) is ExTextField) {
|
?: if (e.getData(PlatformDataKeys.CONTEXT_COMPONENT) is ExTextField) {
|
||||||
@@ -332,7 +317,6 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
|||||||
).build()
|
).build()
|
||||||
|
|
||||||
private const val ACTION_ID = "VimShortcutKeyAction"
|
private const val ACTION_ID = "VimShortcutKeyAction"
|
||||||
private const val CTRL_ALT_MASK = InputEvent.CTRL_DOWN_MASK or InputEvent.ALT_DOWN_MASK
|
|
||||||
|
|
||||||
private val LOG = logger<VimShortcutKeyAction>()
|
private val LOG = logger<VimShortcutKeyAction>()
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package com.maddyhome.idea.vim.extension.argtextobj
|
|||||||
|
|
||||||
import com.intellij.vim.api.VimApi
|
import com.intellij.vim.api.VimApi
|
||||||
import com.intellij.vim.api.VimInitApi
|
import com.intellij.vim.api.VimInitApi
|
||||||
import com.intellij.vim.api.models.CaretId
|
|
||||||
import com.intellij.vim.api.scopes.TextObjectRange
|
import com.intellij.vim.api.scopes.TextObjectRange
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.extension.VimExtension
|
import com.maddyhome.idea.vim.extension.VimExtension
|
||||||
@@ -122,11 +121,11 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
|
|
||||||
override fun init(initApi: VimInitApi) {
|
override fun init(initApi: VimInitApi) {
|
||||||
initApi.textObjects {
|
initApi.textObjects {
|
||||||
register("ia", preserveSelectionAnchor = false) { caret, count ->
|
register("ia", preserveSelectionAnchor = false) { count ->
|
||||||
findArgumentRange(isInner = true, caret, count)
|
findArgumentRange(isInner = true, count)
|
||||||
}
|
}
|
||||||
register("aa", preserveSelectionAnchor = false) { caret, count ->
|
register("aa", preserveSelectionAnchor = false) { count ->
|
||||||
findArgumentRange(isInner = false, caret, count)
|
findArgumentRange(isInner = false, count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -611,7 +610,7 @@ private object ArgTextObjUtil {
|
|||||||
/**
|
/**
|
||||||
* Find argument range using the new VimApi.
|
* Find argument range using the new VimApi.
|
||||||
*/
|
*/
|
||||||
private suspend fun VimApi.findArgumentRange(isInner: Boolean, caret: CaretId, count: Int): TextObjectRange? {
|
private suspend fun VimApi.findArgumentRange(isInner: Boolean, count: Int): TextObjectRange? {
|
||||||
var bracketPairs: BracketPairs = ArgTextObjUtil.DEFAULT_BRACKET_PAIRS
|
var bracketPairs: BracketPairs = ArgTextObjUtil.DEFAULT_BRACKET_PAIRS
|
||||||
val bracketPairsVar: String? = ArgTextObjUtil.bracketPairsVariable()
|
val bracketPairsVar: String? = ArgTextObjUtil.bracketPairsVariable()
|
||||||
if (bracketPairsVar != null) {
|
if (bracketPairsVar != null) {
|
||||||
@@ -626,7 +625,7 @@ private suspend fun VimApi.findArgumentRange(isInner: Boolean, caret: CaretId, c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val (text, caretOffset) = editor { read { text to with(caret) { offset } } }
|
val (text, caretOffset) = editor { read { text to withPrimaryCaret { offset } } }
|
||||||
val finder = ArgBoundsFinder(text, this, bracketPairs)
|
val finder = ArgBoundsFinder(text, this, bracketPairs)
|
||||||
var pos = caretOffset
|
var pos = caretOffset
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ internal class JumpRemoteTopicListener : ProjectRemoteTopicListener<JumpInfo> {
|
|||||||
|
|
||||||
if (event.added) {
|
if (event.added) {
|
||||||
jumpService.addJump(projectId, jump, true)
|
jumpService.addJump(projectId, jump, true)
|
||||||
injector.markService.setJumpMark(event.filepath, event.protocol, event.line, event.col)
|
|
||||||
} else {
|
} else {
|
||||||
jumpService.removeJump(projectId, jump)
|
jumpService.removeJump(projectId, jump)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import com.maddyhome.idea.vim.newapi.vim
|
|||||||
import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual
|
import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual
|
||||||
import com.maddyhome.idea.vim.state.mode.inVisualMode
|
import com.maddyhome.idea.vim.state.mode.inVisualMode
|
||||||
import org.jetbrains.annotations.Contract
|
import org.jetbrains.annotations.Contract
|
||||||
import java.awt.Color
|
|
||||||
import java.awt.Font
|
import java.awt.Font
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.swing.Timer
|
import javax.swing.Timer
|
||||||
@@ -88,7 +87,7 @@ fun addSubstitutionConfirmationHighlight(editor: Editor, start: Int, end: Int):
|
|||||||
}
|
}
|
||||||
|
|
||||||
val removeHighlightsEditors = mutableListOf<Editor>()
|
val removeHighlightsEditors = mutableListOf<Editor>()
|
||||||
val removeHighlightsTimer = Timer(450) {
|
val removeHighlightsTimer = Timer(400) {
|
||||||
removeHighlightsEditors.forEach(::removeSearchHighlights)
|
removeHighlightsEditors.forEach(::removeSearchHighlights)
|
||||||
removeHighlightsEditors.clear()
|
removeHighlightsEditors.clear()
|
||||||
}
|
}
|
||||||
@@ -162,7 +161,7 @@ private fun updateSearchHighlights(
|
|||||||
if (editor === currentEditor?.ij) {
|
if (editor === currentEditor?.ij) {
|
||||||
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
|
currentMatchOffset = findClosestMatch(results, initialOffset, count1, forwards)
|
||||||
}
|
}
|
||||||
highlightSearchResults(editor, results, currentMatchOffset)
|
highlightSearchResults(editor, pattern, results, currentMatchOffset)
|
||||||
if (!isSearching) {
|
if (!isSearching) {
|
||||||
removeHighlightsEditors.add(editor)
|
removeHighlightsEditors.add(editor)
|
||||||
removeHighlightsTimer.restart()
|
removeHighlightsTimer.restart()
|
||||||
@@ -185,7 +184,7 @@ private fun updateSearchHighlights(
|
|||||||
if (result != null) {
|
if (result != null) {
|
||||||
if (!it.inVisualMode && !it.inCommandLineModeWithVisual) {
|
if (!it.inVisualMode && !it.inCommandLineModeWithVisual) {
|
||||||
val results = listOf(result)
|
val results = listOf(result)
|
||||||
highlightSearchResults(editor, results, result.startOffset)
|
highlightSearchResults(editor, pattern, results, result.startOffset)
|
||||||
}
|
}
|
||||||
currentMatchOffset = result.startOffset
|
currentMatchOffset = result.startOffset
|
||||||
}
|
}
|
||||||
@@ -266,18 +265,9 @@ private fun findClosestMatch(
|
|||||||
return sortedResults[nextIndex % results.size].startOffset
|
return sortedResults[nextIndex % results.size].startOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UseJBColor")
|
|
||||||
private val DEFAULT_RESULT_ATTRIBUTES = TextAttributes().apply {
|
|
||||||
backgroundColor = Color(50, 81, 61)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UseJBColor")
|
|
||||||
private val NEARBY_RESULT_ATTRIBUTES = TextAttributes().apply {
|
|
||||||
backgroundColor = Color(89, 80, 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun highlightSearchResults(
|
fun highlightSearchResults(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
|
pattern: String,
|
||||||
results: List<TextRange>,
|
results: List<TextRange>,
|
||||||
currentMatchOffset: Int,
|
currentMatchOffset: Int,
|
||||||
) {
|
) {
|
||||||
@@ -286,28 +276,38 @@ fun highlightSearchResults(
|
|||||||
highlighters = mutableListOf()
|
highlighters = mutableListOf()
|
||||||
editor.vimLastHighlighters = highlighters
|
editor.vimLastHighlighters = highlighters
|
||||||
}
|
}
|
||||||
|
for (range in results) {
|
||||||
val allCaretOffsets = editor.caretModel.allCarets.map { it.offset }
|
val current = range.startOffset == currentMatchOffset
|
||||||
|
val highlighter = highlightMatch(editor, range.startOffset, range.endOffset, current, pattern)
|
||||||
for ((index, range) in results.withIndex()) {
|
highlighters.add(highlighter)
|
||||||
if (allCaretOffsets.any { range.startOffset == it }) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val attributes = if (allCaretOffsets.any { (index > 0 && results[index - 1].startOffset == it) || (index < results.lastIndex && results[index + 1].startOffset == it) })
|
|
||||||
NEARBY_RESULT_ATTRIBUTES
|
|
||||||
else
|
|
||||||
DEFAULT_RESULT_ATTRIBUTES
|
|
||||||
|
|
||||||
highlighters.add(highlightMatch(editor, range.startOffset, range.endOffset, attributes))
|
|
||||||
}
|
}
|
||||||
editor.vimIncsearchCurrentMatchOffset = currentMatchOffset
|
editor.vimIncsearchCurrentMatchOffset = currentMatchOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun highlightMatch(editor: Editor, start: Int, end: Int, attributes: TextAttributes): RangeHighlighter {
|
private fun highlightMatch(editor: Editor, start: Int, end: Int, current: Boolean, tooltip: String): RangeHighlighter {
|
||||||
val layer = HighlighterLayer.SELECTION - 1
|
val layer = HighlighterLayer.SELECTION - 1
|
||||||
val targetArea = HighlighterTargetArea.EXACT_RANGE
|
val targetArea = HighlighterTargetArea.EXACT_RANGE
|
||||||
return editor.markupModel.addRangeHighlighter(start, end, layer, attributes, targetArea)
|
if (!current) {
|
||||||
|
// If we use a text attribute key, it will update automatically when the editor's colour scheme changes
|
||||||
|
val highlighter =
|
||||||
|
editor.markupModel.addRangeHighlighter(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES, start, end, layer, targetArea)
|
||||||
|
highlighter.errorStripeTooltip = tooltip
|
||||||
|
return highlighter
|
||||||
|
}
|
||||||
|
|
||||||
|
// There isn't a text attribute key for current selection. This means we won't update automatically when the editor's
|
||||||
|
// colour scheme changes. However, this is only used during incsearch, so it should be replaced pretty quickly. It's a
|
||||||
|
// small visual glitch that will fix itself quickly. Let's not bother implementing an editor colour scheme listener
|
||||||
|
// just for this.
|
||||||
|
// These are the same modifications that the Find live preview does. We could look at using LivePreviewPresentation,
|
||||||
|
// which might also be useful for text attributes in selection (if we supported that)
|
||||||
|
val attributes = editor.colorsScheme.getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES).clone().apply {
|
||||||
|
effectType = EffectType.ROUNDED_BOX
|
||||||
|
effectColor = editor.colorsScheme.getColor(EditorColors.CARET_COLOR)
|
||||||
|
}
|
||||||
|
return editor.markupModel.addRangeHighlighter(start, end, layer, attributes, targetArea).apply {
|
||||||
|
errorStripeTooltip = tooltip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,14 +29,12 @@ import com.intellij.openapi.actionSystem.AnActionEvent
|
|||||||
import com.intellij.openapi.actionSystem.AnActionResult
|
import com.intellij.openapi.actionSystem.AnActionResult
|
||||||
import com.intellij.openapi.actionSystem.AnActionWrapper
|
import com.intellij.openapi.actionSystem.AnActionWrapper
|
||||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||||
import com.intellij.openapi.actionSystem.IdeActions
|
|
||||||
import com.intellij.openapi.actionSystem.ex.AnActionListener
|
import com.intellij.openapi.actionSystem.ex.AnActionListener
|
||||||
import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
|
import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.openapi.editor.RangeMarker
|
import com.intellij.openapi.editor.RangeMarker
|
||||||
import com.intellij.openapi.editor.actions.EnterAction
|
import com.intellij.openapi.editor.actions.EnterAction
|
||||||
import com.intellij.openapi.editor.impl.ScrollingModelImpl
|
import com.intellij.openapi.editor.impl.ScrollingModelImpl
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
|
||||||
import com.intellij.openapi.keymap.KeymapManager
|
import com.intellij.openapi.keymap.KeymapManager
|
||||||
import com.intellij.openapi.project.DumbAwareToggleAction
|
import com.intellij.openapi.project.DumbAwareToggleAction
|
||||||
import com.intellij.openapi.util.TextRange
|
import com.intellij.openapi.util.TextRange
|
||||||
@@ -89,11 +87,6 @@ internal object IdeaSpecifics {
|
|||||||
caretOffset = hostEditor.caretModel.offset
|
caretOffset = hostEditor.caretModel.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
val actionId = ActionManager.getInstance().getId(action)
|
|
||||||
if (isGotoAction(actionId)) {
|
|
||||||
saveJumpBeforeGoto(event, editor)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction
|
val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction
|
||||||
if (!isVimAction && injector.vimState.mode == Mode.INSERT && action !is EnterAction) {
|
if (!isVimAction && injector.vimState.mode == Mode.INSERT && action !is EnterAction) {
|
||||||
val undoService = injector.undo as VimTimestampBasedUndoService
|
val undoService = injector.undo as VimTimestampBasedUndoService
|
||||||
@@ -213,20 +206,6 @@ internal object IdeaSpecifics {
|
|||||||
this.completionData = null
|
this.completionData = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isGotoAction(actionId: String?): Boolean =
|
|
||||||
actionId == IdeActions.ACTION_GOTO_BACK || actionId == IdeActions.ACTION_GOTO_FORWARD
|
|
||||||
|
|
||||||
private fun saveJumpBeforeGoto(event: AnActionEvent, editor: Editor?) {
|
|
||||||
val project = event.dataContext.getData(CommonDataKeys.PROJECT)
|
|
||||||
val currentEditor = editor
|
|
||||||
?: event.dataContext.getData(CommonDataKeys.EDITOR)
|
|
||||||
?: project?.let { VimListenerManager.VimLastSelectedEditorTracker.getLastSelectedEditor(it) }
|
|
||||||
?: project?.let { FileEditorManager.getInstance(it).selectedTextEditor }
|
|
||||||
if (currentEditor != null && !currentEditor.isIdeaVimDisabledHere) {
|
|
||||||
injector.jumpService.saveJumpLocation(currentEditor.vim)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class CompletionData(
|
private data class CompletionData(
|
||||||
val completionStartMarker: RangeMarker,
|
val completionStartMarker: RangeMarker,
|
||||||
val originalStartOffset: Int,
|
val originalStartOffset: Int,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2023 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -9,7 +9,9 @@
|
|||||||
package com.maddyhome.idea.vim.newapi
|
package com.maddyhome.idea.vim.newapi
|
||||||
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.maddyhome.idea.vim.api.MessageType
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.project.ProjectManager
|
||||||
|
import com.intellij.openapi.wm.WindowManager
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
import com.maddyhome.idea.vim.api.VimMessagesBase
|
import com.maddyhome.idea.vim.api.VimMessagesBase
|
||||||
import com.maddyhome.idea.vim.api.globalOptions
|
import com.maddyhome.idea.vim.api.globalOptions
|
||||||
@@ -23,50 +25,56 @@ internal class IjVimMessages : VimMessagesBase() {
|
|||||||
private var message: String? = null
|
private var message: String? = null
|
||||||
private var error = false
|
private var error = false
|
||||||
private var lastBeepTimeMillis = 0L
|
private var lastBeepTimeMillis = 0L
|
||||||
|
private var allowClearStatusBarMessage = true
|
||||||
|
|
||||||
override fun showMessage(editor: VimEditor, message: String?) {
|
|
||||||
injector.outputPanel.clear(editor, injector.executionContextManager.getEditorExecutionContext(editor))
|
|
||||||
showMessageInternal(editor, message, MessageType.STANDARD)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showErrorMessage(editor: VimEditor, message: String?) {
|
|
||||||
injector.outputPanel.clear(editor, injector.executionContextManager.getEditorExecutionContext(editor))
|
|
||||||
showMessageInternal(editor, message, MessageType.ERROR)
|
|
||||||
indicateError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun appendErrorMessage(editor: VimEditor, message: String?) {
|
|
||||||
showMessageInternal(editor, message, MessageType.ERROR)
|
|
||||||
indicateError()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showMessageInternal(editor: VimEditor, message: String?, messageType: MessageType) {
|
|
||||||
this.message = message
|
|
||||||
|
|
||||||
if (message.isNullOrBlank()) {
|
|
||||||
clearStatusBarMessage()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val context = injector.executionContextManager.getEditorExecutionContext(editor)
|
|
||||||
injector.outputPanel.output(editor, context, message, messageType)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
|
override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
|
||||||
if (editor != null) {
|
fun setStatusBarMessage(project: Project, message: String?) {
|
||||||
showMessage(editor, message)
|
WindowManager.getInstance().getStatusBar(project)?.let {
|
||||||
} else {
|
it.info = if (message.isNullOrBlank()) "" else "Vim - $message"
|
||||||
// Legacy path for when editor is null - just store the message
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.message = message
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redraw happens automatically based on changes or scrolling. If we've just set the message (e.g., searching for a
|
||||||
|
// string, hitting the bottom and scrolling to the top), make sure we don't immediately clear it when scrolling.
|
||||||
|
allowClearStatusBarMessage = false
|
||||||
|
ApplicationManager.getApplication().invokeLater {
|
||||||
|
allowClearStatusBarMessage = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatusBarMessage(): String? = message
|
override fun getStatusBarMessage(): String? = message
|
||||||
|
|
||||||
|
// Vim doesn't appear to have a policy about clearing the status bar, other than on "redraw". This can be forced with
|
||||||
|
// <C-L> or the `:redraw` command, but also happens as the screen changes, e.g., when inserting or deleting lines,
|
||||||
|
// scrolling, entering Command-line mode and probably lots more. We should manually clear the status bar when these
|
||||||
|
// things happen.
|
||||||
override fun clearStatusBarMessage() {
|
override fun clearStatusBarMessage() {
|
||||||
if (message.isNullOrEmpty()) return
|
val currentMessage = message
|
||||||
injector.outputPanel.getCurrentOutputPanel()?.close()
|
if (currentMessage.isNullOrEmpty()) return
|
||||||
|
|
||||||
|
// Don't clear the status bar message if we've only just set it
|
||||||
|
if (!allowClearStatusBarMessage) return
|
||||||
|
|
||||||
|
ProjectManager.getInstance().openProjects.forEach { project ->
|
||||||
|
WindowManager.getInstance().getStatusBar(project)?.let { statusBar ->
|
||||||
|
// Only clear the status bar if it's showing our last message
|
||||||
|
if (statusBar.info?.contains(currentMessage) == true) {
|
||||||
|
statusBar.info = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
message = null
|
message = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ open class IjVimSearchGroup : VimSearchGroupBase(), PersistentStateComponent<Ele
|
|||||||
editor, pattern, startLine, endLine,
|
editor, pattern, startLine, endLine,
|
||||||
shouldIgnoreCase(pattern, lastIgnoreSmartCase)
|
shouldIgnoreCase(pattern, lastIgnoreSmartCase)
|
||||||
)
|
)
|
||||||
highlightSearchResults(editor.ij, results, -1)
|
highlightSearchResults(editor.ij, pattern, results, -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,12 @@ import com.intellij.ide.ui.LafManager
|
|||||||
import com.intellij.ide.ui.LafManagerListener
|
import com.intellij.ide.ui.LafManagerListener
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.openapi.wm.ex.ToolWindowManagerListener
|
|
||||||
import com.intellij.openapi.wm.impl.IdeBackgroundUtil
|
import com.intellij.openapi.wm.impl.IdeBackgroundUtil
|
||||||
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
|
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
|
||||||
import com.intellij.ui.ClientProperty
|
import com.intellij.ui.ClientProperty
|
||||||
import com.intellij.ui.JBColor
|
|
||||||
import com.intellij.ui.components.JBPanel
|
import com.intellij.ui.components.JBPanel
|
||||||
import com.intellij.ui.components.JBScrollPane
|
import com.intellij.ui.components.JBScrollPane
|
||||||
import com.intellij.util.IJSwingUtilities
|
import com.intellij.util.IJSwingUtilities
|
||||||
import com.intellij.util.messages.MessageBusConnection
|
|
||||||
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
|
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||||
@@ -27,6 +24,7 @@ import com.maddyhome.idea.vim.api.MessageType
|
|||||||
import com.maddyhome.idea.vim.api.VimOutputPanel
|
import com.maddyhome.idea.vim.api.VimOutputPanel
|
||||||
import com.maddyhome.idea.vim.api.globalOptions
|
import com.maddyhome.idea.vim.api.globalOptions
|
||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
|
import com.maddyhome.idea.vim.diagnostic.VimLogger
|
||||||
import com.maddyhome.idea.vim.helper.requestFocus
|
import com.maddyhome.idea.vim.helper.requestFocus
|
||||||
import com.maddyhome.idea.vim.helper.selectEditorFont
|
import com.maddyhome.idea.vim.helper.selectEditorFont
|
||||||
import com.maddyhome.idea.vim.helper.vimMorePanel
|
import com.maddyhome.idea.vim.helper.vimMorePanel
|
||||||
@@ -38,167 +36,121 @@ import java.awt.event.ComponentAdapter
|
|||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.awt.event.KeyAdapter
|
import java.awt.event.KeyAdapter
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JLabel
|
import javax.swing.JLabel
|
||||||
|
import javax.swing.JRootPane
|
||||||
import javax.swing.JScrollPane
|
import javax.swing.JScrollPane
|
||||||
import javax.swing.JTextPane
|
import javax.swing.JTextArea
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
import javax.swing.text.DefaultCaret
|
|
||||||
import javax.swing.text.SimpleAttributeSet
|
|
||||||
import javax.swing.text.StyleConstants
|
|
||||||
import javax.swing.text.StyledDocument
|
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Panel that displays text in a `more` like window overlaid on the editor.
|
* This panel displays text in a `more` like window and implements [VimOutputPanel].
|
||||||
*/
|
*/
|
||||||
class OutputPanel private constructor(
|
class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), VimOutputPanel {
|
||||||
private val editor: Editor,
|
private val myEditorRef: WeakReference<Editor> = editorRef
|
||||||
) : JBPanel<OutputPanel>(), VimOutputPanel {
|
val editor: Editor? get() = myEditorRef.get()
|
||||||
|
|
||||||
private val textPane = JTextPane()
|
val myLabel: JLabel = JLabel("more")
|
||||||
private val resizeAdapter: ComponentAdapter
|
private val myText = JTextArea()
|
||||||
private var defaultForeground: Color? = null
|
private val myScrollPane: JScrollPane =
|
||||||
|
JBScrollPane(myText, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
|
||||||
|
private val myAdapter: ComponentAdapter
|
||||||
|
private var myLineHeight = 0
|
||||||
|
|
||||||
private var glassPane: JComponent? = null
|
private var myOldGlass: JComponent? = null
|
||||||
private var originalLayout: LayoutManager? = null
|
private var myOldLayout: LayoutManager? = null
|
||||||
private var wasOpaque = false
|
private var myWasOpaque = false
|
||||||
private var toolWindowListenerConnection: MessageBusConnection? = null
|
|
||||||
|
|
||||||
var active: Boolean = false
|
var myActive: Boolean = false
|
||||||
private val segments = mutableListOf<TextLine>()
|
|
||||||
|
|
||||||
private val labelComponent: JLabel = JLabel("more")
|
val isActive: Boolean
|
||||||
private val scrollPane: JScrollPane =
|
get() = myActive
|
||||||
JBScrollPane(textPane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
|
|
||||||
private var cachedLineHeight = 0
|
|
||||||
private var isSingleLine = false
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
textPane.isEditable = false
|
// Create a text editor for the text and a label for the prompt
|
||||||
textPane.caret = object : DefaultCaret() {
|
val layout = BorderLayout(0, 0)
|
||||||
override fun setVisible(v: Boolean) {
|
setLayout(layout)
|
||||||
super.setVisible(false)
|
add(myScrollPane, BorderLayout.CENTER)
|
||||||
}
|
add(myLabel, BorderLayout.SOUTH)
|
||||||
}
|
|
||||||
textPane.highlighter = null
|
|
||||||
|
|
||||||
resizeAdapter = object : ComponentAdapter() {
|
// Set the text area read only, and support wrap
|
||||||
|
myText.isEditable = false
|
||||||
|
myText.setLineWrap(true)
|
||||||
|
|
||||||
|
myAdapter = object : ComponentAdapter() {
|
||||||
override fun componentResized(e: ComponentEvent?) {
|
override fun componentResized(e: ComponentEvent?) {
|
||||||
positionPanel()
|
positionPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppress the fancy frame background used in the Islands theme
|
// Setup some listeners to handle keystrokes
|
||||||
ClientProperty.putRecursive(this, IdeBackgroundUtil.NO_BACKGROUND, true)
|
val moreKeyListener = MoreKeyListener()
|
||||||
|
addKeyListener(moreKeyListener)
|
||||||
|
myText.addKeyListener(moreKeyListener)
|
||||||
|
|
||||||
// Initialize panel
|
// Suppress the fancy frame background used in the Islands theme, which comes from a custom Graphics implementation
|
||||||
setLayout(BorderLayout(0, 0))
|
// applied to the IdeRoot, and used to paint all children, including this panel. This client property is checked by
|
||||||
add(scrollPane, BorderLayout.CENTER)
|
// JBPanel.getComponentGraphics to give us the original Graphics, opting out of the fancy painting.
|
||||||
add(labelComponent, BorderLayout.SOUTH)
|
ClientProperty.putRecursive<Boolean?>(this, IdeBackgroundUtil.NO_BACKGROUND, true)
|
||||||
|
|
||||||
val keyListener = OutputPanelKeyListener()
|
|
||||||
addKeyListener(keyListener)
|
|
||||||
textPane.addKeyListener(keyListener)
|
|
||||||
editor?.let { putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, it.component) }
|
editor?.let { putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, it.component) }
|
||||||
|
|
||||||
updateUI()
|
updateUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called automatically when the LAF is changed and the component is visible, and manually by the LAF listener handler
|
||||||
override fun updateUI() {
|
override fun updateUI() {
|
||||||
super.updateUI()
|
super.updateUI()
|
||||||
|
|
||||||
setBorder(ExPanelBorder())
|
setBorder(ExPanelBorder())
|
||||||
|
|
||||||
|
// Swing uses a bad pattern of calling updateUI() from the constructor. At this moment, all these variables are null
|
||||||
@Suppress("SENSELESS_COMPARISON")
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
if (textPane != null && labelComponent != null && scrollPane != null) {
|
if (myText != null && myLabel != null && myScrollPane != null) {
|
||||||
setFontForElements()
|
setFontForElements()
|
||||||
textPane.setBorder(null)
|
myText.setBorder(null)
|
||||||
scrollPane.setBorder(null)
|
myScrollPane.setBorder(null)
|
||||||
labelComponent.setForeground(textPane.getForeground())
|
myLabel.setForeground(myText.getForeground())
|
||||||
|
|
||||||
|
// Make sure the panel is positioned correctly in case we're changing font size
|
||||||
positionPanel()
|
positionPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override var text: String
|
override var text: String
|
||||||
get() = textPane.getText() ?: ""
|
get() = myText.text
|
||||||
set(value) {
|
set(value) {
|
||||||
|
// ExOutputPanel will strip a trailing newline. We'll do it now so that tests have the same behaviour.
|
||||||
val newValue = value.removeSuffix("\n")
|
val newValue = value.removeSuffix("\n")
|
||||||
segments.clear()
|
myText.text = newValue
|
||||||
if (newValue.isEmpty()) return
|
val ed = editor
|
||||||
segments.add(TextLine(newValue, null))
|
if (ed != null) {
|
||||||
|
myText.setFont(selectEditorFont(ed, newValue))
|
||||||
|
}
|
||||||
|
myText.setCaretPosition(0)
|
||||||
|
if (newValue.isNotEmpty()) {
|
||||||
|
activate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override var label: String
|
override var label: String
|
||||||
get() = labelComponent.text
|
get() = myLabel.text ?: ""
|
||||||
set(value) {
|
set(value) {
|
||||||
labelComponent.text = value
|
myLabel.text = value
|
||||||
|
val ed = editor
|
||||||
|
if (ed != null) {
|
||||||
|
myLabel.setFont(selectEditorFont(ed, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets styled text with multiple segments, each potentially having a different color.
|
|
||||||
*/
|
|
||||||
fun setStyledText(lines: List<TextLine>) {
|
|
||||||
val doc = textPane.styledDocument
|
|
||||||
doc.remove(0, doc.length)
|
|
||||||
|
|
||||||
if (defaultForeground == null) {
|
|
||||||
defaultForeground = textPane.foreground
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lines.size > 1) {
|
|
||||||
setMultiLineText(lines, doc)
|
|
||||||
} else {
|
|
||||||
doc.insertString(doc.length, lines[0].text.removeSuffix("\n"), getLineColor(lines[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
val fullText = doc.getText(0, doc.length)
|
|
||||||
textPane.setFont(selectEditorFont(editor, fullText))
|
|
||||||
textPane.setCaretPosition(0)
|
|
||||||
if (fullText.isNotEmpty()) {
|
|
||||||
activate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setMultiLineText(
|
|
||||||
lines: List<TextLine>,
|
|
||||||
doc: StyledDocument,
|
|
||||||
) {
|
|
||||||
for ((index, line) in lines.withIndex()) {
|
|
||||||
val text = line.text.removeSuffix("\n")
|
|
||||||
val attrs = getLineColor(line)
|
|
||||||
val separator = if (index < lines.size - 1) "\n" else ""
|
|
||||||
doc.insertString(doc.length, text + separator, attrs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLineColor(segment: TextLine): SimpleAttributeSet {
|
|
||||||
val attrs = SimpleAttributeSet()
|
|
||||||
val color = segment.color ?: defaultForeground
|
|
||||||
if (color != null) {
|
|
||||||
StyleConstants.setForeground(attrs, color)
|
|
||||||
}
|
|
||||||
return attrs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addText(text: String, isNewLine: Boolean, messageType: MessageType) {
|
override fun addText(text: String, isNewLine: Boolean, messageType: MessageType) {
|
||||||
val color = when (messageType) {
|
if (this.text.isNotEmpty() && isNewLine) {
|
||||||
MessageType.ERROR -> JBColor.RED
|
this.text += "\n$text"
|
||||||
MessageType.STANDARD -> null
|
} else {
|
||||||
}
|
this.text += text
|
||||||
segments.add(TextLine(text, color))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun show() {
|
|
||||||
val currentPanel = injector.outputPanel.getCurrentOutputPanel()
|
|
||||||
if (currentPanel != null && currentPanel != this) currentPanel.close()
|
|
||||||
|
|
||||||
setStyledText(segments)
|
|
||||||
if (!active) {
|
|
||||||
activate()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,15 +159,20 @@ class OutputPanel private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun clearText() {
|
override fun clearText() {
|
||||||
segments.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear() {
|
|
||||||
text = ""
|
text = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleKey(key: KeyStroke) {
|
override fun show() {
|
||||||
|
editor ?: return
|
||||||
|
val currentPanel = injector.outputPanel.getCurrentOutputPanel()
|
||||||
|
if (currentPanel != null && currentPanel != this) currentPanel.close()
|
||||||
|
|
||||||
|
if (!myActive) {
|
||||||
|
activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleKey(key: KeyStroke) {
|
||||||
if (isAtEnd) {
|
if (isAtEnd) {
|
||||||
close(key)
|
close(key)
|
||||||
return
|
return
|
||||||
@@ -240,77 +197,183 @@ class OutputPanel private constructor(
|
|||||||
|
|
||||||
override fun getForeground(): Color? {
|
override fun getForeground(): Color? {
|
||||||
@Suppress("SENSELESS_COMPARISON")
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
if (textPane == null) {
|
if (myText == null) {
|
||||||
|
// Swing uses a bad pattern of calling getForeground() from the constructor. At this moment, `myText` is null.
|
||||||
return super.getForeground()
|
return super.getForeground()
|
||||||
}
|
}
|
||||||
return textPane.getForeground()
|
return myText.getForeground()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBackground(): Color? {
|
override fun getBackground(): Color? {
|
||||||
@Suppress("SENSELESS_COMPARISON")
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
if (textPane == null) {
|
if (myText == null) {
|
||||||
|
// Swing uses a bad pattern of calling getBackground() from the constructor. At this moment, `myText` is null.
|
||||||
return super.getBackground()
|
return super.getBackground()
|
||||||
}
|
}
|
||||||
return textPane.getBackground()
|
return myText.getBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns off the output panel and optionally puts the focus back to the original component.
|
* Turns off the ex entry field and optionally puts the focus back to the original component
|
||||||
*/
|
*/
|
||||||
fun deactivate(refocusOwningEditor: Boolean) {
|
fun deactivate(refocusOwningEditor: Boolean) {
|
||||||
if (!active) return
|
if (!myActive) return
|
||||||
active = false
|
myActive = false
|
||||||
clearText()
|
myText.text = ""
|
||||||
textPane.text = ""
|
val ed = editor
|
||||||
if (refocusOwningEditor) {
|
if (refocusOwningEditor && ed != null) {
|
||||||
requestFocus(editor.contentComponent)
|
requestFocus(ed.contentComponent)
|
||||||
}
|
}
|
||||||
if (glassPane != null) {
|
if (myOldGlass != null) {
|
||||||
glassPane!!.removeComponentListener(resizeAdapter)
|
myOldGlass!!.removeComponentListener(myAdapter)
|
||||||
toolWindowListenerConnection?.disconnect()
|
myOldGlass!!.isVisible = false
|
||||||
toolWindowListenerConnection = null
|
myOldGlass!!.remove(this)
|
||||||
glassPane!!.isVisible = false
|
myOldGlass!!.setOpaque(myWasOpaque)
|
||||||
glassPane!!.remove(this)
|
myOldGlass!!.setLayout(myOldLayout)
|
||||||
glassPane!!.setOpaque(wasOpaque)
|
|
||||||
glassPane!!.setLayout(originalLayout)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns on the output panel for the given editor.
|
* Turns on the more window for the given editor
|
||||||
*/
|
*/
|
||||||
fun activate() {
|
fun activate() {
|
||||||
disableOldGlass()
|
val ed = editor ?: return
|
||||||
|
val root = SwingUtilities.getRootPane(ed.contentComponent)
|
||||||
|
deactivateOldGlass(root)
|
||||||
|
|
||||||
setFontForElements()
|
setFontForElements()
|
||||||
positionPanel()
|
positionPanel()
|
||||||
|
|
||||||
if (glassPane != null) {
|
if (myOldGlass != null) {
|
||||||
glassPane!!.isVisible = true
|
myOldGlass!!.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
active = true
|
myActive = true
|
||||||
if (isSingleLine) return
|
requestFocus(myText)
|
||||||
|
|
||||||
requestFocus(textPane)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun disableOldGlass() {
|
private fun deactivateOldGlass(root: JRootPane?) {
|
||||||
val root = SwingUtilities.getRootPane(editor.contentComponent) ?: return
|
if (root == null) return
|
||||||
glassPane = root.getGlassPane() as JComponent?
|
myOldGlass = root.getGlassPane() as JComponent?
|
||||||
if (glassPane == null) {
|
if (myOldGlass != null) {
|
||||||
|
myOldLayout = myOldGlass!!.layout
|
||||||
|
myWasOpaque = myOldGlass!!.isOpaque
|
||||||
|
myOldGlass!!.setLayout(null)
|
||||||
|
myOldGlass!!.setOpaque(false)
|
||||||
|
myOldGlass!!.add(this)
|
||||||
|
myOldGlass!!.addComponentListener(myAdapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setFontForElements() {
|
||||||
|
val ed = editor ?: return
|
||||||
|
myText.setFont(selectEditorFont(ed, myText.getText()))
|
||||||
|
myLabel.setFont(selectEditorFont(ed, myLabel.text))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun scrollLine() {
|
||||||
|
scrollOffset(myLineHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun scrollPage() {
|
||||||
|
scrollOffset(myScrollPane.getVerticalScrollBar().visibleAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun scrollHalfPage() {
|
||||||
|
val sa = myScrollPane.getVerticalScrollBar().visibleAmount / 2.0
|
||||||
|
val offset = ceil(sa / myLineHeight) * myLineHeight
|
||||||
|
scrollOffset(offset.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBadKey() {
|
||||||
|
val ed = editor ?: return
|
||||||
|
myLabel.setText(injector.messages.message("message.ex.output.more.prompt.full"))
|
||||||
|
myLabel.setFont(selectEditorFont(ed, myLabel.text))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrollOffset(more: Int) {
|
||||||
|
val ed = editor ?: return
|
||||||
|
val `val` = myScrollPane.getVerticalScrollBar().value
|
||||||
|
myScrollPane.getVerticalScrollBar().setValue(`val` + more)
|
||||||
|
myScrollPane.getHorizontalScrollBar().setValue(0)
|
||||||
|
if (isAtEnd) {
|
||||||
|
myLabel.setText(injector.messages.message("message.ex.output.end.prompt"))
|
||||||
|
} else {
|
||||||
|
myLabel.setText(injector.messages.message("message.ex.output.more.prompt"))
|
||||||
|
}
|
||||||
|
myLabel.setFont(selectEditorFont(ed, myLabel.text))
|
||||||
|
}
|
||||||
|
|
||||||
|
val isAtEnd: Boolean
|
||||||
|
get() {
|
||||||
|
val isSingleLine = myText.getLineCount() == 1
|
||||||
|
if (isSingleLine) return true
|
||||||
|
val scrollBar = myScrollPane.getVerticalScrollBar()
|
||||||
|
val value = scrollBar.value
|
||||||
|
if (!scrollBar.isVisible) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return value >= scrollBar.maximum - scrollBar.visibleAmount ||
|
||||||
|
scrollBar.maximum <= scrollBar.visibleAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun positionPanel() {
|
||||||
|
val ed = editor ?: return
|
||||||
|
val contentComponent = ed.contentComponent
|
||||||
|
val scroll = SwingUtilities.getAncestorOfClass(JScrollPane::class.java, contentComponent)
|
||||||
|
val rootPane = SwingUtilities.getRootPane(contentComponent)
|
||||||
|
if (scroll == null || rootPane == null) {
|
||||||
|
// These might be null if we're invoked during component initialisation and before it's been added to the tree
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
originalLayout = glassPane!!.layout
|
|
||||||
wasOpaque = glassPane!!.isOpaque
|
size = scroll.size
|
||||||
glassPane!!.setLayout(null)
|
|
||||||
glassPane!!.setOpaque(false)
|
myLineHeight = myText.getFontMetrics(myText.getFont()).height
|
||||||
glassPane!!.add(this)
|
val count: Int = countLines(myText.getText())
|
||||||
glassPane!!.addComponentListener(resizeAdapter)
|
val visLines = size.height / myLineHeight - 1
|
||||||
val project = editor.project
|
val lines = min(count, visLines)
|
||||||
if (project != null) {
|
setSize(
|
||||||
toolWindowListenerConnection = project.messageBus.connect()
|
size.width,
|
||||||
toolWindowListenerConnection!!.subscribe(ToolWindowManagerListener.TOPIC, ToolWindowPositioningListener { positionPanel() })
|
lines * myLineHeight + myLabel.getPreferredSize().height + border.getBorderInsets(this).top * 2
|
||||||
|
)
|
||||||
|
|
||||||
|
val height = size.height
|
||||||
|
val bounds = scroll.bounds
|
||||||
|
bounds.translate(0, scroll.getHeight() - height)
|
||||||
|
bounds.height = height
|
||||||
|
val pos = SwingUtilities.convertPoint(scroll.getParent(), bounds.location, rootPane.getGlassPane())
|
||||||
|
bounds.location = pos
|
||||||
|
setBounds(bounds)
|
||||||
|
|
||||||
|
myScrollPane.getVerticalScrollBar().setValue(0)
|
||||||
|
if (!injector.globalOptions().more) {
|
||||||
|
// FIX
|
||||||
|
scrollOffset(100000)
|
||||||
|
} else {
|
||||||
|
scrollOffset(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close(key: KeyStroke? = null) {
|
||||||
|
val ed = editor ?: return
|
||||||
|
ApplicationManager.getApplication().invokeLater {
|
||||||
|
deactivate(true)
|
||||||
|
val project = ed.project
|
||||||
|
if (project != null && key != null && key.keyChar != '\n') {
|
||||||
|
val keys: MutableList<KeyStroke> = ArrayList(1)
|
||||||
|
keys.add(key)
|
||||||
|
if (LOG.isTrace()) {
|
||||||
|
LOG.trace(
|
||||||
|
"Adding new keys to keyStack as part of playback. State before adding keys: " +
|
||||||
|
getInstance().keyStack.dump()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getInstance().keyStack.addKeys(keys)
|
||||||
|
val context: ExecutionContext =
|
||||||
|
injector.executionContextManager.getEditorExecutionContext(IjVimEditor(ed))
|
||||||
|
VimPlugin.getMacro().playbackKeys(IjVimEditor(ed), context, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,88 +381,61 @@ class OutputPanel private constructor(
|
|||||||
close(null)
|
close(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close(key: KeyStroke?) {
|
private class MoreKeyListener : KeyAdapter() {
|
||||||
val passKeyBack = isSingleLine
|
/**
|
||||||
ApplicationManager.getApplication().invokeLater {
|
* Invoked when a key has been pressed.
|
||||||
deactivate(true)
|
*/
|
||||||
val project = editor.project
|
override fun keyTyped(e: KeyEvent) {
|
||||||
// For single line messages, pass any key back to the editor (including Enter)
|
val currentPanel: VimOutputPanel = injector.outputPanel.getCurrentOutputPanel() ?: return
|
||||||
// For multi-line messages, don't pass Enter back (it was used to dismiss)
|
|
||||||
if (project != null && key != null && (passKeyBack || key.keyChar != '\n')) {
|
val keyCode = e.getKeyCode()
|
||||||
val keys: MutableList<KeyStroke> = ArrayList(1)
|
val keyChar = e.getKeyChar()
|
||||||
keys.add(key)
|
val modifiers = e.modifiersEx
|
||||||
getInstance().keyStack.addKeys(keys)
|
val keyStroke = if (keyChar == KeyEvent.CHAR_UNDEFINED)
|
||||||
val context: ExecutionContext =
|
KeyStroke.getKeyStroke(keyCode, modifiers)
|
||||||
injector.executionContextManager.getEditorExecutionContext(IjVimEditor(editor))
|
else
|
||||||
VimPlugin.getMacro().playbackKeys(IjVimEditor(editor), context, 1)
|
KeyStroke.getKeyStroke(keyChar, modifiers)
|
||||||
|
currentPanel.handleKey(keyStroke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
IJSwingUtilities.updateComponentTreeUI(getInstance(editor))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setFontForElements() {
|
companion object {
|
||||||
textPane.setFont(selectEditorFont(editor, textPane.getText()))
|
private val LOG: VimLogger = injector.getLogger<OutputPanel>(OutputPanel::class.java)
|
||||||
labelComponent.setFont(selectEditorFont(editor, labelComponent.text))
|
|
||||||
|
fun getNullablePanel(editor: Editor): OutputPanel? {
|
||||||
|
return editor.vimMorePanel as? OutputPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun positionPanel() {
|
fun isPanelActive(editor: Editor): Boolean {
|
||||||
val scroll = positionPanelStart() ?: return
|
return getNullablePanel(editor)?.myActive ?: false
|
||||||
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? {
|
fun getInstance(editor: Editor): OutputPanel {
|
||||||
val contentComponent = editor.contentComponent
|
var panel: OutputPanel? = getNullablePanel(editor)
|
||||||
val scroll = SwingUtilities.getAncestorOfClass(JScrollPane::class.java, contentComponent) as? JScrollPane
|
if (panel == null) {
|
||||||
val rootPane = SwingUtilities.getRootPane(contentComponent)
|
panel = OutputPanel(WeakReference(editor))
|
||||||
if (scroll == null || rootPane == null) {
|
editor.vimMorePanel = panel
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
return panel
|
||||||
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 {
|
private fun countLines(text: String): Int {
|
||||||
if (text.isEmpty()) {
|
if (text.isEmpty()) {
|
||||||
return 1
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = 0
|
var count = 0
|
||||||
@@ -414,124 +450,5 @@ class OutputPanel private constructor(
|
|||||||
|
|
||||||
return 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 keyChar = e.keyChar
|
|
||||||
val modifiers = e.modifiersEx
|
|
||||||
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
|
|
||||||
|
|
||||||
for (vimEditor in injector.editorGroup.getEditors()) {
|
|
||||||
val editor = (vimEditor as IjVimEditor).editor
|
|
||||||
if (!isPanelActive(editor)) continue
|
|
||||||
IJSwingUtilities.updateComponentTreeUI(getInstance(editor))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getNullablePanel(editor: Editor): OutputPanel? {
|
|
||||||
return editor.vimMorePanel as OutputPanel?
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPanelActive(editor: Editor): Boolean {
|
|
||||||
return getNullablePanel(editor) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getInstance(editor: Editor): OutputPanel {
|
|
||||||
var panel: OutputPanel? = getNullablePanel(editor)
|
|
||||||
if (panel == null) {
|
|
||||||
panel = OutputPanel(editor)
|
|
||||||
editor.vimMorePanel = panel
|
|
||||||
}
|
|
||||||
return panel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
data class TextLine(val text: String, val color: Color?)
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by an MIT-style
|
|
||||||
* license that can be found in the LICENSE.txt file or at
|
|
||||||
* https://opensource.org/licenses/MIT.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.maddyhome.idea.vim.ui
|
|
||||||
|
|
||||||
import com.intellij.openapi.wm.ToolWindowManager
|
|
||||||
import com.intellij.openapi.wm.ex.ToolWindowManagerListener
|
|
||||||
import javax.swing.SwingUtilities
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repositions a panel whenever a tool window visibility state changes.
|
|
||||||
* Shared between [com.maddyhome.idea.vim.ui.ex.ExEntryPanel] and [OutputPanel].
|
|
||||||
*/
|
|
||||||
internal class ToolWindowPositioningListener(private val reposition: () -> Unit) : ToolWindowManagerListener {
|
|
||||||
override fun stateChanged(toolWindowManager: ToolWindowManager) {
|
|
||||||
SwingUtilities.invokeLater(reposition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,11 +15,9 @@ import com.intellij.openapi.diagnostic.Logger
|
|||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.openapi.editor.colors.EditorColors
|
import com.intellij.openapi.editor.colors.EditorColors
|
||||||
import com.intellij.openapi.wm.IdeFocusManager
|
import com.intellij.openapi.wm.IdeFocusManager
|
||||||
import com.intellij.openapi.wm.ex.ToolWindowManagerListener
|
|
||||||
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
|
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
|
||||||
import com.intellij.ui.DocumentAdapter
|
import com.intellij.ui.DocumentAdapter
|
||||||
import com.intellij.util.IJSwingUtilities
|
import com.intellij.util.IJSwingUtilities
|
||||||
import com.intellij.util.messages.MessageBusConnection
|
|
||||||
import com.maddyhome.idea.vim.EventFacade
|
import com.maddyhome.idea.vim.EventFacade
|
||||||
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
|
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
@@ -41,7 +39,6 @@ import com.maddyhome.idea.vim.key.interceptors.VimInputInterceptor
|
|||||||
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
||||||
import com.maddyhome.idea.vim.ui.ExPanelBorder
|
import com.maddyhome.idea.vim.ui.ExPanelBorder
|
||||||
import com.maddyhome.idea.vim.ui.ToolWindowPositioningListener
|
|
||||||
import com.maddyhome.idea.vim.vimscript.model.commands.Command
|
import com.maddyhome.idea.vim.vimscript.model.commands.Command
|
||||||
import com.maddyhome.idea.vim.vimscript.model.commands.GlobalCommand
|
import com.maddyhome.idea.vim.vimscript.model.commands.GlobalCommand
|
||||||
import com.maddyhome.idea.vim.vimscript.model.commands.SubstituteCommand
|
import com.maddyhome.idea.vim.vimscript.model.commands.SubstituteCommand
|
||||||
@@ -146,11 +143,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
glassPane.setOpaque(false)
|
glassPane.setOpaque(false)
|
||||||
glassPane.add(this)
|
glassPane.add(this)
|
||||||
glassPane.addComponentListener(resizePanelListener)
|
glassPane.addComponentListener(resizePanelListener)
|
||||||
val project = editor.project
|
|
||||||
if (project != null) {
|
|
||||||
toolWindowListenerConnection = project.messageBus.connect()
|
|
||||||
toolWindowListenerConnection!!.subscribe(ToolWindowManagerListener.TOPIC, ToolWindowPositioningListener { positionPanel() })
|
|
||||||
}
|
|
||||||
positionPanel()
|
positionPanel()
|
||||||
glassPane.isVisible = true
|
glassPane.isVisible = true
|
||||||
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, parent)
|
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, parent)
|
||||||
@@ -203,8 +195,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
|
|
||||||
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, null)
|
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, null)
|
||||||
oldGlass!!.removeComponentListener(resizePanelListener)
|
oldGlass!!.removeComponentListener(resizePanelListener)
|
||||||
toolWindowListenerConnection?.disconnect()
|
|
||||||
toolWindowListenerConnection = null
|
|
||||||
oldGlass!!.isVisible = false
|
oldGlass!!.isVisible = false
|
||||||
oldGlass!!.remove(this)
|
oldGlass!!.remove(this)
|
||||||
oldGlass!!.setOpaque(wasOpaque)
|
oldGlass!!.setOpaque(wasOpaque)
|
||||||
@@ -520,8 +510,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var toolWindowListenerConnection: MessageBusConnection? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val layout = GridBagLayout()
|
val layout = GridBagLayout()
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ class IjOutputPanelService : VimOutputPanelServiceBase() {
|
|||||||
private var activeOutputPanel: WeakReference<VimOutputPanel>? = null
|
private var activeOutputPanel: WeakReference<VimOutputPanel>? = null
|
||||||
|
|
||||||
override fun getCurrentOutputPanel(): VimOutputPanel? {
|
override fun getCurrentOutputPanel(): VimOutputPanel? {
|
||||||
return activeOutputPanel?.get()?.takeIf { (it as OutputPanel).active }
|
return activeOutputPanel?.get()?.takeIf { (it as OutputPanel).isActive }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun create(editor: VimEditor, context: ExecutionContext): VimOutputPanel {
|
override fun create(editor: VimEditor, context: ExecutionContext): VimOutputPanel {
|
||||||
val panel = OutputPanel.getInstance(editor.ij)
|
val panel = OutputPanel(WeakReference(editor.ij))
|
||||||
activeOutputPanel = WeakReference(panel)
|
activeOutputPanel = WeakReference(panel)
|
||||||
return panel
|
return panel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,12 @@
|
|||||||
|
|
||||||
package org.jetbrains.plugins.ideavim.action.motion.search
|
package org.jetbrains.plugins.ideavim.action.motion.search
|
||||||
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode
|
import com.maddyhome.idea.vim.state.mode.Mode
|
||||||
import com.maddyhome.idea.vim.state.mode.SelectionType
|
import com.maddyhome.idea.vim.state.mode.SelectionType
|
||||||
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
|
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
import org.jetbrains.plugins.ideavim.VimTestCase
|
||||||
import org.junit.jupiter.api.Disabled
|
import org.junit.jupiter.api.Disabled
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
@@ -29,14 +27,6 @@ class SearchEntryFwdActionTest : VimTestCase() {
|
|||||||
assertStatusLineCleared()
|
assertStatusLineCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test search not found shows only error message on output panel`() {
|
|
||||||
configureByText("lorem ipsum dolor sit amet")
|
|
||||||
enterSearch("nonexistent")
|
|
||||||
val panelText = injector.outputPanel.getCurrentOutputPanel()?.text ?: ""
|
|
||||||
assertEquals("E486: Pattern not found: nonexistent", panelText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `search in visual mode`() {
|
fun `search in visual mode`() {
|
||||||
doTest(
|
doTest(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2023 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -65,49 +65,6 @@ class SearchWholeWordForwardActionTest : VimTestCase() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test repeated star search wraps around`() {
|
|
||||||
configureByText(
|
|
||||||
"""
|
|
||||||
aaa
|
|
||||||
abc
|
|
||||||
def
|
|
||||||
abc
|
|
||||||
dfg
|
|
||||||
abc
|
|
||||||
agg
|
|
||||||
abc
|
|
||||||
xyz
|
|
||||||
""".trimIndent(),
|
|
||||||
)
|
|
||||||
typeText("5j") // move to line 5, "abc"
|
|
||||||
assertPosition(5, 0)
|
|
||||||
|
|
||||||
typeText("*")
|
|
||||||
assertPosition(7, 0) // next "abc" forward
|
|
||||||
|
|
||||||
typeText("*")
|
|
||||||
assertPosition(1, 0) // wraps to first "abc"
|
|
||||||
|
|
||||||
typeText("*")
|
|
||||||
assertPosition(3, 0)
|
|
||||||
|
|
||||||
typeText("*")
|
|
||||||
assertPosition(5, 0)
|
|
||||||
|
|
||||||
typeText("*")
|
|
||||||
assertPosition(7, 0)
|
|
||||||
|
|
||||||
typeText("*")
|
|
||||||
assertPosition(1, 0) // wraps again
|
|
||||||
|
|
||||||
typeText("*")
|
|
||||||
assertPosition(3, 0)
|
|
||||||
|
|
||||||
typeText("*")
|
|
||||||
assertPosition(5, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test last word`() {
|
fun `test last word`() {
|
||||||
doTest(
|
doTest(
|
||||||
|
|||||||
@@ -169,31 +169,4 @@ class AddressTest : VimTestCase() {
|
|||||||
typeText(commandToKeys("/bar//foo/d"))
|
typeText(commandToKeys("/bar//foo/d"))
|
||||||
assertState("a\nfoo\nbar\nbar\nbaz\n")
|
assertState("a\nfoo\nbar\nbar\nbaz\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test backslash-slash range without previous search reports E35`() {
|
|
||||||
// Before this fix, using \/ with no previous search caused a NullPointerException instead of E35
|
|
||||||
configureByText("1\n2\n3\n")
|
|
||||||
typeText(commandToKeys("\\/d"))
|
|
||||||
assertPluginError(true)
|
|
||||||
assertPluginErrorMessage("E35: No previous regular expression")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test backslash-question range without previous search reports E35`() {
|
|
||||||
// Before this fix, using \? with no previous search caused a NullPointerException instead of E35
|
|
||||||
configureByText("1\n2\n3\n")
|
|
||||||
typeText(commandToKeys("\\?d"))
|
|
||||||
assertPluginError(true)
|
|
||||||
assertPluginErrorMessage("E35: No previous regular expression")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test backslash-ampersand range without previous substitute reports E33`() {
|
|
||||||
// Before this fix, using \& with no previous substitute caused a NullPointerException instead of E33
|
|
||||||
configureByText("1\n2\n3\n")
|
|
||||||
typeText(commandToKeys("\\&d"))
|
|
||||||
assertPluginError(true)
|
|
||||||
assertPluginErrorMessage("E33: No previous substitute regular expression")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2023 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -134,7 +134,7 @@ class CmdCommandTest : VimTestCase() {
|
|||||||
VimPlugin.getCommand().resetAliases()
|
VimPlugin.getCommand().resetAliases()
|
||||||
configureByText("\n")
|
configureByText("\n")
|
||||||
typeText(commandToKeys("command! -range Error echo <args>"))
|
typeText(commandToKeys("command! -range Error echo <args>"))
|
||||||
assertPluginError(true)
|
assertPluginError(false)
|
||||||
kotlin.test.assertEquals("'-range' is not supported by `command`", injector.messages.getStatusBarMessage())
|
kotlin.test.assertEquals("'-range' is not supported by `command`", injector.messages.getStatusBarMessage())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ class CmdCommandTest : VimTestCase() {
|
|||||||
VimPlugin.getCommand().resetAliases()
|
VimPlugin.getCommand().resetAliases()
|
||||||
configureByText("\n")
|
configureByText("\n")
|
||||||
typeText(commandToKeys("command! -complete=color Error echo <args>"))
|
typeText(commandToKeys("command! -complete=color Error echo <args>"))
|
||||||
assertPluginError(true)
|
assertPluginError(false)
|
||||||
kotlin.test.assertEquals("'-complete' is not supported by `command`", injector.messages.getStatusBarMessage())
|
kotlin.test.assertEquals("'-complete' is not supported by `command`", injector.messages.getStatusBarMessage())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2023 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -24,6 +24,7 @@ class ExecuteCommandTest : VimTestCase() {
|
|||||||
fun `test execute with range`() {
|
fun `test execute with range`() {
|
||||||
configureByText("\n")
|
configureByText("\n")
|
||||||
typeText(commandToKeys("1,2execute 'echo 42'"))
|
typeText(commandToKeys("1,2execute 'echo 42'"))
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class HistoryCommandTest : VimTestCase() {
|
|||||||
fun `test history with 'history' option set to 0 shows nothing`() {
|
fun `test history with 'history' option set to 0 shows nothing`() {
|
||||||
enterCommand("set history=0")
|
enterCommand("set history=0")
|
||||||
enterCommand("history")
|
enterCommand("history")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(false)
|
assertPluginError(false)
|
||||||
assertPluginErrorMessage("'history' option is zero")
|
assertPluginErrorMessage("'history' option is zero")
|
||||||
}
|
}
|
||||||
@@ -224,6 +225,17 @@ class HistoryCommandTest : VimTestCase() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test history cmd lists empty command history`() {
|
||||||
|
assertCommandOutput(
|
||||||
|
"history cmd",
|
||||||
|
"""
|
||||||
|
| # cmd history
|
||||||
|
|> 1 history cmd
|
||||||
|
""".trimMargin()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test history cmd lists current cmd in history`() {
|
fun `test history cmd lists current cmd in history`() {
|
||||||
assertCommandOutput(
|
assertCommandOutput(
|
||||||
@@ -488,7 +500,7 @@ class HistoryCommandTest : VimTestCase() {
|
|||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test history search with first number lists single entry from search history`() {
|
fun `test history search with first number lists single entry from saerch history`() {
|
||||||
repeat(10) { i -> enterSearch("foo${i + 1}") }
|
repeat(10) { i -> enterSearch("foo${i + 1}") }
|
||||||
injector.outputPanel.getCurrentOutputPanel()?.clearText()
|
injector.outputPanel.getCurrentOutputPanel()?.clearText()
|
||||||
assertCommandOutput(
|
assertCommandOutput(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2025 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -43,6 +43,7 @@ class AndFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test and function with list causes error`() {
|
fun `test and function with list causes error`() {
|
||||||
enterCommand("echo and([1, 2, 3], [2, 3, 4])")
|
enterCommand("echo and([1, 2, 3], [2, 3, 4])")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E745: Using a List as a Number")
|
assertPluginErrorMessage("E745: Using a List as a Number")
|
||||||
}
|
}
|
||||||
@@ -50,6 +51,7 @@ class AndFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test and function with dict causes error`() {
|
fun `test and function with dict causes error`() {
|
||||||
enterCommand("echo and({1: 2, 3: 4}, {3: 4, 5: 6})")
|
enterCommand("echo and({1: 2, 3: 4}, {3: 4, 5: 6})")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
||||||
}
|
}
|
||||||
@@ -57,6 +59,7 @@ class AndFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test and function with float causes error`() {
|
fun `test and function with float causes error`() {
|
||||||
enterCommand("echo and(1.5, 2.5)")
|
enterCommand("echo and(1.5, 2.5)")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E805: Using a Float as a Number")
|
assertPluginErrorMessage("E805: Using a Float as a Number")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2025 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -28,6 +28,7 @@ class InvertFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test invert function with list causes error`() {
|
fun `test invert function with list causes error`() {
|
||||||
enterCommand("echo invert([1, 2, 3])")
|
enterCommand("echo invert([1, 2, 3])")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E745: Using a List as a Number")
|
assertPluginErrorMessage("E745: Using a List as a Number")
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ class InvertFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test invert function with dict causes error`() {
|
fun `test invert function with dict causes error`() {
|
||||||
enterCommand("echo invert({1: 2, 3: 4})")
|
enterCommand("echo invert({1: 2, 3: 4})")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
||||||
}
|
}
|
||||||
@@ -42,6 +44,7 @@ class InvertFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test invert function with float causes error`() {
|
fun `test invert function with float causes error`() {
|
||||||
enterCommand("echo invert(1.5)")
|
enterCommand("echo invert(1.5)")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E805: Using a Float as a Number")
|
assertPluginErrorMessage("E805: Using a Float as a Number")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2025 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -43,6 +43,7 @@ class OrFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test or function with list causes error`() {
|
fun `test or function with list causes error`() {
|
||||||
enterCommand("echo or([1, 2, 3], [2, 3, 4])")
|
enterCommand("echo or([1, 2, 3], [2, 3, 4])")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E745: Using a List as a Number")
|
assertPluginErrorMessage("E745: Using a List as a Number")
|
||||||
}
|
}
|
||||||
@@ -50,6 +51,7 @@ class OrFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test or function with dict causes error`() {
|
fun `test or function with dict causes error`() {
|
||||||
enterCommand("echo or({1: 2, 3: 4}, {3: 4, 5: 6})")
|
enterCommand("echo or({1: 2, 3: 4}, {3: 4, 5: 6})")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
||||||
}
|
}
|
||||||
@@ -57,6 +59,7 @@ class OrFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test or function with float causes error`() {
|
fun `test or function with float causes error`() {
|
||||||
enterCommand("echo or(1.5, 2.5)")
|
enterCommand("echo or(1.5, 2.5)")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E805: Using a Float as a Number")
|
assertPluginErrorMessage("E805: Using a Float as a Number")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2025 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -43,6 +43,7 @@ class XorFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test xor function with list causes error`() {
|
fun `test xor function with list causes error`() {
|
||||||
enterCommand("echo xor([1, 2, 3], [2, 3, 4])")
|
enterCommand("echo xor([1, 2, 3], [2, 3, 4])")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E745: Using a List as a Number")
|
assertPluginErrorMessage("E745: Using a List as a Number")
|
||||||
}
|
}
|
||||||
@@ -50,6 +51,7 @@ class XorFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test xor function with dict causes error`() {
|
fun `test xor function with dict causes error`() {
|
||||||
enterCommand("echo xor({1: 2, 3: 4}, {3: 4, 5: 6})")
|
enterCommand("echo xor({1: 2, 3: 4}, {3: 4, 5: 6})")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
||||||
}
|
}
|
||||||
@@ -57,6 +59,7 @@ class XorFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test xor function with float causes error`() {
|
fun `test xor function with float causes error`() {
|
||||||
enterCommand("echo xor(1.5, 2.5)")
|
enterCommand("echo xor(1.5, 2.5)")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E805: Using a Float as a Number")
|
assertPluginErrorMessage("E805: Using a Float as a Number")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2025 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -33,6 +33,7 @@ class ToLowerFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test tolower with list causes error`() {
|
fun `test tolower with list causes error`() {
|
||||||
enterCommand("echo tolower([1, 2, 3])")
|
enterCommand("echo tolower([1, 2, 3])")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E730: Using a List as a String")
|
assertPluginErrorMessage("E730: Using a List as a String")
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ class ToLowerFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test tolower with dict causes error`() {
|
fun `test tolower with dict causes error`() {
|
||||||
enterCommand("echo tolower({1: 2, 3: 4})")
|
enterCommand("echo tolower({1: 2, 3: 4})")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E731: Using a Dictionary as a String")
|
assertPluginErrorMessage("E731: Using a Dictionary as a String")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2025 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -33,6 +33,7 @@ class ToUpperFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test toupper with list causes error`() {
|
fun `test toupper with list causes error`() {
|
||||||
enterCommand("echo toupper([1, 2, 3])")
|
enterCommand("echo toupper([1, 2, 3])")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E730: Using a List as a String")
|
assertPluginErrorMessage("E730: Using a List as a String")
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ class ToUpperFunctionTest : VimTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test toupper with dict causes error`() {
|
fun `test toupper with dict causes error`() {
|
||||||
enterCommand("echo toupper({1: 2, 3: 4})")
|
enterCommand("echo toupper({1: 2, 3: 4})")
|
||||||
|
assertNoExOutput()
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E731: Using a Dictionary as a String")
|
assertPluginErrorMessage("E731: Using a Dictionary as a String")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2023 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -207,12 +207,7 @@ class FunctionDeclarationTest : VimTestCase() {
|
|||||||
typeText(commandToKeys("echo F1()"))
|
typeText(commandToKeys("echo F1()"))
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertPluginErrorMessage("E121: Undefined variable: x")
|
assertPluginErrorMessage("E121: Undefined variable: x")
|
||||||
assertExOutput(
|
assertExOutput("0")
|
||||||
"""
|
|
||||||
E121: Undefined variable: x
|
|
||||||
0
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
|
|
||||||
typeText(commandToKeys("delf! F1"))
|
typeText(commandToKeys("delf! F1"))
|
||||||
typeText(commandToKeys("delf! F2"))
|
typeText(commandToKeys("delf! F2"))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2023 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -154,12 +154,7 @@ class TryCatchTest : VimTestCase() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
assertExOutput(
|
assertExOutput("finally block")
|
||||||
"""
|
|
||||||
finally block
|
|
||||||
my exception
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2023 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -223,7 +223,7 @@ class SearchGroupTest : VimTestCase() {
|
|||||||
) {
|
) {
|
||||||
enterCommand("set nowrapscan")
|
enterCommand("set nowrapscan")
|
||||||
}
|
}
|
||||||
assertPluginError(true)
|
assertPluginError(false)
|
||||||
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
|
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ class SearchGroupTest : VimTestCase() {
|
|||||||
three
|
three
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
assertPluginError(true)
|
assertPluginError(false)
|
||||||
assertPluginErrorMessage("E486: Pattern not found: banana")
|
assertPluginErrorMessage("E486: Pattern not found: banana")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ class SearchGroupTest : VimTestCase() {
|
|||||||
) {
|
) {
|
||||||
enterCommand("set nowrapscan")
|
enterCommand("set nowrapscan")
|
||||||
}
|
}
|
||||||
assertPluginError(true)
|
assertPluginError(false)
|
||||||
assertPluginErrorMessage("E384: Search hit TOP without match for: three")
|
assertPluginErrorMessage("E384: Search hit TOP without match for: three")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ class SearchGroupTest : VimTestCase() {
|
|||||||
three
|
three
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
assertPluginError(true)
|
assertPluginError(false)
|
||||||
assertPluginErrorMessage("E486: Pattern not found: banana")
|
assertPluginErrorMessage("E486: Pattern not found: banana")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,7 +615,7 @@ class SearchGroupTest : VimTestCase() {
|
|||||||
)
|
)
|
||||||
enterCommand("set nowrapscan")
|
enterCommand("set nowrapscan")
|
||||||
typeText("10", "/", searchCommand("one"))
|
typeText("10", "/", searchCommand("one"))
|
||||||
assertPluginError(true)
|
assertPluginError(false)
|
||||||
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
|
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
|
||||||
assertPosition(2, 0)
|
assertPosition(2, 0)
|
||||||
}
|
}
|
||||||
@@ -679,7 +679,7 @@ class SearchGroupTest : VimTestCase() {
|
|||||||
)
|
)
|
||||||
enterCommand("set nowrapscan")
|
enterCommand("set nowrapscan")
|
||||||
typeText("12", "?one<CR>")
|
typeText("12", "?one<CR>")
|
||||||
assertPluginError(true)
|
assertPluginError(false)
|
||||||
assertPluginErrorMessage("E384: Search hit TOP without match for: one")
|
assertPluginErrorMessage("E384: Search hit TOP without match for: one")
|
||||||
assertPosition(8, 0)
|
assertPosition(8, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ dependencies {
|
|||||||
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
|
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
|
||||||
testImplementation(testFixtures(project(":"))) // The root project
|
testImplementation(testFixtures(project(":"))) // The root project
|
||||||
testImplementation("org.junit.vintage:junit-vintage-engine:6.0.3")
|
testImplementation("org.junit.vintage:junit-vintage-engine:6.0.3")
|
||||||
testImplementation("org.jetbrains:jetCheck:0.2.3")
|
|
||||||
|
|
||||||
intellijPlatform {
|
intellijPlatform {
|
||||||
// Snapshots don't use installers
|
// Snapshots don't use installers
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class FileOpsSplitTest : IdeaVimStarterTestBase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `save file with write command`() {
|
fun `save file with write command`() {
|
||||||
openFile(createFile("src/Save1.txt", "Line 1\nLine 2\nLine 3\n"))
|
openFile(createFile("src/Save1.txt", "Line 1\nLine 2\nLine 3\n"))
|
||||||
|
clickEditor()
|
||||||
typeVimAndEscape("0iSAVED ")
|
typeVimAndEscape("0iSAVED ")
|
||||||
pause()
|
pause()
|
||||||
exCommand("w")
|
exCommand("w")
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import com.intellij.ide.starter.plugins.PluginConfigurator
|
|||||||
import com.intellij.ide.starter.project.LocalProjectInfo
|
import com.intellij.ide.starter.project.LocalProjectInfo
|
||||||
import com.intellij.ide.starter.runner.Starter
|
import com.intellij.ide.starter.runner.Starter
|
||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertFalse
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.BeforeAll
|
import org.junit.jupiter.api.BeforeAll
|
||||||
@@ -124,45 +125,9 @@ abstract class IdeaVimStarterTestBase {
|
|||||||
|
|
||||||
// ── IDE interaction helpers ─────────────────────────────────
|
// ── IDE interaction helpers ─────────────────────────────────
|
||||||
|
|
||||||
/** Opens a file in the editor by relative path and waits until the editor is ready for input. */
|
/** Opens a file in the editor by relative path. */
|
||||||
protected fun openFile(relativePath: String) {
|
protected fun openFile(relativePath: String) {
|
||||||
driver.withContext { openFile(relativePath) }
|
driver.withContext { openFile(relativePath) }
|
||||||
ensureEditorReady(relativePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureEditorReady(relativePath: String, timeoutMs: Long = 10_000) {
|
|
||||||
val deadline = System.currentTimeMillis() + timeoutMs
|
|
||||||
var lastText = ""
|
|
||||||
while (System.currentTimeMillis() < deadline) {
|
|
||||||
try {
|
|
||||||
lastText = editorText()
|
|
||||||
if (lastText.isNotBlank()) break
|
|
||||||
} catch (_: Exception) {
|
|
||||||
// Editor component not yet available — retry
|
|
||||||
}
|
|
||||||
Thread.sleep(200)
|
|
||||||
}
|
|
||||||
check(lastText.isNotBlank()) {
|
|
||||||
"Editor for '$relativePath' did not become ready within ${timeoutMs}ms"
|
|
||||||
}
|
|
||||||
clickEditor()
|
|
||||||
|
|
||||||
// Verify IdeaVim's key handler is actually attached by sending `gg` (go to first line)
|
|
||||||
// and checking the caret moves to line 1. This confirms vim is processing keystrokes.
|
|
||||||
val vimReady = waitUntil(timeoutMs = 10_000, pollMs = 500) {
|
|
||||||
try {
|
|
||||||
driver.withContext {
|
|
||||||
ideFrame { codeEditor().apply { waitFound(); keyboard { typeText("gg") } } }
|
|
||||||
}
|
|
||||||
Thread.sleep(300)
|
|
||||||
caretLine() <= 1
|
|
||||||
} catch (_: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check(vimReady) {
|
|
||||||
"IdeaVim key handler did not attach for '$relativePath' within timeout"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Types vim keys in the active editor. */
|
/** Types vim keys in the active editor. */
|
||||||
@@ -212,27 +177,6 @@ abstract class IdeaVimStarterTestBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Triggers the IDE "Navigate > Back" action (Cmd+[ on macOS, Ctrl+Alt+Left on Linux). */
|
|
||||||
protected fun ideaGoBack() {
|
|
||||||
driver.withContext {
|
|
||||||
ideFrame {
|
|
||||||
codeEditor().apply {
|
|
||||||
if (System.getProperty("os.name").lowercase().contains("mac")) {
|
|
||||||
keyboard { hotKey(java.awt.event.KeyEvent.VK_META, java.awt.event.KeyEvent.VK_OPEN_BRACKET) }
|
|
||||||
} else {
|
|
||||||
keyboard {
|
|
||||||
hotKey(
|
|
||||||
java.awt.event.KeyEvent.VK_CONTROL,
|
|
||||||
java.awt.event.KeyEvent.VK_ALT,
|
|
||||||
java.awt.event.KeyEvent.VK_LEFT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Presses Ctrl-G (file info). */
|
/** Presses Ctrl-G (file info). */
|
||||||
protected fun ctrlG() {
|
protected fun ctrlG() {
|
||||||
driver.withContext {
|
driver.withContext {
|
||||||
@@ -263,23 +207,6 @@ abstract class IdeaVimStarterTestBase {
|
|||||||
Thread.sleep(ms)
|
Thread.sleep(ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retries [check] every [pollMs] until it returns true or [timeoutMs] elapses.
|
|
||||||
* Use instead of fixed pauses to handle variable split-mode RPC latency on CI.
|
|
||||||
*/
|
|
||||||
protected fun waitUntil(
|
|
||||||
timeoutMs: Long = 5000,
|
|
||||||
pollMs: Long = 200,
|
|
||||||
check: () -> Boolean,
|
|
||||||
): Boolean {
|
|
||||||
val deadline = System.currentTimeMillis() + timeoutMs
|
|
||||||
while (System.currentTimeMillis() < deadline) {
|
|
||||||
if (check()) return true
|
|
||||||
Thread.sleep(pollMs)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Editor state helpers ────────────────────────────────────
|
// ── Editor state helpers ────────────────────────────────────
|
||||||
|
|
||||||
/** Reads the current editor text. */
|
/** Reads the current editor text. */
|
||||||
@@ -302,48 +229,42 @@ abstract class IdeaVimStarterTestBase {
|
|||||||
|
|
||||||
// ── Assertions ──────────────────────────────────────────────
|
// ── Assertions ──────────────────────────────────────────────
|
||||||
|
|
||||||
/** Asserts the editor contains the given text, polling until timeout. */
|
/** Asserts the editor contains the given text. */
|
||||||
protected fun assertEditorContains(expected: String, message: String? = null) {
|
protected fun assertEditorContains(expected: String, message: String? = null) {
|
||||||
var text = ""
|
val text = editorText()
|
||||||
val found = waitUntil { text = editorText(); text.contains(expected) }
|
assertTrue(text.contains(expected)) {
|
||||||
assertTrue(found) {
|
|
||||||
(message ?: "Editor should contain '$expected'") + ". Actual: $text"
|
(message ?: "Editor should contain '$expected'") + ". Actual: $text"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Asserts the editor does NOT contain the given text, polling until timeout. */
|
/** Asserts the editor does NOT contain the given text. */
|
||||||
protected fun assertEditorNotContains(unexpected: String, message: String? = null) {
|
protected fun assertEditorNotContains(unexpected: String, message: String? = null) {
|
||||||
// Give operations time to settle, then check
|
|
||||||
pause(1000)
|
|
||||||
val text = editorText()
|
val text = editorText()
|
||||||
assertFalse(text.contains(unexpected)) {
|
assertFalse(text.contains(unexpected)) {
|
||||||
(message ?: "Editor should not contain '$unexpected'") + ". Actual: $text"
|
(message ?: "Editor should not contain '$unexpected'") + ". Actual: $text"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Asserts the caret is at the given line (1-based), polling until timeout. */
|
/** Asserts the caret is at the given line (1-based). */
|
||||||
protected fun assertCaretAtLine(expected: Int, message: String? = null) {
|
protected fun assertCaretAtLine(expected: Int, message: String? = null) {
|
||||||
var actual = 0
|
val actual = caretLine()
|
||||||
val found = waitUntil { actual = caretLine(); actual == expected }
|
assertEquals(expected, actual) {
|
||||||
assertTrue(found) {
|
|
||||||
(message ?: "Caret should be at line $expected") + ". Actual line: $actual"
|
(message ?: "Caret should be at line $expected") + ". Actual line: $actual"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Asserts the caret is before the given line, polling until timeout. */
|
/** Asserts the caret is before the given line. */
|
||||||
protected fun assertCaretBefore(line: Int, message: String? = null) {
|
protected fun assertCaretBefore(line: Int, message: String? = null) {
|
||||||
var actual = 0
|
val actual = caretLine()
|
||||||
val found = waitUntil { actual = caretLine(); actual < line }
|
assertTrue(actual < line) {
|
||||||
assertTrue(found) {
|
|
||||||
(message ?: "Caret should be before line $line") + ". Actual line: $actual"
|
(message ?: "Caret should be before line $line") + ". Actual line: $actual"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Asserts the caret is past the given line, polling until timeout. */
|
/** Asserts the caret is past the given line. */
|
||||||
protected fun assertCaretAfter(line: Int, message: String? = null) {
|
protected fun assertCaretAfter(line: Int, message: String? = null) {
|
||||||
var actual = 0
|
val actual = caretLine()
|
||||||
val found = waitUntil { actual = caretLine(); actual > line }
|
assertTrue(actual > line) {
|
||||||
assertTrue(found) {
|
|
||||||
(message ?: "Caret should be after line $line") + ". Actual line: $actual"
|
(message ?: "Caret should be after line $line") + ". Actual line: $actual"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,21 +46,4 @@ class JumpNavigationSplitTest : IdeaVimStarterTestBase() {
|
|||||||
pause()
|
pause()
|
||||||
assertCaretBefore(10, "Ctrl-O should jump back near start after search")
|
assertCaretBefore(10, "Ctrl-O should jump back near start after search")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `IDE Back navigation records jump so apostrophe mark navigates back`() {
|
|
||||||
openFile(longFile("Jump3"))
|
|
||||||
|
|
||||||
typeVim("G")
|
|
||||||
pause(300)
|
|
||||||
assertCaretAfter(40, "G should move to end of file")
|
|
||||||
|
|
||||||
ideaGoBack()
|
|
||||||
pause(500)
|
|
||||||
assertCaretBefore(10, "IDE Back should return to start of file")
|
|
||||||
|
|
||||||
typeVim("''")
|
|
||||||
pause(300)
|
|
||||||
assertCaretAfter(40, "'' should return to position before IDE Back (end of file)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,10 @@ class RepeatUndoSplitTest : IdeaVimStarterTestBase() {
|
|||||||
typeVimAndEscape("0sX")
|
typeVimAndEscape("0sX")
|
||||||
assertEditorContains("Xbcdef", "First char should be X")
|
assertEditorContains("Xbcdef", "First char should be X")
|
||||||
|
|
||||||
typeVim("l")
|
typeVim("l.")
|
||||||
pause()
|
|
||||||
typeVim(".")
|
|
||||||
assertEditorContains("XXcdef", "Second char should also be X")
|
assertEditorContains("XXcdef", "Second char should also be X")
|
||||||
|
|
||||||
typeVim("u")
|
typeVim("uu")
|
||||||
pause()
|
|
||||||
typeVim("u")
|
|
||||||
assertEditorContains("Xbcdef", "First undo should revert dot-repeat only")
|
assertEditorContains("Xbcdef", "First undo should revert dot-repeat only")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,9 +38,8 @@ class RepeatUndoSplitTest : IdeaVimStarterTestBase() {
|
|||||||
assertEditorContains("HiHi", "Dot repeat should insert again")
|
assertEditorContains("HiHi", "Dot repeat should insert again")
|
||||||
|
|
||||||
typeVim("u")
|
typeVim("u")
|
||||||
var text = ""
|
val text = editorText()
|
||||||
val found = waitUntil { text = editorText(); text.contains("Hi ") && !text.contains("HiHi") }
|
assertTrue(text.contains("Hi ") && !text.contains("HiHi")) {
|
||||||
assertTrue(found) {
|
|
||||||
"Undo should revert dot repeat only. Actual: $text"
|
"Undo should revert dot repeat only. Actual: $text"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,11 +51,8 @@ class RepeatUndoSplitTest : IdeaVimStarterTestBase() {
|
|||||||
typeVimAndEscape("0cwHELLO")
|
typeVimAndEscape("0cwHELLO")
|
||||||
assertEditorContains("HELLO", "Should have changed word")
|
assertEditorContains("HELLO", "Should have changed word")
|
||||||
|
|
||||||
typeVim("w")
|
typeVim("w.")
|
||||||
pause()
|
val helloCount = editorText().lines().first().split("HELLO").size - 1
|
||||||
typeVim(".")
|
|
||||||
var helloCount = 0
|
|
||||||
waitUntil { helloCount = editorText().lines().first().split("HELLO").size - 1; helloCount >= 2 }
|
|
||||||
assertTrue(helloCount >= 2) { "Should have two HELLOs. Actual: ${editorText()}" }
|
assertTrue(helloCount >= 2) { "Should have two HELLOs. Actual: ${editorText()}" }
|
||||||
|
|
||||||
typeVim("u")
|
typeVim("u")
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ import com.maddyhome.idea.vim.helper.endOffsetInclusive
|
|||||||
import com.maddyhome.idea.vim.state.mode.SelectionType.CHARACTER_WISE
|
import com.maddyhome.idea.vim.state.mode.SelectionType.CHARACTER_WISE
|
||||||
import com.maddyhome.idea.vim.state.mode.inVisualMode
|
import com.maddyhome.idea.vim.state.mode.inVisualMode
|
||||||
import com.maddyhome.idea.vim.state.mode.selectionType
|
import com.maddyhome.idea.vim.state.mode.selectionType
|
||||||
|
import java.lang.Long.toHexString
|
||||||
|
|
||||||
abstract class VimFileBase : VimFile {
|
abstract class VimFileBase : VimFile {
|
||||||
override fun displayHexInfo(editor: VimEditor) {
|
override fun displayHexInfo(editor: VimEditor) {
|
||||||
val offset = editor.currentCaret().offset
|
val offset = editor.currentCaret().offset
|
||||||
val ch = editor.text()[offset]
|
val ch = editor.text()[offset]
|
||||||
|
|
||||||
injector.messages.showMessage(editor, ch.code.toString(16))
|
injector.messages.showMessage(editor, toHexString(ch.code.toLong()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun displayLocationInfo(editor: VimEditor) {
|
override fun displayLocationInfo(editor: VimEditor) {
|
||||||
|
|||||||
@@ -150,13 +150,6 @@ interface VimMarkService {
|
|||||||
*/
|
*/
|
||||||
fun loadLegacyState(element: Any) {}
|
fun loadLegacyState(element: Any) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the BEFORE_JUMP_MARK directly from a position, without requiring an open editor or caret.
|
|
||||||
* Used by jump listeners that receive position data from the IDE navigation history.
|
|
||||||
* Default no-op; implementations that maintain local mark state should override this.
|
|
||||||
*/
|
|
||||||
fun setJumpMark(filepath: String, protocol: String, line: Int, col: Int) {}
|
|
||||||
|
|
||||||
fun isValidMark(char: Char, operation: Operation, isCaretPrimary: Boolean): Boolean
|
fun isValidMark(char: Char, operation: Operation, isCaretPrimary: Boolean): Boolean
|
||||||
|
|
||||||
enum class Operation {
|
enum class Operation {
|
||||||
|
|||||||
@@ -248,10 +248,6 @@ abstract class VimMarkServiceBase : VimMarkService {
|
|||||||
return setMark(caret, mark)
|
return setMark(caret, mark)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setJumpMark(filepath: String, protocol: String, line: Int, col: Int) {
|
|
||||||
getLocalMarks(filepath)[BEFORE_JUMP_MARK] = VimMark(BEFORE_JUMP_MARK, line, col, filepath, protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun createGlobalMark(editor: VimEditor, char: Char, offset: Int): Mark? {
|
protected open fun createGlobalMark(editor: VimEditor, char: Char, offset: Int): Mark? {
|
||||||
val markChar = char.normalizeMarkChar()
|
val markChar = char.normalizeMarkChar()
|
||||||
if (!markChar.isGlobalMark()) return null
|
if (!markChar.isGlobalMark()) return null
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2023 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -12,33 +12,7 @@ import com.maddyhome.idea.vim.helper.EngineMessageHelper
|
|||||||
import org.jetbrains.annotations.PropertyKey
|
import org.jetbrains.annotations.PropertyKey
|
||||||
|
|
||||||
interface VimMessages {
|
interface VimMessages {
|
||||||
/**
|
|
||||||
* Displays an informational message to the user.
|
|
||||||
* The message panel closes on any keystroke and passes the key through to the editor.
|
|
||||||
*/
|
|
||||||
fun showMessage(editor: VimEditor, message: String?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays an error message to the user (typically in red).
|
|
||||||
* Clears any existing output panel content before showing the message.
|
|
||||||
* The message panel closes on any keystroke and passes the key through to the editor.
|
|
||||||
*/
|
|
||||||
fun showErrorMessage(editor: VimEditor, message: String?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends an error message to the existing output panel content (typically in red).
|
|
||||||
* Unlike [showErrorMessage], this does not clear prior output.
|
|
||||||
* Use this when reporting errors during script execution where earlier output should be preserved.
|
|
||||||
*/
|
|
||||||
fun appendErrorMessage(editor: VimEditor, message: String?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy method for displaying messages.
|
|
||||||
* @deprecated Use [showMessage] or [showErrorMessage] instead.
|
|
||||||
*/
|
|
||||||
@Deprecated("Use showMessage or showErrorMessage instead", ReplaceWith("showMessage(editor, message)"))
|
|
||||||
fun showStatusBarMessage(editor: VimEditor?, message: String?)
|
fun showStatusBarMessage(editor: VimEditor?, message: String?)
|
||||||
|
|
||||||
fun getStatusBarMessage(): String?
|
fun getStatusBarMessage(): String?
|
||||||
fun clearStatusBarMessage()
|
fun clearStatusBarMessage()
|
||||||
fun indicateError()
|
fun indicateError()
|
||||||
@@ -54,4 +28,13 @@ interface VimMessages {
|
|||||||
fun message(@PropertyKey(resourceBundle = EngineMessageHelper.BUNDLE) key: String, vararg params: Any): String
|
fun message(@PropertyKey(resourceBundle = EngineMessageHelper.BUNDLE) key: String, vararg params: Any): String
|
||||||
|
|
||||||
fun updateStatusBar(editor: VimEditor)
|
fun updateStatusBar(editor: VimEditor)
|
||||||
|
|
||||||
|
fun showMessage(editor: VimEditor, message: String) {
|
||||||
|
showStatusBarMessage(editor, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showErrorMessage(editor: VimEditor, message: String?) {
|
||||||
|
showStatusBarMessage(editor, message)
|
||||||
|
indicateError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2024 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -29,8 +29,7 @@ interface VimOutputPanel {
|
|||||||
* Note: The full text content is not updated in the display until [show] is invoked.
|
* Note: The full text content is not updated in the display until [show] is invoked.
|
||||||
*
|
*
|
||||||
* @param text The text to append.
|
* @param text The text to append.
|
||||||
* @param isNewLine Whether to start the appended text on a new line.
|
* @param isNewLine Whether to start the appended text on a new line. Defaults to true.
|
||||||
* @param messageType The type of message, used to determine text styling.
|
|
||||||
*/
|
*/
|
||||||
fun addText(text: String, isNewLine: Boolean = true, messageType: MessageType = MessageType.STANDARD)
|
fun addText(text: String, isNewLine: Boolean = true, messageType: MessageType = MessageType.STANDARD)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2024 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -26,17 +26,8 @@ interface VimOutputPanelService {
|
|||||||
fun getCurrentOutputPanel(): VimOutputPanel?
|
fun getCurrentOutputPanel(): VimOutputPanel?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends text to the existing output panel or creates a new one with the given text and message type.
|
* Appends text to the existing output panel or creates a new one with the given text.
|
||||||
|
* Basic method that should be sufficient in most cases.
|
||||||
*/
|
*/
|
||||||
fun output(
|
fun output(editor: VimEditor, context: ExecutionContext, text: String)
|
||||||
editor: VimEditor,
|
|
||||||
context: ExecutionContext,
|
|
||||||
text: String,
|
|
||||||
messageType: MessageType = MessageType.STANDARD,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun clear(
|
|
||||||
editor: VimEditor,
|
|
||||||
context: ExecutionContext,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2024 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -13,17 +13,9 @@ abstract class VimOutputPanelServiceBase : VimOutputPanelService {
|
|||||||
return getCurrentOutputPanel() ?: create(editor, context)
|
return getCurrentOutputPanel() ?: create(editor, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun output(editor: VimEditor, context: ExecutionContext, text: String, messageType: MessageType) {
|
override fun output(editor: VimEditor, context: ExecutionContext, text: String) {
|
||||||
val panel = getOrCreate(editor, context)
|
val panel = getOrCreate(editor, context)
|
||||||
panel.addText(text, true, messageType)
|
panel.addText(text)
|
||||||
panel.show()
|
panel.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clear(
|
|
||||||
editor: VimEditor,
|
|
||||||
context: ExecutionContext,
|
|
||||||
) {
|
|
||||||
val panel = getOrCreate(editor, context)
|
|
||||||
panel.clearText()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -65,13 +65,13 @@ abstract class VimScriptExecutorBase : VimscriptExecutor {
|
|||||||
}
|
}
|
||||||
finalResult = ExecutionResult.Error
|
finalResult = ExecutionResult.Error
|
||||||
if (indicateErrors) {
|
if (indicateErrors) {
|
||||||
injector.messages.appendErrorMessage(editor, e.message)
|
injector.messages.showErrorMessage(editor, e.message)
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Failed while executing $unit. " + e.message)
|
logger.warn("Failed while executing $unit. " + e.message)
|
||||||
}
|
}
|
||||||
} catch (e: NotImplementedError) {
|
} catch (e: NotImplementedError) {
|
||||||
if (indicateErrors) {
|
if (indicateErrors) {
|
||||||
injector.messages.appendErrorMessage(editor, "Not implemented yet :(")
|
injector.messages.showErrorMessage(editor, "Not implemented yet :(")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn(e.toString())
|
logger.warn(e.toString())
|
||||||
|
|||||||
@@ -653,9 +653,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
|
|||||||
caret.moveToOffset(matchRange.startOffset)
|
caret.moveToOffset(matchRange.startOffset)
|
||||||
val highlight = addSubstitutionConfirmationHighlight(editor, matchRange.startOffset, matchRange.endOffset)
|
val highlight = addSubstitutionConfirmationHighlight(editor, matchRange.startOffset, matchRange.endOffset)
|
||||||
injector.modalInput.create(
|
injector.modalInput.create(
|
||||||
editor,
|
editor, context, injector.messages.message("command.substitute.replace.with.prompt", lineToNextSubstitute.second.second),
|
||||||
context,
|
|
||||||
injector.messages.message("command.substitute.replace.with.prompt", lineToNextSubstitute.second.second),
|
|
||||||
SubstituteWithAskInputInterceptor(
|
SubstituteWithAskInputInterceptor(
|
||||||
editor, caret, nextSubstitute, highlight, line, 0, parent, pattern, regex,
|
editor, caret, nextSubstitute, highlight, line, 0, parent, pattern, regex,
|
||||||
oldLastSubstituteString, line2, hasExpression, substituteString, options,
|
oldLastSubstituteString, line2, hasExpression, substituteString, options,
|
||||||
@@ -856,7 +854,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
|
|||||||
if (lastMatchLine != -1) {
|
if (lastMatchLine != -1) {
|
||||||
caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, lastMatchLine))
|
caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, lastMatchLine))
|
||||||
} else {
|
} else {
|
||||||
injector.messages.showErrorMessage(editor, "E486: Pattern not found: $pattern")
|
injector.messages.showStatusBarMessage(null, "E486: Pattern not found: $pattern")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,7 +863,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
|
|||||||
// todo throw multiple exceptions at once
|
// todo throw multiple exceptions at once
|
||||||
if (exceptions.isNotEmpty()) {
|
if (exceptions.isNotEmpty()) {
|
||||||
injector.messages.indicateError()
|
injector.messages.indicateError()
|
||||||
injector.messages.showErrorMessage(editor, exceptions[0].message)
|
injector.messages.showStatusBarMessage(null, exceptions[0].message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ private class SearchAddress(pattern: String, offset: Int, move: Boolean) : Addre
|
|||||||
private val logger = vimLogger<SearchAddress>()
|
private val logger = vimLogger<SearchAddress>()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val patterns: MutableList<String> = mutableListOf()
|
private val patterns: MutableList<String?> = mutableListOf()
|
||||||
private val directions: MutableList<Direction> = mutableListOf()
|
private val directions: MutableList<Direction> = mutableListOf()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -216,17 +216,17 @@ private class SearchAddress(pattern: String, offset: Int, move: Boolean) : Addre
|
|||||||
var pat = tok.nextToken()
|
var pat = tok.nextToken()
|
||||||
when (pat) {
|
when (pat) {
|
||||||
"\\/" -> {
|
"\\/" -> {
|
||||||
patterns.add(injector.searchGroup.lastSearchPattern ?: throw exExceptionMessage("E35"))
|
patterns.add(injector.searchGroup.lastSearchPattern)
|
||||||
directions.add(Direction.FORWARDS)
|
directions.add(Direction.FORWARDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
"\\?" -> {
|
"\\?" -> {
|
||||||
patterns.add(injector.searchGroup.lastSearchPattern ?: throw exExceptionMessage("E35"))
|
patterns.add(injector.searchGroup.lastSearchPattern)
|
||||||
directions.add(Direction.BACKWARDS)
|
directions.add(Direction.BACKWARDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
"\\&" -> {
|
"\\&" -> {
|
||||||
patterns.add(injector.searchGroup.lastSubstitutePattern ?: throw exExceptionMessage("E33"))
|
patterns.add(injector.searchGroup.lastSubstitutePattern)
|
||||||
directions.add(Direction.FORWARDS)
|
directions.add(Direction.FORWARDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ private class SearchAddress(pattern: String, offset: Int, move: Boolean) : Addre
|
|||||||
|
|
||||||
// Note that wrapscan, ignorecase, etc. all come from current option values, as expected
|
// Note that wrapscan, ignorecase, etc. all come from current option values, as expected
|
||||||
searchOffset = getSearchOffset(editor, line0, direction)
|
searchOffset = getSearchOffset(editor, line0, direction)
|
||||||
searchOffset = injector.searchGroup.processSearchRange(editor, pattern, patternOffset, searchOffset, direction)
|
searchOffset = injector.searchGroup.processSearchRange(editor, pattern!!, patternOffset, searchOffset, direction)
|
||||||
|
|
||||||
if (searchOffset == -1) {
|
if (searchOffset == -1) {
|
||||||
if (injector.options(editor).wrapscan) {
|
if (injector.options(editor).wrapscan) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2026 The IdeaVim authors
|
* Copyright 2003-2023 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@@ -25,8 +25,10 @@ fun VimEditor.exitVisualMode() {
|
|||||||
if (inBlockSelection) {
|
if (inBlockSelection) {
|
||||||
removeSecondaryCarets()
|
removeSecondaryCarets()
|
||||||
}
|
}
|
||||||
|
injector.application.runWriteAction {
|
||||||
nativeCarets().forEach(VimCaret::removeSelection)
|
nativeCarets().forEach(VimCaret::removeSelection)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (inVisualMode || inCommandLineModeWithVisual) {
|
if (inVisualMode || inCommandLineModeWithVisual) {
|
||||||
vimLastSelectionType = selectionType
|
vimLastSelectionType = selectionType
|
||||||
injector.application.runReadAction {
|
injector.application.runReadAction {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
package com.maddyhome.idea.vim.thinapi
|
package com.maddyhome.idea.vim.thinapi
|
||||||
|
|
||||||
import com.intellij.vim.api.VimApi
|
import com.intellij.vim.api.VimApi
|
||||||
import com.intellij.vim.api.models.CaretId
|
|
||||||
import com.intellij.vim.api.scopes.TextObjectRange
|
import com.intellij.vim.api.scopes.TextObjectRange
|
||||||
import com.intellij.vim.api.scopes.TextObjectScope
|
import com.intellij.vim.api.scopes.TextObjectScope
|
||||||
import com.maddyhome.idea.vim.KeyHandler
|
import com.maddyhome.idea.vim.KeyHandler
|
||||||
@@ -42,7 +41,7 @@ internal class TextObjectScopeImpl(
|
|||||||
keys: String,
|
keys: String,
|
||||||
registerDefaultMapping: Boolean,
|
registerDefaultMapping: Boolean,
|
||||||
preserveSelectionAnchor: Boolean,
|
preserveSelectionAnchor: Boolean,
|
||||||
rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
|
rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
|
||||||
) {
|
) {
|
||||||
val plugKeys = "<Plug>($pluginName-$keys)"
|
val plugKeys = "<Plug>($pluginName-$keys)"
|
||||||
|
|
||||||
@@ -89,7 +88,7 @@ private class TextObjectExtensionHandler(
|
|||||||
private val listenerOwner: ListenerOwner,
|
private val listenerOwner: ListenerOwner,
|
||||||
private val mappingOwner: MappingOwner,
|
private val mappingOwner: MappingOwner,
|
||||||
private val preserveSelectionAnchor: Boolean,
|
private val preserveSelectionAnchor: Boolean,
|
||||||
private val rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
|
private val rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
|
||||||
) : ExtensionHandler {
|
) : ExtensionHandler {
|
||||||
|
|
||||||
override val isRepeatable: Boolean = false
|
override val isRepeatable: Boolean = false
|
||||||
@@ -131,7 +130,7 @@ private class ApiTextObjectActionHandler(
|
|||||||
private val listenerOwner: ListenerOwner,
|
private val listenerOwner: ListenerOwner,
|
||||||
private val mappingOwner: MappingOwner,
|
private val mappingOwner: MappingOwner,
|
||||||
override val preserveSelectionAnchor: Boolean,
|
override val preserveSelectionAnchor: Boolean,
|
||||||
private val rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
|
private val rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
|
||||||
) : TextObjectActionHandler() {
|
) : TextObjectActionHandler() {
|
||||||
|
|
||||||
// Will be set based on the result of rangeProvider
|
// Will be set based on the result of rangeProvider
|
||||||
@@ -150,7 +149,7 @@ private class ApiTextObjectActionHandler(
|
|||||||
val vimApi = VimApiImpl(listenerOwner, mappingOwner, editor.projectId)
|
val vimApi = VimApiImpl(listenerOwner, mappingOwner, editor.projectId)
|
||||||
|
|
||||||
// Execute the range provider (suspend lambda bridged via runBlocking for now)
|
// Execute the range provider (suspend lambda bridged via runBlocking for now)
|
||||||
val apiRange = kotlinx.coroutines.runBlocking { vimApi.rangeProvider(CaretId(caret.id), count) } ?: return null
|
val apiRange = kotlinx.coroutines.runBlocking { vimApi.rangeProvider(count) } ?: return null
|
||||||
|
|
||||||
// Convert API range to internal TextRange and set visual type
|
// Convert API range to internal TextRange and set visual type
|
||||||
return when (apiRange) {
|
return when (apiRange) {
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ data class DelfunctionCommand(
|
|||||||
try {
|
try {
|
||||||
injector.functionService.deleteFunction(name, scope, this)
|
injector.functionService.deleteFunction(name, scope, this)
|
||||||
} catch (e: ExException) {
|
} catch (e: ExException) {
|
||||||
if (e.code != "E130") {
|
if (e.message != null && e.message!!.startsWith("E130")) {
|
||||||
|
// "ignoreIfMissing" flag handles the "E130: Unknown function" exception
|
||||||
|
} else {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import com.maddyhome.idea.vim.api.injector
|
|||||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||||
import com.maddyhome.idea.vim.ex.ranges.Range
|
import com.maddyhome.idea.vim.ex.ranges.Range
|
||||||
import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
|
import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
|
||||||
import kotlin.math.min
|
import java.lang.Integer.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* see "h :[range]"
|
* see "h :[range]"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) :
|
|||||||
val returnValue = executeFunctionBody(exceptionsCaught, editor, context)
|
val returnValue = executeFunctionBody(exceptionsCaught, editor, context)
|
||||||
|
|
||||||
if (exceptionsCaught.isNotEmpty()) {
|
if (exceptionsCaught.isNotEmpty()) {
|
||||||
injector.messages.appendErrorMessage(editor, exceptionsCaught.last().message)
|
injector.messages.showErrorMessage(editor, exceptionsCaught.last().message)
|
||||||
}
|
}
|
||||||
return returnValue ?: VimInt.ZERO
|
return returnValue ?: VimInt.ZERO
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user