mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2026-06-14 17:02:31 +02:00
Compare commits
73 Commits
customized
...
6711486812
| Author | SHA1 | Date | |
|---|---|---|---|
|
6711486812
|
|||
|
e295270cc2
|
|||
|
e05be6da9c
|
|||
|
e4ec2abaef
|
|||
|
07a1ea1a34
|
|||
|
3807bc619f
|
|||
|
38d1caf795
|
|||
|
c22f7ec8a8
|
|||
|
42ea7fa70d
|
|||
|
8a5fb3920d
|
|||
|
920529e112
|
|||
|
c07ce16a19
|
|||
|
f0a6e85895
|
|||
|
be817241d5
|
|||
|
ab34f0bcdc
|
|||
|
6dda6b6772
|
|||
|
8e1bfe6d01
|
|||
|
7ff4d45d1c
|
|||
|
874eeb59a2
|
|||
|
5c9b787f30
|
|||
|
ecc649ea7b
|
|||
|
03bcec7fe0
|
|||
|
9e64c6d36b
|
|||
|
b812ecbd29
|
|||
|
cb5e88d8cf
|
|||
|
f9b071bb47
|
|||
|
8d41d01d6e
|
|||
|
9665ebd1b7
|
|||
|
6ef6425037
|
|||
|
3bc9a7816f
|
|||
|
5c2f67faca
|
|||
|
735dfcbd4c
|
|||
|
cda49601c0
|
|||
|
a8893067c6
|
|||
|
|
8b74fa25a5 | ||
|
|
7db9b0b58b | ||
|
|
0527ad3359 | ||
|
|
4f95756351 | ||
|
|
c17bb06f8a | ||
|
|
7f23a06447 | ||
|
|
5bb18a2cb7 | ||
|
|
7d6315af2d | ||
|
|
ff7b392e67 | ||
|
|
2e5c5d815c | ||
|
|
e8c12a5526 | ||
|
|
4fc912eee7 | ||
|
|
f4418b833f | ||
|
|
925cdbd8ac | ||
|
|
bc5ac2bcf4 | ||
|
|
9278425a5d | ||
|
|
26b9515279 | ||
|
|
a41f197bc5 | ||
|
|
e551bfe535 | ||
|
|
093244c07d | ||
|
|
a08c253e6c | ||
|
|
073d3b40d5 | ||
|
|
281039c76e | ||
|
|
ec06a522da | ||
|
|
c6ca4428ba | ||
|
|
a440bcb108 | ||
|
|
4be7a68ebd | ||
|
|
ec14d06ecd | ||
|
|
a2b0e7b273 | ||
|
|
4e9f3a5e11 | ||
|
|
057e3f6d19 | ||
|
|
b5d2f9c3d8 | ||
|
|
705ce474f1 | ||
|
|
4869fe68e5 | ||
|
|
852ea2feb0 | ||
|
|
3dc0ebd2d1 | ||
|
|
c33b0928dd | ||
|
|
02b4b4969a | ||
|
|
f1d971c239 |
182
.github/workflows/runUiOctopusTests.yml
vendored
182
.github/workflows/runUiOctopusTests.yml
vendored
@@ -1,182 +0,0 @@
|
||||
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,7 +17,6 @@ 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
|
||||
@@ -44,9 +43,6 @@ 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,40 +33,47 @@ object Compatibility : IdeaVimBuildType({
|
||||
name = "Load Verifier"
|
||||
scriptContent = """
|
||||
mkdir verifier1
|
||||
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"
|
||||
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"
|
||||
""".trimIndent()
|
||||
}
|
||||
script {
|
||||
name = "Check"
|
||||
scriptContent = """
|
||||
# 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
|
||||
|
||||
# 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.
|
||||
|
||||
java --version
|
||||
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
|
||||
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
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
43
.teamcity/_Self/buildTypes/SplitModeTests.kt
vendored
43
.teamcity/_Self/buildTypes/SplitModeTests.kt
vendored
@@ -13,6 +13,7 @@ import _Self.IdeaVimBuildType
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.CheckoutMode
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs
|
||||
|
||||
object SplitModeTests : IdeaVimBuildType({
|
||||
name = "Split mode tests"
|
||||
@@ -27,7 +28,6 @@ object SplitModeTests : IdeaVimBuildType({
|
||||
params {
|
||||
param("env.ORG_GRADLE_PROJECT_downloadIdeaSources", "false")
|
||||
param("env.ORG_GRADLE_PROJECT_instrumentPluginCode", "false")
|
||||
param("env.DISPLAY", ":99")
|
||||
}
|
||||
|
||||
vcs {
|
||||
@@ -39,43 +39,16 @@ object SplitModeTests : IdeaVimBuildType({
|
||||
|
||||
steps {
|
||||
script {
|
||||
name = "Start Xvfb and run split mode tests"
|
||||
scriptContent = """
|
||||
# Kill any leftover Xvfb from previous runs
|
||||
pkill -f 'Xvfb :99' || true
|
||||
|
||||
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
|
||||
XVFB_PID=${'$'}!
|
||||
|
||||
# Wait until the display is ready
|
||||
for i in $(seq 1 30); do
|
||||
if xdpyinfo -display :99 >/dev/null 2>&1; then
|
||||
echo "Xvfb is ready on :99"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! xdpyinfo -display :99 >/dev/null 2>&1; then
|
||||
echo "ERROR: Xvfb failed to start on :99"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./gradlew :tests:split-mode-tests:testSplitMode --console=plain --build-cache --configuration-cache --stacktrace
|
||||
TEST_EXIT=${'$'}?
|
||||
|
||||
kill ${'$'}XVFB_PID 2>/dev/null || true
|
||||
exit ${'$'}TEST_EXIT
|
||||
""".trimIndent()
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
// VCS trigger disabled until Xvfb is installed on the TeamCity agent
|
||||
// triggers {
|
||||
// vcs {
|
||||
// branchFilter = "+:<default>"
|
||||
// }
|
||||
// }
|
||||
triggers {
|
||||
vcs {
|
||||
branchFilter = "+:<default>"
|
||||
}
|
||||
}
|
||||
|
||||
requirements {
|
||||
// Use a larger agent for split-mode tests — they launch two full IDE instances
|
||||
|
||||
45
.teamcity/_Self/buildTypes/TypeScriptTest.kt
vendored
45
.teamcity/_Self/buildTypes/TypeScriptTest.kt
vendored
@@ -1,45 +0,0 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
24
CHANGES.md
24
CHANGES.md
@@ -37,6 +37,9 @@ usual beta standards.
|
||||
* [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
|
||||
@@ -79,8 +82,29 @@ usual beta standards.
|
||||
* [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
|
||||
|
||||
@@ -209,7 +209,6 @@ 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")
|
||||
}
|
||||
|
||||
@@ -229,25 +228,16 @@ tasks {
|
||||
val runPycharm by intellijPlatformTesting.runIde.registering {
|
||||
type = IntelliJPlatformType.PyCharmProfessional
|
||||
version = "2026.1"
|
||||
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"
|
||||
task {
|
||||
systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
|
||||
}
|
||||
}
|
||||
|
||||
val runIdeForUiTests by intellijPlatformTesting.runIde.registering {
|
||||
@@ -260,7 +250,6 @@ 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -303,7 +292,7 @@ tasks {
|
||||
}
|
||||
val runCLionSplitMode by intellijPlatformTesting.runIde.registering {
|
||||
type = IntelliJPlatformType.CLion
|
||||
version = "2025.3.2"
|
||||
version = "2026.1"
|
||||
splitMode = true
|
||||
splitModeTarget = SplitModeAware.SplitModeTarget.BOTH
|
||||
|
||||
@@ -485,6 +474,9 @@ intellijPlatform {
|
||||
* <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>
|
||||
@@ -527,8 +519,29 @@ intellijPlatform {
|
||||
* <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>
|
||||
|
||||
@@ -618,6 +618,34 @@ 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>
|
||||
|
||||
|
||||
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/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/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
Executable file → Normal file
31
gradlew.bat
vendored
Executable file → Normal file
@@ -23,8 +23,8 @@
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
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
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
: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
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
@@ -73,21 +73,10 @@ goto fail
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
@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
|
||||
|
||||
: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
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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,6 +8,7 @@
|
||||
|
||||
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
|
||||
@@ -55,22 +56,41 @@ internal class CommentaryRemoteApiImpl : CommentaryRemoteApi {
|
||||
val project = editor.project ?: return
|
||||
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return
|
||||
|
||||
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)
|
||||
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()
|
||||
}
|
||||
}, "Commentary", null)
|
||||
} 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 {
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
<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,12 +10,6 @@
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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())
|
||||
@@ -36,10 +36,6 @@
|
||||
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.handler.IdeaVimKeymapChangedListener"
|
||||
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
|
||||
<listener class="com.maddyhome.idea.vim.handler.IdeaVimCorrectorKeymapChangedListener"
|
||||
topic="com.intellij.openapi.keymap.KeymapManagerListener"/>
|
||||
<listener class="com.maddyhome.idea.vim.listener.VimAppActivationListener"
|
||||
topic="com.intellij.openapi.application.ApplicationActivationListener"/>
|
||||
</applicationListeners>
|
||||
@@ -177,6 +173,12 @@
|
||||
<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"/>
|
||||
|
||||
@@ -225,11 +227,6 @@
|
||||
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"/>
|
||||
@@ -249,35 +246,6 @@
|
||||
<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"/>
|
||||
|
||||
@@ -464,7 +432,8 @@
|
||||
|
||||
<actions>
|
||||
<action class="com.maddyhome.idea.vim.extension.hints.ToggleHintsAction" text="Toggle Hints">
|
||||
<keyboard-shortcut keymap="$default" first-keystroke="ctrl BACK_SLASH"/>
|
||||
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift BACK_SLASH"/>
|
||||
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl meta BACK_SLASH"/>
|
||||
</action>
|
||||
</actions>
|
||||
</idea-plugin>
|
||||
|
||||
@@ -14,12 +14,6 @@
|
||||
<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,6 +45,7 @@ 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> {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* 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,7 +21,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.3.10")
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.3.21")
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
|
||||
@@ -183,7 +183,8 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
|
||||
}
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return getInstance().enabled;
|
||||
final VimPlugin instance = ApplicationManager.getApplication().getService(VimPlugin.class);
|
||||
return instance != null && instance.enabled;
|
||||
}
|
||||
|
||||
public static void setEnabled(final boolean enabled) {
|
||||
|
||||
@@ -32,8 +32,6 @@ 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
|
||||
@@ -92,8 +90,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 (throwable: Throwable) {
|
||||
LOG.error(throwable)
|
||||
} catch (e: Exception) {
|
||||
LOG.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,15 +117,6 @@ 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.
|
||||
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -135,6 +136,9 @@ val navigationMappings: Map<List<KeyStroke>, NerdTreeAction> = mutableMapOf<List
|
||||
register("NERDTreeMapJumpNextSibling", "<C-J>", NerdTreeAction.swing("selectNextSibling"))
|
||||
register("NERDTreeMapJumpPrevSibling", "<C-K>", NerdTreeAction.swing("selectPreviousSibling"))
|
||||
|
||||
register("/", NerdTreeAction.ij("SpeedSearch"))
|
||||
register("/", NerdTreeAction { event, tree ->
|
||||
armSelectionRestoreOnEscape(tree)
|
||||
NerdTreeAction.callAction(null, "SpeedSearch", event.dataContext.vim)
|
||||
})
|
||||
register("<ESC>", NerdTreeAction { _, _ -> })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
@@ -14,11 +14,9 @@ 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
|
||||
@@ -31,7 +29,6 @@ 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.handler.commandContinuation
|
||||
import com.maddyhome.idea.vim.helper.CodeWrapper
|
||||
import com.maddyhome.idea.vim.helper.CommentLeaderParser
|
||||
import com.maddyhome.idea.vim.helper.inInsertMode
|
||||
@@ -99,35 +96,6 @@ 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)
|
||||
|
||||
@@ -326,11 +326,7 @@ 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) {
|
||||
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));
|
||||
}
|
||||
getRequiredShortcutKeys().add(new RequiredShortcut(key, owner));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
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
|
||||
@@ -15,7 +16,9 @@ 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
|
||||
@@ -26,12 +29,14 @@ 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
|
||||
@@ -57,6 +62,39 @@ 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,7 +33,6 @@ 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
|
||||
@@ -161,78 +160,6 @@ 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
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ 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
|
||||
|
||||
/**
|
||||
@@ -32,5 +31,4 @@ interface VimNotifications {
|
||||
fun notifyEapFinished()
|
||||
fun showReenableNotification(project: Project)
|
||||
fun notifyActionId(id: String?, candidates: List<String>? = null, intentionName: String?)
|
||||
fun notifyKeymapIssues(issues: ArrayList<KeyMapIssue>)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* 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>()
|
||||
}
|
||||
}
|
||||
@@ -1,153 +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.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
|
||||
}
|
||||
@@ -1,374 +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.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.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
|
||||
if (EditorHelper.isPythonConsole(editor)) 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,6 +172,7 @@ class CaretVisualAttributesListener : IsReplaceCharListener, ModeChangeListener,
|
||||
@RequiresEdt
|
||||
private fun updateCaretsVisual(editor: VimEditor) {
|
||||
val ijEditor = (editor as IjVimEditor).editor
|
||||
if (ijEditor.isDisposed) return
|
||||
ijEditor.updateCaretsVisualAttributes()
|
||||
ijEditor.updateCaretsVisualPosition()
|
||||
}
|
||||
|
||||
@@ -687,19 +687,20 @@ public class EditorHelper {
|
||||
* Checks if the editor is the Python console, so we can disable Vim features
|
||||
*/
|
||||
public static boolean isPythonConsole(@NotNull Editor editor) {
|
||||
if (editor.getVirtualFile() == null) return false;
|
||||
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 editor.getVirtualFile().getName().contains(PYTHON_CONSOLE_FILE_NAME)
|
||||
|| editor.getVirtualFile().getPath().contains(PYTHON_CONSOLE_FILE_NAME);
|
||||
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) {
|
||||
// The best heuristic we have is the file name, which is Dummy.txt
|
||||
var file = editor.getVirtualFile();
|
||||
@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");
|
||||
}
|
||||
|
||||
@@ -721,8 +722,8 @@ public class EditorHelper {
|
||||
*/
|
||||
public static boolean isKotlinClassDecompiledToJavaFile(@NotNull Editor editor) {
|
||||
@SuppressWarnings("deprecation") @Nullable Key<?> key = Key.findKeyByName("IS_KOTLIN_DECOMPILED_FILE");
|
||||
var file = editor.getVirtualFile();
|
||||
return file != null && key != null && editor.getVirtualFile().getUserData(key) == Boolean.TRUE;
|
||||
var file = EditorHelper.getVirtualFile(editor);
|
||||
return file != null && key != null && file.getUserData(key) == Boolean.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,6 +64,7 @@ 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
|
||||
|
||||
@@ -374,12 +375,11 @@ internal object IdeaSpecifics {
|
||||
|
||||
(VimPlugin.getKey() as VimKeyGroupBase).registerShortcutsForLookup(newLookup)
|
||||
|
||||
// In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
|
||||
// (due to LookupSummaryInfo popup) before the action system runs, so IdeaVim never sees it.
|
||||
// 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.
|
||||
// Listen for explicit lookup cancellation (Escape) to exit insert mode.
|
||||
// Note: we check isRider/isClionNova specifically, not !isOctopusEnabled(), because
|
||||
// JetBrains Client (split mode) also has octopus disabled but doesn't need this workaround,
|
||||
// and isCanceledExplicitly can be true for non-Escape keys (e.g. space) in that environment.
|
||||
// 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.
|
||||
if (isRider() || isClionNova()) {
|
||||
newLookup.addLookupListener(RiderEscLookupListener(newLookup.editor))
|
||||
}
|
||||
@@ -396,13 +396,37 @@ internal object IdeaSpecifics {
|
||||
}
|
||||
|
||||
/**
|
||||
* In Rider/CLion Nova, octopus is disabled (VIM-3815) and Escape is consumed by the popup manager
|
||||
* (due to LookupSummaryInfo parameter info popup) before the action system runs, so IdeaVim never sees it.
|
||||
* 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.
|
||||
* This listener exits insert mode when the lookup is explicitly cancelled (Escape).
|
||||
*/
|
||||
private class RiderEscLookupListener(private val editor: Editor) : com.intellij.codeInsight.lookup.LookupListener {
|
||||
override fun lookupCanceled(event: com.intellij.codeInsight.lookup.LookupEvent) {
|
||||
if (event.isCanceledExplicitly && editor.vim.mode is Mode.INSERT) {
|
||||
if (RiderEscAwtKeyTracker.lastKeyPressedWasEscape && editor.vim.mode is Mode.INSERT) {
|
||||
editor.vim.exitInsertMode(injector.executionContextManager.getEditorExecutionContext(editor.vim))
|
||||
KeyHandler.getInstance().reset(editor.vim)
|
||||
}
|
||||
|
||||
@@ -92,8 +92,6 @@ 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
|
||||
@@ -170,8 +168,6 @@ 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)
|
||||
@@ -204,8 +200,6 @@ object VimListenerManager {
|
||||
GlobalListeners.disable()
|
||||
EditorListeners.removeAll()
|
||||
injector.listenersNotifier.reset()
|
||||
|
||||
check(correctorRequester.tryEmit(Unit))
|
||||
}
|
||||
|
||||
object GlobalListeners {
|
||||
@@ -242,6 +236,18 @@ object VimListenerManager {
|
||||
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() {
|
||||
@@ -381,11 +387,13 @@ 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)
|
||||
}
|
||||
|
||||
@@ -12,12 +12,9 @@ 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
|
||||
@@ -79,14 +76,6 @@ 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,
|
||||
|
||||
@@ -8,68 +8,14 @@
|
||||
|
||||
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.BeforeEach
|
||||
import org.junit.jupiter.api.RepeatedTest
|
||||
import org.junit.jupiter.api.RepetitionInfo
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class InsertEnterActionTest : VimTestCase() {
|
||||
@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)
|
||||
@Test
|
||||
fun `test insert enter`() {
|
||||
val before = """Lorem ipsum dolor sit amet,
|
||||
|${c}consectetur adipiscing elit
|
||||
@@ -85,7 +31,7 @@ class InsertEnterActionTest : VimTestCase() {
|
||||
doTest(listOf("i", "<Enter>"), before, after, Mode.INSERT)
|
||||
}
|
||||
|
||||
@RepeatedTest(3)
|
||||
@Test
|
||||
fun `test insert enter multicaret`() {
|
||||
val before = """Lorem ipsum dolor sit amet,
|
||||
|${c}consectetur adipiscing elit
|
||||
@@ -103,7 +49,7 @@ class InsertEnterActionTest : VimTestCase() {
|
||||
}
|
||||
|
||||
@TestWithoutNeovim(SkipNeovimReason.CTRL_CODES)
|
||||
@RepeatedTest(3)
|
||||
@Test
|
||||
fun `test insert enter with C-M`() {
|
||||
val before = """Lorem ipsum dolor sit amet,
|
||||
|${c}consectetur adipiscing elit
|
||||
@@ -120,7 +66,7 @@ class InsertEnterActionTest : VimTestCase() {
|
||||
}
|
||||
|
||||
@TestWithoutNeovim(SkipNeovimReason.CTRL_CODES)
|
||||
@RepeatedTest(3)
|
||||
@Test
|
||||
fun `test insert enter with C-J`() {
|
||||
val before = """Lorem ipsum dolor sit amet,
|
||||
|${c}consectetur adipiscing elit
|
||||
@@ -137,7 +83,7 @@ class InsertEnterActionTest : VimTestCase() {
|
||||
}
|
||||
|
||||
@TestWithoutNeovim(SkipNeovimReason.OPTION)
|
||||
@RepeatedTest(3)
|
||||
@Test
|
||||
fun `test insert enter scrolls view up at scrolloff`() {
|
||||
configureByLines(50, "Lorem ipsum dolor sit amet,")
|
||||
enterCommand("set scrolloff=10")
|
||||
@@ -147,29 +93,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* 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())
|
||||
}
|
||||
}
|
||||
@@ -321,4 +321,29 @@ class AutoCmdTest : VimTestCase() {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,25 @@ class CommandLineCompletionTest : VimExTestCase() {
|
||||
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>")
|
||||
@@ -428,4 +447,95 @@ class CommandLineCompletionTest : VimExTestCase() {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1479,4 +1479,64 @@ 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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2003-2025 The IdeaVim authors
|
||||
* Copyright 2003-2026 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -8,10 +8,19 @@
|
||||
|
||||
package org.jetbrains.plugins.ideavim.extension.nerdtree
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.options.advanced.AdvancedSettings
|
||||
import com.intellij.testFramework.PlatformTestUtil
|
||||
import com.intellij.ui.treeStructure.Tree
|
||||
import com.maddyhome.idea.vim.extension.nerdtree.armSelectionRestoreOnEscape
|
||||
import org.jetbrains.plugins.ideavim.VimTestCase
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
class NerdTreeTest : VimTestCase() {
|
||||
@Test
|
||||
@@ -20,4 +29,79 @@ class NerdTreeTest : VimTestCase() {
|
||||
AdvancedSettings.getBoolean("ide.tree.collapse.recursively") // will throw if the id is invalid
|
||||
}
|
||||
}
|
||||
|
||||
// VIM-4196: pressing `/` to speed search and then ESC should restore the
|
||||
// original tree selection rather than leave the cursor on the search match.
|
||||
@Test
|
||||
fun `test esc after speed search restores original tree selection`() {
|
||||
onEdt {
|
||||
val tree = createSampleTree()
|
||||
val pathA = tree.pathFor("fileA.txt")
|
||||
val pathB = tree.pathFor("fileB.txt")
|
||||
|
||||
tree.selectionPath = pathA
|
||||
|
||||
armSelectionRestoreOnEscape(tree)
|
||||
|
||||
tree.selectionPath = pathB
|
||||
|
||||
tree.fireKeyPressed(KeyEvent.VK_ESCAPE)
|
||||
PlatformTestUtil.dispatchAllEventsInIdeEventQueue()
|
||||
|
||||
assertEquals(pathA, tree.selectionPath, "ESC after `/` speed search should restore original selection")
|
||||
}
|
||||
}
|
||||
|
||||
// VIM-4196: pressing ENTER (commit) must not roll the selection back to the
|
||||
// pre-search path; the user explicitly chose the matched item.
|
||||
@Test
|
||||
fun `test enter after speed search keeps the search match selected`() {
|
||||
onEdt {
|
||||
val tree = createSampleTree()
|
||||
val pathA = tree.pathFor("fileA.txt")
|
||||
val pathB = tree.pathFor("fileB.txt")
|
||||
|
||||
tree.selectionPath = pathA
|
||||
|
||||
armSelectionRestoreOnEscape(tree)
|
||||
|
||||
tree.selectionPath = pathB
|
||||
|
||||
tree.fireKeyPressed(KeyEvent.VK_ENTER)
|
||||
PlatformTestUtil.dispatchAllEventsInIdeEventQueue()
|
||||
|
||||
assertEquals(pathB, tree.selectionPath, "ENTER after `/` speed search should keep the matched selection")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEdt(block: () -> Unit) {
|
||||
ApplicationManager.getApplication().invokeAndWait(block)
|
||||
}
|
||||
|
||||
private fun createSampleTree(): Tree {
|
||||
val root = DefaultMutableTreeNode("root")
|
||||
root.add(DefaultMutableTreeNode("fileA.txt"))
|
||||
root.add(DefaultMutableTreeNode("fileB.txt"))
|
||||
root.add(DefaultMutableTreeNode("fileC.txt"))
|
||||
return Tree(DefaultTreeModel(root))
|
||||
}
|
||||
|
||||
private fun Tree.pathFor(name: String): TreePath {
|
||||
val root = model.root as DefaultMutableTreeNode
|
||||
val child = (0 until root.childCount)
|
||||
.map { root.getChildAt(it) as DefaultMutableTreeNode }
|
||||
.first { it.userObject == name }
|
||||
return TreePath(arrayOf<Any>(root, child))
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a synthetic KEY_PRESSED to all listeners registered on the tree. We
|
||||
* call the listeners directly because `Component.dispatchEvent` does not fire
|
||||
* KeyListeners on a non-displayed component in headless tests.
|
||||
*/
|
||||
private fun Tree.fireKeyPressed(keyCode: Int) {
|
||||
val keyChar = if (keyCode == KeyEvent.VK_ENTER) '\n' else KeyEvent.CHAR_UNDEFINED
|
||||
val event = KeyEvent(this, KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, keyCode, keyChar)
|
||||
keyListeners.forEach { it.keyPressed(event) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2003-2024 The IdeaVim authors
|
||||
* Copyright 2003-2026 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -8,12 +8,14 @@
|
||||
|
||||
package org.jetbrains.plugins.ideavim.group.search
|
||||
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import com.maddyhome.idea.vim.state.mode.Mode
|
||||
import com.maddyhome.idea.vim.state.mode.SelectionType
|
||||
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
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
class IncsearchTests : VimTestCase() {
|
||||
@@ -1017,4 +1019,36 @@ class IncsearchTests : VimTestCase() {
|
||||
""".trimMargin()
|
||||
)
|
||||
}
|
||||
|
||||
@TestWithoutNeovim(SkipNeovimReason.OPTION)
|
||||
@Test
|
||||
fun `test incsearch with incomplete regex does not indicate error`() {
|
||||
configureByText(
|
||||
"""I found it in a legendary land
|
||||
|${c}all rocks and lavender and tufted grass,
|
||||
|where it was settled on some sodden sand
|
||||
|hard by the torrent of a mountain pass.
|
||||
""".trimMargin(),
|
||||
)
|
||||
enterCommand("set incsearch")
|
||||
injector.messages.clearError()
|
||||
typeText("/", "\\(")
|
||||
assertFalse(injector.messages.isError(), "Incomplete regex during incsearch should not trigger an error/beep")
|
||||
}
|
||||
|
||||
@TestWithoutNeovim(SkipNeovimReason.OPTION)
|
||||
@Test
|
||||
fun `test incsearch in substitute command with incomplete regex does not indicate error`() {
|
||||
configureByText(
|
||||
"""I found it in a legendary land
|
||||
|${c}all rocks and lavender and tufted grass,
|
||||
|where it was settled on some sodden sand
|
||||
|hard by the torrent of a mountain pass.
|
||||
""".trimMargin(),
|
||||
)
|
||||
enterCommand("set incsearch")
|
||||
injector.messages.clearError()
|
||||
typeText(":", "%s/\\(")
|
||||
assertFalse(injector.messages.isError(), "Incomplete regex during incsearch :s should not trigger an error/beep")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,15 +68,14 @@ import com.maddyhome.idea.vim.group.EffectiveIjOptions
|
||||
import com.maddyhome.idea.vim.group.GlobalIjOptions
|
||||
import com.maddyhome.idea.vim.group.IjOptions
|
||||
import com.maddyhome.idea.vim.group.visual.VimVisualTimer.swingTimer
|
||||
import com.maddyhome.idea.vim.handler.isOctopusEnabled
|
||||
import com.maddyhome.idea.vim.helper.EditorHelper
|
||||
import com.maddyhome.idea.vim.helper.TestInputModel
|
||||
import com.maddyhome.idea.vim.helper.getGuiCursorMode
|
||||
import com.maddyhome.idea.vim.mark.Mark
|
||||
import com.maddyhome.idea.vim.key.MappingOwner
|
||||
import com.maddyhome.idea.vim.key.ToKeysMappingInfo
|
||||
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
|
||||
import com.maddyhome.idea.vim.listener.VimListenerManager
|
||||
import com.maddyhome.idea.vim.mark.Mark
|
||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
|
||||
import com.maddyhome.idea.vim.newapi.ijOptions
|
||||
import com.maddyhome.idea.vim.newapi.vim
|
||||
@@ -229,6 +228,9 @@ abstract class VimTestCase(private val defaultEditorText: String? = null) {
|
||||
(VimPlugin.getSearch() as VimSearchGroupBase).resetState()
|
||||
injector.markService.resetAllMarks()
|
||||
injector.jumpService.resetJumps()
|
||||
ApplicationManager.getApplication()
|
||||
.getService(com.maddyhome.idea.vim.group.changelist.ChangeListService::class.java)
|
||||
?.reset()
|
||||
injector.historyGroup.resetHistory()
|
||||
VimPlugin.getChange().resetRepeat()
|
||||
VimPlugin.getKey().savedShortcutConflicts.clear()
|
||||
@@ -1015,7 +1017,14 @@ abstract class VimTestCase(private val defaultEditorText: String? = null) {
|
||||
ActionManager.getInstance(),
|
||||
0,
|
||||
)
|
||||
if (ActionUtil.lastUpdateAndCheckDumb(VimShortcutKeyAction.instance, e, true)) {
|
||||
if (!VimPlugin.isEnabled()) {
|
||||
// VimShortcutKeyAction is a no-op when the plugin is disabled, so deliver
|
||||
// Enter/Escape to the IDE directly.
|
||||
when {
|
||||
key.keyCode == KeyEvent.VK_ENTER && key.modifiers == 0 -> fixture.type('\n')
|
||||
key.keyCode == KeyEvent.VK_ESCAPE -> fixture.performEditorAction("EditorEscape")
|
||||
}
|
||||
} else if (ActionUtil.lastUpdateAndCheckDumb(VimShortcutKeyAction.instance, e, true)) {
|
||||
ActionUtil.performActionDumbAwareWithCallbacks(VimShortcutKeyAction.instance, e)
|
||||
}
|
||||
}
|
||||
@@ -1027,14 +1036,6 @@ abstract class VimTestCase(private val defaultEditorText: String? = null) {
|
||||
|
||||
private fun KeyStroke.getChar(editor: Editor): CharType {
|
||||
if (keyChar != KeyEvent.CHAR_UNDEFINED) return CharType.CharDetected(keyChar)
|
||||
if (isOctopusEnabled(this, editor)) {
|
||||
if (keyCode in setOf(KeyEvent.VK_ENTER)) {
|
||||
if (modifiers == 0) {
|
||||
return CharType.CharDetected(keyCode.toChar())
|
||||
}
|
||||
}
|
||||
if (keyCode == KeyEvent.VK_ESCAPE) return CharType.EditorAction("EditorEscape")
|
||||
}
|
||||
return CharType.UNDEFINED
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
|
||||
|
||||
/*
|
||||
@@ -46,7 +54,7 @@ dependencies {
|
||||
create(ideaType, ideaVersion) { this.useInstaller = useInstaller }
|
||||
testFramework(TestFrameworkType.Platform)
|
||||
testFramework(TestFrameworkType.JUnit5)
|
||||
bundledPlugins("com.intellij.java", "org.jetbrains.plugins.yaml")
|
||||
bundledPlugins("com.intellij.java", "org.jetbrains.plugins.yaml", "org.jetbrains.plugins.textmate")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2003-2024 The IdeaVim authors
|
||||
* Copyright 2003-2026 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -14,7 +14,6 @@ import com.intellij.openapi.application.ApplicationManager
|
||||
import com.maddyhome.idea.vim.api.injector
|
||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
|
||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
|
||||
import org.jetbrains.plugins.ideavim.VimBehaviorDiffers
|
||||
import org.jetbrains.plugins.ideavim.VimJavaTestCase
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
@@ -65,25 +64,12 @@ class ChangeActionJavaTest : VimJavaTestCase() {
|
||||
// VIM-511 |.|
|
||||
@TestWithoutNeovim(SkipNeovimReason.DIFFERENT)
|
||||
@Test
|
||||
@VimBehaviorDiffers(
|
||||
originalVimAfter = """
|
||||
class C {
|
||||
C(int i) {
|
||||
i = 3;
|
||||
}
|
||||
C(int i) {
|
||||
i = 3;
|
||||
}
|
||||
}
|
||||
""", description = """The bracket should be on the new line.
|
||||
|This behaviour was explicitely broken as we migrate to the new handlers and I can't support it"""
|
||||
)
|
||||
fun testAutoCompleteCurlyBraceWithEnterWithinFunctionBody() {
|
||||
configureByJavaText(
|
||||
"""
|
||||
class C $c{
|
||||
}
|
||||
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
typeText(injector.parser.parseKeys("o" + "C(" + "<BS>" + "(int i) {" + "<Enter>" + "i = 3;" + "<Esc>" + "<Down>" + "."))
|
||||
@@ -93,7 +79,8 @@ class ChangeActionJavaTest : VimJavaTestCase() {
|
||||
i = 3;
|
||||
}
|
||||
C(int i) {
|
||||
i = 3;}
|
||||
i = 3;
|
||||
}
|
||||
}
|
||||
""",
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2003-2024 The IdeaVim authors
|
||||
* Copyright 2003-2026 The IdeaVim authors
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -94,6 +94,28 @@ class CommentaryExtensionTest : VimJavaTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLineCommentCppSingleLine() {
|
||||
doTest(
|
||||
keys = "gcc",
|
||||
before = "<caret>#include <cstdint>\n",
|
||||
after = "<caret>// #include <cstdint>\n",
|
||||
modeAfter = Mode.NORMAL(),
|
||||
fileName = "test.cpp",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLineCommentCppMultipleLines() {
|
||||
doTest(
|
||||
keys = "2gcc",
|
||||
before = "<caret>#include <cstdint>\n#include <cstdio>\n",
|
||||
after = "<caret>// #include <cstdint>\n// #include <cstdio>\n",
|
||||
modeAfter = Mode.NORMAL(),
|
||||
fileName = "test.cpp",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLineCommentDownPreservesAbsoluteCaretLocation() {
|
||||
doTest(
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.split
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Split-mode coverage for VIM-519 (`g;` / `g,`).
|
||||
*
|
||||
* In split mode the change list is fed by the backend's `RecentPlacesListener`
|
||||
* over `CHANGE_LIST_REMOTE_TOPIC`. These tests verify the full pipeline:
|
||||
* edit on backend → topic broadcast → frontend `ChangeListService` → `g;` reads it.
|
||||
*
|
||||
* Index logic and error paths are covered by `MotionGotoChangeActionTest` in the
|
||||
* monolith suite; here we only check that the RPC topic delivers events at all.
|
||||
*/
|
||||
class ChangeListSplitTest : IdeaVimStarterTestBase() {
|
||||
|
||||
private fun longFile(name: String): String {
|
||||
val lines = (1..20).joinToString("\n") { "Line $it of content" }
|
||||
return createFile("src/$name.txt", lines + "\n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `g_semicolon returns to last edit after moving away`() {
|
||||
openFile(longFile("ChangeList1"))
|
||||
|
||||
typeVim("5G")
|
||||
pause()
|
||||
typeVim("rX")
|
||||
pause(1000)
|
||||
|
||||
typeVim("G")
|
||||
pause()
|
||||
assertCaretAfter(15, "G should go past line 15")
|
||||
|
||||
typeVim("g;")
|
||||
pause(1000)
|
||||
assertCaretAtLine(5, "g; should bring cursor back to the edited line via the RPC topic")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `g_semicolon walks back through multiple edits`() {
|
||||
openFile(longFile("ChangeList2"))
|
||||
|
||||
typeVim("3G")
|
||||
pause()
|
||||
typeVim("rA")
|
||||
pause(800)
|
||||
|
||||
typeVim("8G")
|
||||
pause()
|
||||
typeVim("rB")
|
||||
pause(800)
|
||||
|
||||
typeVim("13G")
|
||||
pause()
|
||||
typeVim("rC")
|
||||
pause(800)
|
||||
|
||||
typeVim("G")
|
||||
pause()
|
||||
|
||||
typeVim("g;")
|
||||
pause(800)
|
||||
assertCaretAtLine(13, "first g; lands on the newest edit")
|
||||
|
||||
typeVim("g;")
|
||||
pause(800)
|
||||
assertCaretAtLine(8, "second g; walks back one entry")
|
||||
|
||||
typeVim("g;")
|
||||
pause(800)
|
||||
assertCaretAtLine(3, "third g; walks back to the oldest entry")
|
||||
}
|
||||
}
|
||||
@@ -30,18 +30,7 @@ class InsertEnterAction : VimActionHandler.SingleExecution() {
|
||||
cmd: Command,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Boolean {
|
||||
if (injector.application.isOctopusEnabled()) {
|
||||
if (editor.isInForEachCaretScope()) {
|
||||
editor.removeSecondaryCarets()
|
||||
injector.changeGroup.processEnter(editor, editor.primaryCaret(), context)
|
||||
} else {
|
||||
editor.forEachNativeCaret({ caret ->
|
||||
injector.changeGroup.processEnter(editor, caret, context)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
injector.changeGroup.processEnter(editor, context)
|
||||
}
|
||||
injector.changeGroup.processEnter(editor, context)
|
||||
injector.scroll.scrollCaretIntoView(editor)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.group.visual.VimSelection
|
||||
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
|
||||
import com.maddyhome.idea.vim.put.PutData
|
||||
import com.maddyhome.idea.vim.register.Register
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
@@ -73,6 +74,26 @@ sealed class PutVisualTextBaseAction(
|
||||
val visualSelection = selection?.let { PutData.VisualSelection(mapOf(caret to it), it.type) }
|
||||
return PutData(textData, visualSelection, count, insertTextBeforeCaret, indent, caretAfterInsertedText)
|
||||
}
|
||||
|
||||
private fun resolveRegisterForVisualPaste(
|
||||
lastRegisterChar: Char,
|
||||
caret: VimCaret,
|
||||
editor: VimEditor,
|
||||
context: ExecutionContext,
|
||||
): Register? {
|
||||
// IntelliJ updates X11 PRIMARY on visual selection; use the last explicitly-yanked value.
|
||||
return if (isDefaultSystemClipboard(lastRegisterChar)) {
|
||||
injector.registerGroup.getLastExplicitlyWrittenRegister(lastRegisterChar)
|
||||
?: caret.registerStorage.getRegister(editor, context, lastRegisterChar)
|
||||
} else {
|
||||
caret.registerStorage.getRegister(editor, context, lastRegisterChar)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDefaultSystemClipboard(registerChar: Char) =
|
||||
!injector.registerGroup.isRegisterSpecifiedExplicitly &&
|
||||
injector.registerGroup.isSystemClipboard(registerChar) &&
|
||||
injector.registerGroup.isPrimaryRegisterSupported()
|
||||
}
|
||||
|
||||
@CommandOrMotion(keys = ["P"], modes = [Mode.VISUAL])
|
||||
|
||||
@@ -112,12 +112,23 @@ private fun startNewCompletion(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun findMatches(parsed: ParsedCommandLine, context: ExecutionContext): List<String>? {
|
||||
val fullCommandName = injector.vimscriptParser.exCommands.getFullCommandName(parsed.commandName) ?: return null
|
||||
val completionType = CommandCompletionTypes.getCompletionType(fullCommandName)
|
||||
if (completionType == CommandLineCompletionType.NONE) return null
|
||||
private fun findMatches(parsed: CommandLineCompletionContext, context: ExecutionContext): List<String>? {
|
||||
return when (parsed) {
|
||||
is CommandNameCompletionContext -> findCommandNameMatches(parsed)
|
||||
is ArgumentCompletionContext -> findArgumentMatches(parsed, context)
|
||||
}
|
||||
}
|
||||
|
||||
return injector.file.listFilesForCompletion(parsed.argumentPrefix, context)
|
||||
private fun findCommandNameMatches(parsed: CommandNameCompletionContext): List<String> {
|
||||
return injector.vimscriptParser.exCommands.findFullCommandsByPrefix(parsed.prefix)
|
||||
}
|
||||
|
||||
private fun findArgumentMatches(parsed: ArgumentCompletionContext, context: ExecutionContext): List<String>? {
|
||||
val fullCommandName = injector.vimscriptParser.exCommands.getFullCommandName(parsed.commandName) ?: return null
|
||||
return when (CommandCompletionTypes.getCompletionType(fullCommandName)) {
|
||||
CommandLineCompletionType.FILE -> injector.file.listFilesForCompletion(parsed.argumentPrefix, context)
|
||||
CommandLineCompletionType.NONE -> null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun selectMatch(completion: CommandLineCompletion, forward: Boolean): String? {
|
||||
|
||||
@@ -9,31 +9,70 @@
|
||||
package com.maddyhome.idea.vim.action.ex
|
||||
|
||||
/**
|
||||
* Lightweight parser for extracting the command name and argument prefix
|
||||
* from a partially-typed ex command line. Used for Tab completion context detection.
|
||||
* Parsed completion context for a partially-typed ex command line. Used for Tab completion
|
||||
* context detection.
|
||||
*
|
||||
* - [CommandNameCompletionContext]: the user is still typing the command name
|
||||
* (e.g. `:vs` -> complete to `:vsplit`).
|
||||
* - [ArgumentCompletionContext]: a command name plus a separator has been typed,
|
||||
* so completion targets the argument (e.g. `:edit foo` -> complete file paths).
|
||||
*/
|
||||
internal data class ParsedCommandLine(
|
||||
internal sealed interface CommandLineCompletionContext {
|
||||
val completionStart: Int
|
||||
}
|
||||
|
||||
internal data class CommandNameCompletionContext(
|
||||
val prefix: String,
|
||||
override val completionStart: Int,
|
||||
) : CommandLineCompletionContext
|
||||
|
||||
internal data class ArgumentCompletionContext(
|
||||
val commandName: String,
|
||||
val argumentPrefix: String,
|
||||
val completionStart: Int,
|
||||
)
|
||||
override val completionStart: Int,
|
||||
) : CommandLineCompletionContext
|
||||
|
||||
internal fun parseCommandLineForCompletion(text: String): ParsedCommandLine? {
|
||||
internal fun parseCommandLineForCompletion(text: String): CommandLineCompletionContext? {
|
||||
val trimmed = text.trimStart()
|
||||
if (trimmed.isEmpty()) return null
|
||||
|
||||
val commandName = extractCommandName(trimmed) ?: return null
|
||||
val commandEnd = commandName.length
|
||||
val leadingSpacesLength = text.length - trimmed.length
|
||||
|
||||
if (!hasArgumentSeparator(trimmed, commandEnd)) return null
|
||||
|
||||
val argStart = skipSpaces(trimmed, commandEnd)
|
||||
val argPrefix = trimmed.substring(argStart)
|
||||
val leadingSpaces = text.length - trimmed.length
|
||||
|
||||
return ParsedCommandLine(commandName, argPrefix, leadingSpaces + argStart)
|
||||
return if (isCommandNameOnly(trimmed, commandName)) {
|
||||
parseCommandNameContext(commandName, leadingSpacesLength)
|
||||
} else {
|
||||
parseArgumentContext(trimmed, commandName, leadingSpacesLength)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCommandNameOnly(trimmed: String, commandName: String): Boolean =
|
||||
commandName.length == trimmed.length
|
||||
|
||||
private fun parseCommandNameContext(commandName: String, leadingSpacesLength: Int): CommandNameCompletionContext? {
|
||||
if (isExplicitBangForm(commandName)) return null
|
||||
return CommandNameCompletionContext(commandName, leadingSpacesLength)
|
||||
}
|
||||
|
||||
private fun parseArgumentContext(
|
||||
trimmed: String,
|
||||
commandName: String,
|
||||
leadingSpacesLength: Int,
|
||||
): ArgumentCompletionContext? {
|
||||
val commandLength = commandName.length
|
||||
if (!hasArgumentSeparator(trimmed, commandLength)) return null
|
||||
|
||||
val argStart = skipSpaces(trimmed, commandLength)
|
||||
val argPrefix = trimmed.substring(argStart)
|
||||
return ArgumentCompletionContext(commandName, argPrefix, leadingSpacesLength + argStart)
|
||||
}
|
||||
|
||||
/**
|
||||
* A trailing bang means the user has committed to a specific command form;
|
||||
* completing the name (e.g. `vs!` -> `vsplit`) would silently change their intent.
|
||||
*/
|
||||
private fun isExplicitBangForm(commandName: String): Boolean = commandName.endsWith('!')
|
||||
|
||||
private fun extractCommandName(text: String): String? {
|
||||
var end = 0
|
||||
while (end < text.length && (text[end].isLetter() || text[end] == '!')) {
|
||||
|
||||
@@ -33,34 +33,26 @@ class PlaybackRegisterAction : VimActionHandler.SingleExecution() {
|
||||
): Boolean {
|
||||
val argument = cmd.argument as? Argument.Character ?: return false
|
||||
val reg = argument.character
|
||||
val res = arrayOf(false)
|
||||
when {
|
||||
return when {
|
||||
reg == LAST_COMMAND_REGISTER || (reg == '@' && injector.macro.lastRegister == LAST_COMMAND_REGISTER) -> {
|
||||
try {
|
||||
var i = 0
|
||||
while (i < cmd.count) {
|
||||
res[0] = injector.vimscriptExecutor.executeLastCommand(editor, context)
|
||||
if (!res[0]) {
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
var success = false
|
||||
for (i in 0 until cmd.count) {
|
||||
success = injector.vimscriptExecutor.executeLastCommand(editor, context)
|
||||
if (!success) break
|
||||
}
|
||||
if (reg != '@') { // @ is not a register itself, it just tells vim to use the last register
|
||||
injector.macro.lastRegister = reg
|
||||
}
|
||||
success
|
||||
} catch (_: ExException) {
|
||||
res[0] = false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
reg == '@' -> {
|
||||
res[0] = injector.macro.playbackLastRegister(editor, context, cmd.count)
|
||||
}
|
||||
reg == '@' -> injector.macro.playbackLastRegister(editor, context, cmd.count)
|
||||
|
||||
else -> {
|
||||
res[0] = injector.macro.playbackRegister(editor, context, reg, cmd.count)
|
||||
}
|
||||
else -> injector.macro.playbackRegister(editor, context, reg, cmd.count)
|
||||
}
|
||||
return res[0]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.action.motion.changelist
|
||||
|
||||
import com.intellij.vim.annotations.CommandOrMotion
|
||||
import com.intellij.vim.annotations.Mode
|
||||
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.Argument
|
||||
import com.maddyhome.idea.vim.command.MotionType
|
||||
import com.maddyhome.idea.vim.command.OperatorArguments
|
||||
import com.maddyhome.idea.vim.handler.Motion
|
||||
import com.maddyhome.idea.vim.handler.MotionActionHandler
|
||||
|
||||
/** `g;` -- go to [count] older position in the change list. */
|
||||
@CommandOrMotion(keys = ["g;"], modes = [Mode.NORMAL])
|
||||
class MotionGotoChangeOlderAction : MotionActionHandler.ForEachCaret() {
|
||||
override fun getOffset(
|
||||
editor: VimEditor,
|
||||
caret: ImmutableVimCaret,
|
||||
context: ExecutionContext,
|
||||
argument: Argument?,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Motion = injector.motion.moveCaretToChange(editor, caret, -operatorArguments.count1)
|
||||
|
||||
override val motionType: MotionType = MotionType.EXCLUSIVE
|
||||
}
|
||||
|
||||
/** `g,` -- go to [count] newer position in the change list. */
|
||||
@CommandOrMotion(keys = ["g,"], modes = [Mode.NORMAL])
|
||||
class MotionGotoChangeNewerAction : MotionActionHandler.ForEachCaret() {
|
||||
override fun getOffset(
|
||||
editor: VimEditor,
|
||||
caret: ImmutableVimCaret,
|
||||
context: ExecutionContext,
|
||||
argument: Argument?,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Motion = injector.motion.moveCaretToChange(editor, caret, operatorArguments.count1)
|
||||
|
||||
override val motionType: MotionType = MotionType.EXCLUSIVE
|
||||
}
|
||||
@@ -31,18 +31,7 @@ class SelectEnterAction : VimActionHandler.SingleExecution() {
|
||||
cmd: Command,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Boolean {
|
||||
if (injector.application.isOctopusEnabled()) {
|
||||
if (editor.isInForEachCaretScope()) {
|
||||
editor.removeSecondaryCarets()
|
||||
injector.changeGroup.processEnter(editor, editor.primaryCaret(), context)
|
||||
} else {
|
||||
editor.forEachNativeCaret({ caret ->
|
||||
injector.changeGroup.processEnter(editor, caret, context)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
injector.changeGroup.processEnter(editor, context)
|
||||
}
|
||||
injector.changeGroup.processEnter(editor, context)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,5 +24,4 @@ interface VimApplication {
|
||||
|
||||
fun currentStackTrace(): String
|
||||
fun runAfterGotFocus(runnable: Runnable)
|
||||
fun isOctopusEnabled(): Boolean
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ interface VimChangeGroup {
|
||||
|
||||
fun processEscape(editor: VimEditor, context: ExecutionContext?)
|
||||
|
||||
fun processEnter(editor: VimEditor, caret: VimCaret, context: ExecutionContext)
|
||||
fun processEnter(editor: VimEditor, context: ExecutionContext)
|
||||
fun processBackspace(editor: VimEditor, context: ExecutionContext)
|
||||
|
||||
|
||||
@@ -169,13 +169,7 @@ abstract class VimKeyGroupBase : VimKeyGroup {
|
||||
val oldSize = requiredShortcutKeys.size
|
||||
for (key in fromKeys) {
|
||||
if (key.keyChar == KeyEvent.CHAR_UNDEFINED) {
|
||||
if (
|
||||
!injector.application.isOctopusEnabled() ||
|
||||
!(key.keyCode == KeyEvent.VK_ESCAPE && key.modifiers == 0) &&
|
||||
!(key.keyCode == KeyEvent.VK_ENTER && key.modifiers == 0)
|
||||
) {
|
||||
requiredShortcutKeys.add(RequiredShortcut(key, owner))
|
||||
}
|
||||
requiredShortcutKeys.add(RequiredShortcut(key, owner))
|
||||
}
|
||||
}
|
||||
if (requiredShortcutKeys.size != oldSize) {
|
||||
|
||||
@@ -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
|
||||
@@ -85,6 +85,16 @@ interface VimMotionGroup {
|
||||
fun moveCaretToMark(caret: ImmutableVimCaret, ch: Char, toLineStart: Boolean): Motion
|
||||
fun moveCaretToMarkRelative(caret: ImmutableVimCaret, count: Int): Motion
|
||||
fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion
|
||||
|
||||
/**
|
||||
* Move the caret along the change list (`g;` / `g,`). [count] is signed:
|
||||
* negative for `g;` (older), positive for `g,` (newer).
|
||||
*
|
||||
* Emits the appropriate E662 / E663 / E664 message and returns [Motion.Error]
|
||||
* when the index is already at the relevant boundary.
|
||||
*/
|
||||
fun moveCaretToChange(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion
|
||||
|
||||
fun moveCaretToMatchingPair(editor: VimEditor, caret: ImmutableVimCaret): Motion
|
||||
|
||||
/**
|
||||
|
||||
@@ -312,6 +312,12 @@ abstract class VimMotionGroupBase : VimMotionGroup {
|
||||
return offset.toMotionOrError()
|
||||
}
|
||||
|
||||
// Engine has no change list (it lives on the frontend). Frontend MotionGroup overrides.
|
||||
override fun moveCaretToChange(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion {
|
||||
injector.messages.showErrorMessage(editor, injector.messages.message("E664"))
|
||||
return Motion.Error
|
||||
}
|
||||
|
||||
override fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion {
|
||||
val jumpService = injector.jumpService
|
||||
val spot = jumpService.getJumpSpot(editor)
|
||||
|
||||
@@ -644,8 +644,10 @@ abstract class VimSearchGroupBase : VimSearchGroup {
|
||||
options
|
||||
)
|
||||
if (lineToNextSubstitute == null) {
|
||||
injector.messages.indicateError()
|
||||
injector.messages.showStatusBarMessage(null, "E486: Pattern not found: $pattern")
|
||||
if (doError) {
|
||||
injector.messages.indicateError()
|
||||
injector.messages.showStatusBarMessage(null, "E486: Pattern not found: $pattern")
|
||||
}
|
||||
return true
|
||||
}
|
||||
val (line, nextSubstitute) = lineToNextSubstitute
|
||||
@@ -855,7 +857,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
|
||||
if (!gotQuit) {
|
||||
if (lastMatchLine != -1) {
|
||||
caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, lastMatchLine))
|
||||
} else {
|
||||
} else if (doError) {
|
||||
injector.messages.showErrorMessage(editor, "E486: Pattern not found: $pattern")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +294,9 @@ abstract class VimSearchHelperBase : VimSearchHelper {
|
||||
val regex = try {
|
||||
VimRegex(pattern)
|
||||
} catch (e: VimRegexException) {
|
||||
injector.messages.showErrorMessage(editor, e.message)
|
||||
if (showMessages) {
|
||||
injector.messages.showErrorMessage(editor, e.message)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -401,7 +403,6 @@ abstract class VimSearchHelperBase : VimSearchHelper {
|
||||
val regex = try {
|
||||
VimRegex(pattern)
|
||||
} catch (e: VimRegexException) {
|
||||
injector.messages.showErrorMessage(editor, e.message)
|
||||
return emptyList()
|
||||
}
|
||||
return regex.findAll(
|
||||
|
||||
@@ -256,7 +256,7 @@ private fun classify(codePoint: Int): CodePointType {
|
||||
in 0x1100..0x115F, in 0xA960..0xA97C -> CodePointType.L
|
||||
in 0x1160..0x11A7, in 0xD7B0..0xD7C6 -> CodePointType.V
|
||||
in 0x11A8..0x11FF, in 0xD7CB..0xD7FB -> CodePointType.T
|
||||
// LV is encountered every 28 characters, everything in-between is LVT.
|
||||
// LV is encountered every 28 characters, everything in-between is LVT.
|
||||
in 0xAC00..0xD7A3 -> if ((codePoint - 0xAC00) % 28 == 0) CodePointType.LV else CodePointType.LVT
|
||||
else -> CodePointType.OTHER
|
||||
}
|
||||
|
||||
@@ -98,4 +98,6 @@ interface VimRegisterGroup {
|
||||
fun getCurrentRegisterForMulticaret(): Char // `set clipbaard+=unnamedplus` should not make system register the default one when working with multiple carets VIM-2804
|
||||
fun isSystemClipboard(register: Char): Boolean
|
||||
fun isPrimaryRegisterSupported(): Boolean
|
||||
|
||||
fun getLastExplicitlyWrittenRegister(r: Char): Register?
|
||||
}
|
||||
|
||||
@@ -409,6 +409,11 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
|
||||
return System.getenv("DISPLAY") != null && injector.systemInfoService.isXWindow
|
||||
}
|
||||
|
||||
override fun getLastExplicitlyWrittenRegister(r: Char): Register? {
|
||||
require(CLIPBOARD_REGISTERS.contains(r.lowercaseChar())) { "Only clipboard registers are supported: got '$r'" }
|
||||
return myRegisters[r.lowercaseChar()]
|
||||
}
|
||||
|
||||
private fun setSystemPrimaryRegisterText(text: String, rawText: String, transferableData: List<Any>) {
|
||||
logger.trace("Setting text: $text to primary selection...")
|
||||
if (isPrimaryRegisterSupported()) {
|
||||
@@ -436,7 +441,13 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
|
||||
return refreshClipboardRegister()
|
||||
}
|
||||
try {
|
||||
val clipboardData = injector.clipboardManager.getPrimaryContent() ?: return null
|
||||
val clipboardData = injector.clipboardManager.getPrimaryContent()
|
||||
if (clipboardData == null || clipboardData.text.isEmpty()) {
|
||||
// Wayland/XWayland clears PRIMARY on focus change; prefer the last IdeaVim-written value over
|
||||
// an empty result. Differs from Vim only when the user deliberately clears PRIMARY externally.
|
||||
logger.trace("PRIMARY selection is unavailable or empty; falling back to in-memory register value")
|
||||
return myRegisters[PRIMARY_REGISTER]
|
||||
}
|
||||
val currentRegister = myRegisters[PRIMARY_REGISTER]
|
||||
val text = clipboardData.text
|
||||
val transferableData = clipboardData.transferableData.toMutableList()
|
||||
|
||||
@@ -36,6 +36,14 @@ class ExCommandTree {
|
||||
return abbrevToCommand[abbreviation]
|
||||
}
|
||||
|
||||
/**
|
||||
* Results are sorted alphabetically; command-name Tab completion relies on this
|
||||
* order to cycle through matches deterministically (e.g. `:set` -> `set`, `setglobal`, `sethandler`, ...).
|
||||
*/
|
||||
fun findFullCommandsByPrefix(prefix: String): List<String> {
|
||||
return commandToInstance.keys.filter { it.startsWith(prefix) }.sorted()
|
||||
}
|
||||
|
||||
private fun parseCommandPattern(commandsPattern: String): List<Pair<String, String>> {
|
||||
val result = mutableListOf<Pair<String, String>>()
|
||||
val commands = commandsPattern.split(",")
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.vimscript.model.functions.handlers.variousFunctions
|
||||
|
||||
import com.intellij.vim.annotations.VimscriptFunction
|
||||
import com.maddyhome.idea.vim.api.ExecutionContext
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.state.mode.Mode
|
||||
import com.maddyhome.idea.vim.state.mode.SelectionType
|
||||
import com.maddyhome.idea.vim.vimscript.model.VimLContext
|
||||
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
|
||||
import com.maddyhome.idea.vim.vimscript.model.functions.BuiltinFunctionHandler
|
||||
|
||||
@VimscriptFunction(name = "mode")
|
||||
internal class ModeFunctionHandler : BuiltinFunctionHandler<VimString>(minArity = 0, maxArity = 1) {
|
||||
override fun doFunction(
|
||||
arguments: Arguments,
|
||||
editor: VimEditor,
|
||||
context: ExecutionContext,
|
||||
vimContext: VimLContext,
|
||||
): VimString {
|
||||
return VimString(modeString(editor.mode))
|
||||
}
|
||||
|
||||
private fun modeString(mode: Mode): String = when (mode) {
|
||||
is Mode.NORMAL -> "n"
|
||||
is Mode.OP_PENDING -> "no"
|
||||
is Mode.INSERT -> "i"
|
||||
is Mode.REPLACE -> "R"
|
||||
is Mode.VISUAL -> when (mode.selectionType) {
|
||||
SelectionType.CHARACTER_WISE -> "v"
|
||||
SelectionType.LINE_WISE -> "V"
|
||||
SelectionType.BLOCK_WISE -> CTRL_V
|
||||
}
|
||||
|
||||
is Mode.SELECT -> when (mode.selectionType) {
|
||||
SelectionType.CHARACTER_WISE -> "s"
|
||||
SelectionType.LINE_WISE -> "S"
|
||||
SelectionType.BLOCK_WISE -> CTRL_S
|
||||
}
|
||||
|
||||
is Mode.CMD_LINE -> "c"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CTRL_V = ""
|
||||
private const val CTRL_S = ""
|
||||
}
|
||||
}
|
||||
@@ -1644,6 +1644,11 @@
|
||||
"class": "com.maddyhome.idea.vim.action.motion.search.SearchWordForwardAction",
|
||||
"modes": "NXO"
|
||||
},
|
||||
{
|
||||
"keys": "g,",
|
||||
"class": "com.maddyhome.idea.vim.action.motion.changelist.MotionGotoChangeNewerAction",
|
||||
"modes": "N"
|
||||
},
|
||||
{
|
||||
"keys": "g0",
|
||||
"class": "com.maddyhome.idea.vim.action.motion.leftright.MotionFirstScreenColumnAction",
|
||||
@@ -1654,6 +1659,11 @@
|
||||
"class": "com.maddyhome.idea.vim.action.file.FileGetHexAction",
|
||||
"modes": "N"
|
||||
},
|
||||
{
|
||||
"keys": "g;",
|
||||
"class": "com.maddyhome.idea.vim.action.motion.changelist.MotionGotoChangeOlderAction",
|
||||
"modes": "N"
|
||||
},
|
||||
{
|
||||
"keys": "g<C-A>",
|
||||
"class": "com.maddyhome.idea.vim.action.change.change.number.ChangeVisualNumberAvalancheIncAction",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"mapnew": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.collectionFunctions.MapNewFunctionHandler",
|
||||
"max": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.collectionFunctions.MaxFunctionHandler",
|
||||
"min": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.collectionFunctions.MinFunctionHandler",
|
||||
"mode": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.variousFunctions.ModeFunctionHandler",
|
||||
"nr2char": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.stringFunctions.Nr2charFunctionHandler",
|
||||
"or": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.bitwiseFunctions.OrFunctionHandler",
|
||||
"pow": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.floatFunctions.PowFunctionHandler",
|
||||
|
||||
@@ -45,6 +45,9 @@ E69=E69: Missing ] after {0}%[
|
||||
E70=E70: Empty {0}%[]
|
||||
E71=E71: Invalid character after {0}%
|
||||
E76=E76: Too many [
|
||||
E662=E662: At start of changelist
|
||||
E663=E663: At end of changelist
|
||||
E664=E664: changelist is empty
|
||||
E81=E81: Using <SID> not in a script context
|
||||
E86=E86: Buffer {0} does not exist
|
||||
E93=E93: More than one match for {0}
|
||||
|
||||
@@ -10,15 +10,14 @@ package com.maddyhome.idea.vim.action.ex
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class CommandLineParserTest {
|
||||
|
||||
@Test
|
||||
fun `test parse simple command with argument`() {
|
||||
val result = parseCommandLineForCompletion("edit foo.txt")
|
||||
assertNotNull(result)
|
||||
val result = assertIs<ArgumentCompletionContext>(parseCommandLineForCompletion("edit foo.txt"))
|
||||
assertEquals("edit", result.commandName)
|
||||
assertEquals("foo.txt", result.argumentPrefix)
|
||||
assertEquals(5, result.completionStart)
|
||||
@@ -26,8 +25,7 @@ class CommandLineParserTest {
|
||||
|
||||
@Test
|
||||
fun `test parse abbreviated command`() {
|
||||
val result = parseCommandLineForCompletion("e foo.txt")
|
||||
assertNotNull(result)
|
||||
val result = assertIs<ArgumentCompletionContext>(parseCommandLineForCompletion("e foo.txt"))
|
||||
assertEquals("e", result.commandName)
|
||||
assertEquals("foo.txt", result.argumentPrefix)
|
||||
assertEquals(2, result.completionStart)
|
||||
@@ -35,8 +33,7 @@ class CommandLineParserTest {
|
||||
|
||||
@Test
|
||||
fun `test parse command with path argument`() {
|
||||
val result = parseCommandLineForCompletion("edit src/main/")
|
||||
assertNotNull(result)
|
||||
val result = assertIs<ArgumentCompletionContext>(parseCommandLineForCompletion("edit src/main/"))
|
||||
assertEquals("edit", result.commandName)
|
||||
assertEquals("src/main/", result.argumentPrefix)
|
||||
assertEquals(5, result.completionStart)
|
||||
@@ -44,8 +41,7 @@ class CommandLineParserTest {
|
||||
|
||||
@Test
|
||||
fun `test parse command with home path`() {
|
||||
val result = parseCommandLineForCompletion("e ~/.vim")
|
||||
assertNotNull(result)
|
||||
val result = assertIs<ArgumentCompletionContext>(parseCommandLineForCompletion("e ~/.vim"))
|
||||
assertEquals("e", result.commandName)
|
||||
assertEquals("~/.vim", result.argumentPrefix)
|
||||
assertEquals(2, result.completionStart)
|
||||
@@ -53,8 +49,7 @@ class CommandLineParserTest {
|
||||
|
||||
@Test
|
||||
fun `test parse command with empty argument`() {
|
||||
val result = parseCommandLineForCompletion("edit ")
|
||||
assertNotNull(result)
|
||||
val result = assertIs<ArgumentCompletionContext>(parseCommandLineForCompletion("edit "))
|
||||
assertEquals("edit", result.commandName)
|
||||
assertEquals("", result.argumentPrefix)
|
||||
assertEquals(5, result.completionStart)
|
||||
@@ -62,13 +57,79 @@ class CommandLineParserTest {
|
||||
|
||||
@Test
|
||||
fun `test parse command with multiple spaces before argument`() {
|
||||
val result = parseCommandLineForCompletion("edit foo")
|
||||
assertNotNull(result)
|
||||
val result = assertIs<ArgumentCompletionContext>(parseCommandLineForCompletion("edit foo"))
|
||||
assertEquals("edit", result.commandName)
|
||||
assertEquals("foo", result.argumentPrefix)
|
||||
assertEquals(7, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse with leading whitespace`() {
|
||||
val result = assertIs<ArgumentCompletionContext>(parseCommandLineForCompletion(" edit foo"))
|
||||
assertEquals("edit", result.commandName)
|
||||
assertEquals("foo", result.argumentPrefix)
|
||||
assertEquals(7, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse bang command`() {
|
||||
val result = assertIs<ArgumentCompletionContext>(parseCommandLineForCompletion("edit! foo.txt"))
|
||||
assertEquals("edit!", result.commandName)
|
||||
assertEquals("foo.txt", result.argumentPrefix)
|
||||
assertEquals(6, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse write command`() {
|
||||
val result = assertIs<ArgumentCompletionContext>(parseCommandLineForCompletion("w output.txt"))
|
||||
assertEquals("w", result.commandName)
|
||||
assertEquals("output.txt", result.argumentPrefix)
|
||||
assertEquals(2, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse source command with path`() {
|
||||
val result = assertIs<ArgumentCompletionContext>(parseCommandLineForCompletion("source ~/.ideavimrc"))
|
||||
assertEquals("source", result.commandName)
|
||||
assertEquals("~/.ideavimrc", result.argumentPrefix)
|
||||
assertEquals(7, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse command name only returns command-name context`() {
|
||||
val result = assertIs<CommandNameCompletionContext>(parseCommandLineForCompletion("edit"))
|
||||
assertEquals("edit", result.prefix)
|
||||
assertEquals(0, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse single letter abbreviation returns command-name context`() {
|
||||
val result = assertIs<CommandNameCompletionContext>(parseCommandLineForCompletion("e"))
|
||||
assertEquals("e", result.prefix)
|
||||
assertEquals(0, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse partial abbreviation returns command-name context`() {
|
||||
val result = assertIs<CommandNameCompletionContext>(parseCommandLineForCompletion("vs"))
|
||||
assertEquals("vs", result.prefix)
|
||||
assertEquals(0, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse command name with leading whitespace`() {
|
||||
val result = assertIs<CommandNameCompletionContext>(parseCommandLineForCompletion(" vs"))
|
||||
assertEquals("vs", result.prefix)
|
||||
assertEquals(2, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse mixed case command name preserved`() {
|
||||
val result = assertIs<CommandNameCompletionContext>(parseCommandLineForCompletion("tabN"))
|
||||
assertEquals("tabN", result.prefix)
|
||||
assertEquals(0, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse returns null for empty text`() {
|
||||
assertNull(parseCommandLineForCompletion(""))
|
||||
@@ -79,54 +140,19 @@ class CommandLineParserTest {
|
||||
assertNull(parseCommandLineForCompletion(" "))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse returns null for command without space`() {
|
||||
assertNull(parseCommandLineForCompletion("edit"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse returns null for command abbreviation without space`() {
|
||||
assertNull(parseCommandLineForCompletion("e"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse returns null for non-letter start`() {
|
||||
assertNull(parseCommandLineForCompletion("123"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse with leading whitespace`() {
|
||||
val result = parseCommandLineForCompletion(" edit foo")
|
||||
assertNotNull(result)
|
||||
assertEquals("edit", result.commandName)
|
||||
assertEquals("foo", result.argumentPrefix)
|
||||
assertEquals(7, result.completionStart)
|
||||
fun `test parse returns null for bang form without space`() {
|
||||
assertNull(parseCommandLineForCompletion("vs!"))
|
||||
assertNull(parseCommandLineForCompletion("edit!"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse bang command`() {
|
||||
val result = parseCommandLineForCompletion("edit! foo.txt")
|
||||
assertNotNull(result)
|
||||
assertEquals("edit!", result.commandName)
|
||||
assertEquals("foo.txt", result.argumentPrefix)
|
||||
assertEquals(6, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse write command`() {
|
||||
val result = parseCommandLineForCompletion("w output.txt")
|
||||
assertNotNull(result)
|
||||
assertEquals("w", result.commandName)
|
||||
assertEquals("output.txt", result.argumentPrefix)
|
||||
assertEquals(2, result.completionStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test parse source command with path`() {
|
||||
val result = parseCommandLineForCompletion("source ~/.ideavimrc")
|
||||
assertNotNull(result)
|
||||
assertEquals("source", result.commandName)
|
||||
assertEquals("~/.ideavimrc", result.argumentPrefix)
|
||||
assertEquals(7, result.completionStart)
|
||||
fun `test parse returns null for command followed by non-space chars`() {
|
||||
assertNull(parseCommandLineForCompletion("foo123"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user