mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2026-05-09 06:05:53 +02:00
Compare commits
34 Commits
customized
...
customized
| Author | SHA1 | Date | |
|---|---|---|---|
|
a565dab624
|
|||
|
97bbe7d996
|
|||
|
cdc525d62b
|
|||
|
62cb93b8fd
|
|||
|
412dbfffb1
|
|||
|
fcbd4f3ecd
|
|||
|
efea120fc4
|
|||
|
93a3ef99a2
|
|||
|
7e0825250c
|
|||
|
9b8413a4d4
|
|||
|
5938e20aa9
|
|||
|
bf282dbb8b
|
|||
|
f729d69ebd
|
|||
|
a6648469b2
|
|||
|
42d5a14b97
|
|||
|
ef9f204069
|
|||
|
dcaa3e081d
|
|||
|
480c891e0e
|
|||
|
def269e35f
|
|||
|
0fcbbdf0ce
|
|||
|
59f267e723
|
|||
|
e69921d4d3
|
|||
|
4ae3a9f426
|
|||
|
d44afe5284
|
|||
|
4612f5ce68
|
|||
|
3292bc65fd
|
|||
|
04e67e622a
|
|||
|
b51714e9f9
|
|||
|
f8d3e9d98e
|
|||
|
b152819d2b
|
|||
|
fe90c24a46
|
|||
|
8b636f9dde
|
|||
|
376bf98dee
|
|||
|
a4c70083aa
|
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}}
|
||||
182
.github/workflows/runUiOctopusTests.yml
vendored
Normal file
182
.github/workflows/runUiOctopusTests.yml
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
name: Run Non Octopus UI Tests macOS
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
jobs:
|
||||
build-for-ui-test-mac-os:
|
||||
if: github.repository == 'JetBrains/ideavim'
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: zulu
|
||||
java-version: 21
|
||||
- name: Setup FFmpeg
|
||||
run: brew install ffmpeg
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
- name: Build Plugin
|
||||
run: gradle :buildPlugin
|
||||
- name: Run Idea
|
||||
run: |
|
||||
mkdir -p build/reports
|
||||
gradle runIdeForUiTests -Doctopus.handler=false > build/reports/idea.log &
|
||||
- name: List available capture devices
|
||||
run: ffmpeg -f avfoundation -list_devices true -i "" 2>&1 || true
|
||||
continue-on-error: true
|
||||
- name: Start screen recording
|
||||
run: |
|
||||
mkdir -p build/reports/ci-screen-recording
|
||||
ffmpeg -f avfoundation -capture_cursor 1 -i "0:none" -r 30 -vcodec libx264 -pix_fmt yuv420p build/reports/ci-screen-recording/screen-recording.mp4 &
|
||||
echo $! > /tmp/ffmpeg_pid.txt
|
||||
continue-on-error: true
|
||||
- name: Auto-click Allow button for screen recording permission
|
||||
run: |
|
||||
sleep 3
|
||||
brew install cliclick || true
|
||||
|
||||
for coords in "512:367" "960:540" "640:400" "800:450"; do
|
||||
x=$(echo $coords | cut -d: -f1)
|
||||
y=$(echo $coords | cut -d: -f2)
|
||||
echo "Trying coordinates: $x,$y"
|
||||
|
||||
cliclick c:$x,$y 2>/dev/null && echo "cliclick succeeded" && break
|
||||
sleep 0.5
|
||||
|
||||
osascript -e "tell application \"System Events\" to click at {$x, $y}" 2>/dev/null && echo "AppleScript succeeded" && break
|
||||
sleep 1
|
||||
done
|
||||
continue-on-error: true
|
||||
- name: Wait for Idea started
|
||||
uses: jtalk/url-health-check-action@v3
|
||||
with:
|
||||
url: http://127.0.0.1:8082
|
||||
max-attempts: 20
|
||||
retry-delay: 10s
|
||||
- name: Tests
|
||||
run: gradle :tests:ui-ij-tests:testUi
|
||||
- name: Stop screen recording
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f /tmp/ffmpeg_pid.txt ]; then
|
||||
kill $(cat /tmp/ffmpeg_pid.txt) || true
|
||||
sleep 2
|
||||
fi
|
||||
continue-on-error: true
|
||||
- name: Move sandbox logs
|
||||
if: always()
|
||||
run: mv build/idea-sandbox/IU-*/log_runIdeForUiTests idea-sandbox-log
|
||||
- name: AI Analysis of Test Failures
|
||||
if: failure()
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
claude_args: '--allowed-tools "Read,Write,Edit,Glob,Grep,Bash(ffmpeg:*),Bash(ffprobe:*),Bash(mkdir:*),Bash(touch:*),Bash(echo:*),Bash(ls:*),Bash(cat:*),Bash(grep:*),Bash(find:*),Bash(cd:*),Bash(rm:*),Bash(git:*),Bash(gh:*),Bash(gradle:*),Bash(./gradlew:*),Bash(java:*),Bash(which:*)"'
|
||||
prompt: |
|
||||
## Task: Analyze UI Test Failures
|
||||
|
||||
Please analyze the UI test failures in the current directory.
|
||||
|
||||
Key information:
|
||||
- Test reports are located in: build/reports and tests/ui-ij-tests/build/reports
|
||||
- There is a CI screen recording at build/reports/ci-screen-recording/screen-recording.mp4 that shows what happened during the entire test run - this video is usually very useful for understanding what went wrong visually
|
||||
- There is also a single screenshot at tests/ui-ij-tests/build/reports/ideaVimTest.png showing the state when the test failed
|
||||
- IDE sandbox logs are in the idea-sandbox-log directory
|
||||
- ffmpeg is already installed and available. Useful commands for video analysis:
|
||||
* Extract frame at specific time: `ffmpeg -i video.mp4 -ss 00:01:30 -vframes 1 output.png`
|
||||
* Extract multiple frames: `ffmpeg -i video.mp4 -vf fps=1/10 frame_%04d.png` (1 frame every 10 seconds)
|
||||
* Get video duration: `ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 video.mp4`
|
||||
* Extract last N seconds: `ffmpeg -sseof -10 -i video.mp4 -update 1 last_frame.png` (last 10 seconds)
|
||||
* Create thumbnail grid: `ffmpeg -i video.mp4 -vf "select='not(mod(n\,300))',scale=160:120,tile=5x5" -frames:v 1 grid.png`
|
||||
|
||||
Special troubleshooting for timeout failures:
|
||||
- If the test fails because it waited for something to appear, but according to the video and screenshot the element actually appeared on screen, check the UI hierarchy file at build/reports/hierarchy-ideaVimTest.html
|
||||
- This hierarchy file shows the actual structure of UI elements and their properties at the time of failure
|
||||
- The failure may be caused by a renamed property or changed class name in the UI element
|
||||
- If you find this is the case, suggest a new query or selector that matches the current element structure
|
||||
|
||||
Please provide:
|
||||
1. A detailed analysis of what went wrong
|
||||
2. The root cause of the failure
|
||||
3. Potential fixes or suggestions
|
||||
|
||||
Write your analysis to build/reports/ai-analysis.txt
|
||||
|
||||
## UI Test Best Practices
|
||||
|
||||
When fixing UI tests, follow these principles:
|
||||
|
||||
**Cause-Effect Over Timeouts**: UI tests should wait for specific conditions rather than arbitrary timeouts.
|
||||
- ✅ GOOD: Wait for a button to become visible or enabled before clicking it
|
||||
- ✅ GOOD: Wait for specific text to appear in a component
|
||||
- ✅ GOOD: Wait for a dialog to be present with a specific accessible name
|
||||
- ❌ BAD: Use Thread.sleep() or fixed delays
|
||||
- ❌ BAD: Wait for arbitrary timeouts without checking for specific conditions
|
||||
|
||||
Only use timeouts as a maximum wait duration with explicit condition checks (e.g., "wait up to 30 seconds for dialog to appear").
|
||||
Tests should be deterministic and based on observable state changes in the UI, not time-based assumptions.
|
||||
|
||||
IMPORTANT: If you have a concrete suggestion for fixing the test, ALWAYS proceed with creating a branch and PR. Never ask for permission - just do it.
|
||||
|
||||
If you have a concrete suggestion for fixing the test:
|
||||
1. Create a new branch with a descriptive name (e.g., fix/ui-test-accessible-name)
|
||||
2. Apply your suggested fix to the codebase
|
||||
3. CRITICAL: When staging changes, NEVER use `git add -A` or `git add .`. Always add modified files explicitly by path (e.g., `git add path/to/file.kt path/to/other.kt`). This prevents accidentally staging unrelated files.
|
||||
4. MANDATORY: Verify compilation succeeds with your changes: `gradle compileKotlin compileTestKotlin`
|
||||
5. MANDATORY: Run the specific failing test to verify the fix improves or resolves the issue
|
||||
- For Non-Octopus UI tests: `gradle :tests:ui-ij-tests:testUi --tests "YourTestClassName.yourTestMethod"`
|
||||
- To run all Non-Octopus UI tests: `gradle :tests:ui-ij-tests:testUi`
|
||||
- The test MUST either pass completely or show clear improvement (e.g., progressing further before failure)
|
||||
6. If the test passes or shows improvement with your fix, create a PR with:
|
||||
- Clear title describing the fix
|
||||
- Description explaining the root cause and solution
|
||||
- Test results showing the fix works
|
||||
- Reference to the failing CI run
|
||||
7. Use the base branch 'master' for the PR
|
||||
- name: Save report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ui-test-fails-report-mac
|
||||
path: |
|
||||
build/reports
|
||||
tests/ui-ij-tests/build/reports
|
||||
idea-sandbox-log
|
||||
# build-for-ui-test-linux:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Setup Java
|
||||
# uses: actions/setup-java@v2.1.0
|
||||
# with:
|
||||
# distribution: zulu
|
||||
# java-version: 11
|
||||
# - name: Build Plugin
|
||||
# run: gradle :buildPlugin
|
||||
# - name: Run Idea
|
||||
# run: |
|
||||
# export DISPLAY=:99.0
|
||||
# Xvfb -ac :99 -screen 0 1920x1080x16 &
|
||||
# mkdir -p build/reports
|
||||
# gradle :runIdeForUiTests #> build/reports/idea.log
|
||||
# - name: Wait for Idea started
|
||||
# uses: jtalk/url-health-check-action@1.5
|
||||
# with:
|
||||
# url: http://127.0.0.1:8082
|
||||
# max-attempts: 15
|
||||
# retry-delay: 30s
|
||||
# - name: Tests
|
||||
# run: gradle :testUi
|
||||
# - name: Save fails report
|
||||
# if: ${{ failure() }}
|
||||
# uses: actions/upload-artifact@v2
|
||||
# with:
|
||||
# name: ui-test-fails-report-linux
|
||||
# path: |
|
||||
# ui-test-example/build/reports
|
||||
4
.teamcity/_Self/Project.kt
vendored
4
.teamcity/_Self/Project.kt
vendored
@@ -17,6 +17,7 @@ import _Self.buildTypes.RandomOrderTests
|
||||
import _Self.buildTypes.SplitModeTests
|
||||
|
||||
import _Self.buildTypes.TestingBuildType
|
||||
import _Self.buildTypes.TypeScriptTest
|
||||
import _Self.subprojects.Releases
|
||||
import _Self.vcsRoots.ReleasesVcsRoot
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType
|
||||
@@ -43,6 +44,9 @@ object Project : Project({
|
||||
buildType(Nvim)
|
||||
buildType(PluginVerifier)
|
||||
buildType(Compatibility)
|
||||
|
||||
// TypeScript scripts test
|
||||
buildType(TypeScriptTest)
|
||||
})
|
||||
|
||||
// Agent size configurations (CPU count)
|
||||
|
||||
63
.teamcity/_Self/buildTypes/Compatibility.kt
vendored
63
.teamcity/_Self/buildTypes/Compatibility.kt
vendored
@@ -33,47 +33,40 @@ object Compatibility : IdeaVimBuildType({
|
||||
name = "Load Verifier"
|
||||
scriptContent = """
|
||||
mkdir verifier1
|
||||
curl -f -L -o verifier1/verifier-cli-ideavim.jar "https://github.com/AlexPl292/intellij-plugin-verifier/releases/download/cli-3/verifier-cli-1.403-ideavim-3-all.jar"
|
||||
curl -f -L -o verifier1/verifier-cli-dev-all-2.jar "https://packages.jetbrains.team/files/p/ideavim/plugin-verifier/verifier-cli-dev-all-2.jar"
|
||||
""".trimIndent()
|
||||
}
|
||||
script {
|
||||
name = "Check"
|
||||
scriptContent = """
|
||||
# We use a custom build of plugin-verifier that resolves IdeaVim from the dev channel.
|
||||
# The fork lives at https://github.com/AlexPl292/intellij-plugin-verifier — the patch is in
|
||||
# com.jetbrains.pluginverifier.repository.repositories.marketplace.MarketplaceRepository#getLastCompatibleVersionOfPlugin
|
||||
# (switches the marketplace channel to "dev" when pluginId is org.jetbrains.IdeaVim).
|
||||
#
|
||||
# To refresh against upstream:
|
||||
# 1. In the fork, pull from upstream and re-apply the dev-channel patch.
|
||||
# 2. Run the "Publish verifier-cli" workflow:
|
||||
# https://github.com/AlexPl292/intellij-plugin-verifier/actions/workflows/publish-verifier-cli.yml
|
||||
# It builds the shadow jar and attaches it to a new GitHub Release.
|
||||
# 3. Update the release URL in the "Load Verifier" step above to point at the new jar.
|
||||
|
||||
# We use a custom build of verifier that downloads IdeaVim from dev channel
|
||||
# To create a custom build: Download plugin verifier repo, add an if that switches to dev channel for IdeaVim repo
|
||||
# At the moment it's com.jetbrains.pluginverifier.repository.repositories.marketplace.MarketplaceRepository#getLastCompatibleVersionOfPlugin
|
||||
# Build using gradlew :intellij-plugin-verifier:verifier-cli:shadowJar
|
||||
# Upload verifier-cli-dev-all.jar artifact to the repo in IdeaVim space repo
|
||||
|
||||
java --version
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}org.jetbrains.IdeaVim-EasyMotion' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}eu.theblob42.idea.whichkey' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}IdeaVimExtension' [latest-IU] -team-city
|
||||
# Outdated java -jar verifier/verifier-cli-ideavim.jar check-plugin '${'$'}github.zgqq.intellij-enhance' [latest-IU] -team-city
|
||||
# java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.github.copilot' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.github.dankinsoid.multicursor' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.joshestein.ideavim-quickscope' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.julienphalip.ideavim.peekaboo' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.julienphalip.ideavim.switch' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.julienphalip.ideavim.functiontextobj' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.miksuki.HighlightCursor' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.ugarosa.idea.edgemotion' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}cn.mumukehao.plugin' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.magidc.ideavim.dial' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}dev.ghostflyby.ideavim.toggleIME' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.magidc.ideavim.anyObject' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.yelog.ideavim.cmdfloat' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}gg.ninetyfive' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.github.pooryam92.vimcoach' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}lazyideavim.whichkeylazy' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}com.github.vimkeysuggest' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-ideavim.jar check-plugin '${'$'}dev.ckob.lazygit' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}org.jetbrains.IdeaVim-EasyMotion' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}eu.theblob42.idea.whichkey' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}IdeaVimExtension' [latest-IU] -team-city
|
||||
# Outdated java -jar verifier/verifier-cli-dev-all.jar check-plugin '${'$'}github.zgqq.intellij-enhance' [latest-IU] -team-city
|
||||
# java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.github.copilot' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.github.dankinsoid.multicursor' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.joshestein.ideavim-quickscope' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.julienphalip.ideavim.peekaboo' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.julienphalip.ideavim.switch' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.julienphalip.ideavim.functiontextobj' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.miksuki.HighlightCursor' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.ugarosa.idea.edgemotion' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}cn.mumukehao.plugin' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.magidc.ideavim.dial' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}dev.ghostflyby.ideavim.toggleIME' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.magidc.ideavim.anyObject' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.yelog.ideavim.cmdfloat' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}gg.ninetyfive' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.github.pooryam92.vimcoach' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}lazyideavim.whichkeylazy' [latest-IU] -team-city
|
||||
java -jar verifier1/verifier-cli-dev-all-2.jar check-plugin '${'$'}com.github.vimkeysuggest' [latest-IU] -team-city
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
9
.teamcity/_Self/buildTypes/SplitModeTests.kt
vendored
9
.teamcity/_Self/buildTypes/SplitModeTests.kt
vendored
@@ -28,6 +28,7 @@ object SplitModeTests : IdeaVimBuildType({
|
||||
params {
|
||||
param("env.ORG_GRADLE_PROJECT_downloadIdeaSources", "false")
|
||||
param("env.ORG_GRADLE_PROJECT_instrumentPluginCode", "false")
|
||||
param("env.DISPLAY", ":99")
|
||||
}
|
||||
|
||||
vcs {
|
||||
@@ -39,8 +40,12 @@ object SplitModeTests : IdeaVimBuildType({
|
||||
|
||||
steps {
|
||||
script {
|
||||
name = "Run split mode tests"
|
||||
scriptContent = "xvfb-run -a -s '-screen 0 1920x1080x24' ./gradlew :tests:split-mode-tests:testSplitMode --console=plain --build-cache --configuration-cache --stacktrace"
|
||||
name = "Start Xvfb and run split mode tests"
|
||||
scriptContent = """
|
||||
Xvfb :99 -screen 0 1920x1080x24 &
|
||||
sleep 2
|
||||
./gradlew :tests:split-mode-tests:testSplitMode --console=plain --build-cache --configuration-cache --stacktrace
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
45
.teamcity/_Self/buildTypes/TypeScriptTest.kt
vendored
Normal file
45
.teamcity/_Self/buildTypes/TypeScriptTest.kt
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
package _Self.buildTypes
|
||||
|
||||
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 TypeScriptTest : IdeaVimBuildType({
|
||||
id("IdeaVimTests_TypeScript")
|
||||
name = "TypeScript Scripts Test"
|
||||
description = "Test that TypeScript scripts can run on TeamCity"
|
||||
|
||||
vcs {
|
||||
root(DslContext.settingsRoot)
|
||||
branchFilter = "+:<default>"
|
||||
|
||||
checkoutMode = CheckoutMode.AUTO
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
name = "Set up Node.js"
|
||||
scriptContent = """
|
||||
wget https://nodejs.org/dist/v20.18.1/node-v20.18.1-linux-x64.tar.xz
|
||||
tar xf node-v20.18.1-linux-x64.tar.xz
|
||||
export PATH="${"$"}PWD/node-v20.18.1-linux-x64/bin:${"$"}PATH"
|
||||
node --version
|
||||
npm --version
|
||||
""".trimIndent()
|
||||
}
|
||||
script {
|
||||
name = "Run TypeScript test"
|
||||
scriptContent = """
|
||||
export PATH="${"$"}PWD/node-v20.18.1-linux-x64/bin:${"$"}PATH"
|
||||
cd scripts-ts
|
||||
npm install
|
||||
npx tsx src/teamcityTest.ts
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
requirements {
|
||||
equals("teamcity.agent.os.family", "Linux")
|
||||
}
|
||||
})
|
||||
56
CHANGES.md
56
CHANGES.md
@@ -26,8 +26,6 @@ usual beta standards.
|
||||
## [To Be Released]
|
||||
|
||||
### 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()`
|
||||
* [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"`)
|
||||
@@ -36,16 +34,8 @@ 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 `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-1158](https://youtrack.jetbrains.com/issue/VIM-1158) Added `gw` command - reformat code like `gq` but preserving the cursor position
|
||||
* [VIM-3975](https://youtrack.jetbrains.com/issue/VIM-3975) Added `mode()` VimScript function - returns the current editing mode (e.g., `'n'` for normal, `'i'` for insert, `'v'` for visual, `'R'` for replace)
|
||||
* [VIM-519](https://youtrack.jetbrains.com/issue/VIM-519) Added `g;` and `g,` commands - navigate the change list to jump to previous (`g;`) or next (`g,`) edit location
|
||||
* [VIM-258](https://youtrack.jetbrains.com/issue/VIM-258) Added command name completion in ex commands - press `<Tab>` to cycle through matching command names (e.g., `:e<Tab>` shows `:edit`, `:earlier`, etc.)
|
||||
|
||||
### 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
|
||||
@@ -55,7 +45,6 @@ usual beta standards.
|
||||
* [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-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
|
||||
@@ -71,52 +60,8 @@ usual beta standards.
|
||||
* Fixed `IndexOutOfBoundsException` when using text objects like `a)` at end of file
|
||||
* Fixed high CPU usage while showing command line
|
||||
* 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
|
||||
* [VIM-4209](https://youtrack.jetbrains.com/issue/VIM-4209) Fixed `<Esc>` not exiting insert mode in Rider/CLion when a `<C-Space>` completion popup intercepts the key before IdeaVim
|
||||
* [VIM-4211](https://youtrack.jetbrains.com/issue/VIM-4211) Fixed IdeaVim not working in the Git commit window
|
||||
* [VIM-4202](https://youtrack.jetbrains.com/issue/VIM-4202) Fixed `gcc`/`gc{motion}` commentary not adding space after `//` prefix in C/C++/C# files in Rider/CLion split mode
|
||||
* [VIM-4219](https://youtrack.jetbrains.com/issue/VIM-4219) Fixed NullPointerException when IdeaVim is being disabled/unloaded
|
||||
* [VIM-4221](https://youtrack.jetbrains.com/issue/VIM-4221) Fixed error sound being played on each keypress when `incsearch` is enabled and the typed pattern is an invalid regex
|
||||
* [VIM-4196](https://youtrack.jetbrains.com/issue/VIM-4196) Fixed NERDTree file selection not being restored after pressing `<Esc>` to cancel a `/` speed search
|
||||
* [VIM-4211](https://youtrack.jetbrains.com/issue/VIM-4211) Fixed IdeaVim not working in Git commit window when the Conventional Commits plugin is installed
|
||||
* [VIM-4224](https://youtrack.jetbrains.com/issue/VIM-4224) Fixed `:s` `e` flag now properly suppresses "Pattern not found" errors - e.g., `%s/\s\+$//e` no longer errors when there is no trailing whitespace
|
||||
* [VIM-4226](https://youtrack.jetbrains.com/issue/VIM-4226) Fixed race condition crash when the editor is disposed while the ex panel is open
|
||||
|
||||
### Merged PRs:
|
||||
* [1747](https://github.com/JetBrains/ideavim/pull/1747) by [1grzyb1](https://github.com/1grzyb1): feat(VIM-519): cycle through recent edits with g; and g,
|
||||
* [1745](https://github.com/JetBrains/ideavim/pull/1745) by [1grzyb1](https://github.com/1grzyb1): feat(VIM-258): tab command completion
|
||||
* [1744](https://github.com/JetBrains/ideavim/pull/1744) by [1grzyb1](https://github.com/1grzyb1): fix(VIM-4226): check if editor is disposed on focus
|
||||
* [1741](https://github.com/JetBrains/ideavim/pull/1741) by [1grzyb1](https://github.com/1grzyb1): fix(VIM-4224): respect e flag in search patterns
|
||||
* [1740](https://github.com/JetBrains/ideavim/pull/1740) by [1grzyb1](https://github.com/1grzyb1): feat(VIM-3975): support vim mode() function
|
||||
* [1739](https://github.com/JetBrains/ideavim/pull/1739) by [1grzyb1](https://github.com/1grzyb1): fix(VIM-4196): restore file selection after esc in nerdtree
|
||||
* [1738](https://github.com/JetBrains/ideavim/pull/1738) by [1grzyb1](https://github.com/1grzyb1): fix(VIM-4211): commit window work with conectional commits plugin
|
||||
* [1730](https://github.com/JetBrains/ideavim/pull/1730) by [1grzyb1](https://github.com/1grzyb1): FIX(VIM-4221) Don't make angry sounds on search
|
||||
* [1728](https://github.com/JetBrains/ideavim/pull/1728) by [1grzyb1](https://github.com/1grzyb1): VIM-4202 Add space after c langauges comments
|
||||
* [1727](https://github.com/JetBrains/ideavim/pull/1727) by [1grzyb1](https://github.com/1grzyb1): FIX(VIM-4219) check for in VimPLugin is not null
|
||||
* [1720](https://github.com/JetBrains/ideavim/pull/1720) by [1grzyb1](https://github.com/1grzyb1): fix: make ideavim work in commit window
|
||||
* [1717](https://github.com/JetBrains/ideavim/pull/1717) by [1grzyb1](https://github.com/1grzyb1): Fix(VIM-4209): handle esc in rider before popup
|
||||
* [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
|
||||
@@ -124,7 +69,6 @@ usual beta standards.
|
||||
* [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
|
||||
* [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
|
||||
|
||||
|
||||
18
README.md
18
README.md
@@ -241,24 +241,6 @@ ShowHoverInfo - Quick Documentation and Error Description
|
||||
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
|
||||
------------
|
||||
|
||||
|
||||
@@ -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 [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
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package com.intellij.vim.api.scopes
|
||||
|
||||
import com.intellij.vim.api.VimApi
|
||||
import com.intellij.vim.api.models.CaretId
|
||||
|
||||
/**
|
||||
* Represents the range of a text object selection.
|
||||
@@ -113,6 +114,15 @@ interface TextObjectScope {
|
||||
keys: String,
|
||||
registerDefaultMapping: Boolean = true,
|
||||
preserveSelectionAnchor: Boolean = true,
|
||||
rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
|
||||
rangeProvider: suspend VimApi.(caret: CaretId, count: Int) -> TextObjectRange?,
|
||||
)
|
||||
|
||||
fun register(
|
||||
keys: String,
|
||||
registerDefaultMapping: Boolean = true,
|
||||
preserveSelectionAnchor: Boolean = true,
|
||||
rangeProvider: suspend VimApi.(count: Int) -> TextObjectRange?,
|
||||
) {
|
||||
register(keys, registerDefaultMapping, preserveSelectionAnchor) { _, count -> rangeProvider(count) }
|
||||
}
|
||||
}
|
||||
|
||||
125
build.gradle.kts
125
build.gradle.kts
@@ -27,11 +27,11 @@ buildscript {
|
||||
classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:7.6.0.202603022253-r")
|
||||
classpath("org.kohsuke:github-api:1.305")
|
||||
|
||||
classpath("io.ktor:ktor-client-core:3.4.3")
|
||||
classpath("io.ktor:ktor-client-cio:3.4.3")
|
||||
classpath("io.ktor:ktor-client-auth:3.4.3")
|
||||
classpath("io.ktor:ktor-client-content-negotiation:3.4.3")
|
||||
classpath("io.ktor:ktor-serialization-kotlinx-json:3.4.3")
|
||||
classpath("io.ktor:ktor-client-core:3.4.2")
|
||||
classpath("io.ktor:ktor-client-cio:3.4.2")
|
||||
classpath("io.ktor:ktor-client-auth:3.4.2")
|
||||
classpath("io.ktor:ktor-client-content-negotiation:3.4.2")
|
||||
classpath("io.ktor:ktor-serialization-kotlinx-json:3.4.2")
|
||||
|
||||
// This comes from the changelog plugin
|
||||
// classpath("org.jetbrains:markdown:0.3.1")
|
||||
@@ -209,6 +209,7 @@ tasks {
|
||||
// Note that this will run the plugin installed in the IDE specified in dependencies. To run in a different IDE, use
|
||||
// a custom task (see below)
|
||||
runIde {
|
||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
||||
systemProperty("idea.trust.all.projects", "true")
|
||||
}
|
||||
|
||||
@@ -227,21 +228,29 @@ tasks {
|
||||
|
||||
val runPycharm by intellijPlatformTesting.runIde.registering {
|
||||
type = IntelliJPlatformType.PyCharmProfessional
|
||||
version = "2026.1"
|
||||
version = "2025.3.2"
|
||||
task {
|
||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
||||
}
|
||||
}
|
||||
|
||||
val runWebstorm by intellijPlatformTesting.runIde.registering {
|
||||
type = IntelliJPlatformType.WebStorm
|
||||
version = "2025.3.2"
|
||||
task {
|
||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
||||
}
|
||||
}
|
||||
|
||||
val runClion by intellijPlatformTesting.runIde.registering {
|
||||
type = IntelliJPlatformType.CLion
|
||||
version = "2026.1"
|
||||
version = "2025.3.2"
|
||||
task {
|
||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
||||
}
|
||||
}
|
||||
|
||||
val runIdeForUiTests by intellijPlatformTesting.runIde.registering {
|
||||
version = "2026.1"
|
||||
task {
|
||||
jvmArgumentProviders += CommandLineArgumentProvider {
|
||||
listOf(
|
||||
@@ -250,6 +259,7 @@ tasks {
|
||||
"-Djb.privacy.policy.text=<!--999.999-->",
|
||||
"-Djb.consents.confirmation.enabled=false",
|
||||
"-Dide.show.tips.on.startup.default.value=false",
|
||||
"-Doctopus.handler=" + (System.getProperty("octopus.handler") ?: true),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -292,7 +302,7 @@ tasks {
|
||||
}
|
||||
val runCLionSplitMode by intellijPlatformTesting.runIde.registering {
|
||||
type = IntelliJPlatformType.CLion
|
||||
version = "2026.1"
|
||||
version = "2025.3.2"
|
||||
splitMode = true
|
||||
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
||||
|
||||
@@ -303,7 +313,7 @@ tasks {
|
||||
}
|
||||
val runPycharmSplitMode by intellijPlatformTesting.runIde.registering {
|
||||
type = IntelliJPlatformType.PyCharmProfessional
|
||||
version = "2026.1"
|
||||
version = "2025.3.2"
|
||||
splitMode = true
|
||||
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
||||
|
||||
@@ -352,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 {
|
||||
splitMode = true
|
||||
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
||||
@@ -463,8 +434,6 @@ intellijPlatform {
|
||||
changeNotes.set(
|
||||
"""
|
||||
<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>
|
||||
* <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>
|
||||
@@ -473,16 +442,8 @@ 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>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-1158">VIM-1158</a> Added <code>gw</code> command - reformat code like <code>gq</code> but preserving the cursor position<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-3975">VIM-3975</a> Added <code>mode()</code> VimScript function - returns the current editing mode (e.g., <code>'n'</code> for normal, <code>'i'</code> for insert, <code>'v'</code> for visual, <code>'R'</code> for replace)<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-519">VIM-519</a> Added <code>g;</code> and <code>g,</code> commands - navigate the change list to jump to previous (<code>g;</code>) or next (<code>g,</code>) edit location<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-258">VIM-258</a> Added command name completion in ex commands - press <code><Tab></code> to cycle through matching command names (e.g., <code>:e<Tab></code> shows <code>:edit</code>, <code>:earlier</code>, etc.)<br>
|
||||
<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>
|
||||
@@ -492,7 +453,6 @@ intellijPlatform {
|
||||
* <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-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>
|
||||
@@ -508,52 +468,8 @@ intellijPlatform {
|
||||
* 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 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>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4209">VIM-4209</a> Fixed <code><Esc></code> not exiting insert mode in Rider/CLion when a <code><C-Space></code> completion popup intercepts the key before IdeaVim<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4211">VIM-4211</a> Fixed IdeaVim not working in the Git commit window<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4202">VIM-4202</a> Fixed <code>gcc</code>/<code>gc{motion}</code> commentary not adding space after <code>//</code> prefix in C/C++/C# files in Rider/CLion split mode<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4219">VIM-4219</a> Fixed NullPointerException when IdeaVim is being disabled/unloaded<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4221">VIM-4221</a> Fixed error sound being played on each keypress when <code>incsearch</code> is enabled and the typed pattern is an invalid regex<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4196">VIM-4196</a> Fixed NERDTree file selection not being restored after pressing <code><Esc></code> to cancel a <code>/</code> speed search<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4211">VIM-4211</a> Fixed IdeaVim not working in Git commit window when the Conventional Commits plugin is installed<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4224">VIM-4224</a> Fixed <code>:s</code> <code>e</code> flag now properly suppresses "Pattern not found" errors - e.g., <code>%s/\s\+$//e</code> no longer errors when there is no trailing whitespace<br>
|
||||
* <a href="https://youtrack.jetbrains.com/issue/VIM-4226">VIM-4226</a> Fixed race condition crash when the editor is disposed while the ex panel is open<br>
|
||||
<br>
|
||||
<b>Merged PRs:</b><br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1747">1747</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: feat(VIM-519): cycle through recent edits with g; and g,<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1745">1745</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: feat(VIM-258): tab command completion<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1744">1744</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: fix(VIM-4226): check if editor is disposed on focus<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1741">1741</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: fix(VIM-4224): respect e flag in search patterns<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1740">1740</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: feat(VIM-3975): support vim mode() function<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1739">1739</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: fix(VIM-4196): restore file selection after esc in nerdtree<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1738">1738</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: fix(VIM-4211): commit window work with conectional commits plugin<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1730">1730</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: FIX(VIM-4221) Don't make angry sounds on search<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1728">1728</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: VIM-4202 Add space after c langauges comments<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1727">1727</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: FIX(VIM-4219) check for in VimPLugin is not null<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1720">1720</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: fix: make ideavim work in commit window<br>
|
||||
* <a href="https://github.com/JetBrains/ideavim/pull/1717">1717</a> by <a href="https://github.com/1grzyb1">1grzyb1</a>: Fix(VIM-4209): handle esc in rider before popup<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>
|
||||
@@ -561,7 +477,6 @@ intellijPlatform {
|
||||
* <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/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>
|
||||
<a href="https://youtrack.jetbrains.com/issues/VIM?q=State:%20Fixed%20Fix%20versions:%20${version.get()}">Changelog</a>
|
||||
""".trimIndent()
|
||||
|
||||
@@ -618,34 +618,6 @@ https://github.com/kana/vim-textobj-entire/blob/master/doc/textobj-entire.txt
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h2>VimEverywhere: Keyboard-driven IDE navigation outside the editor</h2></summary>
|
||||
|
||||
### Summary:
|
||||
Brings vim-style keyboard navigation to the rest of the IDE. Enabling `VimEverywhere` turns on three
|
||||
behaviors:
|
||||
|
||||
- **Hints overlay.** Press `Ctrl+Shift+\` (`Ctrl+Cmd+\` on macOS) to display hint labels over
|
||||
interactive UI components — buttons, tool window tabs, tree nodes, text fields, scroll panes, and
|
||||
so on. Type the letters next to a target to focus or click it without touching the mouse.
|
||||
- **NERDTree-style mappings everywhere.** NERDTree file-opening mappings (`o`, `t`, `T`, `s`, `i`,
|
||||
`go`, `gs`, `gi`) work in any focused tree, not just the Project tool window.
|
||||
- **Tool window navigation.** Vim-style window-motion keys work inside tool windows, so you can move
|
||||
between split panes without leaving the keyboard.
|
||||
|
||||
### Setup:
|
||||
- Install the [AceJump](https://plugins.jetbrains.com/plugin/7086-acejump/) plugin.
|
||||
- Add the following command to `~/.ideavimrc`: `set VimEverywhere`
|
||||
|
||||
### Instructions
|
||||
|
||||
Press `Ctrl+Shift+\` (`Ctrl+Cmd+\` on macOS) to toggle the hints overlay. Type the letters shown
|
||||
next to a target to activate it, or press `Esc` to dismiss the overlay without activating anything.
|
||||
NERDTree-style and window-nav mappings are active automatically whenever the corresponding
|
||||
component has focus.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h2>Which-Key: Displays available keybindings in popup</h2></summary>
|
||||
|
||||
|
||||
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
|
||||
ideaType=IU
|
||||
instrumentPluginCode=true
|
||||
version=9999.58-chylex
|
||||
version=chylex-57
|
||||
javaVersion=21
|
||||
remoteRobotVersion=0.11.23
|
||||
antlrVersion=4.10.1
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradlew
vendored
2
gradlew
vendored
@@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
31
gradlew.bat
vendored
Normal file → Executable file
31
gradlew.bat
vendored
Normal file → Executable file
@@ -23,8 +23,8 @@
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@@ -51,7 +51,7 @@ echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
@@ -65,7 +65,7 @@ echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
@@ -73,10 +73,21 @@ echo location of your Java installation. 1>&2
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
||||
@@ -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.group.changelist
|
||||
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl.PlaceInfo
|
||||
import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl.RecentPlacesListener
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.rpc.topics.broadcast
|
||||
|
||||
/**
|
||||
* Bridges IntelliJ's `RecentPlacesListener` into [CHANGE_LIST_REMOTE_TOPIC].
|
||||
*
|
||||
* `PlaceInfo.caretPosition` carries the *post-command* caret (one past `iX<Esc>`,
|
||||
* end of `rA`, etc.) but Vim's `g;` targets where the edit *began*, so we capture
|
||||
* `event.offset` from a `DocumentListener` and prefer it when available.
|
||||
*
|
||||
* Line/col are computed here on the backend (where the document lives) and sent
|
||||
* pre-resolved over the topic; the frontend then has no VirtualFile lookup to
|
||||
* race with editor loading in split mode.
|
||||
*
|
||||
* `recentPlaceRemoved` is intentionally NOT mirrored: IntelliJ's `putLastOrMerge`
|
||||
* fires "remove A, add B" across different lines (`canBeMergedWith(NAVIGATION)`),
|
||||
* which is far more aggressive than Vim's same-line/textwidth merge rule. The
|
||||
* frontend service does its own merging and capping, so platform eviction is moot.
|
||||
*/
|
||||
internal class ChangeListPlacesListener(private val project: Project) : RecentPlacesListener {
|
||||
|
||||
private data class Pending(val document: Document, val offset: Int)
|
||||
|
||||
private val pendingByPath = mutableMapOf<String, Pending>()
|
||||
|
||||
init {
|
||||
EditorFactory.getInstance().eventMulticaster.addDocumentListener(
|
||||
object : DocumentListener {
|
||||
override fun documentChanged(event: DocumentEvent) {
|
||||
val file = FileDocumentManager.getInstance().getFile(event.document) ?: return
|
||||
pendingByPath[file.path] = Pending(event.document, event.offset)
|
||||
}
|
||||
},
|
||||
project,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun recentPlaceAdded(changePlace: PlaceInfo, isChanged: Boolean) {
|
||||
if (!isChanged) return
|
||||
val file = changePlace.file
|
||||
val path = file.path
|
||||
|
||||
val pending = pendingByPath.remove(path)
|
||||
val (document, offset) = pending?.let { it.document to it.offset }
|
||||
?: run {
|
||||
val doc = FileDocumentManager.getInstance().getDocument(file) ?: return
|
||||
val off = changePlace.caretPosition?.startOffset ?: return
|
||||
doc to off
|
||||
}
|
||||
|
||||
val safeOffset = offset.coerceIn(0, document.textLength)
|
||||
val line = document.getLineNumber(safeOffset)
|
||||
val col = safeOffset - document.getLineStartOffset(line)
|
||||
|
||||
CHANGE_LIST_REMOTE_TOPIC.broadcast(
|
||||
project,
|
||||
ChangeListInfo(
|
||||
line = line,
|
||||
col = col,
|
||||
filepath = path,
|
||||
protocol = file.fileSystem.protocol,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun recentPlaceRemoved(changePlace: PlaceInfo, isChanged: Boolean) {
|
||||
// Intentionally empty -- see class kdoc.
|
||||
}
|
||||
}
|
||||
@@ -8,25 +8,23 @@
|
||||
|
||||
package com.maddyhome.idea.vim.group.comment
|
||||
|
||||
import com.intellij.application.options.CodeStyle
|
||||
import com.intellij.codeInsight.actions.MultiCaretCodeInsightActionHandler
|
||||
import com.intellij.codeInsight.generation.CommentByBlockCommentHandler
|
||||
import com.intellij.codeInsight.generation.CommentByLineCommentHandler
|
||||
import com.intellij.lang.LanguageCommenters
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.IdeActions
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.impl.EditorId
|
||||
import com.intellij.openapi.editor.impl.findEditorOrNull
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.maddyhome.idea.vim.group.onEdt
|
||||
|
||||
/**
|
||||
* Handlers are invoked directly rather than via `ActionManager.tryToExecute` because in
|
||||
* Rider / CLion Nova the action dispatch is async — `ActionCallback` signals `done` at
|
||||
* dispatch, not completion — so the action's selection survived `removeSelection()` and
|
||||
* the selection listener dropped IdeaVim into Visual-Line mode.
|
||||
* RPC handler for [CommentaryRemoteApi].
|
||||
*
|
||||
* Sets selection on the backend editor and executes the platform's comment action.
|
||||
* 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 {
|
||||
|
||||
@@ -37,66 +35,40 @@ internal class CommentaryRemoteApiImpl : CommentaryRemoteApi {
|
||||
val startOffset = document.getLineStartOffset(startLine)
|
||||
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) =
|
||||
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,
|
||||
startOffset: Int,
|
||||
endOffset: Int,
|
||||
caretOffset: Int,
|
||||
lineWise: Boolean,
|
||||
) {
|
||||
val project = editor.project ?: return
|
||||
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return
|
||||
|
||||
val invokeHandler = {
|
||||
CommandProcessor.getInstance().executeCommand(project, {
|
||||
ApplicationManager.getApplication().runWriteAction {
|
||||
val caret = editor.caretModel.primaryCaret
|
||||
caret.setSelection(startOffset, endOffset)
|
||||
try {
|
||||
val handler = pickHandler(psiFile, lineWise)
|
||||
handler.invoke(project, editor, caret, psiFile)
|
||||
handler.postInvoke()
|
||||
} finally {
|
||||
caret.removeSelection()
|
||||
if (caretOffset >= 0) {
|
||||
caret.moveToOffset(caretOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, "Commentary", null)
|
||||
}
|
||||
|
||||
// normally comment action goes through rider backend comment action running on .net nto jvm so we cannot call it directly.
|
||||
// But we still want to apply space after comment as it's default bahavior there so we overrite this flag for intelij comment handler
|
||||
if (isCFamily(psiFile)) {
|
||||
val baseSettings = CodeStyle.getSettings(psiFile)
|
||||
CodeStyle.runWithLocalSettings(project, baseSettings) { localSettings ->
|
||||
localSettings.getCommonSettings(psiFile.language).LINE_COMMENT_ADD_SPACE = true
|
||||
invokeHandler()
|
||||
actionId: String,
|
||||
): Boolean {
|
||||
var result = false
|
||||
// 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
|
||||
// selection is set before the command, undo would restore it. The nested tryToExecute
|
||||
// command merges into this outer command.
|
||||
CommandProcessor.getInstance().executeCommand(editor.project, {
|
||||
editor.selectionModel.setSelection(startOffset, endOffset)
|
||||
val action = ActionManager.getInstance().getAction(actionId)
|
||||
result = ActionManager.getInstance().tryToExecute(action, null, editor.contentComponent, "IdeaVim", true)
|
||||
.let { it.waitFor(5_000); it.isDone }
|
||||
editor.selectionModel.removeSelection()
|
||||
if (caretOffset >= 0) {
|
||||
editor.caretModel.moveToOffset(caretOffset)
|
||||
}
|
||||
} else {
|
||||
invokeHandler()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCFamily(psiFile: PsiFile): Boolean {
|
||||
val fileTypeName = psiFile.fileType.name
|
||||
return fileTypeName == "C++" || fileTypeName == "C#" || fileTypeName == "ObjectiveC"
|
||||
}
|
||||
|
||||
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()
|
||||
}, "Commentary", null)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
override suspend fun listFilesForCompletion(pathPrefix: String, projectId: ProjectId?): List<String> = readAction {
|
||||
val basePath = projectId?.findProjectOrNull()?.basePath
|
||||
FileCompletionHelper.listMatchingFiles(pathPrefix, basePath)
|
||||
}
|
||||
|
||||
// ======================== Private helpers ========================
|
||||
|
||||
private fun findFile(filename: String, project: Project): VirtualFile? {
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
<projectListeners>
|
||||
<listener class="com.maddyhome.idea.vim.group.jump.JumpsListener"
|
||||
topic="com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl$RecentPlacesListener"/>
|
||||
<listener class="com.maddyhome.idea.vim.group.changelist.ChangeListPlacesListener"
|
||||
topic="com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl$RecentPlacesListener"/>
|
||||
</projectListeners>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
<dependencies>
|
||||
<plugin id="org.jetbrains.plugins.clion.radler"/>
|
||||
</dependencies>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<editorActionHandler action="EditorEscape"
|
||||
implementationClass="com.maddyhome.idea.vim.handler.VimEscForRiderHandler"
|
||||
id="ideavim-clion-nova-esc"
|
||||
order="first, before idea.only.escape"/>
|
||||
</extensions>
|
||||
<extensions defaultExtensionNs="IdeaVIM">
|
||||
<clionNovaProvider implementation="com.maddyhome.idea.vim.ide.ClionNovaProviderImpl"/>
|
||||
</extensions>
|
||||
|
||||
@@ -1,29 +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.changelist
|
||||
|
||||
import com.intellij.platform.rpc.topics.ProjectRemoteTopic
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Backend-to-frontend change-list event. Line/col are pre-computed on the
|
||||
* backend so the frontend doesn't need a VirtualFile lookup -- which can race
|
||||
* with editor loading in split mode (mirrors the `JumpInfo` pattern).
|
||||
*/
|
||||
@Serializable
|
||||
data class ChangeListInfo(
|
||||
val line: Int,
|
||||
val col: Int,
|
||||
val filepath: String,
|
||||
val protocol: String,
|
||||
val timestamp: Long,
|
||||
)
|
||||
|
||||
val CHANGE_LIST_REMOTE_TOPIC: ProjectRemoteTopic<ChangeListInfo> =
|
||||
ProjectRemoteTopic("ideavim.changelist", ChangeListInfo.serializer())
|
||||
@@ -44,7 +44,6 @@ interface FileRemoteApi : RemoteApi<Unit> {
|
||||
suspend fun selectNextFile(count: Int, projectId: ProjectId?)
|
||||
suspend fun buildFileInfoMessage(editorId: EditorId, fullPath: Boolean): String?
|
||||
suspend fun selectEditor(projectId: ProjectId, documentPath: String, protocol: String): Boolean
|
||||
suspend fun listFilesForCompletion(pathPrefix: String, projectId: ProjectId?): List<String>
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
||||
@@ -36,8 +36,10 @@
|
||||
topic="com.intellij.ide.ui.LafManagerListener"/>
|
||||
<listener class="com.maddyhome.idea.vim.extension.highlightedyank.HighlightColorResetter"
|
||||
topic="com.intellij.ide.ui.LafManagerListener"/>
|
||||
<listener class="com.maddyhome.idea.vim.listener.VimAppActivationListener"
|
||||
topic="com.intellij.openapi.application.ApplicationActivationListener"/>
|
||||
<listener class="com.maddyhome.idea.vim.handler.IdeaVimKeymapChangedListener"
|
||||
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
|
||||
<listener class="com.maddyhome.idea.vim.handler.IdeaVimCorrectorKeymapChangedListener"
|
||||
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
|
||||
</applicationListeners>
|
||||
|
||||
<projectListeners>
|
||||
@@ -167,18 +169,9 @@
|
||||
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.process.IjProcessGroup"
|
||||
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
|
||||
implementation="com.maddyhome.idea.vim.group.JumpRemoteTopicListener"/>
|
||||
|
||||
<!-- Frontend change-list service (g; / g,) + topic listener that mirrors
|
||||
backend RecentPlacesListener events into it. -->
|
||||
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.changelist.ChangeListService"/>
|
||||
<platform.rpc.projectRemoteTopicListener
|
||||
implementation="com.maddyhome.idea.vim.group.changelist.ChangeListRemoteTopicListener"/>
|
||||
|
||||
<applicationService serviceImplementation="com.maddyhome.idea.vim.group.IjFileGroup"
|
||||
serviceInterface="com.maddyhome.idea.vim.api.VimFile"/>
|
||||
|
||||
@@ -227,6 +220,11 @@
|
||||
implementation="com.maddyhome.idea.vim.ui.widgets.macro.MacroWidgetFactory"
|
||||
order="first, after IdeaVimShowCmd"/>
|
||||
|
||||
<!-- Editor-specific startup activities -->
|
||||
<postStartupActivity implementation="com.maddyhome.idea.vim.handler.EditorHandlersChainLogger"/>
|
||||
<postStartupActivity implementation="com.maddyhome.idea.vim.handler.KeymapChecker"/>
|
||||
<postStartupActivity implementation="com.maddyhome.idea.vim.handler.CopilotKeymapCorrector"/>
|
||||
|
||||
<editorFloatingToolbarProvider implementation="com.maddyhome.idea.vim.ui.ReloadFloatingToolbar"/>
|
||||
|
||||
<actionPromoter implementation="com.maddyhome.idea.vim.key.VimActionsPromoter" order="last"/>
|
||||
@@ -246,6 +244,35 @@
|
||||
<statistics.applicationUsagesCollector implementation="com.maddyhome.idea.vim.statistic.WidgetState"/>
|
||||
<statistics.counterUsagesCollector implementationClass="com.maddyhome.idea.vim.statistic.ActionTracker"/>
|
||||
|
||||
<!-- Editor action handlers -->
|
||||
<!-- Do not care about red handlers in order. They are necessary for proper ordering, and they'll be resolved when needed -->
|
||||
<editorActionHandler action="EditorEnter" implementationClass="com.maddyhome.idea.vim.handler.VimEnterHandler"
|
||||
id="ideavim-enter"
|
||||
order="before editorEnter, before inline.completion.enter, before rd.client.editor.enter, after smart-step-into-enter, after AceHandlerEnter, after jupyterCommandModeEnterKeyHandler, after swift.placeholder.enter"/>
|
||||
<editorActionHandler action="EditorEnter"
|
||||
implementationClass="com.maddyhome.idea.vim.handler.CaretShapeEnterEditorHandler"
|
||||
id="ideavim-enter-shape"
|
||||
order="before jupyterCommandModeEnterKeyHandler"/>
|
||||
|
||||
<!-- "first" is not defined for this handler as it leads to "unsatisfied ordering exception". Not sure exectly why, but it appears in tests-->
|
||||
<editorActionHandler action="EditorEscape" implementationClass="com.maddyhome.idea.vim.handler.VimEscHandler"
|
||||
id="ideavim-esc"
|
||||
order="after smart-step-into-escape, after AceHandlerEscape, before jupyterCommandModeEscKeyHandler, before templateEscape, before backend.escape"/>
|
||||
<editorActionHandler action="EditorEscape" implementationClass="com.maddyhome.idea.vim.handler.VimEscLoggerHandler"
|
||||
id="ideavim-esc-logger"
|
||||
order="first"/>
|
||||
<editorActionHandler action="EditorEnter" implementationClass="com.maddyhome.idea.vim.handler.VimEnterLoggerHandler"
|
||||
id="ideavim-enter-logger"
|
||||
order="first"/>
|
||||
<editorActionHandler action="EditorStartNewLine"
|
||||
implementationClass="com.maddyhome.idea.vim.handler.StartNewLineDetector"
|
||||
id="ideavim-start-new-line-detector"
|
||||
order="first"/>
|
||||
<editorActionHandler action="EditorStartNewLineBefore"
|
||||
implementationClass="com.maddyhome.idea.vim.handler.StartNewLineBeforeCurrentDetector"
|
||||
id="ideavim-start-new-line-before-current-detector"
|
||||
order="first"/>
|
||||
|
||||
<editorFactoryDocumentListener
|
||||
implementation="com.maddyhome.idea.vim.listener.VimListenerManager$VimDocumentListener"/>
|
||||
|
||||
@@ -432,8 +459,7 @@
|
||||
|
||||
<actions>
|
||||
<action class="com.maddyhome.idea.vim.extension.hints.ToggleHintsAction" text="Toggle Hints">
|
||||
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift BACK_SLASH"/>
|
||||
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl meta BACK_SLASH"/>
|
||||
<keyboard-shortcut keymap="$default" first-keystroke="ctrl BACK_SLASH"/>
|
||||
</action>
|
||||
</actions>
|
||||
</idea-plugin>
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
<listener class="com.maddyhome.idea.vim.listener.RiderActionListener"
|
||||
topic="com.intellij.openapi.actionSystem.ex.AnActionListener"/>
|
||||
</projectListeners>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<editorActionHandler action="EditorEscape"
|
||||
implementationClass="com.maddyhome.idea.vim.handler.VimEscForRiderHandler"
|
||||
id="ideavim-rider-esc"
|
||||
order="first, before idea.only.escape"/>
|
||||
</extensions>
|
||||
<extensions defaultExtensionNs="IdeaVIM">
|
||||
<riderProvider implementation="com.maddyhome.idea.vim.ide.RiderProviderImpl"/>
|
||||
</extensions>
|
||||
|
||||
@@ -45,7 +45,6 @@ const knownPlugins = new Set([
|
||||
"com.github.pooryam92.vimcoach", // https://plugins.jetbrains.com/plugin/30148-vim-coach
|
||||
"lazyideavim.whichkeylazy", // https://plugins.jetbrains.com/plugin/30446-which-key-lazy
|
||||
"com.github.vimkeysuggest", // https://plugins.jetbrains.com/plugin/30486-vimkeysuggest
|
||||
"dev.ckob.lazygit", // https://plugins.jetbrains.com/plugin/30919-lazygit
|
||||
]);
|
||||
|
||||
async function getPluginLinkByXmlId(xmlId: string): Promise<string | null> {
|
||||
|
||||
31
scripts-ts/src/teamcityTest.ts
Normal file
31
scripts-ts/src/teamcityTest.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Simple test script to verify TeamCity can run TypeScript scripts.
|
||||
* Run with: npx tsx src/teamcityTest.ts
|
||||
*/
|
||||
|
||||
console.log("=== TeamCity TypeScript Test Script ===");
|
||||
console.log(`Node version: ${process.version}`);
|
||||
console.log(`Platform: ${process.platform}`);
|
||||
console.log(`Current directory: ${process.cwd()}`);
|
||||
console.log(`Script arguments: ${process.argv.slice(2).join(", ") || "(none)"}`);
|
||||
|
||||
// Test that we can import modules
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const packageJsonPath = path.join(process.cwd(), "package.json");
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
||||
console.log(`Package name: ${pkg.name}`);
|
||||
console.log(`Package version: ${pkg.version}`);
|
||||
}
|
||||
|
||||
// Demonstrate TeamCity service messages (for build status reporting)
|
||||
// See: https://www.jetbrains.com/help/teamcity/service-messages.html
|
||||
console.log("");
|
||||
console.log("##teamcity[message text='TypeScript script executed successfully' status='NORMAL']");
|
||||
|
||||
// Exit with success
|
||||
console.log("");
|
||||
console.log("✓ Test completed successfully!");
|
||||
process.exit(0);
|
||||
@@ -21,16 +21,16 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.3.21")
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.3.10")
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
|
||||
implementation("io.ktor:ktor-client-core:3.4.3")
|
||||
implementation("io.ktor:ktor-client-cio:3.4.3")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:3.4.3")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.3")
|
||||
implementation("io.ktor:ktor-client-auth:3.4.3")
|
||||
implementation("io.ktor:ktor-client-core:3.4.2")
|
||||
implementation("io.ktor:ktor-client-cio:3.4.2")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:3.4.2")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.2")
|
||||
implementation("io.ktor:ktor-client-auth:3.4.2")
|
||||
implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
|
||||
|
||||
// This is needed for jgit to connect to ssh
|
||||
|
||||
@@ -106,13 +106,9 @@ internal class IjVimPluginActivator : VimPluginActivator {
|
||||
}
|
||||
|
||||
// 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)
|
||||
// VIM-4115: close() clears editor mode, KeyHandlerState.commandLineCommandBuilder, and the panel
|
||||
// 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()
|
||||
?.fullReset()
|
||||
|
||||
// Unregister vim actions in command mode
|
||||
RegisterActions.unregisterActions()
|
||||
|
||||
@@ -22,11 +22,13 @@ import com.intellij.openapi.util.Disposer;
|
||||
import com.maddyhome.idea.vim.api.*;
|
||||
import com.maddyhome.idea.vim.config.VimState;
|
||||
import com.maddyhome.idea.vim.config.migration.ApplicationConfigurationMigrator;
|
||||
import com.maddyhome.idea.vim.group.*;
|
||||
import com.maddyhome.idea.vim.group.visual.VisualMotionGroup;
|
||||
import com.maddyhome.idea.vim.group.ChangeGroup;
|
||||
import com.maddyhome.idea.vim.group.KeyGroup;
|
||||
import com.maddyhome.idea.vim.group.VimNotifications;
|
||||
import com.maddyhome.idea.vim.group.VimWindowGroup;
|
||||
import com.maddyhome.idea.vim.history.VimHistory;
|
||||
import com.maddyhome.idea.vim.macro.VimMacro;
|
||||
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.VimSearchGroupLegacyLoader;
|
||||
import com.maddyhome.idea.vim.put.VimPut;
|
||||
@@ -46,7 +48,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.
|
||||
* 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 static final int STATE_VERSION = 7;
|
||||
@@ -85,48 +87,49 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
|
||||
}
|
||||
|
||||
|
||||
public static @NotNull MotionGroup getMotion() {
|
||||
return ((MotionGroup)VimInjectorKt.getInjector().getMotion());
|
||||
public static @NotNull VimMotionGroup getMotion() {
|
||||
return VimInjectorKt.getInjector().getMotion();
|
||||
}
|
||||
|
||||
public static @NotNull ChangeGroup getChange() {
|
||||
return ((ChangeGroup)VimInjectorKt.getInjector().getChangeGroup());
|
||||
}
|
||||
|
||||
public static @NotNull CommandGroup getCommand() {
|
||||
return ((CommandGroup)VimInjectorKt.getInjector().getCommandGroup());
|
||||
public static @NotNull VimCommandGroup getCommand() {
|
||||
return VimInjectorKt.getInjector().getCommandGroup();
|
||||
}
|
||||
|
||||
public static @NotNull RegisterGroup getRegister() {
|
||||
return ((RegisterGroup)VimInjectorKt.getInjector().getRegisterGroup());
|
||||
public static @NotNull VimRegisterGroup getRegister() {
|
||||
return VimInjectorKt.getInjector().getRegisterGroup();
|
||||
}
|
||||
|
||||
public static @NotNull VimFile getFile() {
|
||||
return VimInjectorKt.getInjector().getFile();
|
||||
}
|
||||
|
||||
public static @NotNull IjVimSearchGroup getSearch() {
|
||||
return ((IjVimSearchGroup)VimInjectorKt.getInjector().getSearchGroup());
|
||||
public static @NotNull VimSearchGroup getSearch() {
|
||||
return VimInjectorKt.getInjector().getSearchGroup();
|
||||
}
|
||||
|
||||
public static @Nullable IjVimSearchGroup getSearchIfCreated() {
|
||||
return ApplicationManager.getApplication().getServiceIfCreated(IjVimSearchGroup.class);
|
||||
public static @Nullable VimSearchGroup getSearchIfCreated() {
|
||||
VimSearchGroup searchGroup = ApplicationManager.getApplication().getServiceIfCreated(VimSearchGroup.class);
|
||||
return searchGroup;
|
||||
}
|
||||
|
||||
public static @NotNull VimProcessGroup getProcess() {
|
||||
return VimInjectorKt.getInjector().getProcessGroup();
|
||||
}
|
||||
|
||||
public static @NotNull MacroGroup getMacro() {
|
||||
return ((MacroGroup)VimInjectorKt.getInjector().getMacro());
|
||||
public static @NotNull VimMacro getMacro() {
|
||||
return VimInjectorKt.getInjector().getMacro();
|
||||
}
|
||||
|
||||
public static @NotNull VimDigraphGroup getDigraph() {
|
||||
return VimInjectorKt.getInjector().getDigraphGroup();
|
||||
}
|
||||
|
||||
public static @NotNull HistoryGroup getHistory() {
|
||||
return ((HistoryGroup)VimInjectorKt.getInjector().getHistoryGroup());
|
||||
public static @NotNull VimHistory getHistory() {
|
||||
return VimInjectorKt.getInjector().getHistoryGroup();
|
||||
}
|
||||
|
||||
public static @NotNull KeyGroup getKey() {
|
||||
@@ -137,20 +140,20 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
|
||||
return ApplicationManager.getApplication().getServiceIfCreated(KeyGroup.class);
|
||||
}
|
||||
|
||||
public static @NotNull WindowGroup getWindow() {
|
||||
return ((WindowGroup)VimInjectorKt.getInjector().getWindow());
|
||||
public static @NotNull VimWindowGroup getWindow() {
|
||||
return VimInjectorKt.getInjector().getWindow();
|
||||
}
|
||||
|
||||
public static @NotNull EditorGroup getEditor() {
|
||||
return ((EditorGroup)VimInjectorKt.getInjector().getEditorGroup());
|
||||
public static @NotNull VimEditorGroup getEditor() {
|
||||
return VimInjectorKt.getInjector().getEditorGroup();
|
||||
}
|
||||
|
||||
public static @Nullable EditorGroup getEditorIfCreated() {
|
||||
return ApplicationManager.getApplication().getServiceIfCreated(EditorGroup.class);
|
||||
public static @Nullable VimEditorGroup getEditorIfCreated() {
|
||||
return ApplicationManager.getApplication().getServiceIfCreated(VimEditorGroup.class);
|
||||
}
|
||||
|
||||
public static @NotNull VisualMotionGroup getVisualMotion() {
|
||||
return ((VisualMotionGroup)VimInjectorKt.getInjector().getVisualMotionGroup());
|
||||
public static @NotNull VimVisualMotionGroup getVisualMotion() {
|
||||
return VimInjectorKt.getInjector().getVisualMotionGroup();
|
||||
}
|
||||
|
||||
public static @NotNull YankGroupBase getYank() {
|
||||
@@ -183,8 +186,7 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
|
||||
}
|
||||
|
||||
public static boolean isEnabled() {
|
||||
final VimPlugin instance = ApplicationManager.getApplication().getService(VimPlugin.class);
|
||||
return instance != null && instance.enabled;
|
||||
return getInstance().enabled;
|
||||
}
|
||||
|
||||
public static void setEnabled(final boolean enabled) {
|
||||
|
||||
@@ -32,6 +32,8 @@ import com.maddyhome.idea.vim.api.globalOptions
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.group.IjOptionConstants
|
||||
import com.maddyhome.idea.vim.group.IjOptions
|
||||
import com.maddyhome.idea.vim.handler.enableOctopus
|
||||
import com.maddyhome.idea.vim.handler.isOctopusEnabled
|
||||
import com.maddyhome.idea.vim.helper.EditorHelper
|
||||
import com.maddyhome.idea.vim.helper.HandlerInjector
|
||||
import com.maddyhome.idea.vim.helper.inNormalMode
|
||||
@@ -90,8 +92,8 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
||||
// Control-flow exceptions (like ProcessCanceledException) should never be logged and should be rethrown
|
||||
// See {@link com.intellij.openapi.diagnostic.Logger.checkException}
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
LOG.error(e)
|
||||
} catch (throwable: Throwable) {
|
||||
LOG.error(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,6 +119,15 @@ class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible*/ {
|
||||
if (VimPlugin.isNotEnabled()) return ActionEnableStatus.no("IdeaVim is disabled", LogLevel.DEBUG)
|
||||
val editor = getEditor(e) ?: return ActionEnableStatus.no("Can't get Editor", LogLevel.DEBUG)
|
||||
|
||||
if (enableOctopus) {
|
||||
if (isOctopusEnabled(keyStroke, editor)) {
|
||||
return ActionEnableStatus.no(
|
||||
"Processing VimShortcutKeyAction for the key that is used in the octopus handler",
|
||||
LogLevel.ERROR
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (e.dataContext.isNotSupportedContextComponent && Registry.`is`("ideavim.only.in.editor.component")) {
|
||||
// Note: Currently, IdeaVim works ONLY in the editor & ExTextField component. However, the presence of the
|
||||
// PlatformDataKeys.EDITOR in the data context does not mean that the current focused component is editor.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2003-2026 The IdeaVim authors
|
||||
* Copyright 2003-2025 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -12,7 +12,6 @@ import com.intellij.openapi.options.advanced.AdvancedSettings
|
||||
import com.intellij.util.ui.tree.TreeUtil
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.newapi.vim
|
||||
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.tree.TreeNode
|
||||
@@ -55,13 +54,10 @@ fun MutableMap<List<KeyStroke>, NerdTreeAction>.register(mapping: String, action
|
||||
*/
|
||||
val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List<KeyStroke>, NerdTreeAction>().apply {
|
||||
// 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).
|
||||
// This avoids ActionManager.tryToExecute which can RPC to backend in split mode,
|
||||
// while preserving platform features (separator skipping, cycle scrolling, loading node handling).
|
||||
register("k", NerdTreeAction.swing("selectPrevious"))
|
||||
register("j", NerdTreeAction.swing("selectNext"))
|
||||
register("G", NerdTreeAction.swing("selectLast"))
|
||||
register("gg", NerdTreeAction.swing("selectFirst"))
|
||||
register("k", NerdTreeAction.ij("Tree-selectPrevious"))
|
||||
register("j", NerdTreeAction.ij("Tree-selectNext"))
|
||||
register("G", NerdTreeAction.ij("Tree-selectLast"))
|
||||
register("gg", NerdTreeAction.ij("Tree-selectFirst"))
|
||||
|
||||
// FIXME lazy loaded tree nodes are not expanded
|
||||
register("NERDTreeMapOpenRecursively", "O", NerdTreeAction.ij("FullyExpandTreeNode"))
|
||||
@@ -106,7 +102,7 @@ val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List
|
||||
tree.selectionPath = path
|
||||
tree.scrollPathToVisible(path)
|
||||
})
|
||||
register("NERDTreeMapJumpParent", "p", NerdTreeAction.swing("selectParentNoCollapse"))
|
||||
register("NERDTreeMapJumpParent", "p", NerdTreeAction.ij("Tree-selectParentNoCollapse"))
|
||||
register(
|
||||
"NERDTreeMapJumpFirstChild",
|
||||
"K",
|
||||
@@ -133,12 +129,9 @@ val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List
|
||||
tree.scrollPathToVisible(path)
|
||||
},
|
||||
)
|
||||
register("NERDTreeMapJumpNextSibling", "<C-J>", NerdTreeAction.swing("selectNextSibling"))
|
||||
register("NERDTreeMapJumpPrevSibling", "<C-K>", NerdTreeAction.swing("selectPreviousSibling"))
|
||||
register("NERDTreeMapJumpNextSibling", "<C-J>", NerdTreeAction.ij("Tree-selectNextSibling"))
|
||||
register("NERDTreeMapJumpPrevSibling", "<C-K>", NerdTreeAction.ij("Tree-selectPreviousSibling"))
|
||||
|
||||
register("/", NerdTreeAction { event, tree ->
|
||||
armSelectionRestoreOnEscape(tree)
|
||||
NerdTreeAction.callAction(null, "SpeedSearch", event.dataContext.vim)
|
||||
})
|
||||
register("/", NerdTreeAction.ij("SpeedSearch"))
|
||||
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
|
||||
* 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.AnActionEvent
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.ui.SwingActionDelegate
|
||||
import com.intellij.ui.treeStructure.Tree
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
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.
|
||||
*/
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.extension.nerdtree
|
||||
|
||||
import com.intellij.ui.treeStructure.Tree
|
||||
import java.awt.event.FocusAdapter
|
||||
import java.awt.event.FocusEvent
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
/**
|
||||
* Captures the current tree selection and arranges to restore it if the
|
||||
* upcoming SpeedSearch session is dismissed via ESC, so the cursor returns
|
||||
* to the file the user was on before pressing `/`.
|
||||
*
|
||||
* No-op if no row is currently selected. Any non-ESC key (e.g. ENTER) cancels
|
||||
* the restoration so committing the search keeps the matched item selected.
|
||||
*/
|
||||
internal fun armSelectionRestoreOnEscape(tree: Tree) {
|
||||
val originalPath = tree.selectionPath ?: return
|
||||
|
||||
lateinit var keyListener: KeyAdapter
|
||||
lateinit var focusListener: FocusAdapter
|
||||
val disarm = {
|
||||
tree.removeKeyListener(keyListener)
|
||||
tree.removeFocusListener(focusListener)
|
||||
}
|
||||
|
||||
keyListener = object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
when (e.keyCode) {
|
||||
KeyEvent.VK_ESCAPE -> {
|
||||
disarm()
|
||||
// Defer until SpeedSearch finishes processing the ESC and clearing
|
||||
// its own state, so our restored selection is the one that sticks.
|
||||
SwingUtilities.invokeLater {
|
||||
tree.selectionPath = originalPath
|
||||
tree.scrollPathToVisible(originalPath)
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.VK_ENTER -> disarm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If focus leaves the tree before ESC/ENTER (mouse click elsewhere, popup
|
||||
// dismissed by tool window switch), drop both listeners so we don't leak
|
||||
// or surprise the user with a delayed jump on a later ESC.
|
||||
focusListener = object : FocusAdapter() {
|
||||
override fun focusLost(e: FocusEvent) = disarm()
|
||||
}
|
||||
|
||||
tree.addKeyListener(keyListener)
|
||||
tree.addFocusListener(focusListener)
|
||||
}
|
||||
@@ -9,8 +9,6 @@
|
||||
package com.maddyhome.idea.vim.extension.replacewithregister
|
||||
|
||||
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
|
||||
|
||||
internal class ReplaceWithRegister : VimExtension {
|
||||
@@ -19,15 +17,21 @@ internal class ReplaceWithRegister : VimExtension {
|
||||
|
||||
override fun init(initApi: VimInitApi) {
|
||||
initApi.mappings {
|
||||
nmapPluginAction("gr", RWR_OPERATOR, keepDefaultMapping = true) {
|
||||
// Step 1: Non-recursive <Plug> → action mappings
|
||||
nnoremap(RWR_OPERATOR) {
|
||||
rewriteMotion()
|
||||
}
|
||||
nmapPluginAction("grr", RWR_LINE, keepDefaultMapping = true) {
|
||||
nnoremap(RWR_LINE) {
|
||||
rewriteLine()
|
||||
}
|
||||
vmapPluginAction("gr", RWR_VISUAL, keepDefaultMapping = true) {
|
||||
vnoremap(RWR_VISUAL) {
|
||||
rewriteVisual()
|
||||
}
|
||||
|
||||
// Step 2: Recursive key → <Plug> mappings
|
||||
nmap("gr", RWR_OPERATOR)
|
||||
nmap("grr", RWR_LINE)
|
||||
vmap("gr", RWR_VISUAL)
|
||||
}
|
||||
|
||||
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.TextType
|
||||
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"
|
||||
|
||||
@VimPlugin(name = PLUGIN_NAME)
|
||||
fun VimInitApi.init() {
|
||||
mappings {
|
||||
nmapPluginAction("gr", RWR_OPERATOR, keepDefaultMapping = true) {
|
||||
// Step 1: Non-recursive <Plug> → action mappings
|
||||
nnoremap(RWR_OPERATOR) {
|
||||
rewriteMotion()
|
||||
}
|
||||
nmapPluginAction("grr", RWR_LINE, keepDefaultMapping = true) {
|
||||
nnoremap(RWR_LINE) {
|
||||
rewriteLine()
|
||||
}
|
||||
vmapPluginAction("gr", RWR_VISUAL, keepDefaultMapping = true) {
|
||||
vnoremap(RWR_VISUAL) {
|
||||
rewriteVisual()
|
||||
}
|
||||
|
||||
// Step 2: Recursive key → <Plug> mappings
|
||||
nmap("gr", RWR_OPERATOR)
|
||||
nmap("grr", RWR_LINE)
|
||||
vmap("gr", RWR_VISUAL)
|
||||
}
|
||||
|
||||
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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -7,273 +7,181 @@
|
||||
*/
|
||||
package com.maddyhome.idea.vim.extension.textobjindent
|
||||
|
||||
import com.intellij.openapi.editor.Caret
|
||||
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
|
||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||
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.intellij.vim.api.VimApi
|
||||
import com.intellij.vim.api.VimInitApi
|
||||
import com.intellij.vim.api.scopes.TextObjectRange
|
||||
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:
|
||||
* [vim-indent-object](https://github.com/michaeljsmith/vim-indent-object)
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* vim-indent-object provides these text objects based on the cursor line's indentation:
|
||||
*
|
||||
*
|
||||
* * `ai` **A**n **I**ndentation level and line above.
|
||||
* * `ii` **I**nner **I**ndentation level (no line above).
|
||||
* * `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`
|
||||
*
|
||||
*
|
||||
*
|
||||
* 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)
|
||||
*/
|
||||
class VimIndentObject : VimExtension {
|
||||
override fun getName(): String {
|
||||
return "textobj-indent"
|
||||
}
|
||||
override fun getName(): String = "textobj-indent"
|
||||
|
||||
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 {
|
||||
override val isRepeatable: Boolean
|
||||
get() = false
|
||||
|
||||
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.
|
||||
var caretLineStartOffset = caretOffset
|
||||
var accumulatedWhitespace = 0
|
||||
while (--caretLineStartOffset >= 0) {
|
||||
val ch = charSequence.get(caretLineStartOffset)
|
||||
if (ch == ' ' || ch == '\t') {
|
||||
++accumulatedWhitespace
|
||||
} else if (ch == '\n') {
|
||||
++caretLineStartOffset
|
||||
break
|
||||
} else {
|
||||
accumulatedWhitespace = 0
|
||||
}
|
||||
}
|
||||
if (caretLineStartOffset < 0) {
|
||||
caretLineStartOffset = 0
|
||||
}
|
||||
|
||||
// `caretLineStartOffset` points to the first character in the line where the caret is located.
|
||||
|
||||
// Part 2: Compute the indentation level of the caret line.
|
||||
// This is done as a separate step so that it works even when the caret is inside the indentation.
|
||||
var offset = caretLineStartOffset
|
||||
var indentSize = 0
|
||||
while (offset < charSequence.length) {
|
||||
val ch = charSequence.get(offset)
|
||||
if (ch == ' ' || ch == '\t') {
|
||||
++indentSize
|
||||
++offset
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// `indentSize` contains the amount of indent to be used for the text object range to be returned.
|
||||
var upperBoundaryOffset: Int? = null
|
||||
// Part 3: Find a line above the caret line, that has an indentation lower than `indentSize`.
|
||||
var pos1 = caretLineStartOffset - 1
|
||||
var isUpperBoundaryFound = false
|
||||
while (upperBoundaryOffset == null) {
|
||||
// 3.1: Going backwards from `caretLineStartOffset`, find the first non-whitespace character.
|
||||
while (--pos1 >= 0) {
|
||||
val ch = charSequence.get(pos1)
|
||||
if (ch != ' ' && ch != '\t' && ch != '\n') {
|
||||
break
|
||||
}
|
||||
}
|
||||
// 3.2: Find the indent size of the line with this non-whitespace character and check against `indentSize`.
|
||||
accumulatedWhitespace = 0
|
||||
while (--pos1 >= 0) {
|
||||
val ch = charSequence.get(pos1)
|
||||
if (ch == ' ' || ch == '\t') {
|
||||
++accumulatedWhitespace
|
||||
} else if (ch == '\n') {
|
||||
if (accumulatedWhitespace < indentSize) {
|
||||
upperBoundaryOffset = pos1 + 1
|
||||
isUpperBoundaryFound = true
|
||||
}
|
||||
break
|
||||
} else {
|
||||
accumulatedWhitespace = 0
|
||||
}
|
||||
}
|
||||
if (pos1 < 0) {
|
||||
// Reached start of the buffer.
|
||||
upperBoundaryOffset = 0
|
||||
isUpperBoundaryFound = accumulatedWhitespace < indentSize
|
||||
}
|
||||
}
|
||||
|
||||
// Now `upperBoundaryOffset` marks the beginning of an `ai` text object.
|
||||
if (isUpperBoundaryFound && !includeAbove) {
|
||||
while (++upperBoundaryOffset < charSequence.length) {
|
||||
val ch = charSequence.get(upperBoundaryOffset)
|
||||
if (ch == '\n') {
|
||||
++upperBoundaryOffset
|
||||
break
|
||||
}
|
||||
}
|
||||
while (charSequence.get(upperBoundaryOffset) == '\n') {
|
||||
++upperBoundaryOffset
|
||||
}
|
||||
}
|
||||
|
||||
// Part 4: Find the start of the caret line.
|
||||
var caretLineEndOffset = caretOffset
|
||||
while (++caretLineEndOffset < charSequence.length) {
|
||||
val ch = charSequence.get(caretLineEndOffset)
|
||||
if (ch == '\n') {
|
||||
++caretLineEndOffset
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// `caretLineEndOffset` points to the first charater in the line below caret line.
|
||||
var lowerBoundaryOffset: Int? = null
|
||||
// Part 5: Find a line below the caret line, that has an indentation lower than `indentSize`.
|
||||
var pos2 = caretLineEndOffset - 1
|
||||
var isLowerBoundaryFound = false
|
||||
while (lowerBoundaryOffset == null) {
|
||||
var accumulatedWhitespace2 = 0
|
||||
var lastNewlinePos = caretLineEndOffset - 1
|
||||
var isInIndent = true
|
||||
while (++pos2 < charSequence.length) {
|
||||
val ch = charSequence.get(pos2)
|
||||
if (isIndentChar(ch) && isInIndent) {
|
||||
++accumulatedWhitespace2
|
||||
} else if (ch == '\n') {
|
||||
accumulatedWhitespace2 = 0
|
||||
lastNewlinePos = pos2
|
||||
isInIndent = true
|
||||
} else {
|
||||
if (isInIndent && accumulatedWhitespace2 < indentSize) {
|
||||
lowerBoundaryOffset = lastNewlinePos
|
||||
isLowerBoundaryFound = true
|
||||
break
|
||||
}
|
||||
isInIndent = false
|
||||
}
|
||||
}
|
||||
if (pos2 >= charSequence.length) {
|
||||
// Reached end of the buffer.
|
||||
lowerBoundaryOffset = charSequence.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
// Now `lowerBoundaryOffset` marks the end of an `ii` text object.
|
||||
if (isLowerBoundaryFound && includeBelow) {
|
||||
while (++lowerBoundaryOffset < charSequence.length) {
|
||||
val ch = charSequence.get(lowerBoundaryOffset)
|
||||
if (ch == '\n') {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TextRange(upperBoundaryOffset, lowerBoundaryOffset)
|
||||
}
|
||||
|
||||
override val visualType: TextObjectVisualType
|
||||
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)
|
||||
}
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun VimApi.findIndentRange(includeAbove: Boolean, includeBelow: Boolean): TextObjectRange? {
|
||||
val charSequence = editor { read { text } }
|
||||
val caretOffset = editor { read { withPrimaryCaret { offset } } }
|
||||
|
||||
// Part 1: Find the start of the caret line.
|
||||
var caretLineStartOffset = caretOffset
|
||||
var accumulatedWhitespace = 0
|
||||
while (--caretLineStartOffset >= 0) {
|
||||
val ch = charSequence[caretLineStartOffset]
|
||||
if (ch == ' ' || ch == '\t') {
|
||||
++accumulatedWhitespace
|
||||
} else if (ch == '\n') {
|
||||
++caretLineStartOffset
|
||||
break
|
||||
} else {
|
||||
accumulatedWhitespace = 0
|
||||
}
|
||||
}
|
||||
if (caretLineStartOffset < 0) {
|
||||
caretLineStartOffset = 0
|
||||
}
|
||||
|
||||
// `caretLineStartOffset` points to the first character in the line where the caret is located.
|
||||
|
||||
// Part 2: Compute the indentation level of the caret line.
|
||||
// This is done as a separate step so that it works even when the caret is inside the indentation.
|
||||
var offset = caretLineStartOffset
|
||||
var indentSize = 0
|
||||
while (offset < charSequence.length) {
|
||||
val ch = charSequence[offset]
|
||||
if (ch == ' ' || ch == '\t') {
|
||||
++indentSize
|
||||
++offset
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// `indentSize` contains the amount of indent to be used for the text object range to be returned.
|
||||
var upperBoundaryOffset: Int? = null
|
||||
// Part 3: Find a line above the caret line, that has an indentation lower than `indentSize`.
|
||||
var pos1 = caretLineStartOffset - 1
|
||||
var isUpperBoundaryFound = false
|
||||
while (upperBoundaryOffset == null) {
|
||||
// 3.1: Going backwards from `caretLineStartOffset`, find the first non-whitespace character.
|
||||
while (--pos1 >= 0) {
|
||||
val ch = charSequence[pos1]
|
||||
if (ch != ' ' && ch != '\t' && ch != '\n') {
|
||||
break
|
||||
}
|
||||
}
|
||||
// 3.2: Find the indent size of the line with this non-whitespace character and check against `indentSize`.
|
||||
accumulatedWhitespace = 0
|
||||
while (--pos1 >= 0) {
|
||||
val ch = charSequence[pos1]
|
||||
if (ch == ' ' || ch == '\t') {
|
||||
++accumulatedWhitespace
|
||||
} else if (ch == '\n') {
|
||||
if (accumulatedWhitespace < indentSize) {
|
||||
upperBoundaryOffset = pos1 + 1
|
||||
isUpperBoundaryFound = true
|
||||
}
|
||||
break
|
||||
} else {
|
||||
accumulatedWhitespace = 0
|
||||
}
|
||||
}
|
||||
if (pos1 < 0) {
|
||||
// Reached start of the buffer.
|
||||
upperBoundaryOffset = 0
|
||||
isUpperBoundaryFound = accumulatedWhitespace < indentSize
|
||||
}
|
||||
}
|
||||
|
||||
// Now `upperBoundaryOffset` marks the beginning of an `ai` text object.
|
||||
if (isUpperBoundaryFound && !includeAbove) {
|
||||
while (++upperBoundaryOffset < charSequence.length) {
|
||||
val ch = charSequence[upperBoundaryOffset]
|
||||
if (ch == '\n') {
|
||||
++upperBoundaryOffset
|
||||
break
|
||||
}
|
||||
}
|
||||
while (charSequence[upperBoundaryOffset] == '\n') {
|
||||
++upperBoundaryOffset
|
||||
}
|
||||
}
|
||||
|
||||
// Part 4: Find the end of the caret line.
|
||||
var caretLineEndOffset = caretOffset
|
||||
while (++caretLineEndOffset < charSequence.length) {
|
||||
val ch = charSequence[caretLineEndOffset]
|
||||
if (ch == '\n') {
|
||||
++caretLineEndOffset
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// `caretLineEndOffset` points to the first character in the line below caret line.
|
||||
var lowerBoundaryOffset: Int? = null
|
||||
// Part 5: Find a line below the caret line, that has an indentation lower than `indentSize`.
|
||||
var pos2 = caretLineEndOffset - 1
|
||||
var isLowerBoundaryFound = false
|
||||
while (lowerBoundaryOffset == null) {
|
||||
var accumulatedWhitespace2 = 0
|
||||
var lastNewlinePos = caretLineEndOffset - 1
|
||||
var isInIndent = true
|
||||
while (++pos2 < charSequence.length) {
|
||||
val ch = charSequence[pos2]
|
||||
if (isIndentChar(ch) && isInIndent) {
|
||||
++accumulatedWhitespace2
|
||||
} else if (ch == '\n') {
|
||||
accumulatedWhitespace2 = 0
|
||||
lastNewlinePos = pos2
|
||||
isInIndent = true
|
||||
} else {
|
||||
if (isInIndent && accumulatedWhitespace2 < indentSize) {
|
||||
lowerBoundaryOffset = lastNewlinePos
|
||||
isLowerBoundaryFound = true
|
||||
break
|
||||
}
|
||||
isInIndent = false
|
||||
}
|
||||
}
|
||||
if (pos2 >= charSequence.length) {
|
||||
// Reached end of the buffer.
|
||||
lowerBoundaryOffset = charSequence.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
// Now `lowerBoundaryOffset` marks the end of an `ii` text object.
|
||||
if (isLowerBoundaryFound && includeBelow) {
|
||||
while (++lowerBoundaryOffset < charSequence.length) {
|
||||
val ch = charSequence[lowerBoundaryOffset]
|
||||
if (ch == '\n') {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert offsets to line numbers for LineWise result
|
||||
val startLine = editor { read { getLine(upperBoundaryOffset).number } }
|
||||
val endLine = editor { read { getLine(lowerBoundaryOffset).number } }
|
||||
return TextObjectRange.LineWise(startLine, endLine)
|
||||
}
|
||||
|
||||
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.ToolWindowType
|
||||
import com.maddyhome.idea.vim.extension.VimExtension
|
||||
import com.maddyhome.idea.vim.helper.EditorHelper
|
||||
import java.awt.Component
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.beans.PropertyChangeListener
|
||||
@@ -32,7 +31,7 @@ internal class ToolWindowNavEverywhere : VimExtension {
|
||||
val oldFocusOwner = evt.oldValue as? JComponent
|
||||
val dispatcher = service<ToolWindowNavDispatcher>()
|
||||
|
||||
if (newFocusOwner != null && isInsideToolWindow(newFocusOwner) && !isPythonConsoleComponent(newFocusOwner)) {
|
||||
if (newFocusOwner != null && isInsideToolWindow(newFocusOwner)) {
|
||||
dispatcher.register(newFocusOwner)
|
||||
}
|
||||
|
||||
@@ -52,18 +51,6 @@ internal class ToolWindowNavEverywhere : VimExtension {
|
||||
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 {
|
||||
for (project in ProjectManager.getInstance().openProjects) {
|
||||
if (project.isDisposed) continue
|
||||
|
||||
@@ -14,14 +14,15 @@ import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.command.UndoConfirmationPolicy
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
|
||||
import com.intellij.openapi.editor.actions.EnterAction
|
||||
import com.intellij.openapi.editor.event.EditorMouseEvent
|
||||
import com.intellij.openapi.editor.event.EditorMouseListener
|
||||
import com.intellij.openapi.editor.impl.editorId
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.psi.codeStyle.CodeStyleManager
|
||||
import com.intellij.psi.util.PsiUtilBase
|
||||
import com.maddyhome.idea.vim.EventFacade
|
||||
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.VimChangeGroupBase
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
@@ -29,15 +30,12 @@ import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.common.TextRange
|
||||
import com.maddyhome.idea.vim.group.change.ChangeRemoteApi
|
||||
import com.maddyhome.idea.vim.group.format.FormatRemoteApi
|
||||
import com.maddyhome.idea.vim.helper.CodeWrapper
|
||||
import com.maddyhome.idea.vim.helper.CommentLeaderParser
|
||||
import com.maddyhome.idea.vim.handler.commandContinuation
|
||||
import com.maddyhome.idea.vim.helper.inInsertMode
|
||||
import com.maddyhome.idea.vim.key.KeyHandlerKeeper
|
||||
import com.maddyhome.idea.vim.listener.VimInsertListener
|
||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
|
||||
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.undo.VimKeyBasedUndoService
|
||||
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
||||
@@ -96,14 +94,41 @@ class ChangeGroup : VimChangeGroupBase() {
|
||||
injector.scroll.scrollCaretIntoView(vimEditor)
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is REPLACE mode we need to turn off OVERWRITE before and then turn OVERWRITE back on after sending the
|
||||
* "ENTER" key.
|
||||
*/
|
||||
override fun processEnter(
|
||||
editor: VimEditor,
|
||||
caret: VimCaret,
|
||||
context: ExecutionContext,
|
||||
) {
|
||||
if (editor.mode is Mode.REPLACE) {
|
||||
editor.insertMode = true
|
||||
}
|
||||
try {
|
||||
val continuation = (context.context as UserDataHolder).getUserData(commandContinuation)
|
||||
val ijEditor = editor.ij
|
||||
val ij = context.ij
|
||||
val ijCaret = caret.ij
|
||||
if (continuation != null) {
|
||||
continuation.execute(ijEditor, ijCaret, ij)
|
||||
} else {
|
||||
EnterAction().handler.execute(ijEditor, ijCaret, ij)
|
||||
}
|
||||
} finally {
|
||||
if (editor.mode is Mode.REPLACE) {
|
||||
editor.insertMode = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun processBackspace(editor: VimEditor, context: ExecutionContext) {
|
||||
injector.actionExecutor.executeAction(editor, name = IdeActions.ACTION_EDITOR_BACKSPACE, context = context)
|
||||
injector.scroll.scrollCaretIntoView(editor)
|
||||
}
|
||||
|
||||
override fun repeatInsertText(editor: VimEditor, context: ExecutionContext, count: Int) {
|
||||
if (count <= 0) return
|
||||
|
||||
val ijEditor = (editor as IjVimEditor).editor
|
||||
val editorId = ijEditor.editorId()
|
||||
|
||||
@@ -127,39 +152,6 @@ class ChangeGroup : VimChangeGroupBase() {
|
||||
injector.application.runWriteAction {
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@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 static final @NonNls String EDITOR_STORE_ELEMENT = "editor";
|
||||
private final CaretListener myLineNumbersCaretListener = new CaretListener() {
|
||||
@@ -321,18 +321,6 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
|
||||
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
|
||||
public @NotNull Collection<VimEditor> getEditorsRaw() {
|
||||
return getLocalEditors().map(IjVimEditor::new).collect(Collectors.toList());
|
||||
|
||||
@@ -164,10 +164,6 @@ class IjFileGroup : VimFileBase() {
|
||||
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 {
|
||||
require(project is Project)
|
||||
return project.projectId().serializeToString()
|
||||
|
||||
@@ -28,8 +28,6 @@ import com.maddyhome.idea.vim.action.change.LazyVimCommand;
|
||||
import com.maddyhome.idea.vim.api.*;
|
||||
import com.maddyhome.idea.vim.command.MappingMode;
|
||||
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.key.*;
|
||||
import com.maddyhome.idea.vim.newapi.IjNativeAction;
|
||||
@@ -53,7 +51,7 @@ import static java.util.stream.Collectors.toList;
|
||||
/**
|
||||
* @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 static final @NonNls String SHORTCUT_CONFLICTS_ELEMENT = "shortcut-conflicts";
|
||||
private static final @NonNls String SHORTCUT_CONFLICT_ELEMENT = "shortcut-conflict";
|
||||
@@ -182,15 +180,9 @@ public class KeyGroup extends VimKeyGroupBase implements PersistentStateComponen
|
||||
|
||||
@Override
|
||||
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(),
|
||||
ShortcutHelper.toShortcutSet(getRequiredShortcutKeys()),
|
||||
ijEditor.getContentComponent());
|
||||
((IjVimEditor)editor).getEditor().getContentComponent());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -326,7 +318,11 @@ public class KeyGroup extends VimKeyGroupBase implements PersistentStateComponen
|
||||
private void registerRequiredShortcut(@NotNull List<KeyStroke> keys, MappingOwner owner) {
|
||||
for (KeyStroke key : keys) {
|
||||
if (key.getKeyChar() == KeyEvent.CHAR_UNDEFINED) {
|
||||
getRequiredShortcutKeys().add(new RequiredShortcut(key, owner));
|
||||
if (!injector.getApplication().isOctopusEnabled() ||
|
||||
!(key.getKeyCode() == KeyEvent.VK_ESCAPE && key.getModifiers() == 0) &&
|
||||
!(key.getKeyCode() == KeyEvent.VK_ENTER && key.getModifiers() == 0)) {
|
||||
getRequiredShortcutKeys().add(new RequiredShortcut(key, owner));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
package com.maddyhome.idea.vim.group
|
||||
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.Caret
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.VisualPosition
|
||||
@@ -16,9 +15,7 @@ import com.intellij.openapi.fileEditor.FileEditorManagerEvent
|
||||
import com.intellij.openapi.fileEditor.TextEditor
|
||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||
import com.intellij.openapi.fileEditor.impl.EditorWindow
|
||||
import com.intellij.platform.project.projectId
|
||||
import com.maddyhome.idea.vim.KeyHandler
|
||||
import com.maddyhome.idea.vim.api.BufferPosition
|
||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||
import com.maddyhome.idea.vim.api.ImmutableVimCaret
|
||||
import com.maddyhome.idea.vim.api.VimChangeGroupBase
|
||||
@@ -29,14 +26,12 @@ import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
|
||||
import com.maddyhome.idea.vim.api.getVisualLineCount
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.api.lineLength
|
||||
import com.maddyhome.idea.vim.api.normalizeOffset
|
||||
import com.maddyhome.idea.vim.api.normalizeVisualLine
|
||||
import com.maddyhome.idea.vim.api.visualLineToBufferLine
|
||||
import com.maddyhome.idea.vim.command.Argument
|
||||
import com.maddyhome.idea.vim.command.MotionType
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.common.TextRange
|
||||
import com.maddyhome.idea.vim.group.changelist.ChangeListService
|
||||
import com.maddyhome.idea.vim.handler.ExternalActionHandler
|
||||
import com.maddyhome.idea.vim.handler.Motion
|
||||
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
|
||||
@@ -62,39 +57,6 @@ import kotlin.math.min
|
||||
*/
|
||||
|
||||
class MotionGroup : VimMotionGroupBase() {
|
||||
|
||||
override fun moveCaretToChange(
|
||||
editor: VimEditor,
|
||||
caret: ImmutableVimCaret,
|
||||
count: Int,
|
||||
): Motion {
|
||||
val project = editor.ij.project ?: return Motion.Error
|
||||
val result = service<ChangeListService>().goToChange(project.projectId().serializeToString(), count)
|
||||
return when (result) {
|
||||
ChangeListService.MoveResult.Empty -> reportChangeListError(editor, "E664")
|
||||
ChangeListService.MoveResult.AtStart -> reportChangeListError(editor, "E662")
|
||||
ChangeListService.MoveResult.AtEnd -> reportChangeListError(editor, "E663")
|
||||
is ChangeListService.MoveResult.At -> motionToChange(editor, result.change)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportChangeListError(editor: VimEditor, code: String): Motion {
|
||||
injector.messages.showErrorMessage(editor, injector.messages.message(code))
|
||||
return Motion.Error
|
||||
}
|
||||
|
||||
private fun motionToChange(editor: VimEditor, change: ChangeListService.Change): Motion {
|
||||
val target = BufferPosition(change.line, change.col, false)
|
||||
if (editor.getPath() == change.filepath) {
|
||||
return AbsoluteOffset(editor.bufferPositionToOffset(target))
|
||||
}
|
||||
injector.file.selectEditor(editor.projectId, change.filepath, change.protocol)?.let { newEditor ->
|
||||
val offset = newEditor.bufferPositionToOffset(target)
|
||||
newEditor.currentCaret().moveToOffset(newEditor.normalizeOffset(offset, false))
|
||||
}
|
||||
return Motion.Error
|
||||
}
|
||||
|
||||
override fun moveCaretToFirstDisplayLine(
|
||||
editor: VimEditor,
|
||||
caret: ImmutableVimCaret,
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.intellij.openapi.util.SystemInfo
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.handler.KeyMapIssue
|
||||
import com.maddyhome.idea.vim.helper.MessageHelper
|
||||
import com.maddyhome.idea.vim.icons.VimIcons
|
||||
import com.maddyhome.idea.vim.key.ShortcutOwner
|
||||
@@ -160,6 +161,78 @@ internal class NotificationService(private val project: Project?) : VimNotificat
|
||||
ActionIdNotifier.notifyActionId(id, project, candidates, intentionName)
|
||||
}
|
||||
|
||||
override fun notifyKeymapIssues(issues: ArrayList<KeyMapIssue>) {
|
||||
val keymapManager = KeymapManagerEx.getInstanceEx()
|
||||
val keymap = keymapManager.activeKeymap
|
||||
val message = buildString {
|
||||
appendLine("Current IDE keymap (${keymap.name}) has issues:<br/>")
|
||||
issues.forEach {
|
||||
when (it) {
|
||||
is KeyMapIssue.AddShortcut -> {
|
||||
appendLine("- ${it.key} key is not assigned to the ${it.action} action.<br/>")
|
||||
}
|
||||
|
||||
is KeyMapIssue.RemoveShortcut -> {
|
||||
appendLine("- ${it.shortcut} key is incorrectly assigned to the ${it.action} action.<br/>")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val notification = IDEAVIM_STICKY_GROUP.createNotification(
|
||||
IDEAVIM_NOTIFICATION_TITLE,
|
||||
message,
|
||||
NotificationType.ERROR,
|
||||
)
|
||||
notification.subtitle = "IDE keymap misconfigured"
|
||||
notification.addAction(object : DumbAwareAction("Fix Keymap") {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
issues.forEach {
|
||||
when (it) {
|
||||
is KeyMapIssue.AddShortcut -> {
|
||||
keymap.addShortcut(it.actionId, KeyboardShortcut(it.keyStroke, null))
|
||||
}
|
||||
|
||||
is KeyMapIssue.RemoveShortcut -> {
|
||||
keymap.removeShortcut(it.actionId, it.shortcut)
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG.info("Shortcuts updated $issues")
|
||||
notification.expire()
|
||||
requiredShortcutsAssigned()
|
||||
}
|
||||
})
|
||||
notification.addAction(object : DumbAwareAction("Open Keymap Settings") {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
ShowSettingsUtil.getInstance().showSettingsDialog(e.project, KeymapPanel::class.java)
|
||||
notification.hideBalloon()
|
||||
}
|
||||
})
|
||||
notification.addAction(object : DumbAwareAction("Ignore") {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
LOG.info("Ignored to update shortcuts $issues")
|
||||
notification.hideBalloon()
|
||||
}
|
||||
})
|
||||
notification.notify(project)
|
||||
}
|
||||
|
||||
private fun requiredShortcutsAssigned() {
|
||||
val notification = Notification(
|
||||
IDEAVIM_NOTIFICATION_ID,
|
||||
IDEAVIM_NOTIFICATION_TITLE,
|
||||
"Keymap fixed",
|
||||
NotificationType.INFORMATION,
|
||||
)
|
||||
notification.addAction(object : DumbAwareAction("Open Keymap Settings") {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
ShowSettingsUtil.getInstance().showSettingsDialog(e.project, KeymapPanel::class.java)
|
||||
notification.hideBalloon()
|
||||
}
|
||||
})
|
||||
notification.notify(project)
|
||||
}
|
||||
|
||||
object ActionIdNotifier {
|
||||
private var notification: Notification? = null
|
||||
|
||||
|
||||
@@ -146,22 +146,6 @@ class OptionGroup : VimOptionGroupBase(), IjVimOptionGroup, InternalOptionValueA
|
||||
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 {
|
||||
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
|
||||
|
||||
@@ -10,6 +10,7 @@ package com.maddyhome.idea.vim.group
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.handler.KeyMapIssue
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
/**
|
||||
@@ -31,4 +32,5 @@ interface VimNotifications {
|
||||
fun notifyEapFinished()
|
||||
fun showReenableNotification(project: Project)
|
||||
fun notifyActionId(id: String?, candidates: List<String>? = null, intentionName: String?)
|
||||
fun notifyKeymapIssues(issues: ArrayList<KeyMapIssue>)
|
||||
}
|
||||
|
||||
@@ -1,26 +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.changelist
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.project.projectId
|
||||
import com.intellij.platform.rpc.topics.ProjectRemoteTopic
|
||||
import com.intellij.platform.rpc.topics.ProjectRemoteTopicListener
|
||||
|
||||
internal class ChangeListRemoteTopicListener : ProjectRemoteTopicListener<ChangeListInfo> {
|
||||
override val topic: ProjectRemoteTopic<ChangeListInfo> = CHANGE_LIST_REMOTE_TOPIC
|
||||
|
||||
override fun handleEvent(project: Project, event: ChangeListInfo) {
|
||||
service<ChangeListService>().addChange(
|
||||
project.projectId().serializeToString(),
|
||||
ChangeListService.Change(event.line, event.col, event.filepath, event.protocol),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 com.maddyhome.idea.vim.group.changelist
|
||||
|
||||
import com.intellij.openapi.components.Service
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
|
||||
/**
|
||||
* Per-project change list backing `g;` and `g,` (`:help changelist`).
|
||||
*
|
||||
* Index/merge semantics follow Neovim's `get_changelist` (`src/nvim/mark.c`)
|
||||
* and `changed_common` (`src/nvim/change.c`): after each recorded change the
|
||||
* index sits past the end, so the first `g;` lands on the newest entry.
|
||||
*/
|
||||
@Service(Service.Level.APP)
|
||||
internal class ChangeListService {
|
||||
|
||||
private val projectToChanges = mutableMapOf<String, MutableList<Change>>()
|
||||
private val projectToIndex = mutableMapOf<String, Int>()
|
||||
|
||||
data class Change(
|
||||
val line: Int,
|
||||
val col: Int,
|
||||
val filepath: String,
|
||||
val protocol: String,
|
||||
)
|
||||
|
||||
sealed interface MoveResult {
|
||||
object Empty : MoveResult
|
||||
object AtStart : MoveResult
|
||||
object AtEnd : MoveResult
|
||||
data class At(val change: Change) : MoveResult
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addChange(projectId: String, change: Change) {
|
||||
val list = projectToChanges.getOrPut(projectId) { mutableListOf() }
|
||||
if (list.lastOrNull()?.shouldMergeWith(change) == true) {
|
||||
list[list.lastIndex] = change
|
||||
} else {
|
||||
list.add(change)
|
||||
if (list.size > CHANGE_LIST_LIMIT) list.removeAt(0)
|
||||
}
|
||||
projectToIndex[projectId] = list.size
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun goToChange(projectId: String, count: Int): MoveResult {
|
||||
val list = projectToChanges[projectId]
|
||||
if (list.isNullOrEmpty()) return MoveResult.Empty
|
||||
|
||||
val current = projectToIndex.getOrPut(projectId) { list.size }
|
||||
val target = current + count
|
||||
|
||||
if (target < 0 && current == 0) return MoveResult.AtStart
|
||||
if (target >= list.size && current == list.size - 1) return MoveResult.AtEnd
|
||||
|
||||
val newIndex = target.coerceIn(0, list.size - 1)
|
||||
projectToIndex[projectId] = newIndex
|
||||
return MoveResult.At(list[newIndex])
|
||||
}
|
||||
|
||||
private fun Change.shouldMergeWith(next: Change): Boolean =
|
||||
filepath == next.filepath &&
|
||||
line == next.line &&
|
||||
kotlin.math.abs(col - next.col) < TEXTWIDTH_FALLBACK
|
||||
|
||||
@TestOnly
|
||||
@Synchronized
|
||||
fun reset() {
|
||||
projectToChanges.clear()
|
||||
projectToIndex.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANGE_LIST_LIMIT = 100
|
||||
private const val TEXTWIDTH_FALLBACK = 79
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2003-2023 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
|
||||
package com.maddyhome.idea.vim.handler
|
||||
|
||||
import com.intellij.openapi.actionSystem.KeyboardShortcut
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.keymap.Keymap
|
||||
import com.intellij.openapi.keymap.KeymapManagerListener
|
||||
import com.intellij.openapi.keymap.ex.KeymapManagerEx
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.api.key
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
|
||||
// We use alarm with delay to avoid many actions in case many events are fired at the same time
|
||||
internal val correctorRequester = MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
private val LOG = logger<CopilotKeymapCorrector>()
|
||||
|
||||
internal class CopilotKeymapCorrector : ProjectActivity {
|
||||
override suspend fun execute(project: Project) {
|
||||
project.service<CopilotKeymapCorrectorService>().start()
|
||||
correctorRequester.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* At the moment of release 2023.3 there is a problem that starting a coroutine like this
|
||||
* right in the project activity will block this project activity in tests.
|
||||
* To avoid that, there is an intermediate service that will allow to avoid this issue.
|
||||
*
|
||||
* However, in general we should start this coroutine right in the [CopilotKeymapCorrector]
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
@Service(Service.Level.PROJECT)
|
||||
internal class CopilotKeymapCorrectorService(private val cs: CoroutineScope) {
|
||||
fun start() {
|
||||
cs.launch {
|
||||
correctorRequester
|
||||
.debounce(5_000)
|
||||
.collectLatest { correctCopilotKeymap() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class IdeaVimCorrectorKeymapChangedListener : KeymapManagerListener {
|
||||
override fun activeKeymapChanged(keymap: Keymap?) {
|
||||
check(correctorRequester.tryEmit(Unit))
|
||||
}
|
||||
|
||||
override fun shortcutsChanged(keymap: Keymap, actionIds: @NonNls Collection<String>, fromSettings: Boolean) {
|
||||
check(correctorRequester.tryEmit(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
private val copilotHideActionMap = ConcurrentHashMap<String, Unit>()
|
||||
|
||||
/**
|
||||
* See VIM-3206
|
||||
* The user expected to both copilot suggestion and the insert mode to be exited on a single esc.
|
||||
* However, for the moment, the first esc hides copilot suggestion and the second one exits insert mode.
|
||||
* To fix this, we remove the esc shortcut from the copilot action if the IdeaVim is active.
|
||||
*
|
||||
* This workaround is not the best solution, however, I don't see the better way with the current architecture of
|
||||
* actions and EditorHandlers. Firstly, I wanted to suggest to copilot to migrate to EditorActionHandler as well,
|
||||
* but this doesn't seem correct for me because in this case the user will lose an ability to change the shorcut for
|
||||
* it. It seems like copilot has a similar problem as we do - we don't want to make a handler for "Editor enter action",
|
||||
* but a handler for the esc key press. And, moreover, be able to communicate with other plugins about the ordering.
|
||||
* Before this feature is implemented, hiding the copilot suggestion on esc looks like a good workaround.
|
||||
*/
|
||||
private fun correctCopilotKeymap() {
|
||||
// This is needed to initialize the injector in case this verification is called to fast
|
||||
VimPlugin.getInstance()
|
||||
|
||||
if (!enableOctopus) return
|
||||
if (injector.enabler.isEnabled()) {
|
||||
val keymap = KeymapManagerEx.getInstanceEx().activeKeymap
|
||||
val res = keymap.getShortcuts("copilot.disposeInlays")
|
||||
if (res.isEmpty()) return
|
||||
|
||||
|
||||
val escapeShortcut = res.find { it.toString() == "[pressed ESCAPE]" } ?: return
|
||||
keymap.removeShortcut("copilot.disposeInlays", escapeShortcut)
|
||||
copilotHideActionMap[keymap.name] = Unit
|
||||
LOG.info("Remove copilot escape shortcut from keymap ${keymap.name}")
|
||||
} else {
|
||||
copilotHideActionMap.forEach { (name, _) ->
|
||||
val keymap = KeymapManagerEx.getInstanceEx().getKeymap(name) ?: return@forEach
|
||||
val currentShortcuts = keymap.getShortcuts("copilot.disposeInlays")
|
||||
if ("[pressed ESCAPE]" !in currentShortcuts.map { it.toString() }) {
|
||||
keymap.addShortcut("copilot.disposeInlays", KeyboardShortcut(key("<esc>"), null))
|
||||
}
|
||||
LOG.info("Restore copilot escape shortcut in keymap ${keymap.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2003-2023 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
|
||||
package com.maddyhome.idea.vim.handler
|
||||
|
||||
import com.intellij.openapi.actionSystem.IdeActions
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.editor.actionSystem.EditorActionHandlerBean
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.keymap.ex.KeymapManagerEx
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.maddyhome.idea.vim.api.key
|
||||
import com.maddyhome.idea.vim.newapi.initInjector
|
||||
|
||||
/**
|
||||
* Logs the chain of handlers for esc and enter
|
||||
*
|
||||
* As we made a migration to the new way of handling esc keys (VIM-2974), we may face several issues around that
|
||||
* One of the possible issues is that some plugin may also register a shortcut for this key and do not pass
|
||||
* the control to the next handler. In this way, the esc won't work, but there will be no exceptions.
|
||||
*
|
||||
* This is a logger that logs the chain of handlers.
|
||||
*
|
||||
* Strictly speaking, such access to the extension point is not allowed by the platform. But we can't do this thing
|
||||
* otherwise, so let's use it as long as we can.
|
||||
*/
|
||||
internal class EditorHandlersChainLogger : ProjectActivity {
|
||||
@Suppress("UnresolvedPluginConfigReference")
|
||||
private val editorHandlers = ExtensionPointName<EditorActionHandlerBean>("com.intellij.editorActionHandler")
|
||||
|
||||
override suspend fun execute(project: Project) {
|
||||
initInjector()
|
||||
|
||||
if (!enableOctopus) return
|
||||
|
||||
val escHandlers = editorHandlers.extensionList
|
||||
.filter { it.action == "EditorEscape" }
|
||||
.joinToString("\n") { it.implementationClass }
|
||||
val enterHandlers = editorHandlers.extensionList
|
||||
.filter { it.action == "EditorEnter" }
|
||||
.joinToString("\n") { it.implementationClass }
|
||||
|
||||
LOG.info("Esc handlers chain:\n$escHandlers")
|
||||
LOG.info("Enter handlers chain:\n$enterHandlers")
|
||||
|
||||
val keymapManager = KeymapManagerEx.getInstanceEx()
|
||||
val keymap = keymapManager.activeKeymap
|
||||
val keymapShortcutsForEsc = keymap.getShortcuts(IdeActions.ACTION_EDITOR_ESCAPE).joinToString()
|
||||
val keymapShortcutsForEnter = keymap.getShortcuts(IdeActions.ACTION_EDITOR_ENTER).joinToString()
|
||||
|
||||
LOG.info("Active keymap (${keymap.name}) shortcuts for esc: $keymapShortcutsForEsc, Shortcuts for enter: $keymapShortcutsForEnter")
|
||||
|
||||
val actionsForEsc = keymap.getActionIds(key("<esc>")).joinToString("\n")
|
||||
val actionsForEnter = keymap.getActionIds(key("<enter>")).joinToString("\n")
|
||||
|
||||
LOG.info(
|
||||
"Also keymap (${keymap.name}) has " +
|
||||
"the following actions assigned to esc:\n$actionsForEsc " +
|
||||
"\nand following actions assigned to enter:\n$actionsForEnter"
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = logger<EditorHandlersChainLogger>()
|
||||
}
|
||||
}
|
||||
153
src/main/java/com/maddyhome/idea/vim/handler/KeymapChecker.kt
Normal file
153
src/main/java/com/maddyhome/idea/vim/handler/KeymapChecker.kt
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.handler
|
||||
|
||||
import com.intellij.openapi.actionSystem.IdeActions
|
||||
import com.intellij.openapi.actionSystem.KeyboardShortcut
|
||||
import com.intellij.openapi.actionSystem.Shortcut
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.keymap.Keymap
|
||||
import com.intellij.openapi.keymap.KeymapManagerListener
|
||||
import com.intellij.openapi.keymap.ex.KeymapManagerEx
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.api.key
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
// We use alarm with delay to avoid many notifications in case many events are fired at the same time
|
||||
internal val keyCheckRequests = MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
/**
|
||||
* This checker verifies that the keymap has a correct configuration that is required for IdeaVim plugin
|
||||
*/
|
||||
internal class KeymapChecker : ProjectActivity {
|
||||
override suspend fun execute(project: Project) {
|
||||
project.service<KeymapCheckerService>().start()
|
||||
keyCheckRequests.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* At the moment of release 2023.3 there is a problem that starting a coroutine like this
|
||||
* right in the project activity will block this project activity in tests.
|
||||
* To avoid that, there is an intermediate service that will allow to avoid this issue.
|
||||
*
|
||||
* However, in general we should start this coroutine right in the [KeymapChecker]
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
@Service(Service.Level.PROJECT)
|
||||
internal class KeymapCheckerService(private val cs: CoroutineScope) {
|
||||
fun start() {
|
||||
cs.launch {
|
||||
keyCheckRequests
|
||||
.debounce(5_000)
|
||||
.collectLatest { verifyKeymap() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class IdeaVimKeymapChangedListener : KeymapManagerListener {
|
||||
override fun activeKeymapChanged(keymap: Keymap?) {
|
||||
check(keyCheckRequests.tryEmit(Unit))
|
||||
}
|
||||
|
||||
override fun shortcutsChanged(keymap: Keymap, actionIds: @NonNls Collection<String>, fromSettings: Boolean) {
|
||||
check(keyCheckRequests.tryEmit(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After migration to the editor action handlers, we have to make sure that the keymap has a correct configuration.
|
||||
* For example, that esc key is assigned to esc editor action
|
||||
*
|
||||
* Usually this is not a problem because this is a standard mapping, but the problem may appear in a misconfiguration
|
||||
* like it was in VIM-3204
|
||||
*/
|
||||
private fun verifyKeymap() {
|
||||
// This is needed to initialize the injector in case this verification is called to fast
|
||||
VimPlugin.getInstance()
|
||||
|
||||
if (!enableOctopus) return
|
||||
if (!injector.enabler.isEnabled()) return
|
||||
|
||||
val keymap = KeymapManagerEx.getInstanceEx().activeKeymap
|
||||
val keymapShortcutsForEsc = keymap.getShortcuts(IdeActions.ACTION_EDITOR_ESCAPE)
|
||||
val keymapShortcutsForEnter = keymap.getShortcuts(IdeActions.ACTION_EDITOR_ENTER)
|
||||
|
||||
val issues = ArrayList<KeyMapIssue>()
|
||||
val correctShortcutMissing = keymapShortcutsForEsc
|
||||
.filterIsInstance<KeyboardShortcut>()
|
||||
.none { it.firstKeyStroke.toString() == "pressed ESCAPE" && it.secondKeyStroke == null }
|
||||
|
||||
// We also check if there are any shortcuts starting from esc and with a second key. This should also be removed.
|
||||
// For example, VIM-3162 has a case when two escapes were assigned to editor escape action
|
||||
val shortcutsStartingFromEsc = keymapShortcutsForEsc
|
||||
.filterIsInstance<KeyboardShortcut>()
|
||||
.filter { it.firstKeyStroke.toString() == "pressed ESCAPE" && it.secondKeyStroke != null }
|
||||
if (correctShortcutMissing) {
|
||||
issues += KeyMapIssue.AddShortcut(
|
||||
"esc",
|
||||
"editor escape",
|
||||
IdeActions.ACTION_EDITOR_ESCAPE,
|
||||
key("<esc>")
|
||||
)
|
||||
}
|
||||
shortcutsStartingFromEsc.forEach {
|
||||
issues += KeyMapIssue.RemoveShortcut("editor escape", IdeActions.ACTION_EDITOR_ESCAPE, it)
|
||||
}
|
||||
|
||||
|
||||
val correctEnterShortcutMissing = keymapShortcutsForEnter
|
||||
.filterIsInstance<KeyboardShortcut>()
|
||||
.none { it.firstKeyStroke.toString() == "pressed ENTER" && it.secondKeyStroke == null }
|
||||
val shortcutsStartingFromEnter = keymapShortcutsForEnter
|
||||
.filterIsInstance<KeyboardShortcut>()
|
||||
.filter { it.firstKeyStroke.toString() == "pressed ENTER" && it.secondKeyStroke != null }
|
||||
if (correctEnterShortcutMissing) {
|
||||
issues += KeyMapIssue.AddShortcut(
|
||||
"enter",
|
||||
"editor enter",
|
||||
IdeActions.ACTION_EDITOR_ENTER,
|
||||
key("<enter>")
|
||||
)
|
||||
}
|
||||
shortcutsStartingFromEnter.forEach {
|
||||
issues += KeyMapIssue.RemoveShortcut("editor enter", IdeActions.ACTION_EDITOR_ENTER, it)
|
||||
}
|
||||
|
||||
if (issues.isNotEmpty()) {
|
||||
VimPlugin.getNotifications(null).notifyKeymapIssues(issues)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface KeyMapIssue {
|
||||
data class AddShortcut(
|
||||
val key: String,
|
||||
val action: String,
|
||||
val actionId: String,
|
||||
val keyStroke: KeyStroke,
|
||||
) : KeyMapIssue
|
||||
|
||||
data class RemoveShortcut(
|
||||
val action: String,
|
||||
val actionId: String,
|
||||
val shortcut: Shortcut,
|
||||
) : KeyMapIssue
|
||||
}
|
||||
379
src/main/java/com/maddyhome/idea/vim/handler/VimEnterHandler.kt
Normal file
379
src/main/java/com/maddyhome/idea/vim/handler/VimEnterHandler.kt
Normal file
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
* 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.handler
|
||||
|
||||
import com.intellij.codeInsight.editorActions.AutoHardWrapHandler
|
||||
import com.intellij.codeInsight.lookup.LookupManager
|
||||
import com.intellij.formatting.LineWrappingUtil
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.invokeLater
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.editor.Caret
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
|
||||
import com.intellij.openapi.editor.actions.SplitLineAction
|
||||
import com.intellij.openapi.editor.impl.CaretModelImpl
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.openapi.util.removeUserData
|
||||
import com.maddyhome.idea.vim.KeyHandler
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.api.key
|
||||
import com.maddyhome.idea.vim.group.IjOptionConstants
|
||||
import com.maddyhome.idea.vim.helper.EditorHelper
|
||||
import com.maddyhome.idea.vim.helper.IjActionExecutor
|
||||
import com.maddyhome.idea.vim.helper.inNormalMode
|
||||
import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere
|
||||
import com.maddyhome.idea.vim.helper.isPrimaryEditor
|
||||
import com.maddyhome.idea.vim.helper.updateCaretsVisualAttributes
|
||||
import com.maddyhome.idea.vim.newapi.actionStartedFromVim
|
||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||
import com.maddyhome.idea.vim.newapi.vim
|
||||
import com.maddyhome.idea.vim.state.mode.Mode
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
internal val commandContinuation = Key.create<EditorActionHandler>("commandContinuation")
|
||||
|
||||
/**
|
||||
* Handler that corrects the shape of the caret in python notebooks.
|
||||
*
|
||||
* By default, py notebooks show a thin caret after entering the cell.
|
||||
* However, we're in normal mode, so this handler fixes it.
|
||||
*/
|
||||
internal class CaretShapeEnterEditorHandler(private val nextHandler: EditorActionHandler) : EditorActionHandler() {
|
||||
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
||||
if (VimPlugin.isEnabled() && !editor.isIdeaVimDisabledHere && enableOctopus) {
|
||||
invokeLater {
|
||||
editor.updateCaretsVisualAttributes()
|
||||
}
|
||||
}
|
||||
nextHandler.execute(editor, caret, dataContext)
|
||||
}
|
||||
|
||||
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
||||
return nextHandler.isEnabled(editor, caret, dataContext)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This handler doesn't work in tests for ex commands
|
||||
*
|
||||
* About this handler: VIM-2974
|
||||
*/
|
||||
internal abstract class OctopusHandler(private val nextHandler: EditorActionHandler?) : EditorActionHandler() {
|
||||
|
||||
abstract fun executeHandler(editor: Editor, caret: Caret?, dataContext: DataContext?)
|
||||
open fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
final override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
||||
if (isThisHandlerEnabled(editor, caret, dataContext)) {
|
||||
val executeInInvokeLater = executeInInvokeLater(editor)
|
||||
val executionHandler = {
|
||||
try {
|
||||
(dataContext as? UserDataHolder)?.putUserData(commandContinuation, nextHandler)
|
||||
executeHandler(editor, caret, dataContext)
|
||||
} finally {
|
||||
(dataContext as? UserDataHolder)?.removeUserData(commandContinuation)
|
||||
}
|
||||
}
|
||||
|
||||
if (executeInInvokeLater) {
|
||||
// This `invokeLater` is used to escape the potential `runForEachCaret` function.
|
||||
//
|
||||
// The `runForEachCaret` function is disallowed to be called recursively. However, with this new handler, we lose
|
||||
// control if we execute the code inside this function or not. See IDEA-300030 for details.
|
||||
// This means the code in IdeaVim MUST NOT call `runForEachCaret` function. While this is possible for most cases,
|
||||
// the user may make a mapping to some intellij action where the `runForEachCaret` is called. This breaks
|
||||
// the condition (see VIM-3103 for example).
|
||||
// Since we can't make sure we don't execute `runForEachCaret`, we have to "escape" out of this function. This is
|
||||
// done by scheduling the execution of our code later via the invokeLater function.
|
||||
//
|
||||
// We run this job only once for a primary caret. In the handler itself, we'll multiply the execution by the
|
||||
// number of carets. If we run this job for each caret, we may end up in the issue like VIM-3186.
|
||||
// However, I think that we may do some refactoring to run this job for each caret (if needed).
|
||||
//
|
||||
// For the moment, the known case when the caret is null - work in injected editor - VIM-3195
|
||||
if (caret == null || caret == editor.caretModel.primaryCaret) {
|
||||
ApplicationManager.getApplication().invokeLater(executionHandler)
|
||||
}
|
||||
} else {
|
||||
executionHandler()
|
||||
}
|
||||
} else {
|
||||
nextHandler?.execute(editor, caret, dataContext)
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeInInvokeLater(editor: Editor): Boolean {
|
||||
// Currently we have a workaround for the PY console VIM-3157
|
||||
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
|
||||
}
|
||||
|
||||
private fun isThisHandlerEnabled(editor: Editor, caret: Caret?, dataContext: DataContext?): Boolean {
|
||||
if (VimPlugin.isNotEnabled()) return false
|
||||
if (editor.isIdeaVimDisabledHere) return false
|
||||
if (!isHandlerEnabled(editor, dataContext)) return false
|
||||
if (isNotActualKeyPress(dataContext)) return false
|
||||
if (!enableOctopus) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* In some cases IJ runs handlers to imitate "enter" or other key. In such cases we should not process it on the
|
||||
* IdeaVim side because the user may have mappings on enter the we'll get an unexpected behaviour.
|
||||
* This method should return true if we detect that this handler is called in such case and this is not an
|
||||
* actual keypress from the user.
|
||||
*/
|
||||
private fun isNotActualKeyPress(dataContext: DataContext?): Boolean {
|
||||
if (dataContext != null) {
|
||||
// This flag is set when the enter handlers are executed as a part of moving the comment on the new line
|
||||
val dataManager = DataManager.getInstance()
|
||||
if (dataManager.loadFromDataContext(dataContext, AutoHardWrapHandler.AUTO_WRAP_LINE_IN_PROGRESS_KEY) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
// From VIM-3177
|
||||
val wrapLongLineDuringFormattingInProgress = dataManager
|
||||
.loadFromDataContext(dataContext, LineWrappingUtil.WRAP_LONG_LINE_DURING_FORMATTING_IN_PROGRESS_KEY)
|
||||
if (wrapLongLineDuringFormattingInProgress == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
// From VIM-3203
|
||||
val splitLineInProgress = dataManager.loadFromDataContext(dataContext, SplitLineAction.SPLIT_LINE_KEY)
|
||||
if (splitLineInProgress == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (dataManager.loadFromDataContext(dataContext, StartNewLineDetectorBase.Util.key) == true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (dataContext?.actionStartedFromVim == true) return true
|
||||
if ((injector.actionExecutor as? IjActionExecutor)?.isRunningActionFromVim == true) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
||||
return isThisHandlerEnabled(editor, caret, dataContext)
|
||||
|| nextHandler?.isEnabled(editor, caret, dataContext) == true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Known conflicts & solutions:
|
||||
* - Smart step into - set handler after
|
||||
* - Python notebooks - set handler after
|
||||
* - Ace jump - set handler after
|
||||
* - Lookup - doesn't intersect with enter anymore
|
||||
* - App code - set handler after
|
||||
* - Template - doesn't intersect with enter anymore
|
||||
* - rd.client.editor.enter - set handler before. Otherwise, rider will add new line on enter even in normal mode
|
||||
* - inline.completion.enter - set handler before. Otherwise, AI completion is not invoked on enter.
|
||||
*
|
||||
* This rule is disabled due to VIM-3124
|
||||
* - before terminalEnter - not necessary, but terminalEnter causes "file is read-only" tooltip for readonly files VIM-3122
|
||||
* - `first` is set to satisfy sorting condition "before terminalEnter".
|
||||
*
|
||||
*
|
||||
* DO NOT add handlers that force to add "first" ordering. This doesn't work with jupyterCommandModeEnterKeyHandler (see VIM-3124)
|
||||
*/
|
||||
internal class VimEnterHandler(nextHandler: EditorActionHandler?) : VimKeyHandler(nextHandler) {
|
||||
override val key: String = "<CR>"
|
||||
|
||||
override fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {
|
||||
if (!super.isHandlerEnabled(editor, dataContext)) return false
|
||||
// This is important for one-line editors, to turn off enter.
|
||||
// Some one-line editors rely on the fact that there are no enter actions registered. For example, hash search in git
|
||||
// See VIM-2974 for example where it was broken
|
||||
return !editor.isOneLineMode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Known conflicts & solutions:
|
||||
*
|
||||
* - Smart step into - set handler after
|
||||
* - Python notebooks - set handler before - yes, we have `<CR>` as "after" and `<esc>` as before. I'm not completely sure
|
||||
* why this combination is correct, but other versions don't work.
|
||||
* - Ace jump - set handler after
|
||||
* - Lookup - It disappears after putting our esc before templateEscape. But I'm not sure why it works like that
|
||||
* - App code - Need to review
|
||||
* - Template - Need to review
|
||||
* - before backend.escape - to handle our handlers before Rider processing. Also, without this rule, we get problems like VIM-3146
|
||||
*/
|
||||
internal class VimEscHandler(nextHandler: EditorActionHandler) : VimKeyHandler(nextHandler) {
|
||||
override val key: String = "<Esc>"
|
||||
|
||||
private val ideaVimSupportDialog
|
||||
get() = injector.globalIjOptions().ideavimsupport.contains(IjOptionConstants.ideavimsupport_dialog)
|
||||
|
||||
override fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {
|
||||
return editor.isPrimaryEditor() ||
|
||||
EditorHelper.isFileEditor(editor) && vimStateNeedsToHandleEscape(editor) ||
|
||||
ideaVimSupportDialog && vimStateNeedsToHandleEscape(editor)
|
||||
}
|
||||
|
||||
private fun vimStateNeedsToHandleEscape(editor: Editor): Boolean {
|
||||
return !editor.vim.mode.inNormalMode || KeyHandler.getInstance().keyHandlerState.mappingState.hasKeys
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rider (and CLion Nova) uses a separate handler for esc to close the completion. IdeaOnlyEscapeHandlerAction is especially
|
||||
* designed to get all the esc presses, and if there is a completion close it and do not pass the execution further.
|
||||
* This doesn't work the same as in IJ.
|
||||
* In IdeaVim, we'd like to exit insert mode on closing completion. This is a requirement as the change of this
|
||||
* behaviour causes a lot of complaining from users. Since the rider handler gets execution control, we don't
|
||||
* receive an event and don't exit the insert mode.
|
||||
* To fix it, this special handler exists only for rider and stands before the rider's handler. We don't execute the
|
||||
* handler from rider because the autocompletion is closed automatically anyway.
|
||||
*
|
||||
* NOTE: This handler only works when octopus is enabled (non-Rider IDEs). For Rider, where octopus is disabled
|
||||
* (VIM-3815) and Escape is consumed by the popup manager before the EditorEscape chain fires, the fix is in
|
||||
* [com.maddyhome.idea.vim.listener.IdeaSpecifics.LookupTopicListener] via a LookupListener.
|
||||
*/
|
||||
internal class VimEscForRiderHandler(nextHandler: EditorActionHandler) : VimKeyHandler(nextHandler) {
|
||||
override val key: String = "<Esc>"
|
||||
|
||||
override fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {
|
||||
if (!enableOctopus) return false
|
||||
return LookupManager.getActiveLookup(editor) != null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty logger for esc presses
|
||||
*
|
||||
* As we made a migration to the new way of handling esc keys (VIM-2974), we may face several issues around that
|
||||
* One of the possible issues is that some plugin may also register a shortcut for this key and do not pass
|
||||
* the control to the next handler. In this way, the esc won't work, but there will be no exceptions.
|
||||
* This handler, that should stand in front of handlers change, just logs the event of pressing the key
|
||||
* and passes the execution.
|
||||
*/
|
||||
internal class VimEscLoggerHandler(private val nextHandler: EditorActionHandler) : EditorActionHandler() {
|
||||
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
||||
if (enableOctopus) {
|
||||
LOG.info("Esc pressed")
|
||||
}
|
||||
nextHandler.execute(editor, caret, dataContext)
|
||||
}
|
||||
|
||||
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
||||
return nextHandler.isEnabled(editor, caret, dataContext)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = logger<VimEscLoggerHandler>()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround to support "Start New Line" action in normal mode.
|
||||
* IJ executes enter handler on "Start New Line". This causes an issue that IdeaVim thinks that this is just an enter key.
|
||||
* This thing should be refactored, but for now we'll use this workaround VIM-3159
|
||||
*
|
||||
* The Same thing happens with "Start New Line Before Current" action.
|
||||
*/
|
||||
internal class StartNewLineDetector(nextHandler: EditorActionHandler) : StartNewLineDetectorBase(nextHandler)
|
||||
internal class StartNewLineBeforeCurrentDetector(nextHandler: EditorActionHandler) :
|
||||
StartNewLineDetectorBase(nextHandler)
|
||||
|
||||
internal open class StartNewLineDetectorBase(private val nextHandler: EditorActionHandler) : EditorActionHandler() {
|
||||
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
||||
if (enableOctopus) {
|
||||
DataManager.getInstance().saveInDataContext(dataContext, Util.key, true)
|
||||
}
|
||||
nextHandler.execute(editor, caret, dataContext)
|
||||
}
|
||||
|
||||
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
||||
return nextHandler.isEnabled(editor, caret, dataContext)
|
||||
}
|
||||
|
||||
object Util {
|
||||
val key = Key.create<Boolean>("vim.is.start.new.line")
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = logger<VimEscLoggerHandler>()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty logger for enter presses
|
||||
*
|
||||
* As we made a migration to the new way of handling enter keys (VIM-2974), we may face several issues around that
|
||||
* One of the possible issues is that some plugin may also register a shortcut for this key and do not pass
|
||||
* the control to the next handler. In this way, the esc won't work, but there will be no exceptions.
|
||||
* This handler, that should stand in front of handlers change, just logs the event of pressing the key
|
||||
* and passes the execution.
|
||||
*/
|
||||
internal class VimEnterLoggerHandler(private val nextHandler: EditorActionHandler) : EditorActionHandler() {
|
||||
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
||||
if (enableOctopus) {
|
||||
LOG.info("Enter pressed")
|
||||
}
|
||||
nextHandler.execute(editor, caret, dataContext)
|
||||
}
|
||||
|
||||
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
||||
return nextHandler.isEnabled(editor, caret, dataContext)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = logger<VimEnterLoggerHandler>()
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class VimKeyHandler(nextHandler: EditorActionHandler?) : OctopusHandler(nextHandler) {
|
||||
|
||||
abstract val key: String
|
||||
|
||||
override fun executeHandler(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
||||
val enterKey = key(key)
|
||||
val context = dataContext?.vim ?: injector.executionContextManager.getEditorExecutionContext(editor.vim)
|
||||
val keyHandler = KeyHandler.getInstance()
|
||||
keyHandler.handleKey(editor.vim, enterKey, context, keyHandler.keyHandlerState)
|
||||
}
|
||||
|
||||
override fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {
|
||||
val enterKey = key(key)
|
||||
return isOctopusEnabled(enterKey, editor)
|
||||
}
|
||||
}
|
||||
|
||||
fun isOctopusEnabled(s: KeyStroke, editor: Editor): Boolean {
|
||||
if (!enableOctopus) return false
|
||||
// CMD line has a different processing mechanizm: the processing actions are registered
|
||||
// for the input field component. These keys are not dispatched via the octopus handler.
|
||||
if (editor.vim.mode is Mode.CMD_LINE) return false
|
||||
when {
|
||||
s.keyCode == KeyEvent.VK_ENTER && s.modifiers == 0 -> return true
|
||||
s.keyCode == KeyEvent.VK_ESCAPE && s.modifiers == 0 -> return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
internal val enableOctopus: Boolean
|
||||
get() = injector.application.isOctopusEnabled()
|
||||
@@ -172,7 +172,6 @@ class CaretVisualAttributesListener : IsReplaceCharListener, ModeChangeListener,
|
||||
@RequiresEdt
|
||||
private fun updateCaretsVisual(editor: VimEditor) {
|
||||
val ijEditor = (editor as IjVimEditor).editor
|
||||
if (ijEditor.isDisposed) return
|
||||
ijEditor.updateCaretsVisualAttributes()
|
||||
ijEditor.updateCaretsVisualPosition()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.intellij.openapi.editor.*;
|
||||
import com.intellij.openapi.editor.ex.util.EditorUtil;
|
||||
import com.intellij.openapi.editor.impl.EditorImpl;
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||
import com.intellij.openapi.util.Key;
|
||||
import com.intellij.openapi.util.SystemInfo;
|
||||
import com.intellij.openapi.util.registry.Registry;
|
||||
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
|
||||
// multiline rendered doc comments, while still providing some visibility of the block inlay (e.g. Rider's single line
|
||||
// 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;
|
||||
|
||||
public static @NotNull Rectangle getVisibleArea(final @NotNull Editor editor) {
|
||||
@@ -683,49 +679,6 @@ public class EditorHelper {
|
||||
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) {
|
||||
var file = EditorHelper.getVirtualFile(editor);
|
||||
if (file == 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 file.getName().contains(PYTHON_CONSOLE_FILE_NAME) || file.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) {
|
||||
@SuppressWarnings("deprecation") Key<?> dataKey = Key.findKeyByName("Vcs.CommitMessage.Panel");
|
||||
if (dataKey != null && editor.getDocument().getUserData(dataKey) != null) return true;
|
||||
var file = EditorHelper.getVirtualFile(editor);
|
||||
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 = EditorHelper.getVirtualFile(editor);
|
||||
return file != null && key != null && file.getUserData(key) == Boolean.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.ex.util.EditorUtil
|
||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
|
||||
import com.intellij.openapi.ui.popup.util.PopupUtil
|
||||
import com.intellij.util.ui.table.JBTableRowEditor
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import com.maddyhome.idea.vim.api.StringListOptionValue
|
||||
@@ -54,7 +53,7 @@ internal val Editor.isIdeaVimDisabledHere: Boolean
|
||||
!ClientId.isCurrentlyUnderLocalId || // CWM-927
|
||||
(ideaVimDisabledForSingleLine(ideaVimSupportValue) && isSingleLine()) ||
|
||||
IdeaVimDisablerExtensionPoint.isDisabledForEditor(this) ||
|
||||
!isAllowedFileEditor()
|
||||
isNotFileEditorExceptAllowed()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,21 +65,18 @@ internal val Editor.isIdeaVimDisabledHere: Boolean
|
||||
* Here are issues when non-file editors were supported:
|
||||
* AI Chat – VIM-3786
|
||||
* 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
|
||||
* files. We don't support the Python console.
|
||||
* However, we still support IdeaVim in a commit window because it works fine there, and removing vim from this place will
|
||||
* 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 {
|
||||
if (EditorHelper.getVirtualFile(this)?.name?.contains("Dummy.txt") == true) {
|
||||
return PopupUtil.getPopupContainerFor(component) == null
|
||||
}
|
||||
if (EditorHelper.isPythonConsole(this)) return false
|
||||
|
||||
return EditorHelper.isCommitWindowEditor(this)
|
||||
|| EditorHelper.isKotlinClassDecompiledToJavaFile(this)
|
||||
|| EditorHelper.isDiffEditor(this)
|
||||
|| EditorHelper.isFileEditor(this)
|
||||
private fun Editor.isNotFileEditorExceptAllowed(): Boolean {
|
||||
if (EditorHelper.getVirtualFile(this)?.name?.contains("Dummy.txt") == true) return false
|
||||
if (EditorHelper.isDiffEditor(this)) return false
|
||||
return !EditorHelper.isFileEditor(this)
|
||||
}
|
||||
|
||||
private fun ideaVimDisabledInDialog(ideaVimSupportValue: StringListOptionValue): Boolean {
|
||||
|
||||
@@ -29,7 +29,6 @@ import com.maddyhome.idea.vim.common.InsertSequence
|
||||
import com.maddyhome.idea.vim.newapi.IjVimCaret
|
||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||
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.inVisualMode
|
||||
import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService
|
||||
@@ -55,7 +54,6 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
|
||||
val textEditor = getTextEditor(editor.ij)
|
||||
val undoManager = UndoManager.getInstance(project)
|
||||
if (undoManager.isUndoAvailable(textEditor)) {
|
||||
val caretCountBeforeUndo = editor.ij.caretModel.allCarets.size
|
||||
val scrollingModel = editor.getScrollingModel()
|
||||
scrollingModel.accumulateViewportChanges()
|
||||
|
||||
@@ -63,8 +61,6 @@ class UndoRedoHelper : VimTimestampBasedUndoService {
|
||||
|
||||
scrollingModel.flushViewportChanges()
|
||||
|
||||
collapseRestoredBlockVisualCarets(editor, caretCountBeforeUndo)
|
||||
|
||||
return true
|
||||
}
|
||||
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) {
|
||||
editor.carets().forEach {
|
||||
val ijCaret = it.ij
|
||||
|
||||
@@ -64,7 +64,6 @@ import com.maddyhome.idea.vim.vimscript.model.options.helpers.IdeaRefactorModeHe
|
||||
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeKeep
|
||||
import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeSelect
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import java.awt.AWTEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
@@ -375,11 +374,12 @@ internal object IdeaSpecifics {
|
||||
|
||||
(VimPlugin.getKey() as VimKeyGroupBase).registerShortcutsForLookup(newLookup)
|
||||
|
||||
// In Rider/CLion Nova, the popup manager (due to LookupSummaryInfo parameter info popup)
|
||||
// consumes Escape before the action system runs, so IdeaVim never sees it.
|
||||
// In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
|
||||
// (due to LookupSummaryInfo popup) before the action system runs, so IdeaVim never sees it.
|
||||
// Listen for explicit lookup cancellation (Escape) to exit insert mode.
|
||||
// Note: this listener must NOT be attached in JetBrains Client (split mode), because
|
||||
// isCanceledExplicitly can be true for non-Escape keys (e.g. space) there.
|
||||
// Note: we check isRider/isClionNova specifically, not !isOctopusEnabled(), because
|
||||
// JetBrains Client (split mode) also has octopus disabled but doesn't need this workaround,
|
||||
// and isCanceledExplicitly can be true for non-Escape keys (e.g. space) in that environment.
|
||||
if (isRider() || isClionNova()) {
|
||||
newLookup.addLookupListener(RiderEscLookupListener(newLookup.editor))
|
||||
}
|
||||
@@ -396,37 +396,13 @@ internal object IdeaSpecifics {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks whether the last KEY_PRESSED was Escape. Needed because [LookupEvent.isCanceledExplicitly]
|
||||
* is also true for non-Esc keys in Rider/CLion Nova (e.g. space), so it can't be used on its own
|
||||
* to decide whether to exit insert mode. Wired up as an IdeEventQueue preprocessor in
|
||||
* [VimListenerManager.GlobalListeners.enable].
|
||||
*/
|
||||
internal object RiderEscAwtKeyTracker {
|
||||
private val LOG = com.intellij.openapi.diagnostic.Logger.getInstance(RiderEscAwtKeyTracker::class.java)
|
||||
|
||||
@Volatile
|
||||
var lastKeyPressedWasEscape: Boolean = false
|
||||
private set
|
||||
|
||||
fun onAwtEvent(event: AWTEvent) {
|
||||
if (event is KeyEvent && event.id == KeyEvent.KEY_PRESSED) {
|
||||
val isEsc = event.keyCode == KeyEvent.VK_ESCAPE
|
||||
lastKeyPressedWasEscape = isEsc
|
||||
if (LOG.isTraceEnabled) {
|
||||
LOG.trace("RiderEscAwtKeyTracker KEY_PRESSED keyCode=${event.keyCode} isEsc=$isEsc")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In Rider/CLion Nova, the popup manager (due to LookupSummaryInfo parameter info popup)
|
||||
* consumes Escape before the action system runs, so IdeaVim never sees it.
|
||||
* In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
|
||||
* (due to LookupSummaryInfo parameter info popup) before the action system runs, so IdeaVim never sees it.
|
||||
* This listener exits insert mode when the lookup is explicitly cancelled (Escape).
|
||||
*/
|
||||
private class RiderEscLookupListener(private val editor: Editor) : com.intellij.codeInsight.lookup.LookupListener {
|
||||
override fun lookupCanceled(event: com.intellij.codeInsight.lookup.LookupEvent) {
|
||||
if (RiderEscAwtKeyTracker.lastKeyPressedWasEscape && editor.vim.mode is Mode.INSERT) {
|
||||
if (event.isCanceledExplicitly && editor.vim.mode is Mode.INSERT) {
|
||||
editor.vim.exitInsertMode(injector.executionContextManager.getEditorExecutionContext(editor.vim))
|
||||
KeyHandler.getInstance().reset(editor.vim)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
import com.intellij.codeWithMe.ClientId
|
||||
import com.intellij.codeWithMe.ClientId.Companion.isLocal
|
||||
import com.intellij.ide.ui.UISettings
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.diagnostic.trace
|
||||
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.EditorFactory
|
||||
import com.intellij.openapi.editor.EditorKind
|
||||
import com.intellij.openapi.editor.actionSystem.TypedAction
|
||||
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.FocusChangeListener
|
||||
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.FileEditorManager
|
||||
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.removeUserData
|
||||
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.SlowOperations
|
||||
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.getLineStartForOffset
|
||||
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.CommentsOptionInitializer
|
||||
import com.maddyhome.idea.vim.group.FileGroupHelper
|
||||
import com.maddyhome.idea.vim.group.IjOptions
|
||||
import com.maddyhome.idea.vim.group.IjVimRedrawService
|
||||
@@ -92,6 +77,8 @@ import com.maddyhome.idea.vim.group.ScrollOptionsChangeListener
|
||||
import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl
|
||||
import com.maddyhome.idea.vim.group.visual.VimVisualTimer
|
||||
import com.maddyhome.idea.vim.group.visual.moveCaretOneCharLeftFromSelectionEnd
|
||||
import com.maddyhome.idea.vim.handler.correctorRequester
|
||||
import com.maddyhome.idea.vim.handler.keyCheckRequests
|
||||
import com.maddyhome.idea.vim.helper.CaretVisualAttributesListener
|
||||
import com.maddyhome.idea.vim.helper.GuicursorChangeListener
|
||||
import com.maddyhome.idea.vim.helper.StrictMode
|
||||
@@ -125,7 +112,6 @@ import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
/**
|
||||
@@ -168,6 +154,8 @@ object VimListenerManager {
|
||||
SlowOperations.knownIssue("VIM-3648, VIM-3649").use {
|
||||
EditorListeners.addAll()
|
||||
}
|
||||
check(correctorRequester.tryEmit(Unit))
|
||||
check(keyCheckRequests.tryEmit(Unit))
|
||||
|
||||
val caretVisualAttributesListener = CaretVisualAttributesListener()
|
||||
injector.listenersNotifier.myEditorListeners.add(caretVisualAttributesListener)
|
||||
@@ -180,9 +168,6 @@ object VimListenerManager {
|
||||
val insertTimeRecorder = InsertTimeRecorder()
|
||||
injector.listenersNotifier.modeChangeListeners.add(insertTimeRecorder)
|
||||
|
||||
injector.listenersNotifier.modeWillChangeListeners.add(AutoCmdInsertEnterListener())
|
||||
injector.listenersNotifier.modeChangeListeners.add(AutoCmdInsertLeaveListener())
|
||||
|
||||
val modeWidgetListener = ModeWidgetListener()
|
||||
injector.listenersNotifier.modeChangeListeners.add(modeWidgetListener)
|
||||
injector.listenersNotifier.myEditorListeners.add(modeWidgetListener)
|
||||
@@ -200,6 +185,8 @@ object VimListenerManager {
|
||||
GlobalListeners.disable()
|
||||
EditorListeners.removeAll()
|
||||
injector.listenersNotifier.reset()
|
||||
|
||||
check(correctorRequester.tryEmit(Unit))
|
||||
}
|
||||
|
||||
object GlobalListeners {
|
||||
@@ -234,20 +221,6 @@ object VimListenerManager {
|
||||
val busConnection =
|
||||
ApplicationManager.getApplication().messageBus.connect(VimPlugin.getInstance().onOffDisposable)
|
||||
busConnection.subscribe(FileOpenedSyncListener.TOPIC, VimEditorFactoryListener)
|
||||
busConnection.subscribe(VirtualFileManager.VFS_CHANGES, BufNewFileTracker)
|
||||
busConnection.subscribe(FileDocumentManagerListener.TOPIC, BufWriteListener)
|
||||
|
||||
// VIM-4205: feed Esc presses to RiderEscAwtKeyTracker. Must be a preprocessor (not a dispatcher)
|
||||
// so it fires before Rider's popup manager consumes the event.
|
||||
if (com.maddyhome.idea.vim.ide.isRider() || com.maddyhome.idea.vim.ide.isClionNova()) {
|
||||
com.intellij.ide.IdeEventQueue.getInstance().addPreprocessor(
|
||||
{ event ->
|
||||
IdeaSpecifics.RiderEscAwtKeyTracker.onAwtEvent(event)
|
||||
false
|
||||
},
|
||||
VimPlugin.getInstance().onOffDisposable,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun disable() {
|
||||
@@ -264,8 +237,6 @@ object VimListenerManager {
|
||||
optionGroup.removeGlobalOptionChangeListener(Options.showmode, modeWidgetOptionListener)
|
||||
optionGroup.removeGlobalOptionChangeListener(Options.showmode, macroWidgetOptionListener)
|
||||
optionGroup.removeEffectiveOptionValueChangeListener(Options.guicursor, GuicursorChangeListener)
|
||||
|
||||
BufNewFileTracker.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +325,6 @@ object VimListenerManager {
|
||||
|
||||
injector.editorGroup.editorCreated(IjVimEditor(editor))
|
||||
(VimPlugin.getChange() as ChangeGroup).editorCreated(IjVimEditor(editor), perEditorDisposable)
|
||||
CommentsOptionInitializer.initializeForEditor(editor)
|
||||
|
||||
(editor as EditorEx).addFocusListener(VimFocusListener, perEditorDisposable)
|
||||
|
||||
@@ -365,18 +335,15 @@ object VimListenerManager {
|
||||
injector.editorGroup.editorDeinit(editor.vim)
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
if (vimDisabled(editor)) {
|
||||
remove(editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(editor: Editor) {
|
||||
val editorDisposable = editor.removeUserData(editorListenersDisposableKey)
|
||||
if (editorDisposable != null) {
|
||||
Disposer.dispose(editorDisposable)
|
||||
} else {
|
||||
// We definitely do not expect this to happen
|
||||
StrictMode.fail("Editor doesn't have disposable attached. $editor")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,13 +354,11 @@ object VimListenerManager {
|
||||
*/
|
||||
private object VimFocusListener : FocusChangeListener {
|
||||
override fun focusGained(editor: Editor) {
|
||||
if (editor.isDisposed) return
|
||||
if (vimDisabled(editor)) return
|
||||
injector.listenersNotifier.notifyEditorFocusGained(editor.vim)
|
||||
}
|
||||
|
||||
override fun focusLost(editor: Editor) {
|
||||
if (editor.isDisposed) return
|
||||
if (vimDisabled(editor)) return
|
||||
injector.listenersNotifier.notifyEditorFocusLost(editor.vim)
|
||||
}
|
||||
@@ -458,17 +423,8 @@ object VimListenerManager {
|
||||
// Breaks relativenumber for some reason
|
||||
// injector.scroll.scrollCaretIntoView(editor.vim)
|
||||
}
|
||||
// Vim order: BufLeave → WinLeave → WinEnter → BufEnter
|
||||
// Buf events only fire when the buffer (file) actually changes
|
||||
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)
|
||||
}
|
||||
|
||||
injector.outputPanel.getCurrentOutputPanel()?.close()
|
||||
MotionGroup.fileEditorManagerSelectionChangedCallback(event)
|
||||
FileGroupHelper.fileEditorManagerSelectionChangedCallback(event)
|
||||
// (VimPlugin.getSearch() as IjVimSearchGroup).fileEditorManagerSelectionChangedCallback(event)
|
||||
@@ -552,7 +508,6 @@ object VimListenerManager {
|
||||
EditorListeners.remove(event.editor)
|
||||
injector.listenersNotifier.notifyEditorReleased(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
|
||||
// editor closing based on file selection (closing an editor would select the next editor - so a null selection
|
||||
@@ -612,8 +567,6 @@ object VimListenerManager {
|
||||
}
|
||||
EditorListeners.add(editor, openingEditor?.editor?.vim ?: injector.fallbackWindow, scenario)
|
||||
firstEditorInitialised = true
|
||||
|
||||
fireBufferLoadedEvents(editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -972,122 +925,3 @@ private object MouseEventsDataHolder {
|
||||
const val allowedSkippedDragEvents = 3
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,12 @@ import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.util.Computable
|
||||
import com.intellij.util.ExceptionUtil
|
||||
import com.intellij.util.PlatformUtils
|
||||
import com.maddyhome.idea.vim.api.VimApplicationBase
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.diagnostic.vimLogger
|
||||
import com.maddyhome.idea.vim.ide.isClionNova
|
||||
import com.maddyhome.idea.vim.ide.isRider
|
||||
import java.awt.Component
|
||||
import java.awt.Toolkit
|
||||
import java.awt.Window
|
||||
@@ -76,6 +79,14 @@ internal class IjVimApplication : VimApplicationBase() {
|
||||
com.maddyhome.idea.vim.helper.runAfterGotFocus(runnable)
|
||||
}
|
||||
|
||||
override fun isOctopusEnabled(): Boolean {
|
||||
// Turn off octopus for some IDEs. They have issues with ENTER and ESC on the octopus like VIM-3815
|
||||
if (isRider() || PlatformUtils.isJetBrainsClient() || isClionNova()) return false
|
||||
val property = System.getProperty("octopus.handler") ?: "true"
|
||||
if (property.isBlank()) return true
|
||||
return property.toBoolean()
|
||||
}
|
||||
|
||||
private fun createKeyEvent(stroke: KeyStroke, component: Component): KeyEvent {
|
||||
return KeyEvent(
|
||||
component,
|
||||
|
||||
@@ -514,11 +514,12 @@ class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase() {
|
||||
val allFolds = editor.foldingModel.allFoldRegions
|
||||
if (allFolds.isEmpty()) return
|
||||
|
||||
val depths = FoldDepthCalculator.computeDepths(allFolds)
|
||||
|
||||
editor.foldingModel.runBatchFoldingOperation {
|
||||
for (i in allFolds.indices) {
|
||||
allFolds[i].isExpanded = depths[i] < foldLevel
|
||||
// I'm aware it's O(n^2) comparison here,
|
||||
// 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
|
||||
if (allFolds.isEmpty()) return 0
|
||||
|
||||
val depths = FoldDepthCalculator.computeDepths(allFolds)
|
||||
var max = 0
|
||||
for (d in depths) if (d > max) max = d
|
||||
return max
|
||||
return allFolds.maxOfOrNull { fold ->
|
||||
calculateFoldDepth(fold, allFolds)
|
||||
} ?: 0
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
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 {
|
||||
return IjVimFoldRegion(ijFoldRegion, editor)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.components.serviceIfCreated
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
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.ExecutionContextManager
|
||||
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.injector
|
||||
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.extension.ExtensionLoader
|
||||
import com.maddyhome.idea.vim.extension.JsonExtensionProvider
|
||||
@@ -219,8 +217,6 @@ internal class IjVimInjector : VimInjectorBase() {
|
||||
get() = service()
|
||||
override val pluginActivator: VimPluginActivator
|
||||
get() = service()
|
||||
|
||||
override val autoCmd: AutoCmdService get() = service<AutoCmdService>()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -254,11 +254,17 @@ class OutputPanel private constructor(
|
||||
return textPane.getBackground()
|
||||
}
|
||||
|
||||
fun deactivate() {
|
||||
/**
|
||||
* Turns off the output panel and optionally puts the focus back to the original component.
|
||||
*/
|
||||
fun deactivate(refocusOwningEditor: Boolean) {
|
||||
if (!active) return
|
||||
active = false
|
||||
clearText()
|
||||
textPane.text = ""
|
||||
if (refocusOwningEditor) {
|
||||
requestFocus(editor.contentComponent)
|
||||
}
|
||||
if (glassPane != null) {
|
||||
glassPane!!.removeComponentListener(resizeAdapter)
|
||||
toolWindowListenerConnection?.disconnect()
|
||||
@@ -315,7 +321,7 @@ class OutputPanel private constructor(
|
||||
fun close(key: KeyStroke?) {
|
||||
val passKeyBack = isSingleLine
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
deactivate()
|
||||
deactivate(true)
|
||||
val project = editor.project
|
||||
// For single line messages, pass any key back to the editor (including Enter)
|
||||
// For multi-line messages, don't pass Enter back (it was used to dismiss)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import com.maddyhome.idea.vim.EventFacade
|
||||
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
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.VimCommandLineCaret
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
@@ -82,46 +81,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
||||
var context: DataContext? = null
|
||||
override var histIndex: Int = 0
|
||||
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?
|
||||
get() = if (weakEditor != null) weakEditor!!.get() else null
|
||||
@@ -212,8 +171,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
||||
if (!this.isActive) return
|
||||
|
||||
clearPromptCharacter()
|
||||
hideCompletionBar()
|
||||
activeCompletion = null
|
||||
try {
|
||||
entry.document.removeDocumentListener(fontListener)
|
||||
// incsearch won't change in the lifetime of this activation
|
||||
@@ -296,7 +253,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val incSearchDocumentListener: DocumentListener = object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
try {
|
||||
@@ -532,25 +488,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
||||
setBounds(bounds)
|
||||
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
|
||||
@@ -571,8 +508,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
||||
private var oldGlass: JComponent? = null
|
||||
private var oldLayout: LayoutManager? = null
|
||||
private var wasOpaque = false
|
||||
private val completionPanel = ExCompletionPanel()
|
||||
private var isCompletionBarVisible = false
|
||||
|
||||
// incsearch stuff
|
||||
private var verticalOffset = 0
|
||||
@@ -621,13 +556,10 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
||||
entry.updateText(string)
|
||||
if (updateLastEntry) entry.saveLastEntry()
|
||||
caret.offset = min(offset, text.length)
|
||||
|
||||
dismissCompletionIfTextChanged()
|
||||
}
|
||||
|
||||
override fun deleteText(offset: Int, length: Int) {
|
||||
entry.deleteText(offset, length)
|
||||
dismissCompletionIfTextChanged()
|
||||
}
|
||||
|
||||
override fun insertText(offset: Int, string: String) {
|
||||
@@ -636,7 +568,6 @@ class ExEntryPanel private constructor() : JPanel(), VimCommandLine {
|
||||
entry.deleteText(offset, string.length)
|
||||
}
|
||||
entry.insertText(offset, string)
|
||||
dismissCompletionIfTextChanged()
|
||||
}
|
||||
|
||||
override fun clearCurrentAction() {
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
||||
import org.jetbrains.plugins.ideavim.waitAndAssert
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@@ -146,33 +145,12 @@ class CopyActionTest : VimTestCase() {
|
||||
one <caret>two
|
||||
three
|
||||
four
|
||||
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
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
|
||||
fun testWrongYankQuoteYankLine() {
|
||||
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
|
||||
* 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
|
||||
fun `test undo with count`() {
|
||||
val keys = listOf("dwdwdw", "2u")
|
||||
|
||||
@@ -8,14 +8,68 @@
|
||||
|
||||
package org.jetbrains.plugins.ideavim.action.change.insert
|
||||
|
||||
import com.intellij.ide.plugins.PluginManagerCore
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.editor.Caret
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
|
||||
import com.intellij.openapi.editor.actionSystem.EditorActionHandlerBean
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.testFramework.ExtensionTestUtil
|
||||
import com.maddyhome.idea.vim.VimPlugin
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.state.mode.Mode
|
||||
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 org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.RepeatedTest
|
||||
import org.junit.jupiter.api.RepetitionInfo
|
||||
|
||||
class InsertEnterActionTest : VimTestCase() {
|
||||
@Test
|
||||
@BeforeEach
|
||||
fun setUp(repetitionInfo: RepetitionInfo) {
|
||||
// Set up a different combination of handlers for enter action
|
||||
// There is a specific that due to IDEA-300030 the existing for "forEach" handler may affect our handlers execution.
|
||||
val mainBean = EditorActionHandlerBean()
|
||||
mainBean.implementationClass = "com.maddyhome.idea.vim.handler.VimEnterHandler"
|
||||
mainBean.action = "EditorEnter"
|
||||
mainBean.setPluginDescriptor(PluginManagerCore.getPlugin(VimPlugin.getPluginId())!!)
|
||||
|
||||
val singleBean = EditorActionHandlerBean()
|
||||
singleBean.implementationClass = DestroyerHandlerSingle::class.java.name
|
||||
singleBean.action = "EditorEnter"
|
||||
singleBean.setPluginDescriptor(PluginManagerCore.getPlugin(VimPlugin.getPluginId())!!)
|
||||
|
||||
val forEachBean = EditorActionHandlerBean()
|
||||
forEachBean.implementationClass = DestroyerHandlerForEach::class.java.name
|
||||
forEachBean.action = "EditorEnter"
|
||||
forEachBean.setPluginDescriptor(PluginManagerCore.getPlugin(VimPlugin.getPluginId())!!)
|
||||
|
||||
if (injector.application.isOctopusEnabled()) {
|
||||
if (repetitionInfo.currentRepetition == 1) {
|
||||
ExtensionTestUtil.maskExtensions(
|
||||
ExtensionPointName("com.intellij.editorActionHandler"),
|
||||
listOf(mainBean),
|
||||
fixture.testRootDisposable
|
||||
)
|
||||
} else if (repetitionInfo.currentRepetition == 2) {
|
||||
ExtensionTestUtil.maskExtensions(
|
||||
ExtensionPointName("com.intellij.editorActionHandler"),
|
||||
listOf(singleBean, mainBean),
|
||||
fixture.testRootDisposable
|
||||
)
|
||||
} else if (repetitionInfo.currentRepetition == 3) {
|
||||
ExtensionTestUtil.maskExtensions(
|
||||
ExtensionPointName("com.intellij.editorActionHandler"),
|
||||
listOf(forEachBean, mainBean),
|
||||
fixture.testRootDisposable
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RepeatedTest(3)
|
||||
fun `test insert enter`() {
|
||||
val before = """Lorem ipsum dolor sit amet,
|
||||
|${c}consectetur adipiscing elit
|
||||
@@ -31,7 +85,7 @@ class InsertEnterActionTest : VimTestCase() {
|
||||
doTest(listOf("i", "<Enter>"), before, after, Mode.INSERT)
|
||||
}
|
||||
|
||||
@Test
|
||||
@RepeatedTest(3)
|
||||
fun `test insert enter multicaret`() {
|
||||
val before = """Lorem ipsum dolor sit amet,
|
||||
|${c}consectetur adipiscing elit
|
||||
@@ -49,7 +103,7 @@ class InsertEnterActionTest : VimTestCase() {
|
||||
}
|
||||
|
||||
@TestWithoutNeovim(SkipNeovimReason.CTRL_CODES)
|
||||
@Test
|
||||
@RepeatedTest(3)
|
||||
fun `test insert enter with C-M`() {
|
||||
val before = """Lorem ipsum dolor sit amet,
|
||||
|${c}consectetur adipiscing elit
|
||||
@@ -66,7 +120,7 @@ class InsertEnterActionTest : VimTestCase() {
|
||||
}
|
||||
|
||||
@TestWithoutNeovim(SkipNeovimReason.CTRL_CODES)
|
||||
@Test
|
||||
@RepeatedTest(3)
|
||||
fun `test insert enter with C-J`() {
|
||||
val before = """Lorem ipsum dolor sit amet,
|
||||
|${c}consectetur adipiscing elit
|
||||
@@ -83,7 +137,7 @@ class InsertEnterActionTest : VimTestCase() {
|
||||
}
|
||||
|
||||
@TestWithoutNeovim(SkipNeovimReason.OPTION)
|
||||
@Test
|
||||
@RepeatedTest(3)
|
||||
fun `test insert enter scrolls view up at scrolloff`() {
|
||||
configureByLines(50, "Lorem ipsum dolor sit amet,")
|
||||
enterCommand("set scrolloff=10")
|
||||
@@ -93,3 +147,29 @@ class InsertEnterActionTest : VimTestCase() {
|
||||
assertVisibleArea(6, 40)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An empty handler that works as run "for each caret"
|
||||
*/
|
||||
internal class DestroyerHandlerForEach(private val nextHandler: EditorActionHandler) : EditorActionHandler(true) {
|
||||
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
||||
nextHandler.execute(editor, caret, dataContext)
|
||||
}
|
||||
|
||||
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
||||
return nextHandler.isEnabled(editor, caret, dataContext)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An empty handler that works as run "single time"
|
||||
*/
|
||||
internal class DestroyerHandlerSingle(private val nextHandler: EditorActionHandler) : EditorActionHandler(false) {
|
||||
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
|
||||
nextHandler.execute(editor, caret, dataContext)
|
||||
}
|
||||
|
||||
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
|
||||
return nextHandler.isEnabled(editor, caret, dataContext)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,325 +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.motion.changelist
|
||||
|
||||
import com.maddyhome.idea.vim.state.mode.Mode
|
||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for VIM-519: cycling between recent edits with `g;` and `g,`.
|
||||
*
|
||||
* Reference: Neovim 0.11 — `nv_pcmark` / `get_changelist` (`src/nvim/normal.c`,
|
||||
* `src/nvim/mark.c`) and `changed_common` (`src/nvim/change.c`).
|
||||
*
|
||||
* Semantics in one breath:
|
||||
* - Each undoable change appends an entry; the per-window index sits AT the
|
||||
* position past the newest entry (so the first `g;` lands on the newest one).
|
||||
* - `g;` walks backwards (`count` older), `g,` walks forwards (`count` newer).
|
||||
* - Same-line edits within `'textwidth'` columns of the prior entry merge into
|
||||
* a single entry at the latest position (default 79 when `'textwidth'` is 0).
|
||||
* - Errors:
|
||||
* E662 "At start of changelist" — `g;` at the oldest entry
|
||||
* E663 "At end of changelist" — `g,` at the newest entry
|
||||
* E664 "Changelist is empty" — either command with no changes recorded
|
||||
* - `g,` from the fresh "past end" position silently clamps to the newest
|
||||
* entry; the error fires on the *next* `g,`.
|
||||
*/
|
||||
class MotionGotoChangeActionTest : VimTestCase() {
|
||||
|
||||
@Test
|
||||
fun `test g_semicolon returns to last change after moving away`() {
|
||||
val before = """
|
||||
aaa
|
||||
${c}bbb
|
||||
ccc
|
||||
ddd
|
||||
eee
|
||||
""".trimIndent()
|
||||
|
||||
// Edit on line 2, jump to bottom, `g;` should bring us back.
|
||||
val keys = listOf("rA", "G\$", "g;")
|
||||
|
||||
val after = """
|
||||
aaa
|
||||
${c}Abb
|
||||
ccc
|
||||
ddd
|
||||
eee
|
||||
""".trimIndent()
|
||||
|
||||
doTest(keys, before, after, Mode.NORMAL())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test g_semicolon walks backwards through multiple changes`() {
|
||||
val before = """
|
||||
${c}aaa
|
||||
bbb
|
||||
ccc
|
||||
ddd
|
||||
""".trimIndent()
|
||||
|
||||
// Three changes on lines 1-3; G$ moves "past end"; first `g;` lands on the
|
||||
// newest (line 3, already where the cursor is); second `g;` lands on B.
|
||||
val keys = listOf("rA", "jrB", "jrC", "G\$", "g;", "g;")
|
||||
|
||||
val after = """
|
||||
Aaa
|
||||
${c}Bbb
|
||||
Ccc
|
||||
ddd
|
||||
""".trimIndent()
|
||||
|
||||
doTest(keys, before, after, Mode.NORMAL())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test g_semicolon then g_comma round trips`() {
|
||||
val before = """
|
||||
${c}aaa
|
||||
bbb
|
||||
ccc
|
||||
ddd
|
||||
""".trimIndent()
|
||||
|
||||
// Need at least 3 entries for a real round trip: the index sits past the
|
||||
// newest, so `g;` first hops to the newest, `g;` again to the middle, then
|
||||
// `g,` advances back to the newest entry.
|
||||
val keys = listOf("rA", "jrB", "jrC", "G\$", "g;", "g;", "g,")
|
||||
|
||||
val after = """
|
||||
Aaa
|
||||
Bbb
|
||||
${c}Ccc
|
||||
ddd
|
||||
""".trimIndent()
|
||||
|
||||
doTest(keys, before, after, Mode.NORMAL())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test g_semicolon with count walks back N entries from past-end`() {
|
||||
val before = """
|
||||
${c}aaa
|
||||
bbb
|
||||
ccc
|
||||
ddd
|
||||
""".trimIndent()
|
||||
|
||||
// 4 changes; index = 4 (past end). `3g;` → index 1 → entry B.
|
||||
// (Not the oldest -- "3 older" from past-end is the third-newest.)
|
||||
val keys = listOf("rA", "jrB", "jrC", "jrD", "3g;")
|
||||
|
||||
val after = """
|
||||
Aaa
|
||||
${c}Bbb
|
||||
Ccc
|
||||
Ddd
|
||||
""".trimIndent()
|
||||
|
||||
doTest(keys, before, after, Mode.NORMAL())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test g_semicolon with large count clamps to oldest change`() {
|
||||
val before = """
|
||||
${c}aaa
|
||||
bbb
|
||||
ccc
|
||||
ddd
|
||||
""".trimIndent()
|
||||
|
||||
val keys = listOf("rA", "jrB", "jrC", "jrD", "999g;")
|
||||
|
||||
val after = """
|
||||
${c}Aaa
|
||||
Bbb
|
||||
Ccc
|
||||
Ddd
|
||||
""".trimIndent()
|
||||
|
||||
doTest(keys, before, after, Mode.NORMAL())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test g_comma with count walks forward N entries`() {
|
||||
val before = """
|
||||
${c}aaa
|
||||
bbb
|
||||
ccc
|
||||
ddd
|
||||
""".trimIndent()
|
||||
|
||||
// Walk all the way to oldest first, then 2 newer → entry C.
|
||||
val keys = listOf("rA", "jrB", "jrC", "jrD", "999g;", "2g,")
|
||||
|
||||
val after = """
|
||||
Aaa
|
||||
Bbb
|
||||
${c}Ccc
|
||||
Ddd
|
||||
""".trimIndent()
|
||||
|
||||
doTest(keys, before, after, Mode.NORMAL())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test g_semicolon on buffer with no changes reports empty changelist`() {
|
||||
val before = """
|
||||
${c}aaa
|
||||
bbb
|
||||
""".trimIndent()
|
||||
|
||||
configureByText(before)
|
||||
typeText("g;")
|
||||
assertPluginError(true)
|
||||
assertStatusLineMessageContains("E664")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test g_semicolon past oldest entry reports start of changelist`() {
|
||||
val before = """
|
||||
${c}aaa
|
||||
bbb
|
||||
ccc
|
||||
""".trimIndent()
|
||||
|
||||
configureByText(before)
|
||||
// One change, then walk to it (oldest == newest), then try to go older.
|
||||
typeText("rA")
|
||||
typeText("G")
|
||||
typeText("g;") // lands on the only entry; idx = 0
|
||||
typeText("g;") // already at oldest → E662
|
||||
assertPluginError(true)
|
||||
assertStatusLineMessageContains("E662")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test g_comma past newest entry reports end of changelist`() {
|
||||
val before = """
|
||||
${c}aaa
|
||||
bbb
|
||||
""".trimIndent()
|
||||
|
||||
configureByText(before)
|
||||
// First `g,` after a single change silently clamps idx to the newest;
|
||||
// the second `g,` is the one that actually errors.
|
||||
typeText("rA")
|
||||
typeText("g,")
|
||||
typeText("g,")
|
||||
assertPluginError(true)
|
||||
assertStatusLineMessageContains("E663")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test nearby same line edits collapse into one change list entry`() {
|
||||
val before = "${c}abcdef"
|
||||
|
||||
// Two single-character edits on the same line, well within 'textwidth'
|
||||
// (default 79). The two changes coalesce into a single entry sitting at
|
||||
// the *latest* position (column 1), so a second `g;` errors instead of
|
||||
// taking us back to column 0.
|
||||
configureByText(before)
|
||||
typeText("rA")
|
||||
typeText("lrB")
|
||||
typeText("G\$")
|
||||
typeText("g;") // lands on the merged entry (col 1, on the 'B')
|
||||
assertState("A${c}Bcdef")
|
||||
typeText("g;") // only one entry → E662
|
||||
assertPluginError(true)
|
||||
assertStatusLineMessageContains("E662")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test insert mode change is recorded at insert position`() {
|
||||
val before = """
|
||||
aaa
|
||||
${c}bbb
|
||||
ccc
|
||||
""".trimIndent()
|
||||
|
||||
val keys = listOf("iX", "<Esc>", "gg", "g;")
|
||||
|
||||
val after = """
|
||||
aaa
|
||||
${c}Xbbb
|
||||
ccc
|
||||
""".trimIndent()
|
||||
|
||||
doTest(keys, before, after, Mode.NORMAL())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test append at end of line is recorded`() {
|
||||
val before = """
|
||||
aaa
|
||||
${c}bbb
|
||||
ccc
|
||||
""".trimIndent()
|
||||
|
||||
val keys = listOf("AZ", "<Esc>", "gg", "g;")
|
||||
|
||||
// Cursor lands on the inserted Z (the recorded position).
|
||||
val after = """
|
||||
aaa
|
||||
bbb${c}Z
|
||||
ccc
|
||||
""".trimIndent()
|
||||
|
||||
doTest(keys, before, after, Mode.NORMAL())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test line delete is recorded in change list`() {
|
||||
val before = """
|
||||
aaa
|
||||
${c}bbb
|
||||
ccc
|
||||
""".trimIndent()
|
||||
|
||||
val keys = listOf("dd", "gg", "g;")
|
||||
|
||||
// After `dd` deletes line 2, the change is remembered at that line; `g;`
|
||||
// returns the cursor to the deletion site (which now holds "ccc").
|
||||
val after = """
|
||||
aaa
|
||||
${c}ccc
|
||||
""".trimIndent()
|
||||
|
||||
doTest(keys, before, after, Mode.NORMAL())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test new change after walking back does not truncate forward history`() {
|
||||
val before = """
|
||||
${c}aaa
|
||||
bbb
|
||||
ccc
|
||||
""".trimIndent()
|
||||
|
||||
// Make 3 changes, walk back to B, edit there. Unlike the jump list,
|
||||
// Vim's change list does NOT prune newer entries when a change is made
|
||||
// mid-list -- it just appends. So C must still be reachable via `g;`.
|
||||
//
|
||||
// list before rX: [A@(1,1), B@(2,1), C@(3,1)] idx=3 (past end)
|
||||
// after G$ g;g; : idx=1, cursor on B
|
||||
// after rX : [A, B, C, X@(2,1)] idx=4 (past end) -- C survives
|
||||
// after 2g; : idx=2, cursor on C (at line 3)
|
||||
val keys = listOf("rA", "jrB", "jrC", "G\$", "g;", "g;", "rX", "2g;")
|
||||
|
||||
val after = """
|
||||
Aaa
|
||||
Xbb
|
||||
${c}Ccc
|
||||
""".trimIndent()
|
||||
|
||||
doTest(keys, before, after, Mode.NORMAL())
|
||||
}
|
||||
}
|
||||
@@ -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,349 +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")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mode() in InsertEnter autocmd returns n (fires before transition)`() {
|
||||
enterCommand("autocmd InsertEnter * echo mode()")
|
||||
typeText(injector.parser.parseKeys("i"))
|
||||
assertState(Mode.INSERT)
|
||||
assertExOutput("n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mode() in InsertEnter autocmd for Replace returns n (fires before transition)`() {
|
||||
enterCommand("autocmd InsertEnter * echo mode()")
|
||||
typeText(injector.parser.parseKeys("R"))
|
||||
assertState(Mode.REPLACE)
|
||||
assertExOutput("n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mode() in InsertLeave autocmd returns n (fires after transition)`() {
|
||||
enterCommand("autocmd InsertLeave * echo mode()")
|
||||
typeText(injector.parser.parseKeys("i"))
|
||||
typeText(injector.parser.parseKeys("<esc>"))
|
||||
assertState(Mode.NORMAL())
|
||||
assertExOutput("n")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -361,7 +361,7 @@ class CommandParserTest : VimTestCase() {
|
||||
\ endif
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertEquals(1, script.units.size)
|
||||
assertEquals(0, script.units.size)
|
||||
assertTrue(IdeavimErrorListener.testLogger.isEmpty())
|
||||
|
||||
script = VimscriptParser.parse(
|
||||
@@ -369,7 +369,7 @@ class CommandParserTest : VimTestCase() {
|
||||
autocmd BufReadPost * echo "oh, hi Mark"
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertEquals(1, script.units.size)
|
||||
assertEquals(0, script.units.size)
|
||||
assertTrue(IdeavimErrorListener.testLogger.isEmpty())
|
||||
}
|
||||
|
||||
|
||||
@@ -9,20 +9,13 @@
|
||||
package org.jetbrains.plugins.ideavim.ex
|
||||
|
||||
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.api.injector
|
||||
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.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ExEntryTest : VimExTestCase() {
|
||||
@@ -217,56 +210,4 @@ class ExEntryTest : VimExTestCase() {
|
||||
typeText(":echo <C-V>x80")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,20 +191,6 @@ class CmdCommandTest : VimTestCase() {
|
||||
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
|
||||
fun `test run command with arguments`() {
|
||||
VimPlugin.getCommand().resetAliases()
|
||||
|
||||
@@ -1,541 +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 tab does not file-complete echo argument even when prefix matches a real file`() {
|
||||
// The argument prefix would match `alpha.txt` if file completion ran -- it must not.
|
||||
typeText(":echo $tempPath/a<Tab>")
|
||||
assertExText("echo $tempPath/a")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test tab does not file-complete let argument even when prefix matches a real file`() {
|
||||
typeText(":let $tempPath/b<Tab>")
|
||||
assertExText("let $tempPath/b")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test tab does not file-complete map argument even when prefix matches a real file`() {
|
||||
typeText(":map $tempPath/s<Tab>")
|
||||
assertExText("map $tempPath/s")
|
||||
}
|
||||
|
||||
@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")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test tab completes command name from abbreviation`() {
|
||||
typeText(":vs<Tab>")
|
||||
assertExText("vsplit")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test tab completes command name with single match`() {
|
||||
typeText(":tabc<Tab>")
|
||||
assertExText("tabclose")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test tab on full command name with no longer match keeps it unchanged`() {
|
||||
typeText(":edit<Tab>")
|
||||
assertExText("edit")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test tab cycles through command names sharing a prefix`() {
|
||||
typeText(":set<Tab>")
|
||||
assertExText("set")
|
||||
|
||||
typeText("<Tab>")
|
||||
assertExText("setglobal")
|
||||
|
||||
typeText("<Tab>")
|
||||
assertExText("sethandler")
|
||||
|
||||
typeText("<Tab>")
|
||||
assertExText("setlocal")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test tab wraps after last command name match`() {
|
||||
typeText(":set<Tab>")
|
||||
typeText("<Tab>")
|
||||
typeText("<Tab>")
|
||||
typeText("<Tab>")
|
||||
assertExText("setlocal")
|
||||
|
||||
typeText("<Tab>")
|
||||
assertExText("set")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test shift tab cycles command names backwards`() {
|
||||
typeText(":set<S-Tab>")
|
||||
assertExText("setlocal")
|
||||
|
||||
typeText("<S-Tab>")
|
||||
assertExText("sethandler")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test right arrow cycles command names forward after tab`() {
|
||||
typeText(":set<Tab>")
|
||||
assertExText("set")
|
||||
|
||||
typeText("<Right>")
|
||||
assertExText("setglobal")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test left arrow cycles command names backward after tab`() {
|
||||
typeText(":set<Tab>")
|
||||
assertExText("set")
|
||||
|
||||
typeText("<Left>")
|
||||
assertExText("setlocal")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test tab on unknown command prefix does not change text`() {
|
||||
typeText(":xyzzy<Tab>")
|
||||
assertExText("xyzzy")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test typing after command name completion invalidates session`() {
|
||||
typeText(":set<Tab>")
|
||||
assertExText("set")
|
||||
|
||||
typeText(" foo")
|
||||
assertExText("set foo")
|
||||
|
||||
// `set` has no argument completion type registered, so Tab in argument position is a no-op.
|
||||
typeText("<Tab>")
|
||||
assertExText("set foo")
|
||||
}
|
||||
}
|
||||
@@ -195,7 +195,6 @@ class SetCommandTest : VimTestCase() {
|
||||
|nohlsearch nonumber nosneak wrap
|
||||
| ide=IntelliJ IDEA operatorfunc= startofline wrapscan
|
||||
| clipboard=ideaput,autoselect
|
||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
||||
| 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
|
||||
|noideacopypreprocess
|
||||
@@ -259,7 +258,6 @@ class SetCommandTest : VimTestCase() {
|
||||
| clipboard=ideaput,autoselect
|
||||
| colorcolumn=
|
||||
|nocommentary
|
||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
||||
|nocursorline
|
||||
|nodigraph
|
||||
|noexchange
|
||||
|
||||
@@ -449,7 +449,6 @@ class SetglobalCommandTest : VimTestCase() {
|
||||
|nohlsearch operatorfunc= nosurround
|
||||
| ide=IntelliJ IDEA norelativenumber notextobj-entire
|
||||
| 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
|
||||
|noideacopypreprocess
|
||||
| idearefactormode=select
|
||||
@@ -513,7 +512,6 @@ class SetglobalCommandTest : VimTestCase() {
|
||||
| clipboard=ideaput,autoselect
|
||||
| colorcolumn=
|
||||
|nocommentary
|
||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
||||
|nocursorline
|
||||
|nodigraph
|
||||
|noexchange
|
||||
|
||||
@@ -500,7 +500,6 @@ class SetlocalCommandTest : VimTestCase() {
|
||||
|nohlsearch nrformats=hex nosmartcase wrap
|
||||
| ide=IntelliJ IDEA nonumber nosneak wrapscan
|
||||
| clipboard=ideaput,autoselect
|
||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
||||
| 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
|
||||
|--ideacopypreprocess
|
||||
@@ -564,7 +563,6 @@ class SetlocalCommandTest : VimTestCase() {
|
||||
| clipboard=ideaput,autoselect
|
||||
| colorcolumn=
|
||||
|nocommentary
|
||||
| comments=s1:/*,mb:*,ex:*/,://,b:#,:%,:XCOMM,n:>,fb:-
|
||||
|nocursorline
|
||||
|nodigraph
|
||||
|noexchange
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2003-2026 The IdeaVim authors
|
||||
* Copyright 2003-2023 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -1479,64 +1479,4 @@ class SubstituteCommandTest : VimTestCase() {
|
||||
enterCommand("set nooldundo")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(reason = SkipNeovimReason.OPTION)
|
||||
fun `test substitute with e flag suppresses pattern not found error`() {
|
||||
configureByText("${c}Hello world\n")
|
||||
enterCommand("s/missing//e")
|
||||
assertPluginError(false)
|
||||
assertStatusLineCleared()
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(reason = SkipNeovimReason.OPTION)
|
||||
fun `test substitute without e flag reports pattern not found error`() {
|
||||
configureByText("${c}Hello world\n")
|
||||
enterCommand("s/missing//")
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E486: Pattern not found: missing")
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(reason = SkipNeovimReason.OPTION)
|
||||
fun `test substitute with e flag and trailing whitespace pattern`() {
|
||||
// The classic autocmd use case: %s/\s\+$//e should not produce errors when there is no trailing whitespace
|
||||
configureByText("${c}Hello world\n")
|
||||
enterCommand("%s/\\s\\+$//e")
|
||||
assertPluginError(false)
|
||||
assertStatusLineCleared()
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(reason = SkipNeovimReason.OPTION)
|
||||
fun `test substitute with e flag combined with g flag`() {
|
||||
configureByText("${c}Hello world\n")
|
||||
enterCommand("s/missing//ge")
|
||||
assertPluginError(false)
|
||||
assertStatusLineCleared()
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(reason = SkipNeovimReason.OPTION)
|
||||
fun `test substitute with e flag still performs substitution when pattern matches`() {
|
||||
doTest(
|
||||
exCommand("s/world/universe/e"),
|
||||
"${c}Hello world\n",
|
||||
"${c}Hello universe\n",
|
||||
)
|
||||
assertPluginError(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestWithoutNeovim(reason = SkipNeovimReason.OPTION)
|
||||
fun `test substitute e flag does not persist to next substitute`() {
|
||||
// :h :&& - flags are not kept between substitute commands
|
||||
configureByText("${c}Hello world\n")
|
||||
enterCommand("s/missing//e")
|
||||
assertPluginError(false)
|
||||
enterCommand("s/missing//")
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E486: Pattern not found: missing")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,82 +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.functions.variousFunctions
|
||||
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
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 ModeFunctionTest : VimTestCase() {
|
||||
@BeforeEach
|
||||
override fun setUp(testInfo: TestInfo) {
|
||||
super.setUp(testInfo)
|
||||
configureByText("\n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mode in normal mode returns n`() {
|
||||
assertCommandOutput("echo mode()", "n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mode with zero argument in normal mode returns n`() {
|
||||
assertCommandOutput("echo mode(0)", "n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mode with truthy argument in normal mode returns n`() {
|
||||
assertCommandOutput("echo mode(1)", "n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mode with string argument in normal mode returns n`() {
|
||||
assertCommandOutput("echo mode('x')", "n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mode reports too many arguments`() {
|
||||
enterCommand("echo mode(0, 1)")
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E118: Too many arguments for function: mode")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mode in insert returns i`() {
|
||||
configureByText("\n")
|
||||
enterCommand("inoremap <expr> q mode()")
|
||||
typeText(injector.parser.parseKeys("iq<esc>"))
|
||||
assertState("i\n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mode in replace returns R`() {
|
||||
configureByText("a\n")
|
||||
enterCommand("inoremap <expr> q mode()")
|
||||
typeText(injector.parser.parseKeys("Rq<esc>"))
|
||||
assertState("R\n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mode in visual char-wise returns v`() {
|
||||
configureByText("abc\n")
|
||||
enterCommand("vmap <expr> q '<Esc>A - mode='.mode().'<Esc>'")
|
||||
typeText(injector.parser.parseKeys("vq"))
|
||||
assertState("abc - mode=v\n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test mode in visual line-wise returns V`() {
|
||||
configureByText("abc\n")
|
||||
enterCommand("vmap <expr> q '<Esc>A - mode='.mode().'<Esc>'")
|
||||
typeText(injector.parser.parseKeys("Vq"))
|
||||
assertState("abc - mode=V\n")
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ class CommandTests : VimTestCase() {
|
||||
augroup END
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertEquals(4, script.units.size)
|
||||
assertEquals(2, script.units.size)
|
||||
assertTrue(script.units[0] is PlugCommand)
|
||||
assertTrue(script.units[1] is SetCommand)
|
||||
}
|
||||
@@ -145,14 +145,13 @@ class CommandTests : VimTestCase() {
|
||||
augroup myCmds
|
||||
au smthing
|
||||
augroup END
|
||||
|
||||
|
||||
Plug 'danilo-augusto/vim-afterglow'
|
||||
set nu rnu
|
||||
""".trimIndent(),
|
||||
)
|
||||
// `augroup myCmds` and `augroup END` are two units; `au smthing` is malformed and dropped by the parser.
|
||||
assertEquals(4, script.units.size)
|
||||
assertTrue(script.units[2] is PlugCommand)
|
||||
assertTrue(script.units[3] is SetCommand)
|
||||
assertEquals(2, script.units.size)
|
||||
assertTrue(script.units[0] is PlugCommand)
|
||||
assertTrue(script.units[1] 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
|
||||
fun testChangeInnerArgument() {
|
||||
doTest(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user