mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2026-05-13 18:05:53 +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
|
20
.github/workflows/mergeClaudeChangelogPR.yml
vendored
20
.github/workflows/mergeClaudeChangelogPR.yml
vendored
@@ -1,20 +0,0 @@
|
|||||||
name: Claude changelog auto-merge
|
|
||||||
on: pull_request
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
claude-changelog:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.repository == 'JetBrains/ideavim' &&
|
|
||||||
github.event.pull_request.user.login == 'claude[bot]' &&
|
|
||||||
startsWith(github.event.pull_request.title, 'Update changelog:')
|
|
||||||
steps:
|
|
||||||
- name: Auto-merge Claude changelog PR
|
|
||||||
run: gh pr merge --auto --rebase "$PR_URL"
|
|
||||||
env:
|
|
||||||
PR_URL: ${{github.event.pull_request.html_url}}
|
|
||||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
.teamcity/_Self/buildTypes/SplitModeTests.kt
vendored
85
.teamcity/_Self/buildTypes/SplitModeTests.kt
vendored
@@ -1,85 +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
|
|
||||||
|
|
||||||
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 = """
|
|
||||||
# Kill any leftover Xvfb from previous runs
|
|
||||||
pkill -f 'Xvfb :99' || true
|
|
||||||
|
|
||||||
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
|
|
||||||
XVFB_PID=${'$'}!
|
|
||||||
|
|
||||||
# Wait until the display is ready
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
if xdpyinfo -display :99 >/dev/null 2>&1; then
|
|
||||||
echo "Xvfb is ready on :99"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
if ! xdpyinfo -display :99 >/dev/null 2>&1; then
|
|
||||||
echo "ERROR: Xvfb failed to start on :99"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
./gradlew :tests:split-mode-tests:testSplitMode --console=plain --build-cache --configuration-cache --stacktrace
|
|
||||||
TEST_EXIT=${'$'}?
|
|
||||||
|
|
||||||
kill ${'$'}XVFB_PID 2>/dev/null || true
|
|
||||||
exit ${'$'}TEST_EXIT
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// VCS trigger disabled until Xvfb is installed on the TeamCity agent
|
|
||||||
// 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:
|
||||||
|
|
||||||
|
|||||||
55
CHANGES.md
55
CHANGES.md
@@ -26,8 +26,6 @@ usual beta standards.
|
|||||||
## [To Be Released]
|
## [To Be Released]
|
||||||
|
|
||||||
### Features:
|
### Features:
|
||||||
* [VIM-1693](https://youtrack.jetbrains.com/issue/VIM-1693) Added `:autocmd` command - run Ex commands on editor events such as `BufRead`, `BufWrite`, `BufEnter`, `BufLeave`, `InsertEnter`, `InsertLeave`, `WinEnter`, `WinLeave`, `FocusGained`, `FocusLost`, and `FileType`; supports `augroup` and file pattern matching (e.g., `autocmd BufWritePre *.py echo "saving python"`)
|
|
||||||
* [VIM-268](https://youtrack.jetbrains.com/issue/VIM-268) Added file name completion in ex commands - press `<Tab>`/`<S-Tab>` to cycle through file matches in `:edit`, `:split`, `:vsplit`, `:write`, `:read`, `:source`, and `:find` commands; use arrow keys to navigate the completion panel
|
|
||||||
* New VimScript functions: `add()`, `call()`, `extend()`, `extendnew()`, `filter()`, `flatten()`, `flattennew()`, `foreach()`, `has_key()`, `indexof()`, `insert()`, `items()`, `keys()`, `map()`, `mapnew()`, `reduce()`, `remove()`, `slice()`, `sort()`, `uniq()`, `values()`
|
* New VimScript functions: `add()`, `call()`, `extend()`, `extendnew()`, `filter()`, `flatten()`, `flattennew()`, `foreach()`, `has_key()`, `indexof()`, `insert()`, `items()`, `keys()`, `map()`, `mapnew()`, `reduce()`, `remove()`, `slice()`, `sort()`, `uniq()`, `values()`
|
||||||
* [VIM-1595](https://youtrack.jetbrains.com/issue/VIM-1595) Added support for `:read` command - insert file content below current line (e.g., `:read file.txt`, `0read file.txt`)
|
* [VIM-1595](https://youtrack.jetbrains.com/issue/VIM-1595) Added support for `:read` command - insert file content below current line (e.g., `:read file.txt`, `0read file.txt`)
|
||||||
* [VIM-1595](https://youtrack.jetbrains.com/issue/VIM-1595) Added support for `:read!` command - insert shell command output below current line (e.g., `:read! echo "hello"`)
|
* [VIM-1595](https://youtrack.jetbrains.com/issue/VIM-1595) Added support for `:read!` command - insert shell command output below current line (e.g., `:read! echo "hello"`)
|
||||||
@@ -36,71 +34,18 @@ usual beta standards.
|
|||||||
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `zm` command - decrease fold level to hide more folds
|
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `zm` command - decrease fold level to hide more folds
|
||||||
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `zf` command - create fold from selection or motion
|
* [VIM-566](https://youtrack.jetbrains.com/issue/VIM-566) Added support for `zf` command - create fold from selection or motion
|
||||||
* [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
|
||||||
* [VIM-1158](https://youtrack.jetbrains.com/issue/VIM-1158) Added `gw` command - reformat code like `gq` but preserving the cursor position
|
|
||||||
|
|
||||||
### Fixes:
|
### Fixes:
|
||||||
* [VIM-4197](https://youtrack.jetbrains.com/issue/VIM-4197) Fixed Vim features (e.g., `f`, `w`, text objects) not working in Java files decompiled from Kotlin class files
|
|
||||||
* [VIM-4112](https://youtrack.jetbrains.com/issue/VIM-4112) Fixed undo after block-visual edit (`<C-V>...x`, `<C-V>...c`, `<C-V>...I`) leaving stray carets in normal mode
|
|
||||||
* [VIM-4176](https://youtrack.jetbrains.com/issue/VIM-4176) Fixed race condition in single-line output panel that could cause `*` search wrapping to behave unreliably
|
|
||||||
* [VIM-4175](https://youtrack.jetbrains.com/issue/VIM-4175) Fixed search "not found" showing previous "Hit ENTER" text alongside the error - panel is now cleared before displaying errors like "E486: Pattern not found"
|
|
||||||
* [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-4195](https://youtrack.jetbrains.com/issue/VIM-4195) Fixed settings not being saved in remote development (split) mode
|
|
||||||
* [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
|
||||||
* Fixed `\/`, `\?`, and `\&` in Ex command ranges now correctly report E35/E33 errors when no previous search or substitute pattern exists, instead of crashing
|
|
||||||
* [VIM-4172](https://youtrack.jetbrains.com/issue/VIM-4172) IdeaVim is now disabled in Python Console to prevent key interference
|
|
||||||
* [VIM-4113](https://youtrack.jetbrains.com/issue/VIM-4113) Fixed Visual mode commands (e.g., `:'<,'>sort`) failing when run off the Event Dispatch Thread
|
|
||||||
* [VIM-3727](https://youtrack.jetbrains.com/issue/VIM-3727) Fixed Enter and arrow keys not working in Python Console in split mode
|
|
||||||
* Fixed NERDTree navigation (`j`/`k`/`G`/`gg`/`p`/`<C-J>`/`<C-K>`) poor performance in split mode - navigation now uses Swing actions directly instead of going through backend RPC
|
|
||||||
* [VIM-4180](https://youtrack.jetbrains.com/issue/VIM-4180) Fixed ReplaceWithRegister plugin's default `gr`/`grr` mappings overriding user-defined key mappings
|
|
||||||
* Fixed `IndexOutOfBoundsException` when using `:command` with `-nargs` option but without a command name
|
|
||||||
* Fixed spurious beep when pressing `<Esc>` to cancel register selection in normal mode (after pressing `"`)
|
|
||||||
* [VIM-4202](https://youtrack.jetbrains.com/issue/VIM-4202) Fixed `<S-Tab>` being intercepted by IdeaVim - users can now remap `<S-Tab>` to other IntelliJ actions
|
|
||||||
* [VIM-4202](https://youtrack.jetbrains.com/issue/VIM-4202) Fixed `gcc`/`gc{motion}` commentary leaving editor in incorrect mode in Rider/CLion split mode
|
|
||||||
* [VIM-4115](https://youtrack.jetbrains.com/issue/VIM-4115) Fixed NullPointerException in `CommandKeyConsumer` when pressing Esc after disabling and re-enabling IdeaVim with an open command line
|
|
||||||
|
|
||||||
### Merged PRs:
|
### Merged PRs:
|
||||||
* [1704](https://github.com/JetBrains/ideavim/pull/1704) by [1grzyb1](https://github.com/1grzyb1): VIM-4202 Don't intercept all <S-Tab>
|
|
||||||
* [1703](https://github.com/JetBrains/ideavim/pull/1703) by [1grzyb1](https://github.com/1grzyb1): VIM-4202 Fix state after commentary action
|
|
||||||
* [1700](https://github.com/JetBrains/ideavim/pull/1700) by [1grzyb1](https://github.com/1grzyb1): VIM-4139 Compute nesting depth for fold regions
|
|
||||||
* [1699](https://github.com/JetBrains/ideavim/pull/1699) by [1grzyb1](https://github.com/1grzyb1): VIM-4112 collapse restored carets after undo of block-visual edit
|
|
||||||
* [1696](https://github.com/JetBrains/ideavim/pull/1696) by [citizenmatt](https://github.com/citizenmatt): VIM-4197 Fix missing Vim features in Java files decompiled from Kotlin class files
|
|
||||||
* [1695](https://github.com/JetBrains/ideavim/pull/1695) by [1grzyb1](https://github.com/1grzyb1): VIM-1693 Implement autocmd
|
|
||||||
* [1690](https://github.com/JetBrains/ideavim/pull/1690) by [1grzyb1](https://github.com/1grzyb1): Make nerdtree work without calling backend actions
|
|
||||||
* [1688](https://github.com/JetBrains/ideavim/pull/1688) by [1grzyb1](https://github.com/1grzyb1): VIM-4172 Disable ideavim in Python Console
|
|
||||||
* [1687](https://github.com/JetBrains/ideavim/pull/1687) by [1grzyb1](https://github.com/1grzyb1): Restore old VimPLugin method signatures
|
|
||||||
* [1685](https://github.com/JetBrains/ideavim/pull/1685) by [1grzyb1](https://github.com/1grzyb1): VIM-3727 Fix Python console Enter and arrow keys in split mode
|
|
||||||
* [1548](https://github.com/JetBrains/ideavim/pull/1548) by [1grzyb1](https://github.com/1grzyb1): VIM-1158 Add `gw` to reformat code with preserving the cursor position
|
|
||||||
* [1682](https://github.com/JetBrains/ideavim/pull/1682) by [1grzyb1](https://github.com/1grzyb1): VIM-268 Complete file names in edit command
|
|
||||||
* [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
|
||||||
* [1665](https://github.com/JetBrains/ideavim/pull/1665) by [1grzyb1](https://github.com/1grzyb1): Fix visual selection commands failing off-EDT due to nested write-in-read action
|
|
||||||
|
|
||||||
## 2.28.0, 2025-12-09
|
## 2.28.0, 2025-12-09
|
||||||
|
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -241,24 +241,6 @@ ShowHoverInfo - Quick Documentation and Error Description
|
|||||||
QuickImplementations - Quick Definition
|
QuickImplementations - Quick Definition
|
||||||
```
|
```
|
||||||
|
|
||||||
Autocmd
|
|
||||||
----------
|
|
||||||
|
|
||||||
IdeaVim supports Vim’s `:autocmd` for running commands on editor events, including
|
|
||||||
`InsertEnter`/`InsertLeave`, buffer events (`BufEnter`, `BufLeave`, `BufRead`,
|
|
||||||
`BufNewFile`, `BufWritePre`, `BufWritePost`), window events (`WinEnter`, `WinLeave`),
|
|
||||||
focus events (`FocusGained`, `FocusLost`), and `FileType`. Full glob patterns
|
|
||||||
(`*`, `**`, `?`, `[abc]`, `{a,b}`) and augroups are supported.
|
|
||||||
|
|
||||||
```vim
|
|
||||||
autocmd BufWritePre *.py echo "saving python"
|
|
||||||
autocmd FileType python setlocal shiftwidth=4
|
|
||||||
```
|
|
||||||
|
|
||||||
See [doc/autocmd.md](doc/autocmd.md) for the full event reference, firing order, and notes on IntelliJ-specific
|
|
||||||
differences.
|
|
||||||
|
|
||||||
|
|
||||||
Vim Script
|
Vim Script
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ IdeaVim project is licensed under MIT license except the following parts of it:
|
|||||||
|
|
||||||
* File [ScrollViewHelper.kt](com/maddyhome/idea/vim/helper/ScrollViewHelper.kt) is licensed under Vim License.
|
* File [ScrollViewHelper.kt](com/maddyhome/idea/vim/helper/ScrollViewHelper.kt) is licensed under Vim License.
|
||||||
* File [Tutor.kt](src/main/java/com/maddyhome/idea/vim/ui/Tutor.kt) is licensed under Vim License.
|
* File [Tutor.kt](src/main/java/com/maddyhome/idea/vim/ui/Tutor.kt) is licensed under Vim License.
|
||||||
* File [CodeWrapper.kt](vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/CodeWrapper.kt) is licensed under Vim
|
|
||||||
License.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
VIM LICENSE
|
VIM LICENSE
|
||||||
|
|||||||
111
build.gradle.kts
111
build.gradle.kts
@@ -27,11 +27,11 @@ buildscript {
|
|||||||
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.6.0.202603022253-r")
|
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.6.0.202603022253-r")
|
||||||
classpath("org.kohsuke:github-api:1.305")
|
classpath("org.kohsuke:github-api:1.305")
|
||||||
|
|
||||||
classpath("io.ktor:ktor-client-core:3.4.3")
|
classpath("io.ktor:ktor-client-core:3.4.2")
|
||||||
classpath("io.ktor:ktor-client-cio:3.4.3")
|
classpath("io.ktor:ktor-client-cio:3.4.2")
|
||||||
classpath("io.ktor:ktor-client-auth:3.4.3")
|
classpath("io.ktor:ktor-client-auth:3.4.2")
|
||||||
classpath("io.ktor:ktor-client-content-negotiation:3.4.3")
|
classpath("io.ktor:ktor-client-content-negotiation:3.4.2")
|
||||||
classpath("io.ktor:ktor-serialization-kotlinx-json:3.4.3")
|
classpath("io.ktor:ktor-serialization-kotlinx-json:3.4.2")
|
||||||
|
|
||||||
// This comes from the changelog plugin
|
// This comes from the changelog plugin
|
||||||
// classpath("org.jetbrains:markdown:0.3.1")
|
// classpath("org.jetbrains:markdown:0.3.1")
|
||||||
@@ -228,7 +228,7 @@ tasks {
|
|||||||
|
|
||||||
val runPycharm by intellijPlatformTesting.runIde.registering {
|
val runPycharm by intellijPlatformTesting.runIde.registering {
|
||||||
type = IntelliJPlatformType.PyCharmProfessional
|
type = IntelliJPlatformType.PyCharmProfessional
|
||||||
version = "2026.1"
|
version = "2025.3.2"
|
||||||
task {
|
task {
|
||||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
||||||
}
|
}
|
||||||
@@ -244,14 +244,13 @@ tasks {
|
|||||||
|
|
||||||
val runClion by intellijPlatformTesting.runIde.registering {
|
val runClion by intellijPlatformTesting.runIde.registering {
|
||||||
type = IntelliJPlatformType.CLion
|
type = IntelliJPlatformType.CLion
|
||||||
version = "2026.1"
|
version = "2025.3.2"
|
||||||
task {
|
task {
|
||||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val runIdeForUiTests by intellijPlatformTesting.runIde.registering {
|
val runIdeForUiTests by intellijPlatformTesting.runIde.registering {
|
||||||
version = "2026.1"
|
|
||||||
task {
|
task {
|
||||||
jvmArgumentProviders += CommandLineArgumentProvider {
|
jvmArgumentProviders += CommandLineArgumentProvider {
|
||||||
listOf(
|
listOf(
|
||||||
@@ -314,7 +313,7 @@ tasks {
|
|||||||
}
|
}
|
||||||
val runPycharmSplitMode by intellijPlatformTesting.runIde.registering {
|
val runPycharmSplitMode by intellijPlatformTesting.runIde.registering {
|
||||||
type = IntelliJPlatformType.PyCharmProfessional
|
type = IntelliJPlatformType.PyCharmProfessional
|
||||||
version = "2026.1"
|
version = "2025.3.2"
|
||||||
splitMode = true
|
splitMode = true
|
||||||
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
||||||
|
|
||||||
@@ -363,45 +362,6 @@ tasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val runPycharmSplitModeDebugFrontend by intellijPlatformTesting.runIde.registering {
|
|
||||||
type = IntelliJPlatformType.PyCharmProfessional
|
|
||||||
version = "2026.1"
|
|
||||||
splitMode = true
|
|
||||||
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
plugin("AceJump", "3.8.22")
|
|
||||||
plugin("org.jetbrains.IdeaVim-EasyMotion", "1.16")
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareSandboxTask {
|
|
||||||
val sandboxDir = project.layout.buildDirectory.dir("idea-sandbox").map { it.asFile }
|
|
||||||
doLast {
|
|
||||||
val debugLine = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5006"
|
|
||||||
val vmoptions = sandboxDir.get().walkTopDown()
|
|
||||||
.filter { it.name == "jetbrains_client64.vmoptions" && it.path.contains("runPycharmSplitModeDebugFrontend") }
|
|
||||||
.firstOrNull()
|
|
||||||
?: sandboxDir.get().walkTopDown()
|
|
||||||
.filter { it.name == "jetbrains_client64.vmoptions" }
|
|
||||||
.firstOrNull()
|
|
||||||
|
|
||||||
if (vmoptions != null) {
|
|
||||||
val content = vmoptions.readText()
|
|
||||||
if (debugLine !in content) {
|
|
||||||
vmoptions.appendText("\n$debugLine\n")
|
|
||||||
logger.lifecycle("Patched frontend vmoptions with JDWP debug agent: ${vmoptions.absolutePath}")
|
|
||||||
}
|
|
||||||
logger.lifecycle("Connect a Remote JVM Debug configuration to localhost:5006")
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
"Could not find jetbrains_client64.vmoptions in sandbox. " +
|
|
||||||
"Run `./gradlew runPycharmSplitMode` once first to populate the sandbox, then use this task."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val testIdeSplitMode by intellijPlatformTesting.testIde.registering {
|
val testIdeSplitMode by intellijPlatformTesting.testIde.registering {
|
||||||
splitMode = true
|
splitMode = true
|
||||||
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
||||||
@@ -474,8 +434,6 @@ intellijPlatform {
|
|||||||
changeNotes.set(
|
changeNotes.set(
|
||||||
"""
|
"""
|
||||||
<b>Features:</b><br>
|
<b>Features:</b><br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-1693">VIM-1693</a> Added <code>:autocmd</code> command - run Ex commands on editor events such as <code>BufRead</code>, <code>BufWrite</code>, <code>BufEnter</code>, <code>BufLeave</code>, <code>InsertEnter</code>, <code>InsertLeave</code>, <code>WinEnter</code>, <code>WinLeave</code>, <code>FocusGained</code>, <code>FocusLost</code>, and <code>FileType</code>; supports <code>augroup</code> and file pattern matching (e.g., <code>autocmd BufWritePre *.py echo "saving python"</code>)<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-268">VIM-268</a> Added file name completion in ex commands - press <code><Tab></code>/<code><S-Tab></code> to cycle through file matches in <code>:edit</code>, <code>:split</code>, <code>:vsplit</code>, <code>:write</code>, <code>:read</code>, <code>:source</code>, and <code>:find</code> commands; use arrow keys to navigate the completion panel<br>
|
|
||||||
* New VimScript functions: <code>add()</code>, <code>call()</code>, <code>extend()</code>, <code>extendnew()</code>, <code>filter()</code>, <code>flatten()</code>, <code>flattennew()</code>, <code>foreach()</code>, <code>has_key()</code>, <code>indexof()</code>, <code>insert()</code>, <code>items()</code>, <code>keys()</code>, <code>map()</code>, <code>mapnew()</code>, <code>reduce()</code>, <code>remove()</code>, <code>slice()</code>, <code>sort()</code>, <code>uniq()</code>, <code>values()</code><br>
|
* New VimScript functions: <code>add()</code>, <code>call()</code>, <code>extend()</code>, <code>extendnew()</code>, <code>filter()</code>, <code>flatten()</code>, <code>flattennew()</code>, <code>foreach()</code>, <code>has_key()</code>, <code>indexof()</code>, <code>insert()</code>, <code>items()</code>, <code>keys()</code>, <code>map()</code>, <code>mapnew()</code>, <code>reduce()</code>, <code>remove()</code>, <code>slice()</code>, <code>sort()</code>, <code>uniq()</code>, <code>values()</code><br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-1595">VIM-1595</a> Added support for <code>:read</code> command - insert file content below current line (e.g., <code>:read file.txt</code>, <code>0read file.txt</code>)<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-1595">VIM-1595</a> Added support for <code>:read</code> command - insert file content below current line (e.g., <code>:read file.txt</code>, <code>0read file.txt</code>)<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-1595">VIM-1595</a> Added support for <code>:read!</code> command - insert shell command output below current line (e.g., <code>:read! echo "hello"</code>)<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-1595">VIM-1595</a> Added support for <code>:read!</code> command - insert shell command output below current line (e.g., <code>:read! echo "hello"</code>)<br>
|
||||||
@@ -484,71 +442,18 @@ intellijPlatform {
|
|||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>zm</code> command - decrease fold level to hide more folds<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>zm</code> command - decrease fold level to hide more folds<br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>zf</code> command - create fold from selection or motion<br>
|
* <a href="https://youtrack.jetbrains.com/issue/VIM-566">VIM-566</a> Added support for <code>zf</code> command - create fold from selection or motion<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>
|
* <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-1158">VIM-1158</a> Added <code>gw</code> command - reformat code like <code>gq</code> but preserving the cursor position<br>
|
|
||||||
<br>
|
<br>
|
||||||
<b>Fixes:</b><br>
|
<b>Fixes:</b><br>
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4197">VIM-4197</a> Fixed Vim features (e.g., <code>f</code>, <code>w</code>, text objects) not working in Java files decompiled from Kotlin class files<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4112">VIM-4112</a> Fixed undo after block-visual edit (<code><C-V>...x</code>, <code><C-V>...c</code>, <code><C-V>...I</code>) leaving stray carets in normal mode<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4176">VIM-4176</a> Fixed race condition in single-line output panel that could cause <code>*</code> search wrapping to behave unreliably<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4175">VIM-4175</a> Fixed search "not found" showing previous "Hit ENTER" text alongside the error - panel is now cleared before displaying errors like "E486: Pattern not found"<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-4195">VIM-4195</a> Fixed settings not being saved in remote development (split) mode<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>
|
||||||
* Fixed <code>\/</code>, <code>\?</code>, and <code>\&</code> in Ex command ranges now correctly report E35/E33 errors when no previous search or substitute pattern exists, instead of crashing<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4172">VIM-4172</a> IdeaVim is now disabled in Python Console to prevent key interference<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4113">VIM-4113</a> Fixed Visual mode commands (e.g., <code>:'<,'>sort</code>) failing when run off the Event Dispatch Thread<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-3727">VIM-3727</a> Fixed Enter and arrow keys not working in Python Console in split mode<br>
|
|
||||||
* Fixed NERDTree navigation (<code>j</code>/<code>k</code>/<code>G</code>/<code>gg</code>/<code>p</code>/<code><C-J></code>/<code><C-K></code>) poor performance in split mode - navigation now uses Swing actions directly instead of going through backend RPC<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4180">VIM-4180</a> Fixed ReplaceWithRegister plugin's default <code>gr</code>/<code>grr</code> mappings overriding user-defined key mappings<br>
|
|
||||||
* Fixed <code>IndexOutOfBoundsException</code> when using <code>:command</code> with <code>-nargs</code> option but without a command name<br>
|
|
||||||
* Fixed spurious beep when pressing <code><Esc></code> to cancel register selection in normal mode (after pressing <code>"</code>)<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4202">VIM-4202</a> Fixed <code><S-Tab></code> being intercepted by IdeaVim - users can now remap <code><S-Tab></code> to other IntelliJ actions<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4202">VIM-4202</a> Fixed <code>gcc</code>/<code>gc{motion}</code> commentary leaving editor in incorrect mode in Rider/CLion split mode<br>
|
|
||||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4115">VIM-4115</a> Fixed NullPointerException in <code>CommandKeyConsumer</code> when pressing Esc after disabling and re-enabling IdeaVim with an open command line<br>
|
|
||||||
<br>
|
<br>
|
||||||
<b>Merged PRs:</b><br>
|
<b>Merged PRs:</b><br>
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1704">1704</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4202 Don't intercept all <S-Tab><br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1703">1703</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4202 Fix state after commentary action<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1700">1700</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4139 Compute nesting depth for fold regions<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1699">1699</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4112 collapse restored carets after undo of block-visual edit<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1696">1696</a> by <a href="https://github.com/citizenmatt">citizenmatt</a>: VIM-4197 Fix missing Vim features in Java files decompiled from Kotlin class files<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1695">1695</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-1693 Implement autocmd<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1690">1690</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Make nerdtree work without calling backend actions<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1688">1688</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4172 Disable ideavim in Python Console<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1687">1687</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Restore old VimPLugin method signatures<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1685">1685</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-3727 Fix Python console Enter and arrow keys in split mode<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1548">1548</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-1158 Add <code>gw</code> to reformat code with preserving the cursor position<br>
|
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1682">1682</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-268 Complete file names in edit command<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>
|
||||||
* <a href="https://github.com/JetBrains/ideavim/pull/1665">1665</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Fix visual selection commands failing off-EDT due to nested write-in-read action<br>
|
|
||||||
<br>
|
<br>
|
||||||
<a href="https://youtrack.jetbrains.com/issues/VIM?q=State:%20Fixed%20Fix%20versions:%20${version.get()}">Changelog</a>
|
<a href="https://youtrack.jetbrains.com/issues/VIM?q=State:%20Fixed%20Fix%20versions:%20${version.get()}">Changelog</a>
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|||||||
137
doc/autocmd.md
137
doc/autocmd.md
@@ -1,137 +0,0 @@
|
|||||||
Autocommands
|
|
||||||
============
|
|
||||||
|
|
||||||
IdeaVim supports Vim's `:autocmd` for running commands on editor events.
|
|
||||||
Handlers are registered from `~/.ideavimrc` or interactively in Command-line mode.
|
|
||||||
Every effort is made to match Vim's behaviour, but some differences are inevitable
|
|
||||||
because the IDE's event model doesn't map 1:1 onto Vim's.
|
|
||||||
|
|
||||||
Syntax
|
|
||||||
------
|
|
||||||
|
|
||||||
```
|
|
||||||
autocmd [group] {event}[,{event}...] {pattern} {command}
|
|
||||||
autocmd!
|
|
||||||
autocmd! {group}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `{event}` — one or more comma-separated event names (see below).
|
|
||||||
- `{pattern}` — file pattern (see "Patterns" below). For `FileType`, the pattern matches the filetype name, not the file
|
|
||||||
path.
|
|
||||||
- `{command}` — any Ex command or Vimscript expression.
|
|
||||||
- `autocmd!` — clears all registered handlers, or all handlers in the given augroup.
|
|
||||||
|
|
||||||
```vim
|
|
||||||
augroup my_group
|
|
||||||
autocmd!
|
|
||||||
autocmd BufWritePre *.py echo "saving python"
|
|
||||||
augroup END
|
|
||||||
```
|
|
||||||
|
|
||||||
Patterns
|
|
||||||
--------
|
|
||||||
|
|
||||||
Autocmd file patterns support the following glob syntax:
|
|
||||||
|
|
||||||
| Pattern | Matches |
|
|
||||||
|-------------|------------------------------------------|
|
|
||||||
| `*` | Any characters except path separators |
|
|
||||||
| `**` | Any characters including path separators |
|
|
||||||
| `?` | Any single non-separator character |
|
|
||||||
| `[abc]` | Any character in the set |
|
|
||||||
| `{foo,bar}` | Either `foo` or `bar` |
|
|
||||||
|
|
||||||
If the pattern contains `/` or `\`, it matches against the full path;
|
|
||||||
otherwise it matches against the filename only.
|
|
||||||
|
|
||||||
`FileType` is special: its pattern matches against the filetype name
|
|
||||||
(e.g. `python`, `java`) rather than the file path.
|
|
||||||
|
|
||||||
Supported events
|
|
||||||
----------------
|
|
||||||
|
|
||||||
### Insert mode
|
|
||||||
|
|
||||||
| Event | Fires when |
|
|
||||||
|---------------|----------------------|
|
|
||||||
| `InsertEnter` | Entering Insert mode |
|
|
||||||
| `InsertLeave` | Leaving Insert mode |
|
|
||||||
|
|
||||||
### Buffers
|
|
||||||
|
|
||||||
| Event | Fires when |
|
|
||||||
|----------------|-------------------------------------------------------------------|
|
|
||||||
| `BufEnter` | A buffer becomes active (every switch) |
|
|
||||||
| `BufLeave` | A buffer stops being active |
|
|
||||||
| `BufRead` | A file is loaded into a buffer for the first time |
|
|
||||||
| `BufReadPost` | Alias of `BufRead` (same event, two names) |
|
|
||||||
| `BufNewFile` | Editing a file that was just created (fires instead of `BufRead`) |
|
|
||||||
| `BufWrite` | Alias of `BufWritePre` |
|
|
||||||
| `BufWritePre` | Before the buffer is written to disk |
|
|
||||||
| `BufWritePost` | After the buffer has been written to disk |
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
| Event | Fires when |
|
|
||||||
|------------|----------------------------------------|
|
|
||||||
| `WinEnter` | A window becomes active (every switch) |
|
|
||||||
| `WinLeave` | A window stops being active |
|
|
||||||
|
|
||||||
### Files
|
|
||||||
|
|
||||||
| Event | Fires when |
|
|
||||||
|------------|--------------------------------------------------------------------------------------------------|
|
|
||||||
| `FileType` | A buffer's filetype is determined (typically once per file load). Pattern matches filetype name. |
|
|
||||||
|
|
||||||
### Focus
|
|
||||||
|
|
||||||
| Event | Fires when |
|
|
||||||
|---------------|----------------------------|
|
|
||||||
| `FocusGained` | The IDE window gains focus |
|
|
||||||
| `FocusLost` | The IDE window loses focus |
|
|
||||||
|
|
||||||
### Event order
|
|
||||||
|
|
||||||
When opening a file for the first time:
|
|
||||||
|
|
||||||
```
|
|
||||||
BufRead/BufReadPost → FileType → BufEnter
|
|
||||||
```
|
|
||||||
|
|
||||||
When opening a just-created file:
|
|
||||||
|
|
||||||
```
|
|
||||||
BufNewFile → FileType → BufEnter
|
|
||||||
```
|
|
||||||
|
|
||||||
When switching buffers:
|
|
||||||
|
|
||||||
```
|
|
||||||
BufLeave → WinLeave → WinEnter → BufEnter
|
|
||||||
```
|
|
||||||
|
|
||||||
When saving:
|
|
||||||
|
|
||||||
```
|
|
||||||
BufWrite/BufWritePre → (write) → BufWritePost
|
|
||||||
```
|
|
||||||
|
|
||||||
Differences from Vim
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
**`FileType` names.** IdeaVim maps IntelliJ's file type name to a Vim-style
|
|
||||||
filetype. For most languages the lowercased IJ name matches Vim's filetype
|
|
||||||
(`Python`→`python`, `JAVA`→`java`). A small override table handles cases where
|
|
||||||
Vim's convention differs: `PLAIN_TEXT`→`text`, `C++`→`cpp`, `C#`→`cs`,
|
|
||||||
`Shell Script`→`sh`, `ObjectiveC`→`objc`, `JavaScript`→`javascript`,
|
|
||||||
`TypeScript`→`typescript`, `Vue.js`→`vue`, `CMakeLists.txt`→`cmake`,
|
|
||||||
`Handlebars/Mustache`→`handlebars`.
|
|
||||||
|
|
||||||
**`BufNewFile` detection.** IdeaVim tracks files created during the session
|
|
||||||
via the VFS. When such a file is opened in an editor, `BufNewFile` fires
|
|
||||||
instead of `BufRead`. Files created by VCS pulls, build tools, or external
|
|
||||||
processes that you later open in an editor will also be treated as new files.
|
|
||||||
|
|
||||||
**`BufWritePre` / `BufWritePost` frequency.** IntelliJ auto-saves on focus
|
|
||||||
loss, tab switch, build, and other events. These autocmds fire more often
|
|
||||||
than Vim's `:w`, so handlers should be idempotent.
|
|
||||||
@@ -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=9999.58-chylex
|
version=chylex-56
|
||||||
javaVersion=21
|
javaVersion=21
|
||||||
remoteRobotVersion=0.11.23
|
remoteRobotVersion=0.11.23
|
||||||
antlrVersion=4.10.1
|
antlrVersion=4.10.1
|
||||||
|
|||||||
@@ -8,24 +8,23 @@
|
|||||||
|
|
||||||
package com.maddyhome.idea.vim.group.comment
|
package com.maddyhome.idea.vim.group.comment
|
||||||
|
|
||||||
import com.intellij.codeInsight.actions.MultiCaretCodeInsightActionHandler
|
import com.intellij.openapi.actionSystem.ActionManager
|
||||||
import com.intellij.codeInsight.generation.CommentByBlockCommentHandler
|
import com.intellij.openapi.actionSystem.IdeActions
|
||||||
import com.intellij.codeInsight.generation.CommentByLineCommentHandler
|
|
||||||
import com.intellij.lang.LanguageCommenters
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
|
||||||
import com.intellij.openapi.command.CommandProcessor
|
import com.intellij.openapi.command.CommandProcessor
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.openapi.editor.impl.EditorId
|
import com.intellij.openapi.editor.impl.EditorId
|
||||||
import com.intellij.openapi.editor.impl.findEditorOrNull
|
import com.intellij.openapi.editor.impl.findEditorOrNull
|
||||||
import com.intellij.psi.PsiDocumentManager
|
|
||||||
import com.intellij.psi.PsiFile
|
|
||||||
import com.maddyhome.idea.vim.group.onEdt
|
import com.maddyhome.idea.vim.group.onEdt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handlers are invoked directly rather than via `ActionManager.tryToExecute` because in
|
* RPC handler for [CommentaryRemoteApi].
|
||||||
* Rider / CLion Nova the action dispatch is async — `ActionCallback` signals `done` at
|
*
|
||||||
* dispatch, not completion — so the action's selection survived `removeSelection()` and
|
* Sets selection on the backend editor and executes the platform's comment action.
|
||||||
* the selection listener dropped IdeaVim into Visual-Line mode.
|
* Because this runs on the backend, [com.intellij.openapi.command.CommandProcessor]
|
||||||
|
* groups all document modifications as a single undo step.
|
||||||
|
*
|
||||||
|
* The selection is set on the backend editor only — it doesn't affect the frontend
|
||||||
|
* editor's visual state, and is cleaned up immediately after the action executes.
|
||||||
*/
|
*/
|
||||||
internal class CommentaryRemoteApiImpl : CommentaryRemoteApi {
|
internal class CommentaryRemoteApiImpl : CommentaryRemoteApi {
|
||||||
|
|
||||||
@@ -36,47 +35,40 @@ internal class CommentaryRemoteApiImpl : CommentaryRemoteApi {
|
|||||||
val startOffset = document.getLineStartOffset(startLine)
|
val startOffset = document.getLineStartOffset(startLine)
|
||||||
val endOffset = document.getLineEndOffset(endLine)
|
val endOffset = document.getLineEndOffset(endLine)
|
||||||
|
|
||||||
runCommenter(editor, startOffset, endOffset, caretOffset, lineWise = true)
|
executeCommentAction(editor, startOffset, endOffset, caretOffset, IdeActions.ACTION_COMMENT_LINE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toggleBlockComment(editorId: EditorId, startOffset: Int, endOffset: Int, caretOffset: Int) =
|
override suspend fun toggleBlockComment(editorId: EditorId, startOffset: Int, endOffset: Int, caretOffset: Int) =
|
||||||
onEdt {
|
onEdt {
|
||||||
val editor = editorId.findEditorOrNull() ?: return@onEdt
|
val editor = editorId.findEditorOrNull() ?: return@onEdt
|
||||||
runCommenter(editor, startOffset, endOffset, caretOffset, lineWise = false)
|
// Try block comment first, fall back to line comment
|
||||||
|
if (!executeCommentAction(editor, startOffset, endOffset, caretOffset, IdeActions.ACTION_COMMENT_BLOCK)) {
|
||||||
|
executeCommentAction(editor, startOffset, endOffset, caretOffset, IdeActions.ACTION_COMMENT_LINE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runCommenter(
|
private fun executeCommentAction(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
startOffset: Int,
|
startOffset: Int,
|
||||||
endOffset: Int,
|
endOffset: Int,
|
||||||
caretOffset: Int,
|
caretOffset: Int,
|
||||||
lineWise: Boolean,
|
actionId: String,
|
||||||
) {
|
): Boolean {
|
||||||
val project = editor.project ?: return
|
var result = false
|
||||||
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return
|
// Wrap selection + action + caret reset + cleanup in a single command so everything
|
||||||
|
// is a single undo step. In remdev, undo restores pre-command editor state — if
|
||||||
CommandProcessor.getInstance().executeCommand(project, {
|
// selection is set before the command, undo would restore it. The nested tryToExecute
|
||||||
ApplicationManager.getApplication().runWriteAction {
|
// command merges into this outer command.
|
||||||
val caret = editor.caretModel.primaryCaret
|
CommandProcessor.getInstance().executeCommand(editor.project, {
|
||||||
caret.setSelection(startOffset, endOffset)
|
editor.selectionModel.setSelection(startOffset, endOffset)
|
||||||
try {
|
val action = ActionManager.getInstance().getAction(actionId)
|
||||||
val handler = pickHandler(psiFile, lineWise)
|
result = ActionManager.getInstance().tryToExecute(action, null, editor.contentComponent, "IdeaVim", true)
|
||||||
handler.invoke(project, editor, caret, psiFile)
|
.let { it.waitFor(5_000); it.isDone }
|
||||||
handler.postInvoke()
|
editor.selectionModel.removeSelection()
|
||||||
} finally {
|
|
||||||
caret.removeSelection()
|
|
||||||
if (caretOffset >= 0) {
|
if (caretOffset >= 0) {
|
||||||
caret.moveToOffset(caretOffset)
|
editor.caretModel.moveToOffset(caretOffset)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, "Commentary", null)
|
}, "Commentary", null)
|
||||||
}
|
return result
|
||||||
|
|
||||||
private fun pickHandler(psiFile: PsiFile, lineWise: Boolean): MultiCaretCodeInsightActionHandler {
|
|
||||||
if (lineWise) return CommentByLineCommentHandler()
|
|
||||||
val commenter = LanguageCommenters.INSTANCE.forLanguage(psiFile.language)
|
|
||||||
val hasBlock = commenter?.blockCommentPrefix != null && commenter.blockCommentSuffix != null
|
|
||||||
return if (hasBlock) CommentByBlockCommentHandler() else CommentByLineCommentHandler()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +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.group.file
|
|
||||||
|
|
||||||
import com.intellij.openapi.vfs.LocalFileSystem
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile
|
|
||||||
import kotlin.io.path.Path
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a user-typed path prefix into a list of matching file/directory names
|
|
||||||
* for command-line completion. Directories are suffixed with `/`.
|
|
||||||
*/
|
|
||||||
internal object FileCompletionHelper {
|
|
||||||
|
|
||||||
fun listMatchingFiles(pathPrefix: String, basePath: String?): List<String> {
|
|
||||||
val (parentDir, namePrefix) = resolveParentAndPrefix(pathPrefix, basePath)
|
|
||||||
if (parentDir == null || !parentDir.isDirectory) return emptyList()
|
|
||||||
|
|
||||||
return filterAndFormat(parentDir, namePrefix, pathPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun filterAndFormat(parentDir: VirtualFile, namePrefix: String, pathPrefix: String): List<String> {
|
|
||||||
val dirPrefix = pathPrefix.substringBeforeLast('/', "")
|
|
||||||
|
|
||||||
return parentDir.children
|
|
||||||
.filter { it.name.startsWith(namePrefix, ignoreCase = true) }
|
|
||||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
|
||||||
.map { formatChild(it, dirPrefix) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatChild(child: VirtualFile, dirPrefix: String): String {
|
|
||||||
val name = if (child.isDirectory) child.name + "/" else child.name
|
|
||||||
if (dirPrefix.isEmpty()) return name
|
|
||||||
return "$dirPrefix/$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveParentAndPrefix(pathPrefix: String, basePath: String?): Pair<VirtualFile?, String> {
|
|
||||||
if (pathPrefix.isEmpty()) return resolveProjectRoot(basePath)
|
|
||||||
if (pathPrefix.startsWith("~/") || pathPrefix.startsWith("~\\")) return resolveHomePath(pathPrefix)
|
|
||||||
if (Path(pathPrefix).isAbsolute) return resolveAbsolutePath(pathPrefix)
|
|
||||||
return resolveRelativePath(pathPrefix, basePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveProjectRoot(basePath: String?): Pair<VirtualFile?, String> {
|
|
||||||
val dir = basePath?.let { LocalFileSystem.getInstance().findFileByNioFile(Path(it)) }
|
|
||||||
return dir to ""
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveHomePath(pathPrefix: String): Pair<VirtualFile?, String> {
|
|
||||||
val home = System.getProperty("user.home")
|
|
||||||
val relativePath = pathPrefix.substring(2)
|
|
||||||
return splitDirAndPrefix(relativePath) { dirPath ->
|
|
||||||
LocalFileSystem.getInstance().findFileByNioFile(Path(home, dirPath))
|
|
||||||
} ?: (LocalFileSystem.getInstance().findFileByNioFile(Path(home)) to relativePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveAbsolutePath(pathPrefix: String): Pair<VirtualFile?, String> {
|
|
||||||
return splitDirAndPrefix(pathPrefix) { dirPath ->
|
|
||||||
LocalFileSystem.getInstance().findFileByNioFile(Path(dirPath.ifEmpty { "/" }))
|
|
||||||
} ?: (null to "")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveRelativePath(pathPrefix: String, basePath: String?): Pair<VirtualFile?, String> {
|
|
||||||
val baseDir = basePath?.let { LocalFileSystem.getInstance().findFileByNioFile(Path(it)) }
|
|
||||||
return splitDirAndPrefix(pathPrefix) { dirPath ->
|
|
||||||
baseDir?.findFileByRelativePath(dirPath)
|
|
||||||
} ?: (baseDir to pathPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun splitDirAndPrefix(
|
|
||||||
path: String,
|
|
||||||
resolveDir: (String) -> VirtualFile?,
|
|
||||||
): Pair<VirtualFile?, String>? {
|
|
||||||
val lastSlash = path.lastIndexOf('/')
|
|
||||||
if (lastSlash < 0) return null
|
|
||||||
|
|
||||||
val dirPath = path.substring(0, lastSlash)
|
|
||||||
val prefix = path.substring(lastSlash + 1)
|
|
||||||
return resolveDir(dirPath) to prefix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -162,11 +162,6 @@ internal class FileRemoteApiImpl : FileRemoteApi {
|
|||||||
if (first is TextEditor) !first.editor.isDisposed else false
|
if (first is TextEditor) !first.editor.isDisposed else false
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun listFilesForCompletion(pathPrefix: String, projectId: ProjectId?): List<String> = readAction {
|
|
||||||
val basePath = projectId?.findProjectOrNull()?.basePath
|
|
||||||
FileCompletionHelper.listMatchingFiles(pathPrefix, basePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== Private helpers ========================
|
// ======================== Private helpers ========================
|
||||||
|
|
||||||
private fun findFile(filename: String, project: Project): VirtualFile? {
|
private fun findFile(filename: String, project: Project): VirtualFile? {
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ interface FileRemoteApi : RemoteApi<Unit> {
|
|||||||
suspend fun selectNextFile(count: Int, projectId: ProjectId?)
|
suspend fun selectNextFile(count: Int, projectId: ProjectId?)
|
||||||
suspend fun buildFileInfoMessage(editorId: EditorId, fullPath: Boolean): String?
|
suspend fun buildFileInfoMessage(editorId: EditorId, fullPath: Boolean): String?
|
||||||
suspend fun selectEditor(projectId: ProjectId, documentPath: String, protocol: String): Boolean
|
suspend fun selectEditor(projectId: ProjectId, documentPath: String, protocol: String): Boolean
|
||||||
suspend fun listFilesForCompletion(pathPrefix: String, projectId: ProjectId?): List<String>
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|||||||
@@ -40,8 +40,6 @@
|
|||||||
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
|
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
|
||||||
<listener class="com.maddyhome.idea.vim.handler.IdeaVimCorrectorKeymapChangedListener"
|
<listener class="com.maddyhome.idea.vim.handler.IdeaVimCorrectorKeymapChangedListener"
|
||||||
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
|
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
|
||||||
<listener class="com.maddyhome.idea.vim.listener.VimAppActivationListener"
|
|
||||||
topic="com.intellij.openapi.application.ApplicationActivationListener"/>
|
|
||||||
</applicationListeners>
|
</applicationListeners>
|
||||||
|
|
||||||
<projectListeners>
|
<projectListeners>
|
||||||
@@ -171,9 +169,6 @@
|
|||||||
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.process.IjProcessGroup"
|
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.process.IjProcessGroup"
|
||||||
serviceInterface="com.maddyhome.idea.vim.api.VimProcessGroup"/>
|
serviceInterface="com.maddyhome.idea.vim.api.VimProcessGroup"/>
|
||||||
|
|
||||||
<applicationService serviceImplementation="com.maddyhome.idea.vim.autocmd.AutoCmdImpl"
|
|
||||||
serviceInterface="com.maddyhome.idea.vim.api.AutoCmdService"/>
|
|
||||||
|
|
||||||
<platform.rpc.projectRemoteTopicListener
|
<platform.rpc.projectRemoteTopicListener
|
||||||
implementation="com.maddyhome.idea.vim.group.JumpRemoteTopicListener"/>
|
implementation="com.maddyhome.idea.vim.group.JumpRemoteTopicListener"/>
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ dependencies {
|
|||||||
testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")
|
testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-core:3.4.3")
|
implementation("io.ktor:ktor-client-core:3.4.2")
|
||||||
implementation("io.ktor:ktor-client-cio:3.4.3")
|
implementation("io.ktor:ktor-client-cio:3.4.2")
|
||||||
implementation("io.ktor:ktor-client-content-negotiation:3.4.3")
|
implementation("io.ktor:ktor-client-content-negotiation:3.4.2")
|
||||||
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.3")
|
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.2")
|
||||||
implementation("io.ktor:ktor-client-auth:3.4.3")
|
implementation("io.ktor:ktor-client-auth:3.4.2")
|
||||||
implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
|
implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
|
||||||
|
|
||||||
// This is needed for jgit to connect to ssh
|
// This is needed for jgit to connect to ssh
|
||||||
|
|||||||
@@ -106,13 +106,9 @@ internal class IjVimPluginActivator : VimPluginActivator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use getServiceIfCreated to avoid creating the service during the dispose (this is prohibited by the platform)
|
// Use getServiceIfCreated to avoid creating the service during the dispose (this is prohibited by the platform)
|
||||||
val commandLineService = ApplicationManager.getApplication()
|
ApplicationManager.getApplication()
|
||||||
.getServiceIfCreated(com.maddyhome.idea.vim.api.VimCommandLineService::class.java)
|
.getServiceIfCreated(com.maddyhome.idea.vim.api.VimCommandLineService::class.java)
|
||||||
// VIM-4115: close() clears editor mode, KeyHandlerState.commandLineCommandBuilder, and the panel
|
?.fullReset()
|
||||||
// together. fullReset() alone only deactivates the panel; the KeyHandler singleton retains the
|
|
||||||
// stale CMD_LINE builder across disable/enable and NPEs on the next Esc.
|
|
||||||
commandLineService?.getActiveCommandLine()?.close(refocusOwningEditor = true, resetCaret = false)
|
|
||||||
commandLineService?.fullReset()
|
|
||||||
|
|
||||||
// Unregister vim actions in command mode
|
// Unregister vim actions in command mode
|
||||||
RegisterActions.unregisterActions()
|
RegisterActions.unregisterActions()
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ 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.*;
|
import com.maddyhome.idea.vim.group.KeyGroup;
|
||||||
import com.maddyhome.idea.vim.group.visual.VisualMotionGroup;
|
import com.maddyhome.idea.vim.group.VimNotifications;
|
||||||
|
import com.maddyhome.idea.vim.group.VimWindowGroup;
|
||||||
import com.maddyhome.idea.vim.history.VimHistory;
|
import com.maddyhome.idea.vim.history.VimHistory;
|
||||||
|
import com.maddyhome.idea.vim.macro.VimMacro;
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimInjectorKt;
|
import com.maddyhome.idea.vim.newapi.IjVimInjectorKt;
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimSearchGroup;
|
|
||||||
import com.maddyhome.idea.vim.newapi.VimLegacyStateLoader;
|
import com.maddyhome.idea.vim.newapi.VimLegacyStateLoader;
|
||||||
import com.maddyhome.idea.vim.newapi.VimSearchGroupLegacyLoader;
|
import com.maddyhome.idea.vim.newapi.VimSearchGroupLegacyLoader;
|
||||||
import com.maddyhome.idea.vim.put.VimPut;
|
import com.maddyhome.idea.vim.put.VimPut;
|
||||||
@@ -46,7 +47,7 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
* This is an application level plugin meaning that all open projects will share a common instance of the plugin.
|
* This is an application level plugin meaning that all open projects will share a common instance of the plugin.
|
||||||
* Registers and marks are shared across open projects so you can copy and paste between files of different projects.
|
* Registers and marks are shared across open projects so you can copy and paste between files of different projects.
|
||||||
*/
|
*/
|
||||||
@State(name = "VimSettings", storages = {@Storage("vim_settings.xml")})
|
@State(name = "VimSettings", storages = {@Storage("$APP_CONFIG$/vim_settings.xml")})
|
||||||
public class VimPlugin implements PersistentStateComponent<Element>, Disposable {
|
public class VimPlugin implements PersistentStateComponent<Element>, Disposable {
|
||||||
|
|
||||||
public static final int STATE_VERSION = 7;
|
public static final int STATE_VERSION = 7;
|
||||||
@@ -85,48 +86,49 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static @NotNull MotionGroup getMotion() {
|
public static @NotNull VimMotionGroup getMotion() {
|
||||||
return ((MotionGroup)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 CommandGroup getCommand() {
|
public static @NotNull VimCommandGroup getCommand() {
|
||||||
return ((CommandGroup)VimInjectorKt.getInjector().getCommandGroup());
|
return VimInjectorKt.getInjector().getCommandGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull RegisterGroup getRegister() {
|
public static @NotNull VimRegisterGroup getRegister() {
|
||||||
return ((RegisterGroup)VimInjectorKt.getInjector().getRegisterGroup());
|
return VimInjectorKt.getInjector().getRegisterGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimFile getFile() {
|
public static @NotNull VimFile getFile() {
|
||||||
return VimInjectorKt.getInjector().getFile();
|
return VimInjectorKt.getInjector().getFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull IjVimSearchGroup getSearch() {
|
public static @NotNull VimSearchGroup getSearch() {
|
||||||
return ((IjVimSearchGroup)VimInjectorKt.getInjector().getSearchGroup());
|
return VimInjectorKt.getInjector().getSearchGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @Nullable IjVimSearchGroup getSearchIfCreated() {
|
public static @Nullable VimSearchGroup getSearchIfCreated() {
|
||||||
return ApplicationManager.getApplication().getServiceIfCreated(IjVimSearchGroup.class);
|
VimSearchGroup searchGroup = ApplicationManager.getApplication().getServiceIfCreated(VimSearchGroup.class);
|
||||||
|
return searchGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimProcessGroup getProcess() {
|
public static @NotNull VimProcessGroup getProcess() {
|
||||||
return VimInjectorKt.getInjector().getProcessGroup();
|
return VimInjectorKt.getInjector().getProcessGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull MacroGroup getMacro() {
|
public static @NotNull VimMacro getMacro() {
|
||||||
return ((MacroGroup)VimInjectorKt.getInjector().getMacro());
|
return VimInjectorKt.getInjector().getMacro();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VimDigraphGroup getDigraph() {
|
public static @NotNull VimDigraphGroup getDigraph() {
|
||||||
return VimInjectorKt.getInjector().getDigraphGroup();
|
return VimInjectorKt.getInjector().getDigraphGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull HistoryGroup getHistory() {
|
public static @NotNull VimHistory getHistory() {
|
||||||
return ((HistoryGroup)VimInjectorKt.getInjector().getHistoryGroup());
|
return VimInjectorKt.getInjector().getHistoryGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull KeyGroup getKey() {
|
public static @NotNull KeyGroup getKey() {
|
||||||
@@ -137,20 +139,20 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
|
|||||||
return ApplicationManager.getApplication().getServiceIfCreated(KeyGroup.class);
|
return ApplicationManager.getApplication().getServiceIfCreated(KeyGroup.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull WindowGroup getWindow() {
|
public static @NotNull VimWindowGroup getWindow() {
|
||||||
return ((WindowGroup)VimInjectorKt.getInjector().getWindow());
|
return VimInjectorKt.getInjector().getWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull EditorGroup getEditor() {
|
public static @NotNull VimEditorGroup getEditor() {
|
||||||
return ((EditorGroup)VimInjectorKt.getInjector().getEditorGroup());
|
return VimInjectorKt.getInjector().getEditorGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @Nullable EditorGroup getEditorIfCreated() {
|
public static @Nullable VimEditorGroup getEditorIfCreated() {
|
||||||
return ApplicationManager.getApplication().getServiceIfCreated(EditorGroup.class);
|
return ApplicationManager.getApplication().getServiceIfCreated(VimEditorGroup.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull VisualMotionGroup getVisualMotion() {
|
public static @NotNull VimVisualMotionGroup getVisualMotion() {
|
||||||
return ((VisualMotionGroup)VimInjectorKt.getInjector().getVisualMotionGroup());
|
return VimInjectorKt.getInjector().getVisualMotionGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull YankGroupBase getYank() {
|
public static @NotNull YankGroupBase getYank() {
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +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.autocmd
|
|
||||||
|
|
||||||
data class AuCommand(val command: String, val group: String?, val pattern: AutoCmdPattern = AutoCmdPattern("*"))
|
|
||||||
@@ -1,64 +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.autocmd
|
|
||||||
|
|
||||||
import com.maddyhome.idea.vim.api.AutoCmdService
|
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
|
||||||
|
|
||||||
class AutoCmdImpl : AutoCmdService {
|
|
||||||
|
|
||||||
private val eventHandlers: MutableMap<AutoCmdEvent, MutableList<AuCommand>> = ConcurrentHashMap()
|
|
||||||
private var currentAugroup: String? = null
|
|
||||||
|
|
||||||
override fun registerEventCommand(command: String, event: AutoCmdEvent, pattern: String) {
|
|
||||||
eventHandlers.getOrPut(event.canonical) { CopyOnWriteArrayList() }
|
|
||||||
.add(AuCommand(command, currentAugroup, AutoCmdPattern(pattern)))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clearEvents() {
|
|
||||||
val group = currentAugroup
|
|
||||||
if (group != null) {
|
|
||||||
clearAugroup(group)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
eventHandlers.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun startAugroup(name: String) {
|
|
||||||
currentAugroup = name
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun endAugroup() {
|
|
||||||
currentAugroup = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clearAugroup(name: String) {
|
|
||||||
eventHandlers.values.forEach { handlers ->
|
|
||||||
handlers.removeAll { it.group == name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleEvent(event: AutoCmdEvent, filePath: String?, editor: VimEditor?) {
|
|
||||||
val resolvedEditor = editor ?: injector.editorGroup.getSelectedEditor() ?: return
|
|
||||||
val path = filePath ?: resolvedEditor.getPath()
|
|
||||||
eventHandlers[event.canonical]?.forEach { auCommand ->
|
|
||||||
if (auCommand.pattern.matches(path)) {
|
|
||||||
executeCommand(auCommand.command, resolvedEditor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun executeCommand(command: String, editor: VimEditor) {
|
|
||||||
val context = injector.executionContextManager.getEditorExecutionContext(editor)
|
|
||||||
injector.vimscriptExecutor.execute(command, editor, context, skipHistory = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +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.autocmd
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vim-style file pattern for autocmd matching.
|
|
||||||
*
|
|
||||||
* Supports glob patterns:
|
|
||||||
* - `*` matches any characters except path separators
|
|
||||||
* - `**` matches any characters including path separators
|
|
||||||
* - `?` matches a single non-separator character
|
|
||||||
* - `[abc]` matches any character in the set
|
|
||||||
* - `{foo,bar}` matches "foo" or "bar"
|
|
||||||
*
|
|
||||||
* If the pattern contains `/`, it matches against the full path.
|
|
||||||
* Otherwise, it matches against only the file name.
|
|
||||||
*/
|
|
||||||
class AutoCmdPattern(val pattern: String) {
|
|
||||||
|
|
||||||
private val matchesAll = pattern == "*"
|
|
||||||
private val matchesFullPath = '/' in pattern || '\\' in pattern
|
|
||||||
private val regex: Regex by lazy { toRegex(pattern) }
|
|
||||||
|
|
||||||
fun matches(filePath: String?): Boolean {
|
|
||||||
if (matchesAll) return true
|
|
||||||
if (filePath == null) return false
|
|
||||||
|
|
||||||
val target = if (matchesFullPath) filePath else fileName(filePath)
|
|
||||||
return regex.matches(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fileName(path: String): String {
|
|
||||||
return path.substringAfterLast('/').substringAfterLast('\\')
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val REGEX_SPECIAL = "\\+^$|()"
|
|
||||||
|
|
||||||
private fun toRegex(pattern: String): Regex {
|
|
||||||
val result = StringBuilder("^")
|
|
||||||
var i = 0
|
|
||||||
var inGroup = false
|
|
||||||
|
|
||||||
while (i < pattern.length) {
|
|
||||||
when (val ch = pattern[i]) {
|
|
||||||
'*' -> if (isDoubleStar(pattern, i)) {
|
|
||||||
result.append(".*")
|
|
||||||
i++
|
|
||||||
} else {
|
|
||||||
result.append("[^/\\\\]*")
|
|
||||||
}
|
|
||||||
|
|
||||||
'?' -> result.append("[^/\\\\]")
|
|
||||||
'.' -> result.append("\\.")
|
|
||||||
'{' -> {
|
|
||||||
result.append("(?:"); inGroup = true
|
|
||||||
}
|
|
||||||
|
|
||||||
'}' -> {
|
|
||||||
result.append(")"); inGroup = false
|
|
||||||
}
|
|
||||||
|
|
||||||
',' -> if (inGroup) result.append("|") else result.append(",")
|
|
||||||
'[' -> result.append("[")
|
|
||||||
']' -> result.append("]")
|
|
||||||
in REGEX_SPECIAL -> {
|
|
||||||
result.append("\\"); result.append(ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> result.append(ch)
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
result.append("$")
|
|
||||||
return Regex(result.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isDoubleStar(pattern: String, i: Int): Boolean {
|
|
||||||
return i + 1 < pattern.length && pattern[i + 1] == '*'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +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.autocmd
|
|
||||||
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps IntelliJ's [com.intellij.openapi.fileTypes.FileType] name to a Vim-style filetype string
|
|
||||||
* suitable for matching against a `FileType` autocmd pattern.
|
|
||||||
*
|
|
||||||
* Most Vim filetypes are just the lowercase form of the IntelliJ name (e.g. `JAVA` → `java`,
|
|
||||||
* `Python` → `python`). A small override table covers the common cases where the conventional
|
|
||||||
* Vim name differs from IntelliJ's, so users can write `autocmd FileType python ...` and have
|
|
||||||
* it work out of the box.
|
|
||||||
*/
|
|
||||||
object IjFileTypeMapping {
|
|
||||||
|
|
||||||
private val overrides: Map<String, String> = mapOf(
|
|
||||||
"PLAIN_TEXT" to "text",
|
|
||||||
"C++" to "cpp",
|
|
||||||
"C#" to "cs",
|
|
||||||
"ObjectiveC" to "objc",
|
|
||||||
"Shell Script" to "sh",
|
|
||||||
"JavaScript" to "javascript",
|
|
||||||
"TypeScript" to "typescript",
|
|
||||||
"Vue.js" to "vue",
|
|
||||||
"Handlebars/Mustache" to "handlebars",
|
|
||||||
"CMakeLists.txt" to "cmake",
|
|
||||||
)
|
|
||||||
|
|
||||||
fun toVimFileType(virtualFile: VirtualFile?): String? {
|
|
||||||
val name = virtualFile?.fileType?.name ?: return null
|
|
||||||
return overrides[name] ?: name.lowercase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2023 The IdeaVim authors
|
* Copyright 2003-2026 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* 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
|
||||||
@@ -7,78 +7,23 @@
|
|||||||
*/
|
*/
|
||||||
package com.maddyhome.idea.vim.extension.argtextobj
|
package com.maddyhome.idea.vim.extension.argtextobj
|
||||||
|
|
||||||
import com.intellij.openapi.editor.Document
|
import com.intellij.vim.api.VimApi
|
||||||
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
|
import com.intellij.vim.api.VimInitApi
|
||||||
|
import com.intellij.vim.api.scopes.TextObjectRange
|
||||||
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.ImmutableVimCaret
|
|
||||||
import com.maddyhome.idea.vim.api.VimCaret
|
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.command.MappingMode
|
|
||||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
|
||||||
import com.maddyhome.idea.vim.command.TextObjectVisualType
|
|
||||||
import com.maddyhome.idea.vim.common.TextRange
|
|
||||||
import com.maddyhome.idea.vim.extension.ExtensionHandler
|
|
||||||
import com.maddyhome.idea.vim.extension.VimExtension
|
import com.maddyhome.idea.vim.extension.VimExtension
|
||||||
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping
|
|
||||||
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing
|
|
||||||
import com.maddyhome.idea.vim.group.visual.vimSetSelection
|
|
||||||
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
|
|
||||||
import com.maddyhome.idea.vim.helper.MessageHelper
|
import com.maddyhome.idea.vim.helper.MessageHelper
|
||||||
import com.maddyhome.idea.vim.helper.VimNlsSafe
|
import com.maddyhome.idea.vim.helper.VimNlsSafe
|
||||||
import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset
|
|
||||||
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
|
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode.OP_PENDING
|
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode.VISUAL
|
|
||||||
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
|
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
|
||||||
import org.jetbrains.annotations.Nls
|
import org.jetbrains.annotations.Nls
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Consumer
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class VimArgTextObjExtension : VimExtension {
|
/**
|
||||||
override fun getName(): String = "argtextobj"
|
|
||||||
|
|
||||||
override fun init() {
|
|
||||||
putExtensionHandlerMapping(
|
|
||||||
MappingMode.XO,
|
|
||||||
injector.parser.parseKeys("<Plug>InnerArgument"),
|
|
||||||
owner,
|
|
||||||
ArgumentHandler(true),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
putExtensionHandlerMapping(
|
|
||||||
MappingMode.XO,
|
|
||||||
injector.parser.parseKeys("<Plug>OuterArgument"),
|
|
||||||
owner,
|
|
||||||
ArgumentHandler(false),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
putKeyMappingIfMissing(
|
|
||||||
MappingMode.XO,
|
|
||||||
injector.parser.parseKeys("ia"),
|
|
||||||
owner,
|
|
||||||
injector.parser.parseKeys("<Plug>InnerArgument"),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
putKeyMappingIfMissing(
|
|
||||||
MappingMode.XO,
|
|
||||||
injector.parser.parseKeys("aa"),
|
|
||||||
owner,
|
|
||||||
injector.parser.parseKeys("<Plug>OuterArgument"),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pairs of brackets that delimit different types of argument lists.
|
* The pairs of brackets that delimit different types of argument lists.
|
||||||
*/
|
*/
|
||||||
private class BracketPairs(openBrackets: String, closeBrackets: String) {
|
private class BracketPairs(openBrackets: String, closeBrackets: String) {
|
||||||
// NOTE: brackets must match by the position, and ordered by rank (highest to lowest).
|
// NOTE: brackets must match by the position, and ordered by rank (highest to lowest).
|
||||||
private val openBrackets: String
|
private val openBrackets: String
|
||||||
private val closeBrackets: String
|
private val closeBrackets: String
|
||||||
@@ -169,94 +114,32 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
return BracketPairs(openBrackets.toString(), closeBrackets.toString())
|
return BracketPairs(openBrackets.toString(), closeBrackets.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
class VimArgTextObjExtension : VimExtension {
|
||||||
* A text object for an argument to a function definition or a call.
|
override fun getName(): String = "argtextobj"
|
||||||
*/
|
|
||||||
internal class ArgumentHandler(val isInner: Boolean) : ExtensionHandler {
|
|
||||||
override val isRepeatable: Boolean
|
|
||||||
get() = false
|
|
||||||
|
|
||||||
internal class ArgumentTextObjectHandler(private val isInner: Boolean) : TextObjectActionHandler() {
|
override fun init(initApi: VimInitApi) {
|
||||||
override fun getRange(
|
initApi.textObjects {
|
||||||
editor: VimEditor,
|
register("ia", preserveSelectionAnchor = false) { count ->
|
||||||
caret: ImmutableVimCaret,
|
findArgumentRange(isInner = true, count)
|
||||||
context: ExecutionContext,
|
}
|
||||||
count: Int,
|
register("aa", preserveSelectionAnchor = false) { count ->
|
||||||
rawCount: Int
|
findArgumentRange(isInner = false, count)
|
||||||
): TextRange? {
|
|
||||||
var bracketPairs: BracketPairs = Util.DEFAULT_BRACKET_PAIRS
|
|
||||||
val bracketPairsVar: String? = Util.bracketPairsVariable()
|
|
||||||
if (bracketPairsVar != null) {
|
|
||||||
try {
|
|
||||||
bracketPairs = BracketPairs.fromBracketPairList(bracketPairsVar)
|
|
||||||
} catch (parseException: BracketPairs.ParseException) {
|
|
||||||
@VimNlsSafe val message =
|
|
||||||
MessageHelper.message("argtextobj.error.invalid.value.of.g.argtextobj.pairs.0", parseException.message!!)
|
|
||||||
VimPlugin.showMessage(message)
|
|
||||||
VimPlugin.indicateError()
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val finder = ArgBoundsFinder((editor as IjVimEditor).editor.document, bracketPairs)
|
}
|
||||||
var pos = (caret as IjVimCaret).caret.offset
|
}
|
||||||
|
|
||||||
for (i in 0..<count) {
|
/**
|
||||||
if (!finder.findBoundsAt(pos)) {
|
|
||||||
VimPlugin.showMessage(finder.errorMessage())
|
|
||||||
VimPlugin.indicateError()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (i + 1 < count) {
|
|
||||||
finder.extendTillNext()
|
|
||||||
}
|
|
||||||
|
|
||||||
pos = finder.rightBound
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInner) {
|
|
||||||
finder.adjustForInner()
|
|
||||||
} else {
|
|
||||||
finder.adjustForOuter()
|
|
||||||
}
|
|
||||||
return TextRange(finder.leftBound, finder.rightBound)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val visualType: TextObjectVisualType
|
|
||||||
get() = TextObjectVisualType.CHARACTER_WISE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
|
||||||
val keyHandlerState = getInstance().keyHandlerState
|
|
||||||
|
|
||||||
val textObjectHandler = ArgumentTextObjectHandler(isInner)
|
|
||||||
if (editor.mode !is OP_PENDING) {
|
|
||||||
val count0 = operatorArguments.count0
|
|
||||||
editor.nativeCarets().forEach(Consumer { caret: VimCaret ->
|
|
||||||
val range = textObjectHandler.getRange(editor, caret, context, max(1, count0), count0)
|
|
||||||
if (range != null) {
|
|
||||||
SelectionVimListenerSuppressor.lock().use { _ ->
|
|
||||||
if (editor.mode is VISUAL) {
|
|
||||||
caret.vimSetSelection(range.startOffset, range.endOffset - 1, true)
|
|
||||||
} else {
|
|
||||||
(caret as IjVimCaret).caret.moveToInlayAwareOffset(range.startOffset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
keyHandlerState.commandBuilder.addAction(textObjectHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class to find argument boundaries starting at the specified
|
* Helper class to find argument boundaries starting at the specified
|
||||||
* position
|
* position
|
||||||
*/
|
*/
|
||||||
private class ArgBoundsFinder(private val document: Document, private val brackets: BracketPairs) {
|
private class ArgBoundsFinder(
|
||||||
private val text: CharSequence = document.immutableCharSequence
|
private val text: CharSequence,
|
||||||
|
private val api: VimApi,
|
||||||
|
private val brackets: BracketPairs,
|
||||||
|
) {
|
||||||
var leftBound: Int = Int.MAX_VALUE
|
var leftBound: Int = Int.MAX_VALUE
|
||||||
private set
|
private set
|
||||||
var rightBound: Int = Int.MIN_VALUE
|
var rightBound: Int = Int.MIN_VALUE
|
||||||
@@ -267,6 +150,12 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
@Nls
|
@Nls
|
||||||
private var error: @Nls String? = null
|
private var error: @Nls String? = null
|
||||||
|
|
||||||
|
// Line info methods that call VimApi directly
|
||||||
|
private suspend fun getLineNumber(offset: Int) = api.editor { read { getLine(offset).number } }
|
||||||
|
private suspend fun getLineStartOffset(lineNo: Int) = api.editor { read { getLineStartOffset(lineNo) } }
|
||||||
|
private suspend fun getLineEndOffset(lineNo: Int) = api.editor { read { getLineEndOffset(lineNo, true) } }
|
||||||
|
private suspend fun getLineCount() = api.editor { read { lineCount } }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds left and right boundaries of an argument at the specified
|
* Finds left and right boundaries of an argument at the specified
|
||||||
* position. If successful @ref getLeftBound() will point to the left
|
* position. If successful @ref getLeftBound() will point to the left
|
||||||
@@ -277,14 +166,14 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
* @param position starting position.
|
* @param position starting position.
|
||||||
*/
|
*/
|
||||||
@Throws(IllegalStateException::class)
|
@Throws(IllegalStateException::class)
|
||||||
fun findBoundsAt(position: Int): Boolean {
|
suspend fun findBoundsAt(position: Int): Boolean {
|
||||||
if (text.isEmpty()) {
|
if (text.isEmpty()) {
|
||||||
error = "empty document"
|
error = "empty document"
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
leftBound = min(position, leftBound)
|
leftBound = min(position, leftBound)
|
||||||
rightBound = max(position, rightBound)
|
rightBound = max(position, rightBound)
|
||||||
this.outOfQuotedText
|
this.adjustForQuotedText()
|
||||||
if (rightBound == leftBound) {
|
if (rightBound == leftBound) {
|
||||||
if (brackets.isCloseBracket(getCharAt(rightBound).code)) {
|
if (brackets.isCloseBracket(getCharAt(rightBound).code)) {
|
||||||
--leftBound
|
--leftBound
|
||||||
@@ -375,18 +264,17 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
return (idEnd - i) > 0 && Character.isJavaIdentifierStart(getCharAt(i + 1))
|
return (idEnd - i) > 0 && Character.isJavaIdentifierStart(getCharAt(i + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
val outOfQuotedText: Unit
|
|
||||||
/**
|
/**
|
||||||
* Detects if current position is inside a quoted string and adjusts
|
* Detects if current position is inside a quoted string and adjusts
|
||||||
* left and right bounds to the boundaries of the string.
|
* left and right bounds to the boundaries of the string.
|
||||||
*
|
*
|
||||||
* NOTE: Does not support line continuations for quoted string ('\' at the end of line).
|
* NOTE: Does not support line continuations for quoted string ('\' at the end of line).
|
||||||
*/
|
*/
|
||||||
get() {
|
suspend fun adjustForQuotedText() {
|
||||||
// TODO this method should use IdeaVim methods to determine if the current position is in the string
|
// TODO this method should use IdeaVim methods to determine if the current position is in the string
|
||||||
val lineNo = document.getLineNumber(leftBound)
|
val lineNo = getLineNumber(leftBound)
|
||||||
val lineStartOffset = document.getLineStartOffset(lineNo)
|
val lineStartOffset = getLineStartOffset(lineNo)
|
||||||
val lineEndOffset = document.getLineEndOffset(lineNo)
|
val lineEndOffset = getLineEndOffset(lineNo)
|
||||||
var i = lineStartOffset
|
var i = lineStartOffset
|
||||||
while (i <= leftBound) {
|
while (i <= leftBound) {
|
||||||
if (isQuote(i)) {
|
if (isQuote(i)) {
|
||||||
@@ -403,7 +291,7 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findRightBound() {
|
private fun findRightBound() {
|
||||||
while (rightBound < rightBracket) {
|
while (rightBound < rightBracket) {
|
||||||
val ch = getCharAt(rightBound)
|
val ch = getCharAt(rightBound)
|
||||||
if (ch == ',') {
|
if (ch == ',') {
|
||||||
@@ -420,7 +308,7 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findLeftBound() {
|
private fun findLeftBound() {
|
||||||
while (leftBound > leftBracket) {
|
while (leftBound > leftBracket) {
|
||||||
val ch = getCharAt(leftBound)
|
val ch = getCharAt(leftBound)
|
||||||
if (ch == ',') {
|
if (ch == ',') {
|
||||||
@@ -437,17 +325,19 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isQuote(i: Int): Boolean {
|
private fun isQuote(i: Int): Boolean {
|
||||||
return QUOTES.indexOf(getCharAt(i)) != -1
|
return QUOTES.indexOf(getCharAt(i)) != -1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCharAt(logicalOffset: Int): Char {
|
private fun getCharAt(logicalOffset: Int): Char {
|
||||||
assert(logicalOffset < text.length)
|
require(logicalOffset >= 0 && logicalOffset < text.length) {
|
||||||
|
"Offset $logicalOffset out of bounds [0, ${text.length})"
|
||||||
|
}
|
||||||
return text[logicalOffset]
|
return text[logicalOffset]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun skipQuotedTextForward(start: Int, end: Int): Int {
|
private fun skipQuotedTextForward(start: Int, end: Int): Int {
|
||||||
assert(start < end)
|
require(start < end) { "start ($start) must be less than end ($end)" }
|
||||||
val quoteChar = getCharAt(start)
|
val quoteChar = getCharAt(start)
|
||||||
var backSlash = false
|
var backSlash = false
|
||||||
var i = start + 1
|
var i = start + 1
|
||||||
@@ -465,8 +355,8 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
fun skipQuotedTextBackward(start: Int, end: Int): Int {
|
private fun skipQuotedTextBackward(start: Int, end: Int): Int {
|
||||||
assert(start > end)
|
require(start > end) { "start ($start) must be greater than end ($end)" }
|
||||||
val quoteChar = getCharAt(start)
|
val quoteChar = getCharAt(start)
|
||||||
var i = start - 1
|
var i = start - 1
|
||||||
|
|
||||||
@@ -484,17 +374,17 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
fun leftLimit(pos: Int): Int {
|
private suspend fun leftLimit(pos: Int): Int {
|
||||||
val offsetLimit = max(pos - MAX_SEARCH_OFFSET, 0)
|
val offsetLimit = max(pos - MAX_SEARCH_OFFSET, 0)
|
||||||
val lineNo = document.getLineNumber(pos)
|
val lineNo = getLineNumber(pos)
|
||||||
val lineOffsetLimit = document.getLineStartOffset(max(0, lineNo - MAX_SEARCH_LINES))
|
val lineOffsetLimit = getLineStartOffset(max(0, lineNo - MAX_SEARCH_LINES))
|
||||||
return max(offsetLimit, lineOffsetLimit)
|
return max(offsetLimit, lineOffsetLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rightLimit(pos: Int): Int {
|
private suspend fun rightLimit(pos: Int): Int {
|
||||||
val offsetLimit = min(pos + MAX_SEARCH_OFFSET, text.length)
|
val offsetLimit = min(pos + MAX_SEARCH_OFFSET, text.length)
|
||||||
val lineNo = document.getLineNumber(pos)
|
val lineNo = getLineNumber(pos)
|
||||||
val lineOffsetLimit = document.getLineEndOffset(min(document.lineCount - 1, lineNo + MAX_SEARCH_LINES))
|
val lineOffsetLimit = getLineEndOffset(min(getLineCount() - 1, lineNo + MAX_SEARCH_LINES))
|
||||||
return min(offsetLimit, lineOffsetLimit)
|
return min(offsetLimit, lineOffsetLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,7 +456,7 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
* @return position after the S-expression, or the next to the start position if
|
* @return position after the S-expression, or the next to the start position if
|
||||||
* unbalanced.
|
* unbalanced.
|
||||||
*/
|
*/
|
||||||
fun skipSexp(start: Int, end: Int, dir: SexpDirection): Int {
|
private fun skipSexp(start: Int, end: Int, dir: SexpDirection): Int {
|
||||||
val lastChar = getCharAt(start)
|
val lastChar = getCharAt(start)
|
||||||
assert(dir.isOpenBracket(lastChar))
|
assert(dir.isOpenBracket(lastChar))
|
||||||
val bracketStack: Deque<Char?> = ArrayDeque<Char?>()
|
val bracketStack: Deque<Char?> = ArrayDeque<Char?>()
|
||||||
@@ -613,8 +503,10 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
* @param end maximum position
|
* @param end maximum position
|
||||||
* @return true if found
|
* @return true if found
|
||||||
*/
|
*/
|
||||||
fun findOuterBrackets(start: Int, end: Int): Boolean {
|
private fun findOuterBrackets(start: Int, end: Int): Boolean {
|
||||||
var hasNewBracket = findPrevOpenBracket(start) && findNextCloseBracket(end)
|
val foundPrev = findPrevOpenBracket(start)
|
||||||
|
val foundNext = if (foundPrev) findNextCloseBracket(end) else false
|
||||||
|
var hasNewBracket = foundPrev && foundNext
|
||||||
while (hasNewBracket) {
|
while (hasNewBracket) {
|
||||||
val leftPrio = brackets.getBracketPrio(getCharAt(leftBracket))
|
val leftPrio = brackets.getBracketPrio(getCharAt(leftBracket))
|
||||||
val rightPrio = brackets.getBracketPrio(getCharAt(rightBracket))
|
val rightPrio = brackets.getBracketPrio(getCharAt(rightBracket))
|
||||||
@@ -648,7 +540,7 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
* @param start minimum position.
|
* @param start minimum position.
|
||||||
* @return true if found
|
* @return true if found
|
||||||
*/
|
*/
|
||||||
fun findPrevOpenBracket(start: Int): Boolean {
|
private fun findPrevOpenBracket(start: Int): Boolean {
|
||||||
var ch: Char
|
var ch: Char
|
||||||
while (!brackets.isOpenBracket(getCharAt(leftBracket).also { ch = it }.code)) {
|
while (!brackets.isOpenBracket(getCharAt(leftBracket).also { ch = it }.code)) {
|
||||||
if (brackets.isCloseBracket(ch.code)) {
|
if (brackets.isCloseBracket(ch.code)) {
|
||||||
@@ -673,7 +565,7 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
* @param end maximum position.
|
* @param end maximum position.
|
||||||
* @return true if found
|
* @return true if found
|
||||||
*/
|
*/
|
||||||
fun findNextCloseBracket(end: Int): Boolean {
|
private fun findNextCloseBracket(end: Int): Boolean {
|
||||||
var ch: Char
|
var ch: Char
|
||||||
while (!brackets.isCloseBracket(getCharAt(rightBracket).also { ch = it }.code)) {
|
while (!brackets.isCloseBracket(getCharAt(rightBracket).also { ch = it }.code)) {
|
||||||
if (brackets.isOpenBracket(ch.code)) {
|
if (brackets.isOpenBracket(ch.code)) {
|
||||||
@@ -701,9 +593,9 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
return QUOTES.indexOf(ch.toChar()) != -1
|
return QUOTES.indexOf(ch.toChar()) != -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object Util {
|
private object ArgTextObjUtil {
|
||||||
val DEFAULT_BRACKET_PAIRS = BracketPairs("(", ")")
|
val DEFAULT_BRACKET_PAIRS = BracketPairs("(", ")")
|
||||||
|
|
||||||
fun bracketPairsVariable(): String? {
|
fun bracketPairsVariable(): String? {
|
||||||
@@ -713,5 +605,47 @@ class VimArgTextObjExtension : VimExtension {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find argument range using the new VimApi.
|
||||||
|
*/
|
||||||
|
private suspend fun VimApi.findArgumentRange(isInner: Boolean, count: Int): TextObjectRange? {
|
||||||
|
var bracketPairs: BracketPairs = ArgTextObjUtil.DEFAULT_BRACKET_PAIRS
|
||||||
|
val bracketPairsVar: String? = ArgTextObjUtil.bracketPairsVariable()
|
||||||
|
if (bracketPairsVar != null) {
|
||||||
|
try {
|
||||||
|
bracketPairs = BracketPairs.fromBracketPairList(bracketPairsVar)
|
||||||
|
} catch (parseException: BracketPairs.ParseException) {
|
||||||
|
@VimNlsSafe val message =
|
||||||
|
MessageHelper.message("argtextobj.error.invalid.value.of.g.argtextobj.pairs.0", parseException.message ?: "")
|
||||||
|
VimPlugin.showMessage(message)
|
||||||
|
VimPlugin.indicateError()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val (text, caretOffset) = editor { read { text to withPrimaryCaret { offset } } }
|
||||||
|
val finder = ArgBoundsFinder(text, this, bracketPairs)
|
||||||
|
var pos = caretOffset
|
||||||
|
|
||||||
|
for (i in 0..<count) {
|
||||||
|
if (!finder.findBoundsAt(pos)) {
|
||||||
|
VimPlugin.showMessage(finder.errorMessage())
|
||||||
|
VimPlugin.indicateError()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (i + 1 < count) {
|
||||||
|
finder.extendTillNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = finder.rightBound
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInner) {
|
||||||
|
finder.adjustForInner()
|
||||||
|
} else {
|
||||||
|
finder.adjustForOuter()
|
||||||
|
}
|
||||||
|
return TextObjectRange.CharacterWise(finder.leftBound, finder.rightBound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -54,13 +54,10 @@ fun MutableMap<List<KeyStroke>, NerdTreeAction>.register(mapping: String, action
|
|||||||
*/
|
*/
|
||||||
val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List<KeyStroke>, NerdTreeAction>().apply {
|
val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List<KeyStroke>, NerdTreeAction>().apply {
|
||||||
// TODO support going [count] lines upward/downward or to line [count]
|
// TODO support going [count] lines upward/downward or to line [count]
|
||||||
// Delegate to JTree's Swing ActionMap (same path as native arrow keys via TreeAction/DefaultTreeUI).
|
register("k", NerdTreeAction.ij("Tree-selectPrevious"))
|
||||||
// This avoids ActionManager.tryToExecute which can RPC to backend in split mode,
|
register("j", NerdTreeAction.ij("Tree-selectNext"))
|
||||||
// while preserving platform features (separator skipping, cycle scrolling, loading node handling).
|
register("G", NerdTreeAction.ij("Tree-selectLast"))
|
||||||
register("k", NerdTreeAction.swing("selectPrevious"))
|
register("gg", NerdTreeAction.ij("Tree-selectFirst"))
|
||||||
register("j", NerdTreeAction.swing("selectNext"))
|
|
||||||
register("G", NerdTreeAction.swing("selectLast"))
|
|
||||||
register("gg", NerdTreeAction.swing("selectFirst"))
|
|
||||||
|
|
||||||
// FIXME lazy loaded tree nodes are not expanded
|
// FIXME lazy loaded tree nodes are not expanded
|
||||||
register("NERDTreeMapOpenRecursively", "O", NerdTreeAction.ij("FullyExpandTreeNode"))
|
register("NERDTreeMapOpenRecursively", "O", NerdTreeAction.ij("FullyExpandTreeNode"))
|
||||||
@@ -105,7 +102,7 @@ val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List
|
|||||||
tree.selectionPath = path
|
tree.selectionPath = path
|
||||||
tree.scrollPathToVisible(path)
|
tree.scrollPathToVisible(path)
|
||||||
})
|
})
|
||||||
register("NERDTreeMapJumpParent", "p", NerdTreeAction.swing("selectParentNoCollapse"))
|
register("NERDTreeMapJumpParent", "p", NerdTreeAction.ij("Tree-selectParentNoCollapse"))
|
||||||
register(
|
register(
|
||||||
"NERDTreeMapJumpFirstChild",
|
"NERDTreeMapJumpFirstChild",
|
||||||
"K",
|
"K",
|
||||||
@@ -132,8 +129,8 @@ val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List
|
|||||||
tree.scrollPathToVisible(path)
|
tree.scrollPathToVisible(path)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
register("NERDTreeMapJumpNextSibling", "<C-J>", NerdTreeAction.swing("selectNextSibling"))
|
register("NERDTreeMapJumpNextSibling", "<C-J>", NerdTreeAction.ij("Tree-selectNextSibling"))
|
||||||
register("NERDTreeMapJumpPrevSibling", "<C-K>", NerdTreeAction.swing("selectPreviousSibling"))
|
register("NERDTreeMapJumpPrevSibling", "<C-K>", NerdTreeAction.ij("Tree-selectPreviousSibling"))
|
||||||
|
|
||||||
register("/", NerdTreeAction.ij("SpeedSearch"))
|
register("/", NerdTreeAction.ij("SpeedSearch"))
|
||||||
register("<ESC>", NerdTreeAction { _, _ -> })
|
register("<ESC>", NerdTreeAction { _, _ -> })
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -11,7 +11,6 @@ package com.maddyhome.idea.vim.extension.nerdtree
|
|||||||
import com.intellij.openapi.actionSystem.ActionManager
|
import com.intellij.openapi.actionSystem.ActionManager
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.ui.SwingActionDelegate
|
|
||||||
import com.intellij.ui.treeStructure.Tree
|
import com.intellij.ui.treeStructure.Tree
|
||||||
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
|
||||||
@@ -48,11 +47,5 @@ class NerdTreeAction(val action: (AnActionEvent, Tree) -> Unit) {
|
|||||||
* @return An [NerdTreeAction] that runs the specified action when triggered.
|
* @return An [NerdTreeAction] that runs the specified action when triggered.
|
||||||
*/
|
*/
|
||||||
fun ij(id: String) = NerdTreeAction { event, _ -> callAction(null, id, event.dataContext.vim) }
|
fun ij(id: String) = NerdTreeAction { event, _ -> callAction(null, id, event.dataContext.vim) }
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an [NerdTreeAction] that delegates to the JTree's Swing ActionMap.
|
|
||||||
*/
|
|
||||||
fun swing(swingActionId: String) =
|
|
||||||
NerdTreeAction { _, tree -> SwingActionDelegate.performAction(swingActionId, tree) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
package com.maddyhome.idea.vim.extension.replacewithregister
|
package com.maddyhome.idea.vim.extension.replacewithregister
|
||||||
|
|
||||||
import com.intellij.vim.api.VimInitApi
|
import com.intellij.vim.api.VimInitApi
|
||||||
import com.intellij.vim.api.scopes.nmapPluginAction
|
|
||||||
import com.intellij.vim.api.scopes.vmapPluginAction
|
|
||||||
import com.maddyhome.idea.vim.extension.VimExtension
|
import com.maddyhome.idea.vim.extension.VimExtension
|
||||||
|
|
||||||
internal class ReplaceWithRegister : VimExtension {
|
internal class ReplaceWithRegister : VimExtension {
|
||||||
@@ -19,15 +17,21 @@ internal class ReplaceWithRegister : VimExtension {
|
|||||||
|
|
||||||
override fun init(initApi: VimInitApi) {
|
override fun init(initApi: VimInitApi) {
|
||||||
initApi.mappings {
|
initApi.mappings {
|
||||||
nmapPluginAction("gr", RWR_OPERATOR, keepDefaultMapping = true) {
|
// Step 1: Non-recursive <Plug> → action mappings
|
||||||
|
nnoremap(RWR_OPERATOR) {
|
||||||
rewriteMotion()
|
rewriteMotion()
|
||||||
}
|
}
|
||||||
nmapPluginAction("grr", RWR_LINE, keepDefaultMapping = true) {
|
nnoremap(RWR_LINE) {
|
||||||
rewriteLine()
|
rewriteLine()
|
||||||
}
|
}
|
||||||
vmapPluginAction("gr", RWR_VISUAL, keepDefaultMapping = true) {
|
vnoremap(RWR_VISUAL) {
|
||||||
rewriteVisual()
|
rewriteVisual()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 2: Recursive key → <Plug> mappings
|
||||||
|
nmap("gr", RWR_OPERATOR)
|
||||||
|
nmap("grr", RWR_LINE)
|
||||||
|
vmap("gr", RWR_VISUAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
initApi.commands {
|
initApi.commands {
|
||||||
|
|||||||
@@ -16,23 +16,27 @@ import com.intellij.vim.api.models.Mode
|
|||||||
import com.intellij.vim.api.models.Range
|
import com.intellij.vim.api.models.Range
|
||||||
import com.intellij.vim.api.models.TextType
|
import com.intellij.vim.api.models.TextType
|
||||||
import com.intellij.vim.api.scopes.editor.caret.CaretTransaction
|
import com.intellij.vim.api.scopes.editor.caret.CaretTransaction
|
||||||
import com.intellij.vim.api.scopes.nmapPluginAction
|
|
||||||
import com.intellij.vim.api.scopes.vmapPluginAction
|
|
||||||
|
|
||||||
private const val PLUGIN_NAME: String = "ReplaceWithRegisterNew"
|
private const val PLUGIN_NAME: String = "ReplaceWithRegisterNew"
|
||||||
|
|
||||||
@VimPlugin(name = PLUGIN_NAME)
|
@VimPlugin(name = PLUGIN_NAME)
|
||||||
fun VimInitApi.init() {
|
fun VimInitApi.init() {
|
||||||
mappings {
|
mappings {
|
||||||
nmapPluginAction("gr", RWR_OPERATOR, keepDefaultMapping = true) {
|
// Step 1: Non-recursive <Plug> → action mappings
|
||||||
|
nnoremap(RWR_OPERATOR) {
|
||||||
rewriteMotion()
|
rewriteMotion()
|
||||||
}
|
}
|
||||||
nmapPluginAction("grr", RWR_LINE, keepDefaultMapping = true) {
|
nnoremap(RWR_LINE) {
|
||||||
rewriteLine()
|
rewriteLine()
|
||||||
}
|
}
|
||||||
vmapPluginAction("gr", RWR_VISUAL, keepDefaultMapping = true) {
|
vnoremap(RWR_VISUAL) {
|
||||||
rewriteVisual()
|
rewriteVisual()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 2: Recursive key → <Plug> mappings
|
||||||
|
nmap("gr", RWR_OPERATOR)
|
||||||
|
nmap("grr", RWR_LINE)
|
||||||
|
vmap("gr", RWR_VISUAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
commands {
|
commands {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2023 The IdeaVim authors
|
* Copyright 2003-2026 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* 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
|
||||||
@@ -7,36 +7,15 @@
|
|||||||
*/
|
*/
|
||||||
package com.maddyhome.idea.vim.extension.textobjindent
|
package com.maddyhome.idea.vim.extension.textobjindent
|
||||||
|
|
||||||
import com.intellij.openapi.editor.Caret
|
import com.intellij.vim.api.VimApi
|
||||||
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
|
import com.intellij.vim.api.VimInitApi
|
||||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
import com.intellij.vim.api.scopes.TextObjectRange
|
||||||
import com.maddyhome.idea.vim.api.ImmutableVimCaret
|
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.command.MappingMode
|
|
||||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
|
||||||
import com.maddyhome.idea.vim.command.TextObjectVisualType
|
|
||||||
import com.maddyhome.idea.vim.common.TextRange
|
|
||||||
import com.maddyhome.idea.vim.extension.ExtensionHandler
|
|
||||||
import com.maddyhome.idea.vim.extension.VimExtension
|
import com.maddyhome.idea.vim.extension.VimExtension
|
||||||
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping
|
|
||||||
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping
|
|
||||||
import com.maddyhome.idea.vim.group.visual.vimSetSelection
|
|
||||||
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
|
|
||||||
import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset
|
|
||||||
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
|
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode.OP_PENDING
|
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode.VISUAL
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Port of vim-indent-object:
|
* Port of vim-indent-object:
|
||||||
* [vim-indent-object](https://github.com/michaeljsmith/vim-indent-object)
|
* [vim-indent-object](https://github.com/michaeljsmith/vim-indent-object)
|
||||||
*
|
*
|
||||||
*
|
|
||||||
*
|
|
||||||
* vim-indent-object provides these text objects based on the cursor line's indentation:
|
* vim-indent-object provides these text objects based on the cursor line's indentation:
|
||||||
*
|
*
|
||||||
* * `ai` **A**n **I**ndentation level and line above.
|
* * `ai` **A**n **I**ndentation level and line above.
|
||||||
@@ -44,73 +23,30 @@ import kotlin.math.max
|
|||||||
* * `aI` **A**n **I**ndentation level and lines above and below.
|
* * `aI` **A**n **I**ndentation level and lines above and below.
|
||||||
* * `iI` **I**nner **I**ndentation level (no lines above and below). Synonym of `ii`
|
* * `iI` **I**nner **I**ndentation level (no lines above and below). Synonym of `ii`
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* See also the reference manual for more details at:
|
* See also the reference manual for more details at:
|
||||||
* [indent-object.txt](https://github.com/michaeljsmith/vim-indent-object/blob/master/doc/indent-object.txt)
|
* [indent-object.txt](https://github.com/michaeljsmith/vim-indent-object/blob/master/doc/indent-object.txt)
|
||||||
*/
|
*/
|
||||||
class VimIndentObject : VimExtension {
|
class VimIndentObject : VimExtension {
|
||||||
override fun getName(): String {
|
override fun getName(): String = "textobj-indent"
|
||||||
return "textobj-indent"
|
|
||||||
|
override fun init(initApi: VimInitApi) {
|
||||||
|
initApi.textObjects {
|
||||||
|
register("ai") { _ -> findIndentRange(includeAbove = true, includeBelow = false) }
|
||||||
|
register("aI") { _ -> findIndentRange(includeAbove = true, includeBelow = true) }
|
||||||
|
register("ii") { _ -> findIndentRange(includeAbove = false, includeBelow = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun init() {
|
|
||||||
putExtensionHandlerMapping(
|
|
||||||
MappingMode.XO, injector.parser.parseKeys("<Plug>textobj-indent-ai"), getOwner(),
|
|
||||||
IndentObject(true, false), false
|
|
||||||
)
|
|
||||||
putExtensionHandlerMapping(
|
|
||||||
MappingMode.XO, injector.parser.parseKeys("<Plug>textobj-indent-aI"), getOwner(),
|
|
||||||
IndentObject(true, true), false
|
|
||||||
)
|
|
||||||
putExtensionHandlerMapping(
|
|
||||||
MappingMode.XO, injector.parser.parseKeys("<Plug>textobj-indent-ii"), getOwner(),
|
|
||||||
IndentObject(false, false), false
|
|
||||||
)
|
|
||||||
|
|
||||||
putKeyMapping(
|
|
||||||
MappingMode.XO,
|
|
||||||
injector.parser.parseKeys("ai"),
|
|
||||||
getOwner(),
|
|
||||||
injector.parser.parseKeys("<Plug>textobj-indent-ai"),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
putKeyMapping(
|
|
||||||
MappingMode.XO,
|
|
||||||
injector.parser.parseKeys("aI"),
|
|
||||||
getOwner(),
|
|
||||||
injector.parser.parseKeys("<Plug>textobj-indent-aI"),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
putKeyMapping(
|
|
||||||
MappingMode.XO,
|
|
||||||
injector.parser.parseKeys("ii"),
|
|
||||||
getOwner(),
|
|
||||||
injector.parser.parseKeys("<Plug>textobj-indent-ii"),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal class IndentObject(val includeAbove: Boolean, val includeBelow: Boolean) : ExtensionHandler {
|
private suspend fun VimApi.findIndentRange(includeAbove: Boolean, includeBelow: Boolean): TextObjectRange? {
|
||||||
override val isRepeatable: Boolean
|
val charSequence = editor { read { text } }
|
||||||
get() = false
|
val caretOffset = editor { read { withPrimaryCaret { offset } } }
|
||||||
|
|
||||||
internal class IndentObjectHandler(val includeAbove: Boolean, val includeBelow: Boolean) :
|
|
||||||
TextObjectActionHandler() {
|
|
||||||
override fun getRange(
|
|
||||||
editor: VimEditor,
|
|
||||||
caret: ImmutableVimCaret,
|
|
||||||
context: ExecutionContext,
|
|
||||||
count: Int,
|
|
||||||
rawCount: Int,
|
|
||||||
): TextRange {
|
|
||||||
val charSequence = (editor as IjVimEditor).editor.getDocument().getCharsSequence()
|
|
||||||
val caretOffset = (caret as IjVimCaret).caret.getOffset()
|
|
||||||
|
|
||||||
// Part 1: Find the start of the caret line.
|
// Part 1: Find the start of the caret line.
|
||||||
var caretLineStartOffset = caretOffset
|
var caretLineStartOffset = caretOffset
|
||||||
var accumulatedWhitespace = 0
|
var accumulatedWhitespace = 0
|
||||||
while (--caretLineStartOffset >= 0) {
|
while (--caretLineStartOffset >= 0) {
|
||||||
val ch = charSequence.get(caretLineStartOffset)
|
val ch = charSequence[caretLineStartOffset]
|
||||||
if (ch == ' ' || ch == '\t') {
|
if (ch == ' ' || ch == '\t') {
|
||||||
++accumulatedWhitespace
|
++accumulatedWhitespace
|
||||||
} else if (ch == '\n') {
|
} else if (ch == '\n') {
|
||||||
@@ -131,7 +67,7 @@ class VimIndentObject : VimExtension {
|
|||||||
var offset = caretLineStartOffset
|
var offset = caretLineStartOffset
|
||||||
var indentSize = 0
|
var indentSize = 0
|
||||||
while (offset < charSequence.length) {
|
while (offset < charSequence.length) {
|
||||||
val ch = charSequence.get(offset)
|
val ch = charSequence[offset]
|
||||||
if (ch == ' ' || ch == '\t') {
|
if (ch == ' ' || ch == '\t') {
|
||||||
++indentSize
|
++indentSize
|
||||||
++offset
|
++offset
|
||||||
@@ -148,7 +84,7 @@ class VimIndentObject : VimExtension {
|
|||||||
while (upperBoundaryOffset == null) {
|
while (upperBoundaryOffset == null) {
|
||||||
// 3.1: Going backwards from `caretLineStartOffset`, find the first non-whitespace character.
|
// 3.1: Going backwards from `caretLineStartOffset`, find the first non-whitespace character.
|
||||||
while (--pos1 >= 0) {
|
while (--pos1 >= 0) {
|
||||||
val ch = charSequence.get(pos1)
|
val ch = charSequence[pos1]
|
||||||
if (ch != ' ' && ch != '\t' && ch != '\n') {
|
if (ch != ' ' && ch != '\t' && ch != '\n') {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -156,7 +92,7 @@ class VimIndentObject : VimExtension {
|
|||||||
// 3.2: Find the indent size of the line with this non-whitespace character and check against `indentSize`.
|
// 3.2: Find the indent size of the line with this non-whitespace character and check against `indentSize`.
|
||||||
accumulatedWhitespace = 0
|
accumulatedWhitespace = 0
|
||||||
while (--pos1 >= 0) {
|
while (--pos1 >= 0) {
|
||||||
val ch = charSequence.get(pos1)
|
val ch = charSequence[pos1]
|
||||||
if (ch == ' ' || ch == '\t') {
|
if (ch == ' ' || ch == '\t') {
|
||||||
++accumulatedWhitespace
|
++accumulatedWhitespace
|
||||||
} else if (ch == '\n') {
|
} else if (ch == '\n') {
|
||||||
@@ -179,28 +115,28 @@ class VimIndentObject : VimExtension {
|
|||||||
// Now `upperBoundaryOffset` marks the beginning of an `ai` text object.
|
// Now `upperBoundaryOffset` marks the beginning of an `ai` text object.
|
||||||
if (isUpperBoundaryFound && !includeAbove) {
|
if (isUpperBoundaryFound && !includeAbove) {
|
||||||
while (++upperBoundaryOffset < charSequence.length) {
|
while (++upperBoundaryOffset < charSequence.length) {
|
||||||
val ch = charSequence.get(upperBoundaryOffset)
|
val ch = charSequence[upperBoundaryOffset]
|
||||||
if (ch == '\n') {
|
if (ch == '\n') {
|
||||||
++upperBoundaryOffset
|
++upperBoundaryOffset
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (charSequence.get(upperBoundaryOffset) == '\n') {
|
while (charSequence[upperBoundaryOffset] == '\n') {
|
||||||
++upperBoundaryOffset
|
++upperBoundaryOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part 4: Find the start of the caret line.
|
// Part 4: Find the end of the caret line.
|
||||||
var caretLineEndOffset = caretOffset
|
var caretLineEndOffset = caretOffset
|
||||||
while (++caretLineEndOffset < charSequence.length) {
|
while (++caretLineEndOffset < charSequence.length) {
|
||||||
val ch = charSequence.get(caretLineEndOffset)
|
val ch = charSequence[caretLineEndOffset]
|
||||||
if (ch == '\n') {
|
if (ch == '\n') {
|
||||||
++caretLineEndOffset
|
++caretLineEndOffset
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// `caretLineEndOffset` points to the first charater in the line below caret line.
|
// `caretLineEndOffset` points to the first character in the line below caret line.
|
||||||
var lowerBoundaryOffset: Int? = null
|
var lowerBoundaryOffset: Int? = null
|
||||||
// Part 5: Find a line below the caret line, that has an indentation lower than `indentSize`.
|
// Part 5: Find a line below the caret line, that has an indentation lower than `indentSize`.
|
||||||
var pos2 = caretLineEndOffset - 1
|
var pos2 = caretLineEndOffset - 1
|
||||||
@@ -210,7 +146,7 @@ class VimIndentObject : VimExtension {
|
|||||||
var lastNewlinePos = caretLineEndOffset - 1
|
var lastNewlinePos = caretLineEndOffset - 1
|
||||||
var isInIndent = true
|
var isInIndent = true
|
||||||
while (++pos2 < charSequence.length) {
|
while (++pos2 < charSequence.length) {
|
||||||
val ch = charSequence.get(pos2)
|
val ch = charSequence[pos2]
|
||||||
if (isIndentChar(ch) && isInIndent) {
|
if (isIndentChar(ch) && isInIndent) {
|
||||||
++accumulatedWhitespace2
|
++accumulatedWhitespace2
|
||||||
} else if (ch == '\n') {
|
} else if (ch == '\n') {
|
||||||
@@ -235,45 +171,17 @@ class VimIndentObject : VimExtension {
|
|||||||
// Now `lowerBoundaryOffset` marks the end of an `ii` text object.
|
// Now `lowerBoundaryOffset` marks the end of an `ii` text object.
|
||||||
if (isLowerBoundaryFound && includeBelow) {
|
if (isLowerBoundaryFound && includeBelow) {
|
||||||
while (++lowerBoundaryOffset < charSequence.length) {
|
while (++lowerBoundaryOffset < charSequence.length) {
|
||||||
val ch = charSequence.get(lowerBoundaryOffset)
|
val ch = charSequence[lowerBoundaryOffset]
|
||||||
if (ch == '\n') {
|
if (ch == '\n') {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TextRange(upperBoundaryOffset, lowerBoundaryOffset)
|
// Convert offsets to line numbers for LineWise result
|
||||||
}
|
val startLine = editor { read { getLine(upperBoundaryOffset).number } }
|
||||||
|
val endLine = editor { read { getLine(lowerBoundaryOffset).number } }
|
||||||
override val visualType: TextObjectVisualType
|
return TextObjectRange.LineWise(startLine, endLine)
|
||||||
get() = TextObjectVisualType.LINE_WISE
|
|
||||||
|
|
||||||
private fun isIndentChar(ch: Char): Boolean {
|
|
||||||
return ch == ' ' || ch == '\t'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
|
||||||
val vimEditor = editor as IjVimEditor
|
|
||||||
val keyHandlerState = getInstance().keyHandlerState
|
|
||||||
|
|
||||||
val textObjectHandler = IndentObjectHandler(includeAbove, includeBelow)
|
|
||||||
|
|
||||||
if (editor.mode !is OP_PENDING) {
|
|
||||||
val count0 = operatorArguments.count0
|
|
||||||
editor.editor.getCaretModel().runForEachCaret { caret: Caret ->
|
|
||||||
val range = textObjectHandler.getRange(vimEditor, IjVimCaret(caret), context, max(1, count0), count0)
|
|
||||||
SelectionVimListenerSuppressor.lock().use { ignored ->
|
|
||||||
if (editor.mode is VISUAL) {
|
|
||||||
IjVimCaret(caret).vimSetSelection(range.startOffset, range.endOffset - 1, true)
|
|
||||||
} else {
|
|
||||||
caret.moveToInlayAwareOffset(range.startOffset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
keyHandlerState.commandBuilder.addAction(textObjectHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isIndentChar(ch: Char): Boolean = ch == ' ' || ch == '\t'
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import com.intellij.openapi.project.ProjectManager
|
|||||||
import com.intellij.openapi.wm.ToolWindowManager
|
import com.intellij.openapi.wm.ToolWindowManager
|
||||||
import com.intellij.openapi.wm.ToolWindowType
|
import com.intellij.openapi.wm.ToolWindowType
|
||||||
import com.maddyhome.idea.vim.extension.VimExtension
|
import com.maddyhome.idea.vim.extension.VimExtension
|
||||||
import com.maddyhome.idea.vim.helper.EditorHelper
|
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.KeyboardFocusManager
|
import java.awt.KeyboardFocusManager
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
@@ -32,7 +31,7 @@ internal class ToolWindowNavEverywhere : VimExtension {
|
|||||||
val oldFocusOwner = evt.oldValue as? JComponent
|
val oldFocusOwner = evt.oldValue as? JComponent
|
||||||
val dispatcher = service<ToolWindowNavDispatcher>()
|
val dispatcher = service<ToolWindowNavDispatcher>()
|
||||||
|
|
||||||
if (newFocusOwner != null && isInsideToolWindow(newFocusOwner) && !isPythonConsoleComponent(newFocusOwner)) {
|
if (newFocusOwner != null && isInsideToolWindow(newFocusOwner)) {
|
||||||
dispatcher.register(newFocusOwner)
|
dispatcher.register(newFocusOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,18 +51,6 @@ internal class ToolWindowNavEverywhere : VimExtension {
|
|||||||
super.dispose()
|
super.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isPythonConsoleComponent(component: Component): Boolean {
|
|
||||||
for (project in ProjectManager.getInstance().openProjects) {
|
|
||||||
if (project.isDisposed) continue
|
|
||||||
val toolWindowManager = ToolWindowManager.getInstance(project)
|
|
||||||
val tw = toolWindowManager.getToolWindow(EditorHelper.PYTHON_CONSOLE_TOOL_WINDOW_ID) ?: continue
|
|
||||||
if (SwingUtilities.isDescendingFrom(component, tw.component)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isInsideToolWindow(component: Component): Boolean {
|
private fun isInsideToolWindow(component: Component): Boolean {
|
||||||
for (project in ProjectManager.getInstance().openProjects) {
|
for (project in ProjectManager.getInstance().openProjects) {
|
||||||
if (project.isDisposed) continue
|
if (project.isDisposed) continue
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import com.intellij.psi.codeStyle.CodeStyleManager
|
|||||||
import com.intellij.psi.util.PsiUtilBase
|
import com.intellij.psi.util.PsiUtilBase
|
||||||
import com.maddyhome.idea.vim.EventFacade
|
import com.maddyhome.idea.vim.EventFacade
|
||||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||||
import com.maddyhome.idea.vim.api.Options
|
|
||||||
import com.maddyhome.idea.vim.api.VimCaret
|
import com.maddyhome.idea.vim.api.VimCaret
|
||||||
import com.maddyhome.idea.vim.api.VimChangeGroupBase
|
import com.maddyhome.idea.vim.api.VimChangeGroupBase
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
@@ -32,15 +31,11 @@ import com.maddyhome.idea.vim.common.TextRange
|
|||||||
import com.maddyhome.idea.vim.group.change.ChangeRemoteApi
|
import com.maddyhome.idea.vim.group.change.ChangeRemoteApi
|
||||||
import com.maddyhome.idea.vim.group.format.FormatRemoteApi
|
import com.maddyhome.idea.vim.group.format.FormatRemoteApi
|
||||||
import com.maddyhome.idea.vim.handler.commandContinuation
|
import com.maddyhome.idea.vim.handler.commandContinuation
|
||||||
import com.maddyhome.idea.vim.helper.CodeWrapper
|
|
||||||
import com.maddyhome.idea.vim.helper.CommentLeaderParser
|
|
||||||
import com.maddyhome.idea.vim.helper.inInsertMode
|
import com.maddyhome.idea.vim.helper.inInsertMode
|
||||||
import com.maddyhome.idea.vim.key.KeyHandlerKeeper
|
import com.maddyhome.idea.vim.key.KeyHandlerKeeper
|
||||||
import com.maddyhome.idea.vim.listener.VimInsertListener
|
import com.maddyhome.idea.vim.listener.VimInsertListener
|
||||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
||||||
import com.maddyhome.idea.vim.newapi.ij
|
import com.maddyhome.idea.vim.newapi.ij
|
||||||
import com.maddyhome.idea.vim.newapi.ijOptions
|
|
||||||
import com.maddyhome.idea.vim.options.OptionAccessScope
|
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode
|
import com.maddyhome.idea.vim.state.mode.Mode
|
||||||
import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService
|
import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService
|
||||||
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
||||||
@@ -134,8 +129,6 @@ class ChangeGroup : VimChangeGroupBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun repeatInsertText(editor: VimEditor, context: ExecutionContext, count: Int) {
|
override fun repeatInsertText(editor: VimEditor, context: ExecutionContext, count: Int) {
|
||||||
if (count <= 0) return
|
|
||||||
|
|
||||||
val ijEditor = (editor as IjVimEditor).editor
|
val ijEditor = (editor as IjVimEditor).editor
|
||||||
val editorId = ijEditor.editorId()
|
val editorId = ijEditor.editorId()
|
||||||
|
|
||||||
@@ -159,39 +152,6 @@ class ChangeGroup : VimChangeGroupBase() {
|
|||||||
injector.application.runWriteAction {
|
injector.application.runWriteAction {
|
||||||
CodeStyleManager.getInstance(project).reformatText(file, listOf(textRange))
|
CodeStyleManager.getInstance(project).reformatText(file, listOf(textRange))
|
||||||
}
|
}
|
||||||
wrapText(editor, start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun wrapText(editor: IjVimEditor, start: Int, end: Int) {
|
|
||||||
val textwidth = injector.ijOptions(editor).textwidth
|
|
||||||
if (textwidth <= 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
wrapTextToWidth(editor, start, end, textwidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun wrapTextToWidth(editor: IjVimEditor, start: Int, end: Int, width: Int) {
|
|
||||||
val ijEditor = editor.editor
|
|
||||||
val document = ijEditor.document
|
|
||||||
|
|
||||||
val text = document.getText(com.intellij.openapi.util.TextRange.create(start, end))
|
|
||||||
val commentsValue = injector.optionGroup
|
|
||||||
.getOptionValue(Options.comments, OptionAccessScope.LOCAL(editor))
|
|
||||||
.value
|
|
||||||
val wrapper = CodeWrapper(
|
|
||||||
width = width,
|
|
||||||
tabWidth = ijEditor.settings.getTabSize(ijEditor.project),
|
|
||||||
leaders = CommentLeaderParser.parse(commentsValue),
|
|
||||||
)
|
|
||||||
val wrapped = wrapper.wrap(text)
|
|
||||||
|
|
||||||
if (wrapped == text) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
injector.application.runWriteAction {
|
|
||||||
document.replaceString(start, end, wrapped)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun autoIndentRange(
|
override fun autoIndentRange(
|
||||||
|
|||||||
@@ -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 com.maddyhome.idea.vim.group
|
|
||||||
|
|
||||||
import com.intellij.lang.LanguageCommenters
|
|
||||||
import com.intellij.openapi.editor.Editor
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
|
||||||
import com.intellij.openapi.vfs.VirtualFile
|
|
||||||
import com.intellij.psi.PsiDocumentManager
|
|
||||||
import com.maddyhome.idea.vim.api.Options
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.autocmd.IjFileTypeMapping
|
|
||||||
import com.maddyhome.idea.vim.helper.CommenterMarkers
|
|
||||||
import com.maddyhome.idea.vim.helper.CommenterToComments
|
|
||||||
import com.maddyhome.idea.vim.helper.FiletypePresets
|
|
||||||
import com.maddyhome.idea.vim.newapi.vim
|
|
||||||
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a buffer-local `'comments'` value when an editor is created.
|
|
||||||
*
|
|
||||||
* Delegates to [OptionGroup.setBufferLocalDefaultIfUntouched], which preserves
|
|
||||||
* any value the user explicitly set via `.ideavimrc` or interactive `:set`.
|
|
||||||
*/
|
|
||||||
object CommentsOptionInitializer {
|
|
||||||
fun initializeForEditor(editor: Editor) {
|
|
||||||
val optionGroup = injector.optionGroup as? OptionGroup ?: return
|
|
||||||
val resolved = resolveComments(editor) ?: return
|
|
||||||
optionGroup.setBufferLocalDefaultIfUntouched(
|
|
||||||
Options.comments,
|
|
||||||
editor.vim,
|
|
||||||
VimString(resolved),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveComments(editor: Editor): String? {
|
|
||||||
val filetypeName = filetypeOf(editor) ?: return null
|
|
||||||
return FiletypePresets.presetFor(filetypeName) ?: deriveFromCommenter(editor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun filetypeOf(editor: Editor): String? {
|
|
||||||
val virtualFile: VirtualFile = FileDocumentManager.getInstance().getFile(editor.document) ?: return null
|
|
||||||
return IjFileTypeMapping.toVimFileType(virtualFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deriveFromCommenter(editor: Editor): String? {
|
|
||||||
val project = editor.project ?: return null
|
|
||||||
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return null
|
|
||||||
val commenter = LanguageCommenters.INSTANCE.forLanguage(psiFile.language) ?: return null
|
|
||||||
return CommenterToComments.derive(
|
|
||||||
CommenterMarkers(
|
|
||||||
linePrefix = commenter.lineCommentPrefix,
|
|
||||||
blockPrefix = commenter.blockCommentPrefix,
|
|
||||||
blockSuffix = commenter.blockCommentSuffix,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -55,7 +55,7 @@ import static com.maddyhome.idea.vim.newapi.IjVimInjectorKt.ijOptions;
|
|||||||
/**
|
/**
|
||||||
* @author vlan
|
* @author vlan
|
||||||
*/
|
*/
|
||||||
@State(name = "VimEditorSettings", storages = {@Storage(value = "vim_settings.xml")})
|
@State(name = "VimEditorSettings", storages = {@Storage(value = "$APP_CONFIG$/vim_settings.xml")})
|
||||||
public class EditorGroup implements PersistentStateComponent<Element>, VimEditorGroup {
|
public class EditorGroup implements PersistentStateComponent<Element>, VimEditorGroup {
|
||||||
public static final @NonNls String EDITOR_STORE_ELEMENT = "editor";
|
public static final @NonNls String EDITOR_STORE_ELEMENT = "editor";
|
||||||
private final CaretListener myLineNumbersCaretListener = new CaretListener() {
|
private final CaretListener myLineNumbersCaretListener = new CaretListener() {
|
||||||
@@ -321,18 +321,6 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable VimEditor getSelectedEditor() {
|
|
||||||
for (Project project : ProjectManager.getInstance().getOpenProjects()) {
|
|
||||||
if (project.isDisposed()) continue;
|
|
||||||
Editor selectedEditor = FileEditorManager.getInstance(project).getSelectedTextEditor();
|
|
||||||
if (selectedEditor != null) {
|
|
||||||
return new IjVimEditor(selectedEditor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull Collection<VimEditor> getEditorsRaw() {
|
public @NotNull Collection<VimEditor> getEditorsRaw() {
|
||||||
return getLocalEditors().map(IjVimEditor::new).collect(Collectors.toList());
|
return getLocalEditors().map(IjVimEditor::new).collect(Collectors.toList());
|
||||||
|
|||||||
@@ -164,10 +164,6 @@ class IjFileGroup : VimFileBase() {
|
|||||||
return if (editor != null) editor.vim else null
|
return if (editor != null) editor.vim else null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun listFilesForCompletion(pathPrefix: String, context: ExecutionContext): List<String> {
|
|
||||||
return rpc { FileRemoteApi.getInstance().listFilesForCompletion(pathPrefix, extractProjectId(context)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getProjectId(project: Any): String {
|
override fun getProjectId(project: Any): String {
|
||||||
require(project is Project)
|
require(project is Project)
|
||||||
return project.projectId().serializeToString()
|
return project.projectId().serializeToString()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import com.maddyhome.idea.vim.action.change.LazyVimCommand;
|
|||||||
import com.maddyhome.idea.vim.api.*;
|
import com.maddyhome.idea.vim.api.*;
|
||||||
import com.maddyhome.idea.vim.command.MappingMode;
|
import com.maddyhome.idea.vim.command.MappingMode;
|
||||||
import com.maddyhome.idea.vim.extension.VimExtensionFacade;
|
import com.maddyhome.idea.vim.extension.VimExtensionFacade;
|
||||||
import com.maddyhome.idea.vim.helper.EditorHelper;
|
|
||||||
import com.maddyhome.idea.vim.helper.EditorHelperRt;
|
|
||||||
import com.maddyhome.idea.vim.helper.ShortcutHelper;
|
import com.maddyhome.idea.vim.helper.ShortcutHelper;
|
||||||
import com.maddyhome.idea.vim.key.*;
|
import com.maddyhome.idea.vim.key.*;
|
||||||
import com.maddyhome.idea.vim.newapi.IjNativeAction;
|
import com.maddyhome.idea.vim.newapi.IjNativeAction;
|
||||||
@@ -53,7 +51,7 @@ import static java.util.stream.Collectors.toList;
|
|||||||
/**
|
/**
|
||||||
* @author vlan
|
* @author vlan
|
||||||
*/
|
*/
|
||||||
@State(name = "VimKeySettings", storages = {@Storage(value = "vim_settings.xml")})
|
@State(name = "VimKeySettings", storages = {@Storage(value = "$APP_CONFIG$/vim_settings.xml")})
|
||||||
public class KeyGroup extends VimKeyGroupBase implements PersistentStateComponent<Element> {
|
public class KeyGroup extends VimKeyGroupBase implements PersistentStateComponent<Element> {
|
||||||
public static final @NonNls String SHORTCUT_CONFLICTS_ELEMENT = "shortcut-conflicts";
|
public static final @NonNls String SHORTCUT_CONFLICTS_ELEMENT = "shortcut-conflicts";
|
||||||
private static final @NonNls String SHORTCUT_CONFLICT_ELEMENT = "shortcut-conflict";
|
private static final @NonNls String SHORTCUT_CONFLICT_ELEMENT = "shortcut-conflict";
|
||||||
@@ -182,15 +180,9 @@ public class KeyGroup extends VimKeyGroupBase implements PersistentStateComponen
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerRequiredShortcutKeys(@NotNull VimEditor editor) {
|
public void registerRequiredShortcutKeys(@NotNull VimEditor editor) {
|
||||||
Editor ijEditor = ((IjVimEditor)editor).getEditor();
|
|
||||||
if (EditorHelperRt.isIdeaVimDisabledHere(ijEditor)) return;
|
|
||||||
|
|
||||||
var vf = editor.getVirtualFile();
|
|
||||||
if (vf != null && vf.getPath().contains(EditorHelper.PYTHON_CONSOLE_FILE_NAME)) return;
|
|
||||||
|
|
||||||
EventFacade.getInstance().registerCustomShortcutSet(VimShortcutKeyAction.getInstance(),
|
EventFacade.getInstance().registerCustomShortcutSet(VimShortcutKeyAction.getInstance(),
|
||||||
ShortcutHelper.toShortcutSet(getRequiredShortcutKeys()),
|
ShortcutHelper.toShortcutSet(getRequiredShortcutKeys()),
|
||||||
ijEditor.getContentComponent());
|
((IjVimEditor)editor).getEditor().getContentComponent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -146,22 +146,6 @@ class OptionGroup : VimOptionGroupBase(), IjVimOptionGroup, InternalOptionValueA
|
|||||||
super.setOptionValueInternal(option, scope, value)
|
super.setOptionValueInternal(option, scope, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the buffer-local value of [option] as a Vim default — but only if the
|
|
||||||
* current value is still a [OptionValue.Default]. Preserves any value the user
|
|
||||||
* explicitly set via `.ideavimrc` or interactive `:set`/`:setlocal`.
|
|
||||||
*/
|
|
||||||
fun <T : VimDataType> setBufferLocalDefaultIfUntouched(
|
|
||||||
option: Option<T>,
|
|
||||||
editor: VimEditor,
|
|
||||||
value: T,
|
|
||||||
) {
|
|
||||||
val scope = OptionAccessScope.LOCAL(editor)
|
|
||||||
val current = getOptionValueInternal(option, scope)
|
|
||||||
if (current !is OptionValue.Default) return
|
|
||||||
setOptionValueInternal(option, scope, OptionValue.Default(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun editorReleased(editor: Editor) {
|
fun editorReleased(editor: Editor) {
|
||||||
// Vim always has at least one window; it's not possible to close it. Editing a new file will open a new buffer in
|
// Vim always has at least one window; it's not possible to close it. Editing a new file will open a new buffer in
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import com.intellij.openapi.editor.Editor
|
|||||||
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
|
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
|
||||||
import com.intellij.openapi.editor.actions.SplitLineAction
|
import com.intellij.openapi.editor.actions.SplitLineAction
|
||||||
import com.intellij.openapi.editor.impl.CaretModelImpl
|
import com.intellij.openapi.editor.impl.CaretModelImpl
|
||||||
|
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
import com.intellij.openapi.util.UserDataHolder
|
import com.intellij.openapi.util.UserDataHolder
|
||||||
import com.intellij.openapi.util.removeUserData
|
import com.intellij.openapi.util.removeUserData
|
||||||
@@ -118,7 +119,11 @@ internal abstract class OctopusHandler(private val nextHandler: EditorActionHand
|
|||||||
|
|
||||||
private fun executeInInvokeLater(editor: Editor): Boolean {
|
private fun executeInInvokeLater(editor: Editor): Boolean {
|
||||||
// Currently we have a workaround for the PY console VIM-3157
|
// Currently we have a workaround for the PY console VIM-3157
|
||||||
if (EditorHelper.isPythonConsole(editor)) return false
|
val fileName = FileDocumentManager.getInstance().getFile(editor.document)?.name
|
||||||
|
if (
|
||||||
|
fileName == "Python Console.py" || // This is the name in 232+
|
||||||
|
fileName == "Python Console" // This is the name in 231
|
||||||
|
) return false
|
||||||
return (editor.caretModel as? CaretModelImpl)?.isIteratingOverCarets ?: true
|
return (editor.caretModel as? CaretModelImpl)?.isIteratingOverCarets ?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import com.intellij.openapi.editor.*;
|
|||||||
import com.intellij.openapi.editor.ex.util.EditorUtil;
|
import com.intellij.openapi.editor.ex.util.EditorUtil;
|
||||||
import com.intellij.openapi.editor.impl.EditorImpl;
|
import com.intellij.openapi.editor.impl.EditorImpl;
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||||
import com.intellij.openapi.util.Key;
|
|
||||||
import com.intellij.openapi.util.SystemInfo;
|
import com.intellij.openapi.util.SystemInfo;
|
||||||
import com.intellij.openapi.util.registry.Registry;
|
import com.intellij.openapi.util.registry.Registry;
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
import com.intellij.openapi.vfs.VirtualFile;
|
||||||
@@ -46,9 +45,6 @@ public class EditorHelper {
|
|||||||
// mitigates the visible area bouncing around too much and even pushing the cursor line off screen with large
|
// mitigates the visible area bouncing around too much and even pushing the cursor line off screen with large
|
||||||
// multiline rendered doc comments, while still providing some visibility of the block inlay (e.g. Rider's single line
|
// multiline rendered doc comments, while still providing some visibility of the block inlay (e.g. Rider's single line
|
||||||
// Code Vision)
|
// Code Vision)
|
||||||
public static final String PYTHON_CONSOLE_FILE_NAME = "Python Console.py";
|
|
||||||
public static final String PYTHON_CONSOLE_TOOL_WINDOW_ID = "Python Console";
|
|
||||||
|
|
||||||
private static final int BLOCK_INLAY_MAX_LINE_HEIGHT = 3;
|
private static final int BLOCK_INLAY_MAX_LINE_HEIGHT = 3;
|
||||||
|
|
||||||
public static @NotNull Rectangle getVisibleArea(final @NotNull Editor editor) {
|
public static @NotNull Rectangle getVisibleArea(final @NotNull Editor editor) {
|
||||||
@@ -683,48 +679,6 @@ public class EditorHelper {
|
|||||||
return editor.getEditorKind() == EditorKind.DIFF;
|
return editor.getEditorKind() == EditorKind.DIFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the editor is the Python console, so we can disable Vim features
|
|
||||||
*/
|
|
||||||
public static boolean isPythonConsole(@NotNull Editor editor) {
|
|
||||||
if (editor.getVirtualFile() == null) return false;
|
|
||||||
// In split mode, the projected VirtualFile may have a different getName() result,
|
|
||||||
// so we also check getPath() to reliably detect the Python console.
|
|
||||||
return editor.getVirtualFile().getName().contains(PYTHON_CONSOLE_FILE_NAME)
|
|
||||||
|| editor.getVirtualFile().getPath().contains(PYTHON_CONSOLE_FILE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the editor is hosted in the Commit tool window, so we can enable Vim features
|
|
||||||
*/
|
|
||||||
public static boolean isCommitWindowEditor(@NotNull Editor editor) {
|
|
||||||
// The best heuristic we have is the file name, which is Dummy.txt
|
|
||||||
var file = editor.getVirtualFile();
|
|
||||||
return file != null && file.getName().contains("Dummy.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the editor is a Kotlin class file decompiled to a Java file, so we can enable Vim features
|
|
||||||
* <p>
|
|
||||||
* The platform changed the implementation of decompiling a Kotlin .class file to Java in 2026.2. Previously, it
|
|
||||||
* used a dummy virtual file implementation. Now it uses an instance of {@link LightVirtualFile}. Typically, this
|
|
||||||
* means an in-memory file that we don't want to have Vim features for, but in this case, we do.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* To test, open a .class file generated from a Kotlin file. Then use the "Decompile to Java" action to create a
|
|
||||||
* separate (in-memory) `.decompiled.java` file. Java-based .class files are decompiled directly in the document for
|
|
||||||
* the .class file, so the editor is always backed by a valid file.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Perhaps a future implementation would have an allow-list for {@link VirtualFile#getFileType()} and allow "JAVA"?
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
public static boolean isKotlinClassDecompiledToJavaFile(@NotNull Editor editor) {
|
|
||||||
@SuppressWarnings("deprecation") @Nullable Key<?> key = Key.findKeyByName("IS_KOTLIN_DECOMPILED_FILE");
|
|
||||||
var file = editor.getVirtualFile();
|
|
||||||
return file != null && key != null && editor.getVirtualFile().getUserData(key) == Boolean.TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the document in the editor is modified.
|
* Checks if the document in the editor is modified.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import com.intellij.openapi.editor.Editor
|
|||||||
import com.intellij.openapi.editor.EditorKind
|
import com.intellij.openapi.editor.EditorKind
|
||||||
import com.intellij.openapi.editor.ex.util.EditorUtil
|
import com.intellij.openapi.editor.ex.util.EditorUtil
|
||||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||||
import com.intellij.openapi.ui.popup.util.PopupUtil
|
|
||||||
import com.intellij.util.ui.table.JBTableRowEditor
|
import com.intellij.util.ui.table.JBTableRowEditor
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.api.StringListOptionValue
|
import com.maddyhome.idea.vim.api.StringListOptionValue
|
||||||
@@ -54,7 +53,7 @@ internal val Editor.isIdeaVimDisabledHere: Boolean
|
|||||||
!ClientId.isCurrentlyUnderLocalId || // CWM-927
|
!ClientId.isCurrentlyUnderLocalId || // CWM-927
|
||||||
(ideaVimDisabledForSingleLine(ideaVimSupportValue) && isSingleLine()) ||
|
(ideaVimDisabledForSingleLine(ideaVimSupportValue) && isSingleLine()) ||
|
||||||
IdeaVimDisablerExtensionPoint.isDisabledForEditor(this) ||
|
IdeaVimDisablerExtensionPoint.isDisabledForEditor(this) ||
|
||||||
!isAllowedFileEditor()
|
isNotFileEditorExceptAllowed()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,21 +65,18 @@ internal val Editor.isIdeaVimDisabledHere: Boolean
|
|||||||
* Here are issues when non-file editors were supported:
|
* Here are issues when non-file editors were supported:
|
||||||
* AI Chat – VIM-3786
|
* AI Chat – VIM-3786
|
||||||
* Debug evaluate console – VIM-3929
|
* Debug evaluate console – VIM-3929
|
||||||
* Python console - VIM-4172
|
|
||||||
*
|
*
|
||||||
* We do want to support Vim actions in some windows, such as the commit window, diff windows, and decompiled Java
|
* However, we still support IdeaVim in a commit window because it works fine there, and removing vim from this place will
|
||||||
* files. We don't support the Python console.
|
* be quite a visible change for users.
|
||||||
|
* We detect the commit window by the name of the editor (Dummy.txt). If this causes issues, let's disable IdeaVim
|
||||||
|
* in the commit window as well.
|
||||||
|
*
|
||||||
|
* Also, we support IdeaVim in diff viewers.
|
||||||
*/
|
*/
|
||||||
private fun Editor.isAllowedFileEditor(): Boolean {
|
private fun Editor.isNotFileEditorExceptAllowed(): Boolean {
|
||||||
if (EditorHelper.getVirtualFile(this)?.name?.contains("Dummy.txt") == true) {
|
if (EditorHelper.getVirtualFile(this)?.name?.contains("Dummy.txt") == true) return false
|
||||||
return PopupUtil.getPopupContainerFor(component) == null
|
if (EditorHelper.isDiffEditor(this)) return false
|
||||||
}
|
return !EditorHelper.isFileEditor(this)
|
||||||
if (EditorHelper.isPythonConsole(this)) return false
|
|
||||||
|
|
||||||
return EditorHelper.isCommitWindowEditor(this)
|
|
||||||
|| EditorHelper.isKotlinClassDecompiledToJavaFile(this)
|
|
||||||
|| EditorHelper.isDiffEditor(this)
|
|
||||||
|| EditorHelper.isFileEditor(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ideaVimDisabledInDialog(ideaVimSupportValue: StringListOptionValue): Boolean {
|
private fun ideaVimDisabledInDialog(ideaVimSupportValue: StringListOptionValue): Boolean {
|
||||||
|
|||||||
@@ -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,7 +29,6 @@ import com.maddyhome.idea.vim.common.InsertSequence
|
|||||||
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
||||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||||
import com.maddyhome.idea.vim.newapi.ij
|
import com.maddyhome.idea.vim.newapi.ij
|
||||||
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.state.mode.inVisualMode
|
import com.maddyhome.idea.vim.state.mode.inVisualMode
|
||||||
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
||||||
@@ -55,7 +54,6 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
|
|||||||
val textEditor = getTextEditor(editor.ij)
|
val textEditor = getTextEditor(editor.ij)
|
||||||
val undoManager = UndoManager.getInstance(project)
|
val undoManager = UndoManager.getInstance(project)
|
||||||
if (undoManager.isUndoAvailable(textEditor)) {
|
if (undoManager.isUndoAvailable(textEditor)) {
|
||||||
val caretCountBeforeUndo = editor.ij.caretModel.allCarets.size
|
|
||||||
val scrollingModel = editor.getScrollingModel()
|
val scrollingModel = editor.getScrollingModel()
|
||||||
scrollingModel.accumulateViewportChanges()
|
scrollingModel.accumulateViewportChanges()
|
||||||
|
|
||||||
@@ -63,8 +61,6 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
|
|||||||
|
|
||||||
scrollingModel.flushViewportChanges()
|
scrollingModel.flushViewportChanges()
|
||||||
|
|
||||||
collapseRestoredBlockVisualCarets(editor, caretCountBeforeUndo)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -195,23 +191,6 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* VIM-4112. IntelliJ's undo restores the pre-edit `CaretState`; for a block-visual edit that
|
|
||||||
* means one caret per block row. A 1 → N caret-count jump across undo uniquely identifies
|
|
||||||
* this, since [com.maddyhome.idea.vim.helper.exitVisualMode] is the only flow that collapses
|
|
||||||
* multi-carets to one. The remaining caret is placed at the block's top-left, matching Vim's
|
|
||||||
* convention of cursor-at-start-of-undone-change.
|
|
||||||
*/
|
|
||||||
private fun collapseRestoredBlockVisualCarets(editor: VimEditor, caretCountBeforeUndo: Int) {
|
|
||||||
val caretModel = editor.ij.caretModel
|
|
||||||
val restoredExtraCarets = caretCountBeforeUndo == 1 && caretModel.allCarets.size > 1
|
|
||||||
if (!restoredExtraCarets || editor.mode !is Mode.NORMAL) return
|
|
||||||
|
|
||||||
val blockTopOffset = caretModel.allCarets.minOf { it.offset }
|
|
||||||
caretModel.removeSecondaryCarets()
|
|
||||||
caretModel.primaryCaret.moveToOffset(blockTopOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeSelections(editor: VimEditor) {
|
private fun removeSelections(editor: VimEditor) {
|
||||||
editor.carets().forEach {
|
editor.carets().forEach {
|
||||||
val ijCaret = it.ij
|
val ijCaret = it.ij
|
||||||
|
|||||||
@@ -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,33 +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.listener
|
|
||||||
|
|
||||||
import com.intellij.openapi.application.ApplicationActivationListener
|
|
||||||
import com.intellij.openapi.wm.IdeFrame
|
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.autocmd.AutoCmdEvent
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires FocusGained/FocusLost autocmd events when the IDE window gains or loses OS-level focus.
|
|
||||||
* This matches Vim's behavior where these events fire on application-level focus changes (e.g., alt-tab),
|
|
||||||
* not on editor-level focus changes within the IDE.
|
|
||||||
*/
|
|
||||||
class VimAppActivationListener : ApplicationActivationListener {
|
|
||||||
|
|
||||||
override fun applicationActivated(ideFrame: IdeFrame) {
|
|
||||||
if (VimPlugin.isNotEnabled()) return
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.FocusGained)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun applicationDeactivated(ideFrame: IdeFrame) {
|
|
||||||
if (VimPlugin.isNotEnabled()) return
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.FocusLost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,17 +9,13 @@
|
|||||||
package com.maddyhome.idea.vim.listener
|
package com.maddyhome.idea.vim.listener
|
||||||
|
|
||||||
import com.intellij.codeWithMe.ClientId
|
import com.intellij.codeWithMe.ClientId
|
||||||
import com.intellij.codeWithMe.ClientId.Companion.isLocal
|
|
||||||
import com.intellij.ide.ui.UISettings
|
import com.intellij.ide.ui.UISettings
|
||||||
import com.intellij.openapi.Disposable
|
import com.intellij.openapi.Disposable
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.diagnostic.trace
|
import com.intellij.openapi.diagnostic.trace
|
||||||
import com.intellij.openapi.editor.Caret
|
import com.intellij.openapi.editor.Caret
|
||||||
import com.intellij.openapi.editor.ClientEditorManager
|
|
||||||
import com.intellij.openapi.editor.Document
|
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import com.intellij.openapi.editor.EditorFactory
|
|
||||||
import com.intellij.openapi.editor.EditorKind
|
import com.intellij.openapi.editor.EditorKind
|
||||||
import com.intellij.openapi.editor.actionSystem.TypedAction
|
import com.intellij.openapi.editor.actionSystem.TypedAction
|
||||||
import com.intellij.openapi.editor.event.CaretEvent
|
import com.intellij.openapi.editor.event.CaretEvent
|
||||||
@@ -38,8 +34,6 @@ import com.intellij.openapi.editor.ex.DocumentEx
|
|||||||
import com.intellij.openapi.editor.ex.EditorEx
|
import com.intellij.openapi.editor.ex.EditorEx
|
||||||
import com.intellij.openapi.editor.ex.FocusChangeListener
|
import com.intellij.openapi.editor.ex.FocusChangeListener
|
||||||
import com.intellij.openapi.editor.impl.EditorComponentImpl
|
import com.intellij.openapi.editor.impl.EditorComponentImpl
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManagerListener
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditor
|
import com.intellij.openapi.fileEditor.FileEditor
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
|
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
|
||||||
@@ -57,10 +51,6 @@ import com.intellij.openapi.util.Disposer
|
|||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
import com.intellij.openapi.util.removeUserData
|
import com.intellij.openapi.util.removeUserData
|
||||||
import com.intellij.openapi.vfs.VirtualFile
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
import com.intellij.openapi.vfs.VirtualFileManager
|
|
||||||
import com.intellij.openapi.vfs.newvfs.BulkFileListener
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
|
|
||||||
import com.intellij.util.ExceptionUtil
|
import com.intellij.util.ExceptionUtil
|
||||||
import com.intellij.util.SlowOperations
|
import com.intellij.util.SlowOperations
|
||||||
import com.maddyhome.idea.vim.EventFacade
|
import com.maddyhome.idea.vim.EventFacade
|
||||||
@@ -75,12 +65,7 @@ import com.maddyhome.idea.vim.api.coerceOffset
|
|||||||
import com.maddyhome.idea.vim.api.getLineEndForOffset
|
import com.maddyhome.idea.vim.api.getLineEndForOffset
|
||||||
import com.maddyhome.idea.vim.api.getLineStartForOffset
|
import com.maddyhome.idea.vim.api.getLineStartForOffset
|
||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
import com.maddyhome.idea.vim.autocmd.AutoCmdEvent
|
|
||||||
import com.maddyhome.idea.vim.autocmd.IjFileTypeMapping
|
|
||||||
import com.maddyhome.idea.vim.common.ModeChangeListener
|
|
||||||
import com.maddyhome.idea.vim.common.ModeWillChangeListener
|
|
||||||
import com.maddyhome.idea.vim.group.ChangeGroup
|
import com.maddyhome.idea.vim.group.ChangeGroup
|
||||||
import com.maddyhome.idea.vim.group.CommentsOptionInitializer
|
|
||||||
import com.maddyhome.idea.vim.group.FileGroupHelper
|
import com.maddyhome.idea.vim.group.FileGroupHelper
|
||||||
import com.maddyhome.idea.vim.group.IjOptions
|
import com.maddyhome.idea.vim.group.IjOptions
|
||||||
import com.maddyhome.idea.vim.group.IjVimRedrawService
|
import com.maddyhome.idea.vim.group.IjVimRedrawService
|
||||||
@@ -127,7 +112,6 @@ import java.awt.event.MouseAdapter
|
|||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -184,9 +168,6 @@ object VimListenerManager {
|
|||||||
val insertTimeRecorder = InsertTimeRecorder()
|
val insertTimeRecorder = InsertTimeRecorder()
|
||||||
injector.listenersNotifier.modeChangeListeners.add(insertTimeRecorder)
|
injector.listenersNotifier.modeChangeListeners.add(insertTimeRecorder)
|
||||||
|
|
||||||
injector.listenersNotifier.modeWillChangeListeners.add(AutoCmdInsertEnterListener())
|
|
||||||
injector.listenersNotifier.modeChangeListeners.add(AutoCmdInsertLeaveListener())
|
|
||||||
|
|
||||||
val modeWidgetListener = ModeWidgetListener()
|
val modeWidgetListener = ModeWidgetListener()
|
||||||
injector.listenersNotifier.modeChangeListeners.add(modeWidgetListener)
|
injector.listenersNotifier.modeChangeListeners.add(modeWidgetListener)
|
||||||
injector.listenersNotifier.myEditorListeners.add(modeWidgetListener)
|
injector.listenersNotifier.myEditorListeners.add(modeWidgetListener)
|
||||||
@@ -240,8 +221,6 @@ object VimListenerManager {
|
|||||||
val busConnection =
|
val busConnection =
|
||||||
ApplicationManager.getApplication().messageBus.connect(VimPlugin.getInstance().onOffDisposable)
|
ApplicationManager.getApplication().messageBus.connect(VimPlugin.getInstance().onOffDisposable)
|
||||||
busConnection.subscribe(FileOpenedSyncListener.TOPIC, VimEditorFactoryListener)
|
busConnection.subscribe(FileOpenedSyncListener.TOPIC, VimEditorFactoryListener)
|
||||||
busConnection.subscribe(VirtualFileManager.VFS_CHANGES, BufNewFileTracker)
|
|
||||||
busConnection.subscribe(FileDocumentManagerListener.TOPIC, BufWriteListener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disable() {
|
fun disable() {
|
||||||
@@ -258,8 +237,6 @@ object VimListenerManager {
|
|||||||
optionGroup.removeGlobalOptionChangeListener(Options.showmode, modeWidgetOptionListener)
|
optionGroup.removeGlobalOptionChangeListener(Options.showmode, modeWidgetOptionListener)
|
||||||
optionGroup.removeGlobalOptionChangeListener(Options.showmode, macroWidgetOptionListener)
|
optionGroup.removeGlobalOptionChangeListener(Options.showmode, macroWidgetOptionListener)
|
||||||
optionGroup.removeEffectiveOptionValueChangeListener(Options.guicursor, GuicursorChangeListener)
|
optionGroup.removeEffectiveOptionValueChangeListener(Options.guicursor, GuicursorChangeListener)
|
||||||
|
|
||||||
BufNewFileTracker.clear()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +325,6 @@ object VimListenerManager {
|
|||||||
|
|
||||||
injector.editorGroup.editorCreated(IjVimEditor(editor))
|
injector.editorGroup.editorCreated(IjVimEditor(editor))
|
||||||
(VimPlugin.getChange() as ChangeGroup).editorCreated(IjVimEditor(editor), perEditorDisposable)
|
(VimPlugin.getChange() as ChangeGroup).editorCreated(IjVimEditor(editor), perEditorDisposable)
|
||||||
CommentsOptionInitializer.initializeForEditor(editor)
|
|
||||||
|
|
||||||
(editor as EditorEx).addFocusListener(VimFocusListener, perEditorDisposable)
|
(editor as EditorEx).addFocusListener(VimFocusListener, perEditorDisposable)
|
||||||
|
|
||||||
@@ -359,18 +335,15 @@ object VimListenerManager {
|
|||||||
injector.editorGroup.editorDeinit(editor.vim)
|
injector.editorGroup.editorDeinit(editor.vim)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationManager.getApplication().invokeLater {
|
|
||||||
if (vimDisabled(editor)) {
|
|
||||||
remove(editor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(editor: Editor) {
|
fun remove(editor: Editor) {
|
||||||
val editorDisposable = editor.removeUserData(editorListenersDisposableKey)
|
val editorDisposable = editor.removeUserData(editorListenersDisposableKey)
|
||||||
if (editorDisposable != null) {
|
if (editorDisposable != null) {
|
||||||
Disposer.dispose(editorDisposable)
|
Disposer.dispose(editorDisposable)
|
||||||
|
} else {
|
||||||
|
// We definitely do not expect this to happen
|
||||||
|
StrictMode.fail("Editor doesn't have disposable attached. $editor")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,17 +423,8 @@ object VimListenerManager {
|
|||||||
// Breaks relativenumber for some reason
|
// Breaks relativenumber for some reason
|
||||||
// injector.scroll.scrollCaretIntoView(editor.vim)
|
// injector.scroll.scrollCaretIntoView(editor.vim)
|
||||||
}
|
}
|
||||||
// Vim order: BufLeave → WinLeave → WinEnter → BufEnter
|
|
||||||
// Buf events only fire when the buffer (file) actually changes
|
injector.outputPanel.getCurrentOutputPanel()?.close()
|
||||||
val bufferChanged = event.oldFile?.path != event.newFile?.path
|
|
||||||
if (bufferChanged) {
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.BufLeave, event.oldFile?.path)
|
|
||||||
}
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.WinLeave, event.oldFile?.path)
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.WinEnter, event.newFile?.path)
|
|
||||||
if (bufferChanged) {
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.BufEnter, event.newFile?.path)
|
|
||||||
}
|
|
||||||
MotionGroup.fileEditorManagerSelectionChangedCallback(event)
|
MotionGroup.fileEditorManagerSelectionChangedCallback(event)
|
||||||
FileGroupHelper.fileEditorManagerSelectionChangedCallback(event)
|
FileGroupHelper.fileEditorManagerSelectionChangedCallback(event)
|
||||||
// (VimPlugin.getSearch() as IjVimSearchGroup).fileEditorManagerSelectionChangedCallback(event)
|
// (VimPlugin.getSearch() as IjVimSearchGroup).fileEditorManagerSelectionChangedCallback(event)
|
||||||
@@ -544,7 +508,6 @@ object VimListenerManager {
|
|||||||
EditorListeners.remove(event.editor)
|
EditorListeners.remove(event.editor)
|
||||||
injector.listenersNotifier.notifyEditorReleased(vimEditor)
|
injector.listenersNotifier.notifyEditorReleased(vimEditor)
|
||||||
injector.markService.editorReleased(vimEditor)
|
injector.markService.editorReleased(vimEditor)
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.BufLeave, event.editor.virtualFile?.path)
|
|
||||||
|
|
||||||
// This ticket will have a different stack trace, but it's the same problem. Originally, we tracked the last
|
// This ticket will have a different stack trace, but it's the same problem. Originally, we tracked the last
|
||||||
// editor closing based on file selection (closing an editor would select the next editor - so a null selection
|
// editor closing based on file selection (closing an editor would select the next editor - so a null selection
|
||||||
@@ -604,8 +567,6 @@ object VimListenerManager {
|
|||||||
}
|
}
|
||||||
EditorListeners.add(editor, openingEditor?.editor?.vim ?: injector.fallbackWindow, scenario)
|
EditorListeners.add(editor, openingEditor?.editor?.vim ?: injector.fallbackWindow, scenario)
|
||||||
firstEditorInitialised = true
|
firstEditorInitialised = true
|
||||||
|
|
||||||
fireBufferLoadedEvents(editor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -964,122 +925,3 @@ private object MouseEventsDataHolder {
|
|||||||
const val allowedSkippedDragEvents = 3
|
const val allowedSkippedDragEvents = 3
|
||||||
var dragEventCount = allowedSkippedDragEvents
|
var dragEventCount = allowedSkippedDragEvents
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires autocmd events that correspond to Vim's "load a buffer" sequence.
|
|
||||||
*/
|
|
||||||
private fun fireBufferLoadedEvents(editor: Editor) {
|
|
||||||
val virtualFile = editor.virtualFile ?: return
|
|
||||||
val vimEditor = editor.vim
|
|
||||||
val path = virtualFile.path
|
|
||||||
|
|
||||||
if (BufNewFileTracker.consumeIfNew(path)) {
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.BufNewFile, path, vimEditor)
|
|
||||||
} else {
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.BufReadPost, path, vimEditor)
|
|
||||||
}
|
|
||||||
|
|
||||||
val vimFileType = IjFileTypeMapping.toVimFileType(virtualFile)
|
|
||||||
if (vimFileType != null) {
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.FileType, vimFileType, vimEditor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires Vim's buffer write events when IntelliJ saves a document to disk.
|
|
||||||
*
|
|
||||||
* `BufWritePre` (== `BufWrite` in Vim) fires before the write; `BufWritePost` after.
|
|
||||||
* Note: IntelliJ auto-saves aggressively (focus loss, tab switch, build, etc.), so these
|
|
||||||
* fire more often than Vim's `:w`. Handlers should be idempotent.
|
|
||||||
*/
|
|
||||||
private object BufWriteListener : FileDocumentManagerListener {
|
|
||||||
override fun beforeDocumentSaving(document: Document) {
|
|
||||||
fireWriteEvent(document, pre = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun afterDocumentSaved(document: Document) {
|
|
||||||
fireWriteEvent(document, pre = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fireWriteEvent(document: Document, pre: Boolean) {
|
|
||||||
val virtualFile = FileDocumentManager.getInstance().getFile(document) ?: return
|
|
||||||
val editor = getMainEditor(document) ?: return
|
|
||||||
val vimEditor = IjVimEditor(editor)
|
|
||||||
val path = virtualFile.path
|
|
||||||
if (pre) {
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.BufWritePre, path, vimEditor)
|
|
||||||
} else {
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.BufWritePost, path, vimEditor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMainEditor(document: Document): Editor? = EditorFactory.getInstance().getEditors(document)
|
|
||||||
.firstOrNull { ed ->
|
|
||||||
ed.editorKind != EditorKind.CONSOLE &&
|
|
||||||
ed.editorKind != EditorKind.DIFF &&
|
|
||||||
ClientEditorManager.getClientId(ed).isLocal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks paths of newly-created VirtualFiles so that when a file is subsequently opened we can fire Vim's `BufNewFile`
|
|
||||||
* event instead of `BufRead`. Entries are removed on first matching open; files created but never opened stay in the
|
|
||||||
* set (bounded by a TTL and max size).
|
|
||||||
*/
|
|
||||||
internal object BufNewFileTracker : BulkFileListener {
|
|
||||||
|
|
||||||
private const val ENTRY_TTL_MILLIS = 60_000L
|
|
||||||
|
|
||||||
private const val MAX_ENTRIES = 256
|
|
||||||
|
|
||||||
private val createdFiles = ConcurrentHashMap<String, Long>()
|
|
||||||
|
|
||||||
@TestOnly
|
|
||||||
internal var clock: () -> Long = System::currentTimeMillis
|
|
||||||
|
|
||||||
override fun after(events: List<VFileEvent>) {
|
|
||||||
val now = clock()
|
|
||||||
for (event in events) {
|
|
||||||
if (event !is VFileCreateEvent || event.isDirectory) continue
|
|
||||||
if (event.isFromRefresh || event.requestor == null) continue
|
|
||||||
createdFiles[event.path] = now
|
|
||||||
}
|
|
||||||
if (createdFiles.size > MAX_ENTRIES) sweepStale(now)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun consumeIfNew(path: String): Boolean {
|
|
||||||
val timestamp = createdFiles.remove(path) ?: return false
|
|
||||||
return clock() - timestamp < ENTRY_TTL_MILLIS
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear() {
|
|
||||||
createdFiles.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
@TestOnly
|
|
||||||
internal fun size(): Int = createdFiles.size
|
|
||||||
|
|
||||||
private fun sweepStale(now: Long) {
|
|
||||||
createdFiles.entries.removeIf { now - it.value > ENTRY_TTL_MILLIS }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AutoCmdInsertEnterListener : ModeWillChangeListener {
|
|
||||||
override fun modeWillChange(editor: VimEditor, oldMode: Mode, newMode: Mode) {
|
|
||||||
if (!oldMode.isInsertish && newMode.isInsertish) {
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.InsertEnter, editor.getPath(), editor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AutoCmdInsertLeaveListener : ModeChangeListener {
|
|
||||||
override fun modeChanged(editor: VimEditor, oldMode: Mode) {
|
|
||||||
if (oldMode.isInsertish && !editor.mode.isInsertish) {
|
|
||||||
injector.autoCmd.handleEvent(AutoCmdEvent.InsertLeave, editor.getPath(), editor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vim fires InsertEnter/Leave for both Insert and Replace modes (`:help InsertEnter`).
|
|
||||||
private val Mode.isInsertish: Boolean
|
|
||||||
get() = this == Mode.INSERT || this == Mode.REPLACE
|
|
||||||
|
|||||||
@@ -1,80 +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.newapi
|
|
||||||
|
|
||||||
import com.intellij.openapi.editor.FoldRegion
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes nesting depth for each fold region in O(N log N).
|
|
||||||
*
|
|
||||||
* A fold's depth is the count of other folds that contain it by offset range,
|
|
||||||
* excluding folds with an identical (start, end) range.
|
|
||||||
*/
|
|
||||||
internal object FoldDepthCalculator {
|
|
||||||
|
|
||||||
fun computeDepths(folds: Array<FoldRegion>): IntArray {
|
|
||||||
if (folds.isEmpty()) return IntArray(0)
|
|
||||||
val ranges = FoldRanges.from(folds)
|
|
||||||
return ranges.sweepDepths(ranges.orderOuterFirst())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FoldRanges(private val starts: IntArray, private val ends: IntArray) {
|
|
||||||
val size: Int get() = starts.size
|
|
||||||
|
|
||||||
fun orderOuterFirst(): IntArray =
|
|
||||||
(0 until size).sortedWith(byStartAscendingEndDescending()).toIntArray()
|
|
||||||
|
|
||||||
fun sweepDepths(orderedFolds: IntArray): IntArray {
|
|
||||||
val depths = IntArray(size)
|
|
||||||
val openFolds = IntArray(size)
|
|
||||||
var openCount = 0
|
|
||||||
|
|
||||||
for (fold in orderedFolds) {
|
|
||||||
openCount = dropFoldsClosedBefore(openFolds, openCount, fold)
|
|
||||||
val duplicates = countDuplicatesAtTop(openFolds, openCount, fold)
|
|
||||||
depths[fold] = openCount - duplicates
|
|
||||||
openFolds[openCount++] = fold
|
|
||||||
}
|
|
||||||
return depths
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun byStartAscendingEndDescending() = Comparator<Int> { a, b ->
|
|
||||||
val byStart = starts[a].compareTo(starts[b])
|
|
||||||
if (byStart != 0) byStart else ends[b].compareTo(ends[a])
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dropFoldsClosedBefore(stack: IntArray, stackSize: Int, fold: Int): Int {
|
|
||||||
var size = stackSize
|
|
||||||
val foldStart = starts[fold]
|
|
||||||
while (size > 0 && ends[stack[size - 1]] <= foldStart) size--
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun countDuplicatesAtTop(stack: IntArray, stackSize: Int, fold: Int): Int {
|
|
||||||
var count = 0
|
|
||||||
var i = stackSize - 1
|
|
||||||
while (i >= 0 && hasSameRange(stack[i], fold)) {
|
|
||||||
count++
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasSameRange(a: Int, b: Int): Boolean =
|
|
||||||
starts[a] == starts[b] && ends[a] == ends[b]
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(folds: Array<FoldRegion>): FoldRanges {
|
|
||||||
val starts = IntArray(folds.size) { folds[it].startOffset }
|
|
||||||
val ends = IntArray(folds.size) { folds[it].endOffset }
|
|
||||||
return FoldRanges(starts, ends)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -514,11 +514,12 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
|
|||||||
val allFolds = editor.foldingModel.allFoldRegions
|
val allFolds = editor.foldingModel.allFoldRegions
|
||||||
if (allFolds.isEmpty()) return
|
if (allFolds.isEmpty()) return
|
||||||
|
|
||||||
val depths = FoldDepthCalculator.computeDepths(allFolds)
|
|
||||||
|
|
||||||
editor.foldingModel.runBatchFoldingOperation {
|
editor.foldingModel.runBatchFoldingOperation {
|
||||||
for (i in allFolds.indices) {
|
// I'm aware it's O(n^2) comparison here,
|
||||||
allFolds[i].isExpanded = depths[i] < foldLevel
|
// but it doesn't affect performance even on a large amount of fold
|
||||||
|
allFolds.forEach { fold ->
|
||||||
|
val depth = calculateFoldDepth(fold, allFolds)
|
||||||
|
fold.isExpanded = depth < foldLevel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -527,10 +528,9 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
|
|||||||
val allFolds = editor.foldingModel.allFoldRegions
|
val allFolds = editor.foldingModel.allFoldRegions
|
||||||
if (allFolds.isEmpty()) return 0
|
if (allFolds.isEmpty()) return 0
|
||||||
|
|
||||||
val depths = FoldDepthCalculator.computeDepths(allFolds)
|
return allFolds.maxOfOrNull { fold ->
|
||||||
var max = 0
|
calculateFoldDepth(fold, allFolds)
|
||||||
for (d in depths) if (d > max) max = d
|
} ?: 0
|
||||||
return max
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createFoldRegion(startOffset: Int, endOffset: Int, collapse: Boolean): VimFoldRegion? {
|
override fun createFoldRegion(startOffset: Int, endOffset: Int, collapse: Boolean): VimFoldRegion? {
|
||||||
@@ -589,6 +589,27 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
|
|||||||
.minByOrNull { fold -> fold.endOffset - fold.startOffset }
|
.minByOrNull { fold -> fold.endOffset - fold.startOffset }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun calculateFoldDepth(fold: FoldRegion, allFolds: Array<FoldRegion>): Int {
|
||||||
|
return allFolds.count { otherFold ->
|
||||||
|
isWrappedBy(fold, otherFold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the inner fold is completely contained by the outer fold (allowing matching boundaries)
|
||||||
|
* but excludes identical folds.
|
||||||
|
*/
|
||||||
|
private fun isWrappedBy(inner: FoldRegion, outer: FoldRegion): Boolean {
|
||||||
|
return outer.startOffset <= inner.startOffset &&
|
||||||
|
outer.endOffset >= inner.endOffset &&
|
||||||
|
areDifferentFolds(inner, outer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun areDifferentFolds(
|
||||||
|
first: FoldRegion,
|
||||||
|
second: FoldRegion,
|
||||||
|
): Boolean = first.startOffset != second.startOffset || first.endOffset != second.endOffset
|
||||||
|
|
||||||
private fun toVimFoldRegion(ijFoldRegion: FoldRegion): VimFoldRegion {
|
private fun toVimFoldRegion(ijFoldRegion: FoldRegion): VimFoldRegion {
|
||||||
return IjVimFoldRegion(ijFoldRegion, editor)
|
return IjVimFoldRegion(ijFoldRegion, editor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import com.intellij.openapi.components.service
|
|||||||
import com.intellij.openapi.components.serviceIfCreated
|
import com.intellij.openapi.components.serviceIfCreated
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.editor.textarea.TextComponentEditorImpl
|
import com.intellij.openapi.editor.textarea.TextComponentEditorImpl
|
||||||
import com.maddyhome.idea.vim.api.AutoCmdService
|
|
||||||
import com.maddyhome.idea.vim.api.EngineEditorHelper
|
import com.maddyhome.idea.vim.api.EngineEditorHelper
|
||||||
import com.maddyhome.idea.vim.api.ExecutionContextManager
|
import com.maddyhome.idea.vim.api.ExecutionContextManager
|
||||||
import com.maddyhome.idea.vim.api.LocalOptionInitialisationScenario
|
import com.maddyhome.idea.vim.api.LocalOptionInitialisationScenario
|
||||||
@@ -63,7 +62,6 @@ import com.maddyhome.idea.vim.api.VimscriptFunctionService
|
|||||||
import com.maddyhome.idea.vim.api.VimscriptParser
|
import com.maddyhome.idea.vim.api.VimscriptParser
|
||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
import com.maddyhome.idea.vim.api.isInjectorInitialized
|
import com.maddyhome.idea.vim.api.isInjectorInitialized
|
||||||
import com.maddyhome.idea.vim.autocmd.AutoCmdImpl
|
|
||||||
import com.maddyhome.idea.vim.diagnostic.VimLogger
|
import com.maddyhome.idea.vim.diagnostic.VimLogger
|
||||||
import com.maddyhome.idea.vim.extension.ExtensionLoader
|
import com.maddyhome.idea.vim.extension.ExtensionLoader
|
||||||
import com.maddyhome.idea.vim.extension.JsonExtensionProvider
|
import com.maddyhome.idea.vim.extension.JsonExtensionProvider
|
||||||
@@ -219,8 +217,6 @@ internal class IjVimInjector : VimInjectorBase() {
|
|||||||
get() = service()
|
get() = service()
|
||||||
override val pluginActivator: VimPluginActivator
|
override val pluginActivator: VimPluginActivator
|
||||||
get() = service()
|
get() = service()
|
||||||
|
|
||||||
override val autoCmd: AutoCmdService get() = service<AutoCmdService>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,71 +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()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deactivate() {
|
/**
|
||||||
if (!active) return
|
* Turns off the ex entry field and optionally puts the focus back to the original component
|
||||||
active = false
|
*/
|
||||||
clearText()
|
fun deactivate(refocusOwningEditor: Boolean) {
|
||||||
textPane.text = ""
|
if (!myActive) return
|
||||||
if (glassPane != null) {
|
myActive = false
|
||||||
glassPane!!.removeComponentListener(resizeAdapter)
|
myText.text = ""
|
||||||
toolWindowListenerConnection?.disconnect()
|
val ed = editor
|
||||||
toolWindowListenerConnection = null
|
if (refocusOwningEditor && ed != null) {
|
||||||
glassPane!!.isVisible = false
|
requestFocus(ed.contentComponent)
|
||||||
glassPane!!.remove(this)
|
}
|
||||||
glassPane!!.setOpaque(wasOpaque)
|
if (myOldGlass != null) {
|
||||||
glassPane!!.setLayout(originalLayout)
|
myOldGlass!!.removeComponentListener(myAdapter)
|
||||||
|
myOldGlass!!.isVisible = false
|
||||||
|
myOldGlass!!.remove(this)
|
||||||
|
myOldGlass!!.setOpaque(myWasOpaque)
|
||||||
|
myOldGlass!!.setLayout(myOldLayout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns on the output panel for the given editor.
|
* Turns on the more window for the given editor
|
||||||
*/
|
*/
|
||||||
fun activate() {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,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()
|
*/
|
||||||
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
|
||||||
@@ -408,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +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.ex
|
|
||||||
|
|
||||||
import com.intellij.ui.JBColor
|
|
||||||
import com.intellij.ui.SideBorder
|
|
||||||
import com.intellij.util.ui.JBUI
|
|
||||||
import java.awt.BorderLayout
|
|
||||||
import java.awt.Color
|
|
||||||
import java.awt.FlowLayout
|
|
||||||
import java.awt.Font
|
|
||||||
import javax.swing.JLabel
|
|
||||||
import javax.swing.JPanel
|
|
||||||
import javax.swing.border.EmptyBorder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Single-row panel showing completion candidates above the command line.
|
|
||||||
* Paginates when items don't fit, keeping the selected item always visible.
|
|
||||||
*/
|
|
||||||
internal class ExCompletionPanel : JPanel(BorderLayout()) {
|
|
||||||
|
|
||||||
private val itemsPanel = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0))
|
|
||||||
|
|
||||||
private var items: List<String> = emptyList()
|
|
||||||
private var selectedIndex: Int? = null
|
|
||||||
private var pageStart: Int = 0
|
|
||||||
private var pageEnd: Int = 0
|
|
||||||
|
|
||||||
private var itemFont: Font = font
|
|
||||||
private var normalFg: Color = JBColor.foreground()
|
|
||||||
private var normalBg: Color = JBColor.background()
|
|
||||||
|
|
||||||
init {
|
|
||||||
itemsPanel.isOpaque = true
|
|
||||||
isOpaque = true
|
|
||||||
border = SideBorder(JBColor.border(), SideBorder.TOP or SideBorder.BOTTOM)
|
|
||||||
add(itemsPanel, BorderLayout.CENTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setItems(matches: List<String>, selected: Int?) {
|
|
||||||
items = matches
|
|
||||||
selectedIndex = selected
|
|
||||||
rebuildPage()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedIndex(index: Int) {
|
|
||||||
if (index == selectedIndex) return
|
|
||||||
selectedIndex = index
|
|
||||||
|
|
||||||
if (isOnCurrentPage(index)) updateHighlight() else rebuildPage()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateColors(fg: Color, bg: Color) {
|
|
||||||
normalFg = fg
|
|
||||||
normalBg = bg
|
|
||||||
background = normalBg
|
|
||||||
itemsPanel.background = normalBg
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateFont(font: Font) {
|
|
||||||
itemFont = font
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun rebuildPage() {
|
|
||||||
itemsPanel.removeAll()
|
|
||||||
if (items.isEmpty()) return refreshLayout()
|
|
||||||
if (items.size == 1) return
|
|
||||||
|
|
||||||
calculateVisibleRange()
|
|
||||||
addLabelsForRange()
|
|
||||||
refreshLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateVisibleRange() {
|
|
||||||
val selected = selectedIndex
|
|
||||||
var start = if (selected != null && selected < pageStart) selected else pageStart
|
|
||||||
var end = fitForward(start)
|
|
||||||
|
|
||||||
if (selected != null && selected >= end) {
|
|
||||||
end = selected + 1
|
|
||||||
start = fitBackward(end)
|
|
||||||
}
|
|
||||||
|
|
||||||
pageStart = start
|
|
||||||
pageEnd = end
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fitForward(from: Int): Int {
|
|
||||||
var usedWidth = 0
|
|
||||||
var end = from
|
|
||||||
while (end < items.size) {
|
|
||||||
val w = measureItem(items[end])
|
|
||||||
if (usedWidth + w > availableWidth() && end > from) break
|
|
||||||
usedWidth += w
|
|
||||||
end++
|
|
||||||
}
|
|
||||||
return end
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fitBackward(from: Int): Int {
|
|
||||||
var usedWidth = 0
|
|
||||||
var start = from
|
|
||||||
while (start > 0) {
|
|
||||||
val w = measureItem(items[start - 1])
|
|
||||||
if (usedWidth + w > availableWidth() && start < from) break
|
|
||||||
usedWidth += w
|
|
||||||
start--
|
|
||||||
}
|
|
||||||
return start
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addLabelsForRange() {
|
|
||||||
for (i in pageStart until pageEnd) {
|
|
||||||
itemsPanel.add(createLabel(items[i], isSelected = i == selectedIndex))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Highlight ---
|
|
||||||
|
|
||||||
private fun updateHighlight() {
|
|
||||||
for ((i, comp) in itemsPanel.components.withIndex()) {
|
|
||||||
if (comp is JLabel) styleLabel(comp, isSelected = pageStart + i == selectedIndex)
|
|
||||||
}
|
|
||||||
repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Label factory ---
|
|
||||||
|
|
||||||
private fun createLabel(text: String, isSelected: Boolean): JLabel {
|
|
||||||
return JLabel(text).apply {
|
|
||||||
font = itemFont
|
|
||||||
isOpaque = true
|
|
||||||
border = ITEM_BORDER
|
|
||||||
styleLabel(this, isSelected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun styleLabel(label: JLabel, isSelected: Boolean) {
|
|
||||||
label.foreground = if (isSelected) normalBg else normalFg
|
|
||||||
label.background = if (isSelected) normalFg else normalBg
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isOnCurrentPage(index: Int) = index in pageStart until pageEnd
|
|
||||||
|
|
||||||
private fun measureItem(text: String) = getFontMetrics(itemFont).stringWidth(text) + ITEM_PADDING
|
|
||||||
|
|
||||||
private fun availableWidth() = if (width > 0) width else Int.MAX_VALUE
|
|
||||||
|
|
||||||
private fun refreshLayout() {
|
|
||||||
revalidate()
|
|
||||||
repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ITEM_PADDING = 12
|
|
||||||
private val ITEM_BORDER = JBUI.Borders.empty(2, 6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,16 +15,13 @@ 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
|
||||||
import com.maddyhome.idea.vim.action.VimShortcutKeyAction
|
import com.maddyhome.idea.vim.action.VimShortcutKeyAction
|
||||||
import com.maddyhome.idea.vim.api.CommandLineCompletion
|
|
||||||
import com.maddyhome.idea.vim.api.VimCommandLine
|
import com.maddyhome.idea.vim.api.VimCommandLine
|
||||||
import com.maddyhome.idea.vim.api.VimCommandLineCaret
|
import com.maddyhome.idea.vim.api.VimCommandLineCaret
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
@@ -42,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
|
||||||
@@ -82,46 +78,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
var context: DataContext? = null
|
var context: DataContext? = null
|
||||||
override var histIndex: Int = 0
|
override var histIndex: Int = 0
|
||||||
override var lastEntry: String? = null
|
override var lastEntry: String? = null
|
||||||
override var activeCompletion: CommandLineCompletion? = null
|
|
||||||
|
|
||||||
override fun isExCommand(): Boolean {
|
|
||||||
return getLabel().startsWith(":")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showCompletionBar(completion: CommandLineCompletion) {
|
|
||||||
if (ApplicationManager.getApplication().isUnitTestMode) return
|
|
||||||
val editor = this.ijEditor ?: return
|
|
||||||
|
|
||||||
completionPanel.updateColors(editor.colorsScheme.defaultForeground, entry.getBackground())
|
|
||||||
completionPanel.updateFont(entry.getFont())
|
|
||||||
completionPanel.setItems(completion.displayNames, completion.currentIndex)
|
|
||||||
|
|
||||||
if (!isCompletionBarVisible) {
|
|
||||||
oldGlass?.add(completionPanel)
|
|
||||||
isCompletionBarVisible = true
|
|
||||||
}
|
|
||||||
positionCompletionPanel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun selectCompletionItem(selectedIndex: Int?) {
|
|
||||||
if (!isCompletionBarVisible || selectedIndex == null) return
|
|
||||||
completionPanel.setSelectedIndex(selectedIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hideCompletionBar() {
|
|
||||||
if (!isCompletionBarVisible) return
|
|
||||||
isCompletionBarVisible = false
|
|
||||||
oldGlass?.remove(completionPanel)
|
|
||||||
oldGlass?.repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dismissCompletionIfTextChanged() {
|
|
||||||
val completion = activeCompletion ?: return
|
|
||||||
if (text != completion.expectedText) {
|
|
||||||
activeCompletion = null
|
|
||||||
hideCompletionBar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val ijEditor: Editor?
|
val ijEditor: Editor?
|
||||||
get() = if (weakEditor != null) weakEditor!!.get() else null
|
get() = if (weakEditor != null) weakEditor!!.get() else null
|
||||||
@@ -187,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)
|
||||||
@@ -212,8 +163,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
if (!this.isActive) return
|
if (!this.isActive) return
|
||||||
|
|
||||||
clearPromptCharacter()
|
clearPromptCharacter()
|
||||||
hideCompletionBar()
|
|
||||||
activeCompletion = null
|
|
||||||
try {
|
try {
|
||||||
entry.document.removeDocumentListener(fontListener)
|
entry.document.removeDocumentListener(fontListener)
|
||||||
// incsearch won't change in the lifetime of this activation
|
// incsearch won't change in the lifetime of this activation
|
||||||
@@ -246,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)
|
||||||
@@ -296,7 +243,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val incSearchDocumentListener: DocumentListener = object : DocumentAdapter() {
|
private val incSearchDocumentListener: DocumentListener = object : DocumentAdapter() {
|
||||||
override fun textChanged(e: DocumentEvent) {
|
override fun textChanged(e: DocumentEvent) {
|
||||||
try {
|
try {
|
||||||
@@ -532,25 +478,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
setBounds(bounds)
|
setBounds(bounds)
|
||||||
repaint()
|
repaint()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCompletionBarVisible) {
|
|
||||||
positionCompletionPanel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun positionCompletionPanel() {
|
|
||||||
val myBounds = bounds
|
|
||||||
if (myBounds.width == 0) return
|
|
||||||
|
|
||||||
val completionHeight = completionPanel.preferredSize.height
|
|
||||||
completionPanel.setBounds(
|
|
||||||
myBounds.x,
|
|
||||||
myBounds.y - completionHeight,
|
|
||||||
myBounds.width,
|
|
||||||
completionHeight,
|
|
||||||
)
|
|
||||||
completionPanel.revalidate()
|
|
||||||
completionPanel.repaint()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val isIncSearchEnabled: Boolean
|
private val isIncSearchEnabled: Boolean
|
||||||
@@ -571,8 +498,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
private var oldGlass: JComponent? = null
|
private var oldGlass: JComponent? = null
|
||||||
private var oldLayout: LayoutManager? = null
|
private var oldLayout: LayoutManager? = null
|
||||||
private var wasOpaque = false
|
private var wasOpaque = false
|
||||||
private val completionPanel = ExCompletionPanel()
|
|
||||||
private var isCompletionBarVisible = false
|
|
||||||
|
|
||||||
// incsearch stuff
|
// incsearch stuff
|
||||||
private var verticalOffset = 0
|
private var verticalOffset = 0
|
||||||
@@ -585,8 +510,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var toolWindowListenerConnection: MessageBusConnection? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
val layout = GridBagLayout()
|
val layout = GridBagLayout()
|
||||||
@@ -621,13 +544,10 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
entry.updateText(string)
|
entry.updateText(string)
|
||||||
if (updateLastEntry) entry.saveLastEntry()
|
if (updateLastEntry) entry.saveLastEntry()
|
||||||
caret.offset = min(offset, text.length)
|
caret.offset = min(offset, text.length)
|
||||||
|
|
||||||
dismissCompletionIfTextChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteText(offset: Int, length: Int) {
|
override fun deleteText(offset: Int, length: Int) {
|
||||||
entry.deleteText(offset, length)
|
entry.deleteText(offset, length)
|
||||||
dismissCompletionIfTextChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun insertText(offset: Int, string: String) {
|
override fun insertText(offset: Int, string: String) {
|
||||||
@@ -636,7 +556,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
|||||||
entry.deleteText(offset, string.length)
|
entry.deleteText(offset, string.length)
|
||||||
}
|
}
|
||||||
entry.insertText(offset, string)
|
entry.insertText(offset, string)
|
||||||
dismissCompletionIfTextChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearCurrentAction() {
|
override fun clearCurrentAction() {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
|||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
import org.jetbrains.plugins.ideavim.VimTestCase
|
||||||
import org.jetbrains.plugins.ideavim.waitAndAssert
|
import org.jetbrains.plugins.ideavim.waitAndAssert
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
@@ -152,27 +151,6 @@ class CopyActionTest : VimTestCase() {
|
|||||||
assertTrue(KeyHandler.getInstance().keyHandlerState.commandBuilder.isEmpty)
|
assertTrue(KeyHandler.getInstance().keyHandlerState.commandBuilder.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regression test: CommandBuilder.isEmpty must return false while waiting for a register character.
|
|
||||||
// Previously, isRegisterPending was not checked in isEmpty, so `"<Esc>` would incorrectly trigger
|
|
||||||
// an error indicator (beep) because EditorResetConsumer treated the builder as empty.
|
|
||||||
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
|
|
||||||
@Test
|
|
||||||
fun `test command builder is not empty while register is pending`() {
|
|
||||||
configureByText("hello world")
|
|
||||||
// Typing `"` starts register selection - command builder should NOT be empty
|
|
||||||
typeText("\"")
|
|
||||||
assertFalse(
|
|
||||||
KeyHandler.getInstance().keyHandlerState.commandBuilder.isEmpty,
|
|
||||||
"Command builder must not be empty while waiting for register character",
|
|
||||||
)
|
|
||||||
// Pressing Escape cancels register selection - command builder should be empty again
|
|
||||||
typeText("<Esc>")
|
|
||||||
assertTrue(
|
|
||||||
KeyHandler.getInstance().keyHandlerState.commandBuilder.isEmpty,
|
|
||||||
"Command builder must be empty after cancelling register selection",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testWrongYankQuoteYankLine() {
|
fun testWrongYankQuoteYankLine() {
|
||||||
assertPluginError(false)
|
assertPluginError(false)
|
||||||
|
|||||||
@@ -1,37 +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 org.jetbrains.plugins.ideavim.action
|
|
||||||
|
|
||||||
import com.maddyhome.idea.vim.action.VimShortcutKeyAction
|
|
||||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
|
||||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import java.awt.event.InputEvent
|
|
||||||
import java.awt.event.KeyEvent
|
|
||||||
import javax.swing.KeyStroke
|
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class VimShortcutKeyActionTest : VimTestCase() {
|
|
||||||
|
|
||||||
@TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING)
|
|
||||||
@Test
|
|
||||||
fun `plain Tab is a Vim-only editor key`() {
|
|
||||||
val tab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0)
|
|
||||||
assertTrue(VimShortcutKeyAction.VIM_ONLY_EDITOR_KEYS.contains(tab))
|
|
||||||
}
|
|
||||||
|
|
||||||
@TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING)
|
|
||||||
@Test
|
|
||||||
fun `S-Tab is not a Vim-only editor key so sethandler can release it to the IDE`() {
|
|
||||||
val shiftTab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK)
|
|
||||||
assertFalse(VimShortcutKeyAction.VIM_ONLY_EDITOR_KEYS.contains(shiftTab))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,34 +65,6 @@ class UndoActionTest : VimTestCase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test undo after visual block mode delete clears leftover native carets`() {
|
|
||||||
configureByText(
|
|
||||||
"""
|
|
||||||
${c}1. Item
|
|
||||||
2. Item
|
|
||||||
3. Item
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
|
|
||||||
typeText("<C-V>jjllx")
|
|
||||||
|
|
||||||
typeText("u")
|
|
||||||
assertState(
|
|
||||||
"""
|
|
||||||
${c}1. Item
|
|
||||||
2. Item
|
|
||||||
3. Item
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
|
|
||||||
assertMode(Mode.NORMAL())
|
|
||||||
ApplicationManager.getApplication().runReadAction {
|
|
||||||
kotlin.test.assertFalse(hasSelection())
|
|
||||||
kotlin.test.assertEquals(1, fixture.editor.caretModel.allCarets.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test undo with count`() {
|
fun `test undo with count`() {
|
||||||
val keys = listOf("dwdwdw", "2u")
|
val keys = listOf("dwdwdw", "2u")
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2003-2025 The IdeaVim authors
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by an MIT-style
|
|
||||||
* license that can be found in the LICENSE.txt file or at
|
|
||||||
* https://opensource.org/licenses/MIT.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.jetbrains.plugins.ideavim.autocmd
|
|
||||||
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.TestInfo
|
|
||||||
|
|
||||||
class AugroupTest : VimTestCase() {
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
override fun setUp(testInfo: TestInfo) {
|
|
||||||
super.setUp(testInfo)
|
|
||||||
configureByText("\n")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should register autocmd inside augroup`() {
|
|
||||||
enterCommand("augroup TestGroup")
|
|
||||||
enterCommand("autocmd InsertEnter * echo 23")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("23")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `autocmd bang inside augroup should clear only that group`() {
|
|
||||||
enterCommand("augroup G1")
|
|
||||||
enterCommand("autocmd InsertEnter * echo 1")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
enterCommand("augroup G2")
|
|
||||||
enterCommand("autocmd InsertEnter * echo 2")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
enterCommand("augroup G1")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("2")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `augroup bang should remove all handlers from group`() {
|
|
||||||
enterCommand("augroup TestGroup")
|
|
||||||
enterCommand("autocmd InsertEnter * echo 23")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
enterCommand("augroup! TestGroup")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `augroup should allow redefining group without bang (append handlers)`() {
|
|
||||||
enterCommand("augroup TestGroup")
|
|
||||||
enterCommand("autocmd InsertEnter * echo 1")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
enterCommand("augroup TestGroup")
|
|
||||||
enterCommand("autocmd InsertEnter * echo 2")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("1\n2")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `augroup bang should redefine group (drop previous handlers)`() {
|
|
||||||
enterCommand("augroup TestGroup")
|
|
||||||
enterCommand("autocmd InsertEnter * echo 1")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
enterCommand("augroup! TestGroup")
|
|
||||||
enterCommand("augroup TestGroup")
|
|
||||||
enterCommand("autocmd InsertEnter * echo 2")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("2")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should keep groups independent`() {
|
|
||||||
enterCommand("augroup G1")
|
|
||||||
enterCommand("autocmd InsertEnter * echo 1")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
enterCommand("augroup G2")
|
|
||||||
enterCommand("autocmd InsertLeave * echo 2")
|
|
||||||
enterCommand("augroup END")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("1")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
assertState(Mode.NORMAL())
|
|
||||||
assertExOutput("2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +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 org.jetbrains.plugins.ideavim.autocmd
|
|
||||||
|
|
||||||
import com.maddyhome.idea.vim.vimscript.model.commands.AutoCmdCommand
|
|
||||||
import com.maddyhome.idea.vim.vimscript.parser.VimscriptParser
|
|
||||||
import com.maddyhome.idea.vim.vimscript.parser.errors.IdeavimErrorListener
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.TestInfo
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertIs
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class AutoCmdParseTest : VimTestCase() {
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
override fun setUp(testInfo: TestInfo) {
|
|
||||||
super.setUp(testInfo)
|
|
||||||
configureByText("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseAutocmd(text: String): AutoCmdCommand {
|
|
||||||
val script = VimscriptParser.parse(text)
|
|
||||||
assertTrue(IdeavimErrorListener.testLogger.isEmpty(), "Parser errors: ${IdeavimErrorListener.testLogger}")
|
|
||||||
assertEquals(1, script.units.size)
|
|
||||||
return assertIs<AutoCmdCommand>(script.units.first())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parse single event with star pattern`() {
|
|
||||||
val cmd = parseAutocmd("autocmd InsertEnter * echo hi")
|
|
||||||
assertEquals(listOf("InsertEnter"), cmd.eventNames)
|
|
||||||
assertEquals("*", cmd.filePattern)
|
|
||||||
assertEquals("echo hi", cmd.commandText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parse single event with extension pattern`() {
|
|
||||||
val cmd = parseAutocmd("autocmd InsertEnter *.py echo hi")
|
|
||||||
assertEquals(listOf("InsertEnter"), cmd.eventNames)
|
|
||||||
assertEquals("*.py", cmd.filePattern)
|
|
||||||
assertEquals("echo hi", cmd.commandText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parse comma-separated events with pattern`() {
|
|
||||||
val cmd = parseAutocmd("autocmd InsertEnter,InsertLeave *.txt echo hi")
|
|
||||||
assertEquals(listOf("InsertEnter", "InsertLeave"), cmd.eventNames)
|
|
||||||
assertEquals("*.txt", cmd.filePattern)
|
|
||||||
assertEquals("echo hi", cmd.commandText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parse events with spaces around commas`() {
|
|
||||||
val cmd = parseAutocmd("autocmd InsertEnter , InsertLeave * echo hi")
|
|
||||||
assertEquals(listOf("InsertEnter", "InsertLeave"), cmd.eventNames)
|
|
||||||
assertEquals("*", cmd.filePattern)
|
|
||||||
assertEquals("echo hi", cmd.commandText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parse brace pattern`() {
|
|
||||||
val cmd = parseAutocmd("autocmd InsertEnter *.{py,txt} echo hi")
|
|
||||||
assertEquals(listOf("InsertEnter"), cmd.eventNames)
|
|
||||||
assertEquals("*.{py,txt}", cmd.filePattern)
|
|
||||||
assertEquals("echo hi", cmd.commandText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parse bang has no events or pattern`() {
|
|
||||||
val cmd = parseAutocmd("autocmd!")
|
|
||||||
assertTrue(cmd.eventNames.isEmpty())
|
|
||||||
assertEquals(null, cmd.filePattern)
|
|
||||||
assertEquals(null, cmd.commandText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parse command with multiple spaces`() {
|
|
||||||
val cmd = parseAutocmd("autocmd InsertEnter * echo \"hello world\"")
|
|
||||||
assertEquals("*", cmd.filePattern)
|
|
||||||
assertEquals("echo \"hello world\"", cmd.commandText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parse exact filename pattern`() {
|
|
||||||
val cmd = parseAutocmd("autocmd InsertEnter Makefile echo hi")
|
|
||||||
assertEquals(listOf("InsertEnter"), cmd.eventNames)
|
|
||||||
assertEquals("Makefile", cmd.filePattern)
|
|
||||||
assertEquals("echo hi", cmd.commandText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parse unknown event name without errors`() {
|
|
||||||
val script = VimscriptParser.parse("autocmd BufReadPost * echo hi")
|
|
||||||
assertTrue(IdeavimErrorListener.testLogger.isEmpty())
|
|
||||||
assertEquals(1, script.units.size)
|
|
||||||
val cmd = assertIs<AutoCmdCommand>(script.units.first())
|
|
||||||
assertEquals(listOf("BufReadPost"), cmd.eventNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `parse multiline autocmd without errors`() {
|
|
||||||
val script = VimscriptParser.parse(
|
|
||||||
"""
|
|
||||||
autocmd BufReadPost *
|
|
||||||
\ if line("'\"") > 0 && line ("'\"") <= line("$") |
|
|
||||||
\ exe "normal! g'\"" |
|
|
||||||
\ endif
|
|
||||||
""".trimIndent(),
|
|
||||||
)
|
|
||||||
assertEquals(1, script.units.size)
|
|
||||||
assertTrue(IdeavimErrorListener.testLogger.isEmpty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +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 org.jetbrains.plugins.ideavim.autocmd
|
|
||||||
|
|
||||||
import com.maddyhome.idea.vim.autocmd.AutoCmdPattern
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class AutoCmdPatternTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `star matches any file`() {
|
|
||||||
assertTrue(AutoCmdPattern("*").matches("/path/to/file.txt"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `star matches null path`() {
|
|
||||||
assertTrue(AutoCmdPattern("*").matches(null))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `non-star pattern does not match null path`() {
|
|
||||||
assertFalse(AutoCmdPattern("*.py").matches(null))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `extension pattern matches correct extension`() {
|
|
||||||
assertTrue(AutoCmdPattern("*.py").matches("/path/to/script.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `extension pattern does not match wrong extension`() {
|
|
||||||
assertFalse(AutoCmdPattern("*.py").matches("/path/to/script.txt"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `extension pattern matches file name only`() {
|
|
||||||
assertTrue(AutoCmdPattern("*.py").matches("/some/deep/path/test.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `brace alternation matches first option`() {
|
|
||||||
assertTrue(AutoCmdPattern("*.{py,txt}").matches("/path/to/file.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `brace alternation matches second option`() {
|
|
||||||
assertTrue(AutoCmdPattern("*.{py,txt}").matches("/path/to/file.txt"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `brace alternation does not match unlisted extension`() {
|
|
||||||
assertFalse(AutoCmdPattern("*.{py,txt}").matches("/path/to/file.kt"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `question mark matches single character`() {
|
|
||||||
assertTrue(AutoCmdPattern("?.txt").matches("/path/to/a.txt"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `question mark does not match multiple characters`() {
|
|
||||||
assertFalse(AutoCmdPattern("?.txt").matches("/path/to/ab.txt"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `exact filename matches`() {
|
|
||||||
assertTrue(AutoCmdPattern("Makefile").matches("/path/to/Makefile"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `exact filename does not match different name`() {
|
|
||||||
assertFalse(AutoCmdPattern("Makefile").matches("/path/to/Rakefile"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `pattern with path matches full path`() {
|
|
||||||
assertTrue(AutoCmdPattern("/home/user/*.py").matches("/home/user/script.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `pattern with path does not match different directory`() {
|
|
||||||
assertFalse(AutoCmdPattern("/home/user/*.py").matches("/other/path/script.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `double star matches across directories`() {
|
|
||||||
assertTrue(AutoCmdPattern("**/*.py").matches("/some/deep/path/script.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `star does not match path separators`() {
|
|
||||||
assertFalse(AutoCmdPattern("src/*.py").matches("src/sub/script.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `double star matches path separators`() {
|
|
||||||
assertTrue(AutoCmdPattern("src/**/*.py").matches("src/sub/script.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `bracket character class matches`() {
|
|
||||||
assertTrue(AutoCmdPattern("*.[ch]").matches("/path/to/file.c"))
|
|
||||||
assertTrue(AutoCmdPattern("*.[ch]").matches("/path/to/file.h"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `bracket character class does not match unlisted`() {
|
|
||||||
assertFalse(AutoCmdPattern("*.[ch]").matches("/path/to/file.o"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `dot in extension is escaped properly`() {
|
|
||||||
assertFalse(AutoCmdPattern("*.py").matches("/path/to/file_py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `prefix pattern matches`() {
|
|
||||||
assertTrue(AutoCmdPattern("test*").matches("/path/to/test_file.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `prefix pattern does not match different prefix`() {
|
|
||||||
assertFalse(AutoCmdPattern("test*").matches("/path/to/prod_file.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `multiple extensions with brace`() {
|
|
||||||
val pattern = AutoCmdPattern("*.{c,h,cpp,hpp}")
|
|
||||||
assertTrue(pattern.matches("/path/to/main.cpp"))
|
|
||||||
assertTrue(pattern.matches("/path/to/main.h"))
|
|
||||||
assertFalse(pattern.matches("/path/to/main.py"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `simple filename without extension`() {
|
|
||||||
assertTrue(AutoCmdPattern("*").matches("/path/to/Makefile"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,324 +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 org.jetbrains.plugins.ideavim.autocmd
|
|
||||||
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.TestInfo
|
|
||||||
|
|
||||||
class AutoCmdTest : VimTestCase() {
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
override fun setUp(testInfo: TestInfo) {
|
|
||||||
super.setUp(testInfo)
|
|
||||||
configureByText("\n")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
injector.outputPanel.getCurrentOutputPanel()?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute command on InsertEnter`() {
|
|
||||||
enterCommand("autocmd InsertEnter * echo \"hi\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("hi")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should do nothing on invalid syntax`() {
|
|
||||||
enterCommand("autocmd InsertEnter echo 23")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute command on InsertLeave`() {
|
|
||||||
enterCommand("autocmd InsertLeave * echo 23")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
assertState(Mode.NORMAL())
|
|
||||||
assertExOutput("23")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire InsertEnter when entering Replace mode`() {
|
|
||||||
enterCommand("autocmd InsertEnter * echo \"enter\"")
|
|
||||||
typeText(injector.parser.parseKeys("R"))
|
|
||||||
assertState(Mode.REPLACE)
|
|
||||||
assertExOutput("enter")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire InsertLeave when leaving Replace mode`() {
|
|
||||||
enterCommand("autocmd InsertLeave * echo \"leave\"")
|
|
||||||
typeText(injector.parser.parseKeys("R"))
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
assertState(Mode.NORMAL())
|
|
||||||
assertExOutput("leave")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should clear commands`() {
|
|
||||||
enterCommand("autocmd InsertEnter * echo 23")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should do nothing when pattern does not match file`() {
|
|
||||||
enterCommand("autocmd InsertEnter *.py echo 23")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute command every time InsertEnter is triggered`() {
|
|
||||||
enterCommand("autocmd InsertEnter * echo 23")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
assertExOutput("23")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
assertExOutput("23")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not execute InsertLeave command if insert mode is not left`() {
|
|
||||||
enterCommand("autocmd InsertLeave * echo 23")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute multiple handlers for same event`() {
|
|
||||||
enterCommand("autocmd InsertEnter * echo 1")
|
|
||||||
enterCommand("autocmd InsertEnter * echo 2")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
|
|
||||||
assertExOutput("1\n2")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute only matching event handlers`() {
|
|
||||||
enterCommand("autocmd InsertEnter * echo 1")
|
|
||||||
enterCommand("autocmd InsertLeave * echo 2")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("1")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
assertExOutput("2")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `autocmd bang should clear all event handlers`() {
|
|
||||||
enterCommand("autocmd InsertEnter * echo 1")
|
|
||||||
enterCommand("autocmd InsertLeave * echo 2")
|
|
||||||
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute InsertEnter when entering insert from visual mode with c`() {
|
|
||||||
configureByText("hello world")
|
|
||||||
enterCommand("autocmd InsertEnter * echo \"entering insert\"")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("viw")) // select word
|
|
||||||
typeText(injector.parser.parseKeys("c")) // change (enters insert)
|
|
||||||
|
|
||||||
assertExOutput("entering insert")
|
|
||||||
assertState(Mode.INSERT)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute InsertEnter when entering insert from visual mode with s`() {
|
|
||||||
configureByText("hello world")
|
|
||||||
enterCommand("autocmd InsertEnter * echo \"substitute\"")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("viw")) // select word
|
|
||||||
typeText(injector.parser.parseKeys("s")) // substitute (enters insert)
|
|
||||||
|
|
||||||
assertExOutput("substitute")
|
|
||||||
assertState(Mode.INSERT)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute InsertLeave after entering from visual mode`() {
|
|
||||||
configureByText("hello world")
|
|
||||||
enterCommand("autocmd InsertLeave * echo \"leaving insert\"")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("viw")) // select word
|
|
||||||
typeText(injector.parser.parseKeys("c")) // change (enters insert)
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
|
|
||||||
assertExOutput("leaving insert")
|
|
||||||
assertState(Mode.NORMAL())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute both InsertEnter and InsertLeave from visual mode`() {
|
|
||||||
configureByText("hello world")
|
|
||||||
enterCommand("autocmd InsertEnter * echo \"enter\"")
|
|
||||||
enterCommand("autocmd InsertLeave * echo \"leave\"")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("viw")) // select word
|
|
||||||
typeText(injector.parser.parseKeys("c")) // change (enters insert)
|
|
||||||
assertExOutput("enter")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
assertExOutput("leave")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should register multiple events with comma-separated syntax`() {
|
|
||||||
enterCommand("autocmd InsertEnter,InsertLeave * echo \"triggered\"")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("triggered")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
assertExOutput("triggered")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should handle spaces around commas in multiple events`() {
|
|
||||||
enterCommand("autocmd InsertEnter , InsertLeave * echo \"triggered\"")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("triggered")
|
|
||||||
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
assertExOutput("triggered")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should register three events with comma-separated syntax`() {
|
|
||||||
configureByText("hello")
|
|
||||||
enterCommand("autocmd InsertEnter,InsertLeave,BufEnter * echo \"event\"")
|
|
||||||
|
|
||||||
// InsertEnter
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("event")
|
|
||||||
|
|
||||||
// InsertLeave
|
|
||||||
typeText(injector.parser.parseKeys("<esc>"))
|
|
||||||
assertExOutput("event")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fail gracefully with invalid event in comma-separated list`() {
|
|
||||||
enterCommand("autocmd InsertEnter,InvalidEvent * echo \"test\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match file extension pattern`() {
|
|
||||||
configureByFileName("test.txt")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("autocmd InsertEnter *.txt echo \"matched\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("matched")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not match wrong file extension pattern`() {
|
|
||||||
configureByFileName("test.txt")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("autocmd InsertEnter *.py echo \"matched\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match brace alternation pattern`() {
|
|
||||||
configureByFileName("test.txt")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("autocmd InsertEnter *.{py,txt} echo \"matched\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("matched")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not match brace alternation when extension not listed`() {
|
|
||||||
configureByFileName("test.kt")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("autocmd InsertEnter *.{py,txt} echo \"matched\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match question mark single char pattern`() {
|
|
||||||
configureByFileName("test.py")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("autocmd InsertEnter *.?y echo \"matched\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("matched")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match exact filename pattern`() {
|
|
||||||
configureByFileName("Makefile")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("autocmd InsertEnter Makefile echo \"matched\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("matched")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not match different exact filename`() {
|
|
||||||
configureByFileName("Rakefile")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("autocmd InsertEnter Makefile echo \"matched\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match star pattern on any file`() {
|
|
||||||
configureByFileName("anything.xyz")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("autocmd InsertEnter * echo \"matched\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("matched")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute only commands with matching pattern`() {
|
|
||||||
configureByFileName("test.py")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("autocmd InsertEnter *.py echo \"python\"")
|
|
||||||
enterCommand("autocmd InsertEnter *.txt echo \"text\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("python")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should execute commands with star and specific pattern`() {
|
|
||||||
configureByFileName("test.py")
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
enterCommand("autocmd InsertEnter * echo \"all\"")
|
|
||||||
enterCommand("autocmd InsertEnter *.py echo \"python\"")
|
|
||||||
typeText(injector.parser.parseKeys("i"))
|
|
||||||
assertExOutput("all\npython")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +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 org.jetbrains.plugins.ideavim.autocmd
|
|
||||||
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
|
||||||
import com.intellij.openapi.command.WriteCommandAction
|
|
||||||
import com.intellij.openapi.components.ComponentManagerEx
|
|
||||||
import com.intellij.openapi.editor.Editor
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
|
||||||
import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl
|
|
||||||
import com.intellij.platform.util.coroutines.childScope
|
|
||||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
|
||||||
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
|
|
||||||
import com.intellij.testFramework.replaceService
|
|
||||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
|
||||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.TestInfo
|
|
||||||
|
|
||||||
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
|
|
||||||
class BufNewFileAutoCmdTest : VimTestCase() {
|
|
||||||
|
|
||||||
private lateinit var fileEditorManager: FileEditorManagerImpl
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
override fun setUp(testInfo: TestInfo) {
|
|
||||||
super.setUp(testInfo)
|
|
||||||
|
|
||||||
fileEditorManager =
|
|
||||||
FileEditorManagerImpl(
|
|
||||||
fixture.project,
|
|
||||||
(fixture.project as ComponentManagerEx)
|
|
||||||
.getCoroutineScope()
|
|
||||||
.childScope(name = "BufNewFileAutoCmdTestScope")
|
|
||||||
)
|
|
||||||
fixture.project.replaceService(FileEditorManager::class.java, fileEditorManager, fixture.testRootDisposable)
|
|
||||||
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
configureByText("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
override fun tearDown(testInfo: TestInfo) {
|
|
||||||
try {
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
} finally {
|
|
||||||
super.tearDown(testInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFixture(factory: IdeaTestFixtureFactory): CodeInsightTestFixture {
|
|
||||||
val fixture = factory.createFixtureBuilder("IdeaVim").fixture
|
|
||||||
return factory.createCodeInsightFixture(fixture)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire BufNewFile when creating and opening a new file`() {
|
|
||||||
enterCommand("autocmd BufNewFile * echo \"new\"")
|
|
||||||
openNewFile("fresh.txt")
|
|
||||||
assertExOutput("new")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not fire BufRead for a newly created file`() {
|
|
||||||
enterCommand("autocmd BufNewFile * echo \"new\"")
|
|
||||||
enterCommand("autocmd BufRead * echo \"read\"")
|
|
||||||
openNewFile("fresh.txt")
|
|
||||||
// Vim semantics: only BufNewFile fires for new files, BufRead is suppressed
|
|
||||||
assertExOutput("new")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire FileType alongside BufNewFile`() {
|
|
||||||
enterCommand("autocmd BufNewFile * echo \"1-new\"")
|
|
||||||
enterCommand("autocmd FileType * echo \"2-filetype\"")
|
|
||||||
openNewFile("fresh.txt")
|
|
||||||
assertExOutput("1-new\n2-filetype")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match BufNewFile against file pattern`() {
|
|
||||||
enterCommand("autocmd BufNewFile *.py echo \"py\"")
|
|
||||||
openNewFile("fresh.txt")
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openNewFile(filename: String): Editor {
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
val parent = fixture.tempDirFixture.getFile(".") ?: error("temp dir unavailable")
|
|
||||||
val file = WriteCommandAction.runWriteCommandAction<com.intellij.openapi.vfs.VirtualFile>(fixture.project) {
|
|
||||||
parent.createChildData(this, filename).apply { setBinaryContent("lorem ipsum".toByteArray()) }
|
|
||||||
}
|
|
||||||
fixture.openFileInEditor(file)
|
|
||||||
}
|
|
||||||
return fixture.editor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +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 org.jetbrains.plugins.ideavim.autocmd
|
|
||||||
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
|
||||||
import com.intellij.openapi.components.ComponentManagerEx
|
|
||||||
import com.intellij.openapi.editor.Editor
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
|
||||||
import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl
|
|
||||||
import com.intellij.platform.util.coroutines.childScope
|
|
||||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
|
||||||
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
|
|
||||||
import com.intellij.testFramework.replaceService
|
|
||||||
import com.maddyhome.idea.vim.listener.BufNewFileTracker
|
|
||||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
|
||||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.TestInfo
|
|
||||||
|
|
||||||
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
|
|
||||||
class BufReadAutoCmdTest : VimTestCase() {
|
|
||||||
|
|
||||||
private lateinit var fileEditorManager: FileEditorManagerImpl
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
override fun setUp(testInfo: TestInfo) {
|
|
||||||
super.setUp(testInfo)
|
|
||||||
|
|
||||||
fileEditorManager =
|
|
||||||
FileEditorManagerImpl(
|
|
||||||
fixture.project,
|
|
||||||
(fixture.project as ComponentManagerEx)
|
|
||||||
.getCoroutineScope()
|
|
||||||
.childScope(name = "BufReadAutoCmdTestScope")
|
|
||||||
)
|
|
||||||
fixture.project.replaceService(FileEditorManager::class.java, fileEditorManager, fixture.testRootDisposable)
|
|
||||||
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
configureByText("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
override fun tearDown(testInfo: TestInfo) {
|
|
||||||
try {
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
} finally {
|
|
||||||
super.tearDown(testInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFixture(factory: IdeaTestFixtureFactory): CodeInsightTestFixture {
|
|
||||||
val fixture = factory.createFixtureBuilder("IdeaVim").fixture
|
|
||||||
return factory.createCodeInsightFixture(fixture)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire BufRead when opening a file`() {
|
|
||||||
enterCommand("autocmd BufRead * echo \"read\"")
|
|
||||||
openFile("hello.txt")
|
|
||||||
assertExOutput("read")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire BufReadPost when opening a file`() {
|
|
||||||
enterCommand("autocmd BufReadPost * echo \"post\"")
|
|
||||||
openFile("hello.txt")
|
|
||||||
assertExOutput("post")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match BufRead against file extension`() {
|
|
||||||
enterCommand("autocmd BufRead *.txt echo \"txt\"")
|
|
||||||
openFile("hello.txt")
|
|
||||||
assertExOutput("txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not fire BufRead for non-matching pattern`() {
|
|
||||||
enterCommand("autocmd BufRead *.py echo \"py\"")
|
|
||||||
openFile("hello.txt")
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire BufRead BufReadPost and FileType in vim order`() {
|
|
||||||
// Vim order for opening an existing file: BufRead == BufReadPost → FileType → BufEnter
|
|
||||||
enterCommand("autocmd BufRead * echo \"1-read\"")
|
|
||||||
enterCommand("autocmd BufReadPost * echo \"2-readpost\"")
|
|
||||||
enterCommand("autocmd FileType * echo \"3-filetype\"")
|
|
||||||
openFile("hello.txt")
|
|
||||||
assertExOutput("1-read\n2-readpost\n3-filetype")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openFile(filename: String): Editor {
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
val file = fixture.createFile(filename, "lorem ipsum")
|
|
||||||
// Simulate opening an existing (already on-disk) file: clear the "newly created"
|
|
||||||
// marker so the open fires BufRead/BufReadPost instead of BufNewFile.
|
|
||||||
BufNewFileTracker.consumeIfNew(file.path)
|
|
||||||
fixture.openFileInEditor(file)
|
|
||||||
}
|
|
||||||
return fixture.editor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +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 org.jetbrains.plugins.ideavim.autocmd
|
|
||||||
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
|
||||||
import com.intellij.openapi.command.WriteCommandAction
|
|
||||||
import com.intellij.openapi.editor.Editor
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
|
||||||
import com.maddyhome.idea.vim.listener.BufNewFileTracker
|
|
||||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
|
||||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.TestInfo
|
|
||||||
|
|
||||||
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
|
|
||||||
class BufWriteAutoCmdTest : VimTestCase() {
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
override fun setUp(testInfo: TestInfo) {
|
|
||||||
super.setUp(testInfo)
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
configureByText("\n")
|
|
||||||
}
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
override fun tearDown(testInfo: TestInfo) {
|
|
||||||
try {
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
} finally {
|
|
||||||
super.tearDown(testInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire BufWritePre on save`() {
|
|
||||||
enterCommand("autocmd BufWritePre * echo \"pre\"")
|
|
||||||
modifyAndSave(openFile("hello.txt"))
|
|
||||||
assertExOutput("pre")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire BufWritePost on save`() {
|
|
||||||
enterCommand("autocmd BufWritePost * echo \"post\"")
|
|
||||||
modifyAndSave(openFile("hello.txt"))
|
|
||||||
assertExOutput("post")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `BufWrite should be alias for BufWritePre`() {
|
|
||||||
enterCommand("autocmd BufWrite * echo \"write\"")
|
|
||||||
modifyAndSave(openFile("hello.txt"))
|
|
||||||
assertExOutput("write")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire BufWritePre before BufWritePost`() {
|
|
||||||
enterCommand("autocmd BufWritePre * echo \"1-pre\"")
|
|
||||||
enterCommand("autocmd BufWritePost * echo \"2-post\"")
|
|
||||||
modifyAndSave(openFile("hello.txt"))
|
|
||||||
assertExOutput("1-pre\n2-post")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not fire for non-matching pattern`() {
|
|
||||||
enterCommand("autocmd BufWritePre *.py echo \"py\"")
|
|
||||||
modifyAndSave(openFile("hello.txt"))
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match pattern against file extension`() {
|
|
||||||
enterCommand("autocmd BufWritePre *.txt echo \"txt\"")
|
|
||||||
modifyAndSave(openFile("hello.txt"))
|
|
||||||
assertExOutput("txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire autocmd against saved document, not focused editor`() {
|
|
||||||
// a.py: 1 line. b.py: 5 lines. Opening b.py last makes it the focused editor.
|
|
||||||
val aEditor = openFile("a.py", "one-line")
|
|
||||||
openFile("b.py", "l1\nl2\nl3\nl4\nl5")
|
|
||||||
|
|
||||||
// line('$') reports the line count of the editor the autocmd runs against.
|
|
||||||
enterCommand("autocmd BufWritePre * echo line('$')")
|
|
||||||
modifyAndSave(aEditor)
|
|
||||||
|
|
||||||
// If the handler mistakenly ran against the focused b.py, output would be "5".
|
|
||||||
assertExOutput("1")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openFile(filename: String, content: String = "initial content"): Editor {
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
val file = fixture.createFile(filename, content)
|
|
||||||
// Clear newly-created marker so this isn't treated as BufNewFile.
|
|
||||||
BufNewFileTracker.consumeIfNew(file.path)
|
|
||||||
fixture.openFileInEditor(file)
|
|
||||||
}
|
|
||||||
return fixture.editor
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun modifyAndSave(editor: Editor) {
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
WriteCommandAction.runWriteCommandAction(fixture.project) {
|
|
||||||
editor.document.insertString(0, "x")
|
|
||||||
}
|
|
||||||
FileDocumentManager.getInstance().saveDocument(editor.document)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,198 +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 org.jetbrains.plugins.ideavim.autocmd
|
|
||||||
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
|
||||||
import com.intellij.openapi.components.ComponentManagerEx
|
|
||||||
import com.intellij.openapi.editor.Editor
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
|
||||||
import com.intellij.openapi.fileEditor.TextEditor
|
|
||||||
import com.intellij.openapi.fileEditor.impl.EditorWindow
|
|
||||||
import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl
|
|
||||||
import com.intellij.platform.util.coroutines.childScope
|
|
||||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
|
||||||
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
|
|
||||||
import com.intellij.testFramework.replaceService
|
|
||||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
|
||||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.jetbrains.plugins.ideavim.waitUntil
|
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.TestInfo
|
|
||||||
import javax.swing.SwingConstants
|
|
||||||
|
|
||||||
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
|
|
||||||
class BuffAutoCmdTest : VimTestCase() {
|
|
||||||
|
|
||||||
private lateinit var fileEditorManager: FileEditorManagerImpl
|
|
||||||
private lateinit var mainWindow: Editor
|
|
||||||
private lateinit var otherBufferWindow: Editor
|
|
||||||
private lateinit var splitWindow: Editor
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
override fun setUp(testInfo: TestInfo) {
|
|
||||||
super.setUp(testInfo)
|
|
||||||
|
|
||||||
fileEditorManager =
|
|
||||||
FileEditorManagerImpl(
|
|
||||||
fixture.project,
|
|
||||||
(fixture.project as ComponentManagerEx)
|
|
||||||
.getCoroutineScope()
|
|
||||||
.childScope(name = "BuffAutoCmdTestScope")
|
|
||||||
)
|
|
||||||
fixture.project.replaceService(FileEditorManager::class.java, fileEditorManager, fixture.testRootDisposable)
|
|
||||||
|
|
||||||
// Create a new editor that will represent a new buffer in a separate window. It will have default values
|
|
||||||
otherBufferWindow = openNewBufferWindow("bbb.txt")
|
|
||||||
|
|
||||||
var curWindow: EditorWindow? = null
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
// Create the original editor last, so that fixture.editor will point to this file
|
|
||||||
// It is STRONGLY RECOMMENDED to use mainWindow instead of fixture.editor, so we know which editor we're using
|
|
||||||
mainWindow = configureByText("\n") // aaa.txt
|
|
||||||
curWindow = fileEditorManager.currentWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
curWindow.let {
|
|
||||||
// Split the original editor into a new window, then reset the focus back to the originalEditor's EditorWindow
|
|
||||||
// We do this before setting any custom state, so it will have default values for everything
|
|
||||||
splitWindow = openSplitWindow(mainWindow) // aaa.txt
|
|
||||||
fileEditorManager.currentWindow = it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start each test with a clean autocmd list
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
override fun tearDown(testInfo: TestInfo) {
|
|
||||||
try {
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
} finally {
|
|
||||||
super.tearDown(testInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFixture(factory: IdeaTestFixtureFactory): CodeInsightTestFixture {
|
|
||||||
val fixture = factory.createFixtureBuilder("IdeaVim").fixture
|
|
||||||
return factory.createCodeInsightFixture(fixture)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should support BufEnter event`() {
|
|
||||||
enterCommand("autocmd BufEnter * echo 2")
|
|
||||||
openNewBufferWindow("test.txt")
|
|
||||||
assertExOutput("2")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should support BufLeave event`() {
|
|
||||||
enterCommand("autocmd BufLeave * echo 3")
|
|
||||||
closeWindow(otherBufferWindow)
|
|
||||||
assertExOutput("3")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire WinEnter when switching to different file`() {
|
|
||||||
enterCommand("autocmd WinEnter * echo \"win\"")
|
|
||||||
openNewBufferWindow("test.txt")
|
|
||||||
assertExOutput("win")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire WinLeave when switching to different file`() {
|
|
||||||
enterCommand("autocmd WinLeave * echo \"left\"")
|
|
||||||
openNewBufferWindow("test.txt")
|
|
||||||
assertExOutput("left")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not fire BufEnter when switching to different file with only WinEnter registered`() {
|
|
||||||
// Only WinEnter is registered — BufEnter should not produce output
|
|
||||||
enterCommand("autocmd WinEnter * echo \"win\"")
|
|
||||||
openNewBufferWindow("test.txt")
|
|
||||||
assertExOutput("win") // only WinEnter output, no BufEnter
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire events in vim order when switching to different buffer`() {
|
|
||||||
// Vim order: BufLeave → WinLeave → WinEnter → BufEnter
|
|
||||||
enterCommand("autocmd BufLeave * echo \"1-BufLeave\"")
|
|
||||||
enterCommand("autocmd WinLeave * echo \"2-WinLeave\"")
|
|
||||||
enterCommand("autocmd WinEnter * echo \"3-WinEnter\"")
|
|
||||||
enterCommand("autocmd BufEnter * echo \"4-BufEnter\"")
|
|
||||||
openNewBufferWindow("test.txt")
|
|
||||||
assertExOutput("1-BufLeave\n2-WinLeave\n3-WinEnter\n4-BufEnter")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not fire BufEnter or BufLeave when reopening same buffer`() {
|
|
||||||
// Opening a file that's already the current buffer should not fire Buf events
|
|
||||||
// (oldFile and newFile are the same path)
|
|
||||||
val currentFile = mainWindow.virtualFile!!
|
|
||||||
enterCommand("autocmd BufEnter * echo \"bufenter\"")
|
|
||||||
enterCommand("autocmd BufLeave * echo \"bufleave\"")
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
fileEditorManager.openFile(currentFile, true)
|
|
||||||
}
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openNewBufferWindow(filename: String): Editor {
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
fixture.openFileInEditor(fixture.createFile(filename, "lorem ipsum"))
|
|
||||||
}
|
|
||||||
return fixture.editor
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openSplitWindow(editor: Editor): Editor {
|
|
||||||
var splitWindow: EditorWindow? = null
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
val currentWindow = fileEditorManager.currentWindow
|
|
||||||
splitWindow = currentWindow!!.split(
|
|
||||||
SwingConstants.VERTICAL,
|
|
||||||
true,
|
|
||||||
editor.virtualFile,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
waitUntil {
|
|
||||||
splitWindow!!.allComposites.first().selectedEditor != null
|
|
||||||
}
|
|
||||||
return (splitWindow!!.allComposites.first().selectedEditor as TextEditor).editor
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the given editor
|
|
||||||
*/
|
|
||||||
private fun closeWindow(editor: Editor) {
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
// Just using fileEditorManager.closeFile(editor.virtualFile) can cause weird side effects, like opening a
|
|
||||||
// different buffer in an open editor. See FileGroup.closeFile
|
|
||||||
// But we can't just rely on the current EditorWindow. E.g., if we're trying to close a file that's not currently
|
|
||||||
// open in the current window, or is open in a split while we want to close the *other* editor...
|
|
||||||
val editorWindow = fileEditorManager.windows.first { window ->
|
|
||||||
window.allComposites.any { composite ->
|
|
||||||
composite.allEditors
|
|
||||||
.filterIsInstance<TextEditor>()
|
|
||||||
.any { textEditor -> textEditor.editor == editor }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val virtualFile = editor.virtualFile
|
|
||||||
|
|
||||||
if (virtualFile != null) {
|
|
||||||
editorWindow.closeFile(virtualFile)
|
|
||||||
editorWindow.requestFocus(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +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 org.jetbrains.plugins.ideavim.autocmd
|
|
||||||
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
|
||||||
import com.intellij.openapi.components.ComponentManagerEx
|
|
||||||
import com.intellij.openapi.editor.Editor
|
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
|
||||||
import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl
|
|
||||||
import com.intellij.platform.util.coroutines.childScope
|
|
||||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
|
||||||
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
|
|
||||||
import com.intellij.testFramework.replaceService
|
|
||||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
|
||||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.TestInfo
|
|
||||||
|
|
||||||
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
|
|
||||||
class FileTypeAutoCmdTest : VimTestCase() {
|
|
||||||
|
|
||||||
private lateinit var fileEditorManager: FileEditorManagerImpl
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
override fun setUp(testInfo: TestInfo) {
|
|
||||||
super.setUp(testInfo)
|
|
||||||
|
|
||||||
fileEditorManager =
|
|
||||||
FileEditorManagerImpl(
|
|
||||||
fixture.project,
|
|
||||||
(fixture.project as ComponentManagerEx)
|
|
||||||
.getCoroutineScope()
|
|
||||||
.childScope(name = "FileTypeAutoCmdTestScope")
|
|
||||||
)
|
|
||||||
fixture.project.replaceService(FileEditorManager::class.java, fileEditorManager, fixture.testRootDisposable)
|
|
||||||
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
configureByText("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
override fun tearDown(testInfo: TestInfo) {
|
|
||||||
try {
|
|
||||||
enterCommand("autocmd!")
|
|
||||||
} finally {
|
|
||||||
super.tearDown(testInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFixture(factory: IdeaTestFixtureFactory): CodeInsightTestFixture {
|
|
||||||
val fixture = factory.createFixtureBuilder("IdeaVim").fixture
|
|
||||||
return factory.createCodeInsightFixture(fixture)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should fire FileType when opening a file`() {
|
|
||||||
enterCommand("autocmd FileType text echo \"text-file\"")
|
|
||||||
openFile("hello.txt")
|
|
||||||
assertExOutput("text-file")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match FileType pattern against filetype name not file path`() {
|
|
||||||
// Pattern `*.txt` matches file paths, not filetype names, so it should NOT fire
|
|
||||||
enterCommand("autocmd FileType *.txt echo \"path\"")
|
|
||||||
openFile("hello.txt")
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match FileType with wildcard pattern`() {
|
|
||||||
enterCommand("autocmd FileType * echo \"any\"")
|
|
||||||
openFile("hello.txt")
|
|
||||||
assertExOutput("any")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should match FileType with alternation pattern`() {
|
|
||||||
enterCommand("autocmd FileType {text,python} echo \"matched\"")
|
|
||||||
openFile("hello.txt")
|
|
||||||
assertExOutput("matched")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `should not fire FileType for non-matching filetype`() {
|
|
||||||
enterCommand("autocmd FileType python echo \"py\"")
|
|
||||||
openFile("hello.txt")
|
|
||||||
assertNoExOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openFile(filename: String): Editor {
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
fixture.openFileInEditor(fixture.createFile(filename, "lorem ipsum"))
|
|
||||||
}
|
|
||||||
return fixture.editor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ class CommandParserTest : VimTestCase() {
|
|||||||
\ endif
|
\ endif
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
assertEquals(1, script.units.size)
|
assertEquals(0, script.units.size)
|
||||||
assertTrue(IdeavimErrorListener.testLogger.isEmpty())
|
assertTrue(IdeavimErrorListener.testLogger.isEmpty())
|
||||||
|
|
||||||
script = VimscriptParser.parse(
|
script = VimscriptParser.parse(
|
||||||
@@ -369,7 +369,7 @@ class CommandParserTest : VimTestCase() {
|
|||||||
autocmd BufReadPost * echo "oh, hi Mark"
|
autocmd BufReadPost * echo "oh, hi Mark"
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
assertEquals(1, script.units.size)
|
assertEquals(0, script.units.size)
|
||||||
assertTrue(IdeavimErrorListener.testLogger.isEmpty())
|
assertTrue(IdeavimErrorListener.testLogger.isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,20 +9,13 @@
|
|||||||
package org.jetbrains.plugins.ideavim.ex
|
package org.jetbrains.plugins.ideavim.ex
|
||||||
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.testFramework.LoggedErrorProcessor
|
|
||||||
import com.maddyhome.idea.vim.KeyHandler
|
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
import com.maddyhome.idea.vim.VimPlugin
|
||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
import com.maddyhome.idea.vim.newapi.vim
|
import com.maddyhome.idea.vim.newapi.vim
|
||||||
import com.maddyhome.idea.vim.state.mode.Mode
|
|
||||||
import org.jetbrains.plugins.ideavim.OnlyThrowLoggedErrorProcessor
|
|
||||||
import org.jetbrains.plugins.ideavim.action.ex.VimExTestCase
|
import org.jetbrains.plugins.ideavim.action.ex.VimExTestCase
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertDoesNotThrow
|
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
import kotlin.test.assertNull
|
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class ExEntryTest : VimExTestCase() {
|
class ExEntryTest : VimExTestCase() {
|
||||||
@@ -217,56 +210,4 @@ class ExEntryTest : VimExTestCase() {
|
|||||||
typeText(":echo <C-V>x80")
|
typeText(":echo <C-V>x80")
|
||||||
assertRenderedExText("echo <80>")
|
assertRenderedExText("echo <80>")
|
||||||
}
|
}
|
||||||
|
|
||||||
// VIM-4115: closing the command line alongside fullReset() must clear editor mode and the
|
|
||||||
// KeyHandler's commandLineCommandBuilder, not just deactivate the panel. Without close(), the
|
|
||||||
// KeyHandler singleton retains the CMD_LINE builder across plugin disable/enable and the next
|
|
||||||
// Esc NPEs in CommandKeyConsumer.
|
|
||||||
@Test
|
|
||||||
fun `test VIM-4115 close before fullReset clears all command line state`() {
|
|
||||||
typeText(":set incsearch")
|
|
||||||
assertExIsActive()
|
|
||||||
assertTrue(fixture.editor.vim.mode is Mode.CMD_LINE)
|
|
||||||
assertNotNull(KeyHandler.getInstance().keyHandlerState.commandLineCommandBuilder)
|
|
||||||
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
val commandLine = injector.commandLine
|
|
||||||
commandLine.getActiveCommandLine()?.close(refocusOwningEditor = true, resetCaret = false)
|
|
||||||
commandLine.fullReset()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertExIsDeactivated()
|
|
||||||
assertFalse(fixture.editor.vim.mode is Mode.CMD_LINE)
|
|
||||||
assertNull(KeyHandler.getInstance().keyHandlerState.commandLineCommandBuilder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VIM-4115: if some other path still desyncs command-line state (panel gone but
|
|
||||||
// commandLineCommandBuilder set with the CMD_LINE trie), Esc in the editor must not NPE. The
|
|
||||||
// defensive branch logs an error and clears the leftover builder.
|
|
||||||
@Test
|
|
||||||
fun `test VIM-4115 escape with stale command line builder does not crash`() {
|
|
||||||
typeText(":set incsearch")
|
|
||||||
assertExIsActive()
|
|
||||||
|
|
||||||
// Reproduce the pre-fix plugin-disable state: panel gone, builder and mode left behind. Use
|
|
||||||
// INSERT (not NORMAL) so EditorResetConsumer won't claim Esc and the key actually reaches
|
|
||||||
// CommandKeyConsumer where the crash lives.
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
injector.commandLine.fullReset()
|
|
||||||
fixture.editor.vim.mode = Mode.INSERT
|
|
||||||
}
|
|
||||||
assertNotNull(KeyHandler.getInstance().keyHandlerState.commandLineCommandBuilder)
|
|
||||||
|
|
||||||
// The defensive path logs an error; rethrow it so we can assert no NPE slips through.
|
|
||||||
try {
|
|
||||||
LoggedErrorProcessor.executeWith<Throwable>(OnlyThrowLoggedErrorProcessor) {
|
|
||||||
assertDoesNotThrow { typeText("<Esc>") }
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
val message = generateSequence(e) { it.cause }.mapNotNull { it.message }.joinToString(" / ")
|
|
||||||
assertTrue(message.contains("VIM-4115"), "Expected VIM-4115 logger.error, got: $message")
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNull(KeyHandler.getInstance().keyHandlerState.commandLineCommandBuilder)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,20 +191,6 @@ class CmdCommandTest : VimTestCase() {
|
|||||||
assertPluginError(true)
|
assertPluginError(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test add command with nargs but missing name should not crash`() {
|
|
||||||
// Regression test: alias[0] on an empty string threw IndexOutOfBoundsException
|
|
||||||
// when only -nargs was provided without a command name (e.g. "command -nargs=0")
|
|
||||||
VimPlugin.getCommand().resetAliases()
|
|
||||||
configureByText("\n")
|
|
||||||
typeText(commandToKeys("command -nargs=0"))
|
|
||||||
assertPluginError(true)
|
|
||||||
typeText(commandToKeys("command! -nargs=1"))
|
|
||||||
assertPluginError(true)
|
|
||||||
typeText(commandToKeys("command -nargs=*"))
|
|
||||||
assertPluginError(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test run command with arguments`() {
|
fun `test run command with arguments`() {
|
||||||
VimPlugin.getCommand().resetAliases()
|
VimPlugin.getCommand().resetAliases()
|
||||||
|
|||||||
@@ -1,431 +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 org.jetbrains.plugins.ideavim.ex.implementation.commands
|
|
||||||
|
|
||||||
import com.intellij.openapi.vfs.LocalFileSystem
|
|
||||||
import org.jetbrains.plugins.ideavim.action.ex.VimExTestCase
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.TestInfo
|
|
||||||
import org.junit.jupiter.api.io.TempDir
|
|
||||||
import java.nio.file.Path
|
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
import kotlin.io.path.createDirectories
|
|
||||||
import kotlin.io.path.createFile
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
class CommandLineCompletionTest : VimExTestCase() {
|
|
||||||
|
|
||||||
@TempDir
|
|
||||||
lateinit var tempDir: Path
|
|
||||||
|
|
||||||
private lateinit var tempPath: String
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
override fun setUp(testInfo: TestInfo) {
|
|
||||||
super.setUp(testInfo)
|
|
||||||
createTestFiles()
|
|
||||||
tempPath = tempDir.absolutePathString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createTestFiles() {
|
|
||||||
tempDir.resolve("alpha.txt").createFile()
|
|
||||||
tempDir.resolve("beta.txt").createFile()
|
|
||||||
tempDir.resolve("bravo.kt").createFile()
|
|
||||||
tempDir.resolve("subdir").createDirectories()
|
|
||||||
tempDir.resolve("subdir/nested.txt").createFile()
|
|
||||||
tempDir.resolve("subdir/notes.md").createFile()
|
|
||||||
|
|
||||||
// Make sure VFS knows about these files
|
|
||||||
LocalFileSystem.getInstance().refreshAndFindFileByNioFile(tempDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab completes first file match`() {
|
|
||||||
typeText(":edit $tempPath/a<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab cycles through matches`() {
|
|
||||||
typeText(":edit $tempPath/b<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab wraps around to first match`() {
|
|
||||||
typeText(":edit $tempPath/b<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test shift tab cycles backwards`() {
|
|
||||||
typeText(":edit $tempPath/b<S-Tab>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test shift tab then tab`() {
|
|
||||||
typeText(":edit $tempPath/b<S-Tab>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab completes directory with trailing slash`() {
|
|
||||||
typeText(":edit $tempPath/s<Tab>")
|
|
||||||
assertExText("edit $tempPath/subdir/")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab lists all files when prefix is empty`() {
|
|
||||||
typeText(":edit $tempPath/<Tab>")
|
|
||||||
// First match alphabetically
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab with no matches does not change text`() {
|
|
||||||
typeText(":edit $tempPath/zzz<Tab>")
|
|
||||||
assertExText("edit $tempPath/zzz")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab does nothing in search mode`() {
|
|
||||||
typeText("/search<Tab>")
|
|
||||||
// Tab is not handled by the completion action in search mode,
|
|
||||||
// but it's still consumed by the action framework -- no literal tab inserted
|
|
||||||
assertExText("search")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab works with abbreviated command`() {
|
|
||||||
typeText(":e $tempPath/a<Tab>")
|
|
||||||
assertExText("e $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab works with write command`() {
|
|
||||||
typeText(":w $tempPath/a<Tab>")
|
|
||||||
assertExText("w $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab does not complete for commands without file completion`() {
|
|
||||||
typeText(":set foo<Tab>")
|
|
||||||
// Tab is consumed by the action but set has no completion type registered
|
|
||||||
assertExText("set foo")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test typing after completion invalidates session`() {
|
|
||||||
typeText(":edit $tempPath/b<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
// Type a character -- this changes text, invalidating the completion session
|
|
||||||
typeText("x")
|
|
||||||
assertExText("edit $tempPath/beta.txtx")
|
|
||||||
|
|
||||||
// Tab starts a fresh completion for prefix "beta.txtx" -- no matches
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txtx")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test undo after completion resumes cycling`() {
|
|
||||||
typeText(":edit $tempPath/b<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
// Type and undo -- text reverts to the expected completion text
|
|
||||||
typeText("x")
|
|
||||||
typeText("<BS>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
// Tab resumes cycling since text matches the active completion
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test completion with single match`() {
|
|
||||||
typeText(":edit $tempPath/al<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
|
|
||||||
// Tab again cycles (single match wraps to itself)
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test completion is case insensitive`() {
|
|
||||||
typeText(":edit $tempPath/A<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Arrow key completion cycling tests ---
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test right arrow cycles forward after tab`() {
|
|
||||||
typeText(":edit $tempPath/b<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test left arrow cycles backward after tab`() {
|
|
||||||
typeText(":edit $tempPath/b<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test right arrow wraps around to first match`() {
|
|
||||||
typeText(":edit $tempPath/b<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test left arrow wraps around to last match`() {
|
|
||||||
typeText(":edit $tempPath/b<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test right then left returns to same match`() {
|
|
||||||
typeText(":edit $tempPath/b<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab then right continues cycling`() {
|
|
||||||
typeText(":edit $tempPath/<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab then left goes back`() {
|
|
||||||
typeText(":edit $tempPath/<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test shift tab then left continues backward`() {
|
|
||||||
typeText(":edit $tempPath/b<S-Tab>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test shift tab then right reverses direction`() {
|
|
||||||
typeText(":edit $tempPath/b<S-Tab>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test right arrow with single match stays on same item`() {
|
|
||||||
typeText(":edit $tempPath/al<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test left arrow with single match stays on same item`() {
|
|
||||||
typeText(":edit $tempPath/al<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test right arrow without completion moves caret`() {
|
|
||||||
typeText(":edit foo")
|
|
||||||
assertExText("edit foo")
|
|
||||||
|
|
||||||
val offsetBefore = exEntryPanel.caret.offset
|
|
||||||
typeText("<Left>")
|
|
||||||
assertEquals(offsetBefore - 1, exEntryPanel.caret.offset)
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertEquals(offsetBefore, exEntryPanel.caret.offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test typing after arrow completion invalidates session`() {
|
|
||||||
typeText(":edit $tempPath/b<Tab>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("x")
|
|
||||||
assertExText("edit $tempPath/bravo.ktx")
|
|
||||||
|
|
||||||
// Arrow key now moves caret instead of cycling
|
|
||||||
val offsetBefore = exEntryPanel.caret.offset
|
|
||||||
typeText("<Left>")
|
|
||||||
assertEquals(offsetBefore - 1, exEntryPanel.caret.offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test arrow keys with no matches do not change text`() {
|
|
||||||
typeText(":edit $tempPath/zzz<Tab>")
|
|
||||||
assertExText("edit $tempPath/zzz")
|
|
||||||
|
|
||||||
// No active completion, so arrows move caret
|
|
||||||
val offsetBefore = exEntryPanel.caret.offset
|
|
||||||
typeText("<Left>")
|
|
||||||
assertEquals(offsetBefore - 1, exEntryPanel.caret.offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test mixed tab and arrow key cycling`() {
|
|
||||||
typeText(":edit $tempPath/<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<S-Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test arrow cycles through all files with empty prefix`() {
|
|
||||||
typeText(":edit $tempPath/<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/subdir/")
|
|
||||||
|
|
||||||
// Wraps
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test left arrow cycles all files backwards with empty prefix`() {
|
|
||||||
typeText(":edit $tempPath/<Tab>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/subdir/")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/bravo.kt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/beta.txt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/alpha.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Subdirectory completion tests ---
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab completes inside subdirectory`() {
|
|
||||||
typeText(":edit $tempPath/subdir/ne<Tab>")
|
|
||||||
assertExText("edit $tempPath/subdir/nested.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tab cycles through files in subdirectory`() {
|
|
||||||
typeText(":edit $tempPath/subdir/n<Tab>")
|
|
||||||
assertExText("edit $tempPath/subdir/nested.txt")
|
|
||||||
|
|
||||||
typeText("<Tab>")
|
|
||||||
assertExText("edit $tempPath/subdir/notes.md")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test right arrow cycles in subdirectory`() {
|
|
||||||
typeText(":edit $tempPath/subdir/n<Tab>")
|
|
||||||
assertExText("edit $tempPath/subdir/nested.txt")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/subdir/notes.md")
|
|
||||||
|
|
||||||
typeText("<Right>")
|
|
||||||
assertExText("edit $tempPath/subdir/nested.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test left arrow cycles backwards in subdirectory`() {
|
|
||||||
typeText(":edit $tempPath/subdir/n<Tab>")
|
|
||||||
assertExText("edit $tempPath/subdir/nested.txt")
|
|
||||||
|
|
||||||
typeText("<Left>")
|
|
||||||
assertExText("edit $tempPath/subdir/notes.md")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ class SetCommandTest : VimTestCase() {
|
|||||||
|nohlsearch nonumber nosneak wrap
|
|nohlsearch nonumber nosneak wrap
|
||||||
| ide=IntelliJ IDEA operatorfunc= startofline wrapscan
|
| ide=IntelliJ IDEA operatorfunc= startofline wrapscan
|
||||||
| clipboard=ideaput,autoselect
|
| clipboard=ideaput,autoselect
|
||||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
|
||||||
| fileencoding=utf-8
|
| fileencoding=utf-8
|
||||||
| guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175
|
| guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175
|
||||||
|noideacopypreprocess
|
|noideacopypreprocess
|
||||||
@@ -259,7 +258,6 @@ class SetCommandTest : VimTestCase() {
|
|||||||
| clipboard=ideaput,autoselect
|
| clipboard=ideaput,autoselect
|
||||||
| colorcolumn=
|
| colorcolumn=
|
||||||
|nocommentary
|
|nocommentary
|
||||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
|
||||||
|nocursorline
|
|nocursorline
|
||||||
|nodigraph
|
|nodigraph
|
||||||
|noexchange
|
|noexchange
|
||||||
|
|||||||
@@ -449,7 +449,6 @@ class SetglobalCommandTest : VimTestCase() {
|
|||||||
|nohlsearch operatorfunc= nosurround
|
|nohlsearch operatorfunc= nosurround
|
||||||
| ide=IntelliJ IDEA norelativenumber notextobj-entire
|
| ide=IntelliJ IDEA norelativenumber notextobj-entire
|
||||||
| clipboard=ideaput,autoselect
|
| clipboard=ideaput,autoselect
|
||||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
|
||||||
| guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175
|
| guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175
|
||||||
|noideacopypreprocess
|
|noideacopypreprocess
|
||||||
| idearefactormode=select
|
| idearefactormode=select
|
||||||
@@ -513,7 +512,6 @@ class SetglobalCommandTest : VimTestCase() {
|
|||||||
| clipboard=ideaput,autoselect
|
| clipboard=ideaput,autoselect
|
||||||
| colorcolumn=
|
| colorcolumn=
|
||||||
|nocommentary
|
|nocommentary
|
||||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
|
||||||
|nocursorline
|
|nocursorline
|
||||||
|nodigraph
|
|nodigraph
|
||||||
|noexchange
|
|noexchange
|
||||||
|
|||||||
@@ -500,7 +500,6 @@ class SetlocalCommandTest : VimTestCase() {
|
|||||||
|nohlsearch nrformats=hex nosmartcase wrap
|
|nohlsearch nrformats=hex nosmartcase wrap
|
||||||
| ide=IntelliJ IDEA nonumber nosneak wrapscan
|
| ide=IntelliJ IDEA nonumber nosneak wrapscan
|
||||||
| clipboard=ideaput,autoselect
|
| clipboard=ideaput,autoselect
|
||||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
|
||||||
| fileencoding=utf-8
|
| fileencoding=utf-8
|
||||||
| guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175
|
| guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175
|
||||||
|--ideacopypreprocess
|
|--ideacopypreprocess
|
||||||
@@ -564,7 +563,6 @@ class SetlocalCommandTest : VimTestCase() {
|
|||||||
| clipboard=ideaput,autoselect
|
| clipboard=ideaput,autoselect
|
||||||
| colorcolumn=
|
| colorcolumn=
|
||||||
|nocommentary
|
|nocommentary
|
||||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
|
||||||
|nocursorline
|
|nocursorline
|
||||||
|nodigraph
|
|nodigraph
|
||||||
|noexchange
|
|noexchange
|
||||||
|
|||||||
@@ -1,80 +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 org.jetbrains.plugins.ideavim.ex.implementation.commands
|
|
||||||
|
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
|
||||||
import com.intellij.openapi.command.WriteCommandAction
|
|
||||||
import com.intellij.openapi.editor.Editor
|
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
|
||||||
import com.maddyhome.idea.vim.listener.BufNewFileTracker
|
|
||||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
|
||||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
|
|
||||||
class UpdateCommandTest : VimTestCase() {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `update saves modified buffer`() {
|
|
||||||
val editor = openFile("hello.txt")
|
|
||||||
modifyDocument(editor)
|
|
||||||
val fdm = FileDocumentManager.getInstance()
|
|
||||||
assertTrue(fdm.isDocumentUnsaved(editor.document))
|
|
||||||
|
|
||||||
enterCommand("update")
|
|
||||||
|
|
||||||
assertPluginError(false)
|
|
||||||
assertFalse(fdm.isDocumentUnsaved(editor.document))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `update is noop when buffer is not modified`() {
|
|
||||||
val editor = openFile("hello.txt")
|
|
||||||
val fdm = FileDocumentManager.getInstance()
|
|
||||||
assertFalse(fdm.isDocumentUnsaved(editor.document))
|
|
||||||
|
|
||||||
enterCommand("update")
|
|
||||||
|
|
||||||
assertPluginError(false)
|
|
||||||
assertFalse(fdm.isDocumentUnsaved(editor.document))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `update short form saves modified buffer`() {
|
|
||||||
val editor = openFile("hello.txt")
|
|
||||||
modifyDocument(editor)
|
|
||||||
val fdm = FileDocumentManager.getInstance()
|
|
||||||
assertTrue(fdm.isDocumentUnsaved(editor.document))
|
|
||||||
|
|
||||||
enterCommand("up")
|
|
||||||
|
|
||||||
assertPluginError(false)
|
|
||||||
assertFalse(fdm.isDocumentUnsaved(editor.document))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openFile(filename: String, content: String = "initial content"): Editor {
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
val file = fixture.createFile(filename, content)
|
|
||||||
BufNewFileTracker.consumeIfNew(file.path)
|
|
||||||
fixture.openFileInEditor(file)
|
|
||||||
}
|
|
||||||
return fixture.editor
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun modifyDocument(editor: Editor) {
|
|
||||||
ApplicationManager.getApplication().invokeAndWait {
|
|
||||||
WriteCommandAction.runWriteCommandAction(fixture.project) {
|
|
||||||
editor.document.insertString(0, "x")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class CommandTests : VimTestCase() {
|
|||||||
augroup END
|
augroup END
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
assertEquals(4, script.units.size)
|
assertEquals(2, script.units.size)
|
||||||
assertTrue(script.units[0] is PlugCommand)
|
assertTrue(script.units[0] is PlugCommand)
|
||||||
assertTrue(script.units[1] is SetCommand)
|
assertTrue(script.units[1] is SetCommand)
|
||||||
}
|
}
|
||||||
@@ -150,9 +150,8 @@ class CommandTests : VimTestCase() {
|
|||||||
set nu rnu
|
set nu rnu
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
// `augroup myCmds` and `augroup END` are two units; `au smthing` is malformed and dropped by the parser.
|
assertEquals(2, script.units.size)
|
||||||
assertEquals(4, script.units.size)
|
assertTrue(script.units[0] is PlugCommand)
|
||||||
assertTrue(script.units[2] is PlugCommand)
|
assertTrue(script.units[1] is SetCommand)
|
||||||
assertTrue(script.units[3] is SetCommand)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,48 +47,6 @@ class VimArgTextObjExtensionTest : VimTestCase() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VIM-4193: daa must operate per-caret when multiple carets are active.
|
|
||||||
@Test
|
|
||||||
fun testDeleteAnArgumentWithMultipleCarets() {
|
|
||||||
doTest(
|
|
||||||
Lists.newArrayList("daa"),
|
|
||||||
"""
|
|
||||||
fun test() {
|
|
||||||
println(<caret>"abc", 1)
|
|
||||||
println(<caret>"def", 2)
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
fun test() {
|
|
||||||
println(<caret>1)
|
|
||||||
println(<caret>2)
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
Mode.NORMAL(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VIM-4193: dia must operate per-caret when multiple carets are active.
|
|
||||||
@Test
|
|
||||||
fun testDeleteInnerArgumentWithMultipleCarets() {
|
|
||||||
doTest(
|
|
||||||
Lists.newArrayList("dia"),
|
|
||||||
"""
|
|
||||||
fun test() {
|
|
||||||
println(<caret>"abc", 1)
|
|
||||||
println(<caret>"def", 2)
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
fun test() {
|
|
||||||
println(<caret>, 1)
|
|
||||||
println(<caret>, 2)
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
Mode.NORMAL(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testChangeInnerArgument() {
|
fun testChangeInnerArgument() {
|
||||||
doTest(
|
doTest(
|
||||||
|
|||||||
@@ -1,57 +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 org.jetbrains.plugins.ideavim.extension.replacewithregister
|
|
||||||
|
|
||||||
import com.maddyhome.idea.vim.VimPlugin
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.command.MappingMode
|
|
||||||
import com.maddyhome.idea.vim.key.ToKeysMappingInfo
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
// Reproduction for VIM-4180: user's `nmap gr <nop>` in .ideavimrc is overridden by the
|
|
||||||
// ReplaceWithRegister extension's default mappings when they are applied after the vimrc
|
|
||||||
// execution as part of the delayed extension-init flow.
|
|
||||||
class ReplaceWithRegisterMapOverrideTest : VimTestCase() {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `user nmap gr nop is not overridden by plugin default`() {
|
|
||||||
configureByText("hello")
|
|
||||||
|
|
||||||
injector.vimscriptExecutor.executingVimscript = true
|
|
||||||
injector.vimscriptExecutor.executingIdeaVimRcConfiguration = true
|
|
||||||
executeVimscript(
|
|
||||||
"""
|
|
||||||
set ReplaceWithRegister
|
|
||||||
xmap s <Plug>ReplaceWithRegisterVisual
|
|
||||||
nmap s <Plug>ReplaceWithRegisterOperator
|
|
||||||
nmap ss <Plug>ReplaceWithRegisterLine
|
|
||||||
nmap gr <nop>
|
|
||||||
nmap grr <nop>
|
|
||||||
vmap gr <nop>
|
|
||||||
""".trimIndent(),
|
|
||||||
skipHistory = false,
|
|
||||||
)
|
|
||||||
injector.vimscriptExecutor.executingIdeaVimRcConfiguration = false
|
|
||||||
injector.vimscriptExecutor.executingVimscript = false
|
|
||||||
|
|
||||||
val nop = injector.parser.parseKeys("<nop>")
|
|
||||||
val grKeys = injector.parser.parseKeys("gr")
|
|
||||||
val grrKeys = injector.parser.parseKeys("grr")
|
|
||||||
|
|
||||||
val nGr = VimPlugin.getKey().getKeyMapping(MappingMode.NORMAL)[grKeys]
|
|
||||||
val nGrr = VimPlugin.getKey().getKeyMapping(MappingMode.NORMAL)[grrKeys]
|
|
||||||
val vGr = VimPlugin.getKey().getKeyMapping(MappingMode.VISUAL)[grKeys]
|
|
||||||
|
|
||||||
assertEquals(nop, (nGr as? ToKeysMappingInfo)?.toKeys, "normal gr should map to <nop>")
|
|
||||||
assertEquals(nop, (nGrr as? ToKeysMappingInfo)?.toKeys, "normal grr should map to <nop>")
|
|
||||||
assertEquals(nop, (vGr as? ToKeysMappingInfo)?.toKeys, "visual gr should map to <nop>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -256,30 +256,6 @@ class VimIndentObjectTest : VimTestCase() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VIM-4193: dii must operate per-caret when multiple carets are active —
|
|
||||||
// each caret must delete the indent block at its own location, not the
|
|
||||||
// primary caret's block for every caret.
|
|
||||||
@Test
|
|
||||||
fun testDeleteInnerIndentWithMultipleCarets() {
|
|
||||||
doTest(
|
|
||||||
"dii",
|
|
||||||
"""
|
|
||||||
one
|
|
||||||
<caret>aa
|
|
||||||
bb
|
|
||||||
two
|
|
||||||
<caret>cc
|
|
||||||
dd
|
|
||||||
three
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
one
|
|
||||||
<caret>two
|
|
||||||
<caret>three
|
|
||||||
""".trimIndent(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSelectNestedTabs() {
|
fun testSelectNestedTabs() {
|
||||||
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
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +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 org.jetbrains.plugins.ideavim.option
|
|
||||||
|
|
||||||
import com.maddyhome.idea.vim.api.Options
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import com.maddyhome.idea.vim.newapi.vim
|
|
||||||
import com.maddyhome.idea.vim.options.OptionAccessScope
|
|
||||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
|
||||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.TestInfo
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertNotEquals
|
|
||||||
|
|
||||||
@TestWithoutNeovim(reason = SkipNeovimReason.OPTION)
|
|
||||||
class CommentsOptionTest : VimTestCase() {
|
|
||||||
|
|
||||||
private val vimDefault = "s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-"
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
override fun setUp(testInfo: TestInfo) {
|
|
||||||
super.setUp(testInfo)
|
|
||||||
configureByText("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun commentsValue(): String =
|
|
||||||
injector.optionGroup.getOptionValue(
|
|
||||||
Options.comments,
|
|
||||||
OptionAccessScope.LOCAL(fixture.editor.vim),
|
|
||||||
).value
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `comments option default value matches Vim`() {
|
|
||||||
assertEquals(vimDefault, commentsValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `set comments changes the value`() {
|
|
||||||
enterCommand("set comments=://,b:#")
|
|
||||||
assertEquals("://,b:#", commentsValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `set comments& resets to default`() {
|
|
||||||
val original = commentsValue()
|
|
||||||
enterCommand("set comments=://")
|
|
||||||
assertNotEquals(original, commentsValue())
|
|
||||||
enterCommand("set comments&")
|
|
||||||
assertEquals(original, commentsValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `comments abbreviation com is accepted`() {
|
|
||||||
enterCommand("set com=://")
|
|
||||||
assertEquals("://", commentsValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `set comments inspect displays name and value`() {
|
|
||||||
assertCommandOutput("set comments?", " comments=$vimDefault")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `set comments+= appends a list entry`() {
|
|
||||||
enterCommand("set comments=://")
|
|
||||||
enterCommand("set comments+=b:#")
|
|
||||||
assertEquals("://,b:#", commentsValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `set comments-= removes a list entry`() {
|
|
||||||
enterCommand("set comments=://,b:#")
|
|
||||||
enterCommand("set comments-=://")
|
|
||||||
assertEquals("b:#", commentsValue())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +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 org.jetbrains.plugins.ideavim.action
|
|
||||||
|
|
||||||
import com.intellij.openapi.fileTypes.PlainTextFileType
|
|
||||||
import com.maddyhome.idea.vim.api.injector
|
|
||||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
|
||||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
|
||||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
/**
|
|
||||||
* End-to-end coverage of `gq` honoring the buffer-local `'comments'` value.
|
|
||||||
*
|
|
||||||
* Uses a leader not present in the default `'comments'` string so the assertion
|
|
||||||
* fails if the option value is not read at wrap time.
|
|
||||||
*/
|
|
||||||
@TestWithoutNeovim(
|
|
||||||
reason = SkipNeovimReason.SEE_DESCRIPTION,
|
|
||||||
description = "IdeaVim wraps via the 'comments' option and its filetype presets.",
|
|
||||||
)
|
|
||||||
class CommentsDrivenReformatTest : VimTestCase() {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `custom REM marker from setlocal drives wrap continuation`() {
|
|
||||||
configureByText(
|
|
||||||
PlainTextFileType.INSTANCE,
|
|
||||||
"REM ${c}some long custom-marker comment text that must wrap to respect textwidth",
|
|
||||||
)
|
|
||||||
enterCommand("setlocal comments=:REM")
|
|
||||||
enterCommand("set textwidth=30")
|
|
||||||
typeText(injector.parser.parseKeys("gqq"))
|
|
||||||
assertState(
|
|
||||||
"""
|
|
||||||
${c}REM some long custom-marker
|
|
||||||
REM comment text that must
|
|
||||||
REM wrap to respect textwidth
|
|
||||||
""".trimIndent(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user