diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c4f9fa..ba7132a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,46 +26,33 @@ concurrency: jobs: - # Prepare environment and build the plugin + # Prepare the environment and build the plugin build: name: Build runs-on: ubuntu-latest - outputs: - version: ${{ steps.properties.outputs.version }} - changelog: ${{ steps.properties.outputs.changelog }} - pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} steps: + # Free GitHub Actions Environment Disk Space + - name: Maximize Build Space + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + large-packages: false + # Check out the current repository - name: Fetch Sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 - # Set up Java environment for the next steps + # Set up the Java environment for the next steps - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: zulu java-version: 21 # Setup Gradle - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - # Set environment variables - - name: Export Properties - id: properties - shell: bash - run: | - PROPERTIES="$(./gradlew properties --console=plain -q)" - VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" - CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" - - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT - - echo "changelog<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + uses: gradle/actions/setup-gradle@v5 # Build plugin - name: Build plugin @@ -82,9 +69,9 @@ jobs: echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT - # Store already-built plugin as an artifact for downloading + # Store an already-built plugin as an artifact for downloading - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ steps.artifact.outputs.filename }} path: ./build/distributions/content/*/* @@ -96,20 +83,29 @@ jobs: runs-on: ubuntu-latest steps: + # Free GitHub Actions Environment Disk Space + - name: Maximize Build Space + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + large-packages: false + # Check out the current repository - name: Fetch Sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 - # Set up Java environment for the next steps + # Set up the Java environment for the next steps - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: zulu java-version: 21 # Setup Gradle - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: true # Run tests - name: Run Tests @@ -118,7 +114,7 @@ jobs: # Collect Tests Result of failed tests - name: Collect Tests Result if: ${{ failure() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: tests-result path: ${{ github.workspace }}/build/reports/tests @@ -128,8 +124,9 @@ jobs: uses: codecov/codecov-action@v5 with: files: ${{ github.workspace }}/build/reports/kover/report.xml + token: ${{ secrets.CODECOV_TOKEN }} - # Run Qodana inspections and provide report + # Run Qodana inspections and provide a report inspectCode: name: Inspect code needs: [ build ] @@ -142,28 +139,28 @@ jobs: # Free GitHub Actions Environment Disk Space - name: Maximize Build Space - uses: jlumbroso/free-disk-space@main + uses: jlumbroso/free-disk-space@v1.3.1 with: tool-cache: false large-packages: false # Check out the current repository - name: Fetch Sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: - ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit - fetch-depth: 0 # a full history is required for pull request analysis + ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit + fetch-depth: 0 # a full history is required for pull request analysis - # Set up Java environment for the next steps + # Set up the Java environment for the next steps - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: zulu java-version: 21 # Run Qodana inspections - name: Qodana - Code Inspection - uses: JetBrains/qodana-action@v2024.2 + uses: JetBrains/qodana-action@v2025.1.1 with: cache-default-branch-only: true @@ -176,47 +173,42 @@ jobs: # Free GitHub Actions Environment Disk Space - name: Maximize Build Space - uses: jlumbroso/free-disk-space@main + uses: jlumbroso/free-disk-space@v1.3.1 with: tool-cache: false large-packages: false # Check out the current repository - name: Fetch Sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 - # Set up Java environment for the next steps + # Set up the Java environment for the next steps - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: zulu java-version: 21 # Setup Gradle - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - # Cache Plugin Verifier IDEs - - name: Setup Plugin Verifier IDEs Cache - uses: actions/cache@v4 + uses: gradle/actions/setup-gradle@v5 with: - path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides - key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} + cache-read-only: true # Run Verify Plugin task and IntelliJ Plugin Verifier tool - name: Run Plugin Verification tasks - run: ./gradlew verifyPlugin -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }} + run: ./gradlew verifyPlugin # Collect Plugin Verifier Result - name: Collect Plugin Verifier Result if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: pluginVerifier-result path: ${{ github.workspace }}/build/reports/pluginVerifier # Prepare a draft release for GitHub Releases page for the manual verification - # If accepted and published, release workflow would be triggered + # If accepted and published, the release workflow would be triggered releaseDraft: name: Release draft if: github.event_name != 'pull_request' @@ -228,7 +220,7 @@ jobs: # Check out the current repository - name: Fetch Sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Remove old release drafts by using the curl request for the available releases with a draft flag - name: Remove Old Release Drafts @@ -244,10 +236,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh release create "v${{ needs.build.outputs.version }}" \ + VERSION=$(./gradlew properties --property version --quiet --console=plain | tail -n 1 | cut -f2- -d ' ') + RELEASE_NOTE="./build/tmp/release_note.txt" + ./gradlew getChangelog --unreleased --no-header --quiet --console=plain --output-file=$RELEASE_NOTE + + gh release create $VERSION \ --draft \ - --title "v${{ needs.build.outputs.version }}" \ - --notes "$(cat << 'EOM' - ${{ needs.build.outputs.changelog }} - EOM - )" + --title $VERSION \ + --notes-file $RELEASE_NOTE diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1a512c..49fb01c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. -# Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. +# Running the publishPlugin task requires all the following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. name: Release @@ -18,44 +18,43 @@ jobs: pull-requests: write steps: + # Free GitHub Actions Environment Disk Space + - name: Maximize Build Space + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + large-packages: false + # Check out the current repository - name: Fetch Sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.event.release.tag_name }} - # Set up Java environment for the next steps + # Set up the Java environment for the next steps - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: zulu java-version: 21 # Setup Gradle - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - # Set environment variables - - name: Export Properties - id: properties - shell: bash - run: | - CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' - ${{ github.event.release.body }} - EOM - )" - - echo "changelog<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: true # Update the Unreleased section with the current release note - name: Patch Changelog - if: ${{ steps.properties.outputs.changelog != '' }} + if: ${{ github.event.release.body != '' }} env: - CHANGELOG: ${{ steps.properties.outputs.changelog }} + CHANGELOG: ${{ github.event.release.body }} run: | - ./gradlew patchChangelog --release-note="$CHANGELOG" + RELEASE_NOTE="./build/tmp/release_note.txt" + mkdir -p "$(dirname "$RELEASE_NOTE")" + echo "$CHANGELOG" > $RELEASE_NOTE + + ./gradlew patchChangelog --release-note-file=$RELEASE_NOTE # Publish the plugin to JetBrains Marketplace - name: Publish Plugin @@ -66,7 +65,7 @@ jobs: PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} run: ./gradlew publishPlugin - # Upload artifact as a release asset + # Upload an artifact as a release asset - name: Upload Release Asset env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -74,7 +73,7 @@ jobs: # Create a pull request - name: Create Pull Request - if: ${{ steps.properties.outputs.changelog != '' }} + if: ${{ github.event.release.body != '' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -88,7 +87,7 @@ jobs: git checkout -b $BRANCH git commit -am "Changelog update - $VERSION" git push --set-upstream origin $BRANCH - + gh label create "$LABEL" \ --description "Pull requests with release changelog update" \ --force \ diff --git a/.github/workflows/run-ui-tests.yml b/.github/workflows/run-ui-tests.yml index 230d6c5..dca12a7 100644 --- a/.github/workflows/run-ui-tests.yml +++ b/.github/workflows/run-ui-tests.yml @@ -33,18 +33,20 @@ jobs: # Check out the current repository - name: Fetch Sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 - # Set up Java environment for the next steps + # Set up the Java environment for the next steps - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: zulu java-version: 21 # Setup Gradle - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: true # Run IDEA prepared for UI testing - name: Run IDE diff --git a/.gitignore b/.gitignore index daaed5a..f9a9db1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ +.DS_Store .gradle .idea .intellijPlatform +.kotlin .qodana build .envrc diff --git a/.java-version b/.java-version index aabe6ec..25e6ec2 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -21 +21.0.7 diff --git a/.run/Run IDE for UI Tests.run.xml b/.run/Run IDE for UI Tests.run.xml deleted file mode 100644 index 9b028c3..0000000 --- a/.run/Run IDE for UI Tests.run.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - true - true - false - - - \ No newline at end of file diff --git a/.run/Run Plugin Verification.run.xml b/.run/Run Plugin Verification.run.xml deleted file mode 100644 index 3a8d688..0000000 --- a/.run/Run Plugin Verification.run.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - true - true - false - - - - \ No newline at end of file diff --git a/.run/Run Qodana.run.xml b/.run/Run Plugin.run.xml similarity index 58% rename from .run/Run Qodana.run.xml rename to .run/Run Plugin.run.xml index 9603583..9db62fb 100644 --- a/.run/Run Qodana.run.xml +++ b/.run/Run Plugin.run.xml @@ -1,26 +1,25 @@ - + + - - - true + false true false + false \ No newline at end of file diff --git a/.run/Run Plugin Tests.run.xml b/.run/Run Tests.run.xml similarity index 96% rename from .run/Run Plugin Tests.run.xml rename to .run/Run Tests.run.xml index 132d9ad..f281bdc 100644 --- a/.run/Run Plugin Tests.run.xml +++ b/.run/Run Tests.run.xml @@ -19,6 +19,7 @@ true true false + true diff --git a/.run/Run IDE with Plugin.run.xml b/.run/Run Verifications.run.xml similarity index 82% rename from .run/Run IDE with Plugin.run.xml rename to .run/Run Verifications.run.xml index d15ff68..32783f5 100644 --- a/.run/Run IDE with Plugin.run.xml +++ b/.run/Run Verifications.run.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec242e..3a65762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ## [Unreleased] +### Added + +- Compatibility with IntelliJ 2025.3 + ## [0.0.18] - 2025-05-04 ### Added diff --git a/build.gradle.kts b/build.gradle.kts index 69ca8f4..9c026e0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,14 +29,14 @@ repositories { } } -// Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog +// Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/version_catalogs.html dependencies { testImplementation(libs.junit) testImplementation(libs.opentest4j) // IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html intellijPlatform { - create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion")) + intellijIdea(providers.gradleProperty("platformVersion")) // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') }) @@ -44,6 +44,9 @@ dependencies { // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace. plugins(providers.gradleProperty("platformPlugins").map { it.split(',') }) + // Module Dependencies. Uses `platformBundledModules` property from the gradle.properties file for bundled IntelliJ Platform modules. + bundledModules(providers.gradleProperty("platformBundledModules").map { it.split(',') }) + testFramework(TestFrameworkType.Platform) } } @@ -82,7 +85,6 @@ intellijPlatform { ideaVersion { sinceBuild = providers.gradleProperty("pluginSinceBuild") - untilBuild = providers.gradleProperty("pluginUntilBuild") } } @@ -96,7 +98,7 @@ intellijPlatform { token = providers.environmentVariable("PUBLISH_TOKEN") // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: - // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel + // https://plugins.jetbrains.com/docs/intellij/publishing-plugin.html#specifying-a-release-channel channels = providers.gradleProperty("pluginVersion").map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } } @@ -113,7 +115,7 @@ changelog { repositoryUrl = providers.gradleProperty("pluginRepositoryUrl") } -// Configure Gradle Kover Plugin - read more: https://github.com/Kotlin/kotlinx-kover#configuration +// Configure Gradle Kover Plugin - read more: https://kotlin.github.io/kotlinx-kover/gradle-plugin/#configuration-details kover { reports { total { diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..f6e0f07 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,10 @@ +coverage: + status: + project: + default: + informational: true + threshold: 0% + base: auto + patch: + default: + informational: true diff --git a/gradle.properties b/gradle.properties index 10acecc..b0d910b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,24 +4,24 @@ pluginGroup = io.github.srizzo pluginName = codebuddy pluginRepositoryUrl = https://github.com/srizzo/code-buddy-plugin # SemVer format -> https://semver.org -pluginVersion = 0.0.18 +pluginVersion = 0.0.19 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html -pluginSinceBuild = 251 -pluginUntilBuild = 252.* +pluginSinceBuild = 252 # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension -platformType = IC -platformVersion = 2025.1 +platformVersion = 2025.3 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP platformPlugins = # Example: platformBundledPlugins = com.intellij.java platformBundledPlugins = +# Example: platformBundledModules = intellij.spellchecker +platformBundledModules = # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion = 8.13 +gradleVersion = 9.2.1 # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib kotlin.stdlib.default.dependency = false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1fd5723..3fd64a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,11 +4,11 @@ junit = "4.13.2" opentest4j = "1.3.0" # plugins -changelog = "2.2.1" -intelliJPlatform = "2.5.0" -kotlin = "2.1.20" -kover = "0.9.1" -qodana = "2024.3.4" +changelog = "2.5.0" +intelliJPlatform = "2.10.5" +kotlin = "2.2.21" +kover = "0.9.3" +qodana = "2025.2.2" [libraries] junit = { group = "junit", name = "junit", version.ref = "junit" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975..f8e1ee3 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index faf9300..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/qodana.yml b/qodana.yml index 54b11bc..81f13b9 100644 --- a/qodana.yml +++ b/qodana.yml @@ -2,7 +2,7 @@ # https://www.jetbrains.com/help/qodana/qodana-yaml.html version: "1.0" -linter: jetbrains/qodana-jvm-community:latest +linter: jetbrains/qodana-jvm-community:2024.3 projectJDK: "21" profile: name: qodana.recommended diff --git a/settings.gradle.kts b/settings.gradle.kts index 7b29669..1b882c2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } rootProject.name = "codebuddy" diff --git a/src/main/java/io/github/srizzo/codebuddy/columnmode/ColumnModeStartupActivity.java b/src/main/java/io/github/srizzo/codebuddy/columnmode/ColumnModeStartupActivity.java new file mode 100644 index 0000000..045c00b --- /dev/null +++ b/src/main/java/io/github/srizzo/codebuddy/columnmode/ColumnModeStartupActivity.java @@ -0,0 +1,116 @@ +package io.github.srizzo.codebuddy.columnmode; + +import com.intellij.ide.IdeEventQueue; +import com.intellij.openapi.actionSystem.ActionPlaces; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.startup.ProjectActivity; +import io.github.srizzo.codebuddy.settings.CodeBuddySettingsState; +import io.github.srizzo.codebuddy.util.BlockSelectionUtil; +import io.github.srizzo.codebuddy.util.RunActionUtil; +import kotlin.Unit; +import kotlin.coroutines.Continuation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.awt.event.KeyEvent; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +public class ColumnModeStartupActivity implements ProjectActivity { + private static final String ACTION_TOGGLE_BLOCK_SELECTION = "io.github.srizzo.codebuddy.columnmode.ToggleBlockSelectionAction"; + private static final String ACTION_ENTER_COLUMN_MODE = "io.github.srizzo.codebuddy.columnmode.EnterTemporaryColumnModeAction"; + private static final String ACTION_EXIT_COLUMN_MODE = "io.github.srizzo.codebuddy.columnmode.ExitTemporaryColumnModeAction"; + private static final String ACTION_TO_SINGLE_CARET = "io.github.srizzo.codebuddy.columnmode.ToSingleCaretAction"; + + private static final AtomicBoolean initialized = new AtomicBoolean(false); + private static final AtomicBoolean actionStarted = new AtomicBoolean(false); + private static final AtomicLong actionStartedAt = new AtomicLong(0); + + @Nullable + @Override + public Object execute(@NotNull Project project, @NotNull Continuation continuation) { + if (initialized.getAndSet(true)) { + return Unit.INSTANCE; + } + + ApplicationManager.getApplication().getService(CrosshairCursorOnColumnModeUpdater.class); + + IdeEventQueue.getInstance().addDispatcher(new IdeEventQueue.EventDispatcher() { + @Override + public boolean dispatch(@NotNull AWTEvent event) { + if (!(event instanceof KeyEvent)) { + return false; + } + + KeyEvent keyEvent = (KeyEvent) event; + CodeBuddySettingsState settings = CodeBuddySettingsState.getInstance(); + + int expectedKeyCode = BlockSelectionUtil.getMultiCaretActionKeyCode(); + if (keyEvent.getKeyCode() == expectedKeyCode) { + handleModifierKey(keyEvent, settings); + } + + if (settings.arrowUpAndDownReturnToSingleCursorStatus) { + if (keyEvent.getID() == KeyEvent.KEY_PRESSED && keyEvent.getModifiersEx() == 0) { + if (keyEvent.getKeyCode() == KeyEvent.VK_UP || keyEvent.getKeyCode() == KeyEvent.VK_DOWN) { + RunActionUtil.runAction(keyEvent, ACTION_TO_SINGLE_CARET, ActionPlaces.KEYBOARD_SHORTCUT); + } + } + } + + if (settings.modifiersExitColumnSelectionModeStatus) { + if (keyEvent.getID() == KeyEvent.KEY_PRESSED) { + switch (keyEvent.getKeyCode()) { + case KeyEvent.VK_UP: + case KeyEvent.VK_DOWN: + case KeyEvent.VK_SHIFT: + case KeyEvent.VK_CONTROL: + case KeyEvent.VK_META: + RunActionUtil.runAction(keyEvent, ACTION_EXIT_COLUMN_MODE, ActionPlaces.KEYBOARD_SHORTCUT); + break; + } + } + } + + return false; + } + }, ApplicationManager.getApplication()); + + return Unit.INSTANCE; + } + + private static void handleModifierKey(KeyEvent keyEvent, CodeBuddySettingsState settings) { + int expectedModifier = BlockSelectionUtil.getMultiCaretActionKeyModifier(); + + switch (keyEvent.getID()) { + case KeyEvent.KEY_PRESSED: + if (settings.toggleBlockSelectionOnModifierKeyPressStatus) { + if (keyEvent.getModifiersEx() == expectedModifier) { + actionStarted.set(true); + actionStartedAt.set(keyEvent.getWhen()); + } + } + if (settings.holdingModifierActivatesColumnSelectionModeStatus) { + RunActionUtil.runAction(keyEvent, ACTION_ENTER_COLUMN_MODE, ActionPlaces.KEYBOARD_SHORTCUT); + } + break; + + case KeyEvent.KEY_RELEASED: + if (settings.toggleBlockSelectionOnModifierKeyPressStatus) { + if (keyEvent.getModifiersEx() == 0) { + long elapsed = keyEvent.getWhen() - actionStartedAt.get(); + if (actionStarted.get() && elapsed < 200) { + RunActionUtil.runAction(keyEvent, ACTION_TOGGLE_BLOCK_SELECTION, ActionPlaces.KEYBOARD_SHORTCUT); + } + actionStarted.set(false); + } + } + if (settings.holdingModifierActivatesColumnSelectionModeStatus) { + RunActionUtil.runAction(keyEvent, ACTION_EXIT_COLUMN_MODE, ActionPlaces.KEYBOARD_SHORTCUT); + } + break; + } + } +} diff --git a/src/main/java/io/github/srizzo/codebuddy/columnmode/CrosshairCursorOnColumnModeUpdater.java b/src/main/java/io/github/srizzo/codebuddy/columnmode/CrosshairCursorOnColumnModeUpdater.java index af15949..cf18862 100644 --- a/src/main/java/io/github/srizzo/codebuddy/columnmode/CrosshairCursorOnColumnModeUpdater.java +++ b/src/main/java/io/github/srizzo/codebuddy/columnmode/CrosshairCursorOnColumnModeUpdater.java @@ -1,44 +1,62 @@ package io.github.srizzo.codebuddy.columnmode; +import com.intellij.ide.IdeEventQueue; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.Service; +import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; -import com.intellij.openapi.editor.event.EditorEventMulticaster; -import com.intellij.openapi.editor.ex.EditorEventMulticasterEx; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.util.ui.UIUtil; import io.github.srizzo.codebuddy.settings.CodeBuddySettingsState; +import io.github.srizzo.codebuddy.util.BlockSelectionUtil; import org.jetbrains.annotations.NotNull; import java.awt.*; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.util.Objects; +import java.awt.event.KeyEvent; @Service -final public class CrosshairCursorOnColumnModeUpdater implements PropertyChangeListener { +final public class CrosshairCursorOnColumnModeUpdater { public CrosshairCursorOnColumnModeUpdater() { - EditorEventMulticaster multicaster = EditorFactory.getInstance().getEventMulticaster(); - if (multicaster instanceof EditorEventMulticasterEx) { - ((EditorEventMulticasterEx) multicaster). - addPropertyChangeListener(this, ApplicationManager.getApplication()); - } - } + IdeEventQueue.getInstance().addDispatcher(new IdeEventQueue.EventDispatcher() { + @Override + public boolean dispatch(@NotNull AWTEvent event) { + CodeBuddySettingsState settings = CodeBuddySettingsState.getInstance(); - private static EditorEx getEditor(@NotNull PropertyChangeEvent e) { - return (EditorEx) e.getSource(); - } + if (!settings.crosshairCursorOnColumnSelectionModeStatus) { + return false; + } + + if (!settings.holdingModifierActivatesColumnSelectionModeStatus) { + return false; + } + + if (!(event instanceof KeyEvent)) { + return false; + } - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (!CodeBuddySettingsState.getInstance().crosshairCursorOnColumnSelectionModeStatus) return; - if (!EditorEx.PROP_COLUMN_MODE.equals(evt.getPropertyName())) return; + KeyEvent keyEvent = (KeyEvent) event; + int expectedKeyCode = BlockSelectionUtil.getMultiCaretActionKeyCode(); + + if (keyEvent.getKeyCode() != expectedKeyCode) { + return false; + } + + if (keyEvent.getID() == KeyEvent.KEY_PRESSED) { + setCursorOnAllEditors(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); + } else if (keyEvent.getID() == KeyEvent.KEY_RELEASED) { + setCursorOnAllEditors(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + + return false; + } + }, ApplicationManager.getApplication()); + } - EditorEx editor = getEditor(evt); - if (Objects.equals(evt.getNewValue(), true)) { - UIUtil.setCursor(editor.getContentComponent(), Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); - } else { - UIUtil.setCursor(editor.getContentComponent(), Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + private void setCursorOnAllEditors(Cursor cursor) { + for (Editor editor : EditorFactory.getInstance().getAllEditors()) { + if (editor instanceof EditorEx) { + UIUtil.setCursor(((EditorEx) editor).getContentComponent(), cursor); + } } } } diff --git a/src/main/java/io/github/srizzo/codebuddy/columnmode/EnterTemporaryColumnModeAction.java b/src/main/java/io/github/srizzo/codebuddy/columnmode/EnterTemporaryColumnModeAction.java index f42e706..9274adc 100644 --- a/src/main/java/io/github/srizzo/codebuddy/columnmode/EnterTemporaryColumnModeAction.java +++ b/src/main/java/io/github/srizzo/codebuddy/columnmode/EnterTemporaryColumnModeAction.java @@ -1,59 +1,37 @@ package io.github.srizzo.codebuddy.columnmode; -import com.intellij.ide.IdeEventQueue; -import com.intellij.openapi.actionSystem.ActionPlaces; -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.actionSystem.EditorActionHandler; -import com.intellij.openapi.editor.actions.TextComponentEditorAction; import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.project.DumbAwareAction; import io.github.srizzo.codebuddy.settings.CodeBuddySettingsState; -import io.github.srizzo.codebuddy.util.BlockSelectionUtil; -import io.github.srizzo.codebuddy.util.RunActionUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.awt.*; -import java.awt.event.KeyEvent; +public class EnterTemporaryColumnModeAction extends DumbAwareAction { -public class EnterTemporaryColumnModeAction extends TextComponentEditorAction { - public static final String ENTER_TEMPORARY_COLUMN_MODE_ACTION_ID = EnterTemporaryColumnModeAction.class.getName(); - public static final boolean DONT_STOP = false; - - static { - IdeEventQueue.getInstance().addDispatcher(event -> handleEvent(event), ApplicationManager.getApplication()); - } - - public EnterTemporaryColumnModeAction() { - super(new EnterTemporaryColumnModeAction.Handler()); + private static EditorEx getEditor(@NotNull AnActionEvent e) { + Editor editor = e.getData(CommonDataKeys.EDITOR); + return editor instanceof EditorEx ? (EditorEx) editor : null; } - private static boolean handleEvent(AWTEvent event) { - if (!CodeBuddySettingsState.getInstance().holdingModifierActivatesColumnSelectionModeStatus) return DONT_STOP; - - if (!(event instanceof KeyEvent)) return DONT_STOP; - KeyEvent keyEvent = (KeyEvent) event; - - if (keyEvent.getID() == KeyEvent.KEY_PRESSED && - keyEvent.getKeyCode() == BlockSelectionUtil.getMultiCaretActionKeyCode()) { - RunActionUtil.runAction(keyEvent, ENTER_TEMPORARY_COLUMN_MODE_ACTION_ID, - ActionPlaces.KEYBOARD_SHORTCUT); + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + if (!CodeBuddySettingsState.getInstance().holdingModifierActivatesColumnSelectionModeStatus) { + return; } + + EditorEx editor = getEditor(e); + if (editor == null) return; - return DONT_STOP; + editor.setColumnMode(true); } - private static class Handler extends EditorActionHandler { - Handler() { - super(false); - } - - public void doExecute(@NotNull Editor editor, @Nullable Caret caret, DataContext dataContext) { - if (editor instanceof EditorEx) { - ((EditorEx) editor).setColumnMode(true); - } - } + @Override + public void update(@NotNull AnActionEvent e) { + EditorEx editor = getEditor(e); + boolean enabled = editor != null && !editor.isOneLineMode() && !editor.isColumnMode() && + CodeBuddySettingsState.getInstance().holdingModifierActivatesColumnSelectionModeStatus; + e.getPresentation().setEnabledAndVisible(enabled); } } diff --git a/src/main/java/io/github/srizzo/codebuddy/columnmode/EventQueueProxy.java b/src/main/java/io/github/srizzo/codebuddy/columnmode/EventQueueProxy.java new file mode 100644 index 0000000..a81100d --- /dev/null +++ b/src/main/java/io/github/srizzo/codebuddy/columnmode/EventQueueProxy.java @@ -0,0 +1,69 @@ +package io.github.srizzo.codebuddy.columnmode; + +import com.intellij.ide.IdeEventQueue; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import org.jetbrains.annotations.NotNull; + +import java.awt.AWTEvent; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; + +public class EventQueueProxy { + private static final AtomicBoolean active = new AtomicBoolean(false); + private static volatile Predicate currentHandler = null; + + private static final IdeEventQueue.EventDispatcher dispatcher = new IdeEventQueue.EventDispatcher() { + @Override + public boolean dispatch(@NotNull AWTEvent event) { + Predicate handler = currentHandler; + if (!active.get() || handler == null) { + return true; + } + return handler.test(event); + } + }; + + public static void setHandler(Predicate handler) { + currentHandler = handler; + if (handler != null && !active.get()) { + activate(); + } + } + + public static void activate() { + if (active.get()) { + return; + } + + try { + Disposable parentDisposable = ApplicationManager.getApplication(); + if (parentDisposable == null) { + parentDisposable = new Disposable() { + @Override + public void dispose() { + } + }; + } + + IdeEventQueue.getInstance().addDispatcher(dispatcher, parentDisposable); + active.set(true); + } catch (Exception e) { + } + } + + public static void deactivate() { + if (active.get()) { + try { + IdeEventQueue.getInstance().removeDispatcher(dispatcher); + } catch (Exception e) { + } + active.set(false); + currentHandler = null; + } + } + + public static boolean isActive() { + return active.get(); + } +} diff --git a/src/main/java/io/github/srizzo/codebuddy/columnmode/ExitTemporaryColumnModeAction.java b/src/main/java/io/github/srizzo/codebuddy/columnmode/ExitTemporaryColumnModeAction.java index 12504c6..4508492 100644 --- a/src/main/java/io/github/srizzo/codebuddy/columnmode/ExitTemporaryColumnModeAction.java +++ b/src/main/java/io/github/srizzo/codebuddy/columnmode/ExitTemporaryColumnModeAction.java @@ -1,74 +1,31 @@ package io.github.srizzo.codebuddy.columnmode; -import com.intellij.ide.IdeEventQueue; -import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.application.ApplicationManager; -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.TextComponentEditorAction; import com.intellij.openapi.editor.ex.EditorEx; -import io.github.srizzo.codebuddy.settings.CodeBuddySettingsState; -import io.github.srizzo.codebuddy.util.BlockSelectionUtil; -import io.github.srizzo.codebuddy.util.RunActionUtil; +import com.intellij.openapi.project.DumbAwareAction; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.awt.*; -import java.awt.event.KeyEvent; +public class ExitTemporaryColumnModeAction extends DumbAwareAction { -public class ExitTemporaryColumnModeAction extends TextComponentEditorAction { - public static final String EXIT_TEMPORARY_COLUMN_MODE_ACTION_ID = ExitTemporaryColumnModeAction.class.getName(); - - static { - IdeEventQueue.getInstance().addDispatcher(event -> handleEvent(event), ApplicationManager.getApplication()); - } - - public static final boolean DONT_STOP = false; - - public ExitTemporaryColumnModeAction() { - super(new ExitTemporaryColumnModeAction.Handler()); + private static EditorEx getEditor(@NotNull AnActionEvent e) { + Editor editor = e.getData(CommonDataKeys.EDITOR); + return editor instanceof EditorEx ? (EditorEx) editor : null; } - private static boolean handleEvent(AWTEvent event) { - if (!CodeBuddySettingsState.getInstance().holdingModifierActivatesColumnSelectionModeStatus) return DONT_STOP; - - if (!(event instanceof KeyEvent)) return DONT_STOP; - KeyEvent keyEvent = (KeyEvent) event; - - if (keyEvent.getID() == KeyEvent.KEY_RELEASED && - keyEvent.getKeyCode() == BlockSelectionUtil.getMultiCaretActionKeyCode()) { - RunActionUtil.runAction(keyEvent, EXIT_TEMPORARY_COLUMN_MODE_ACTION_ID, - ActionPlaces.KEYBOARD_SHORTCUT); - } + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + EditorEx editor = getEditor(e); + if (editor == null) return; - return DONT_STOP; + editor.setColumnMode(false); } @Override public void update(@NotNull AnActionEvent e) { - Editor editor = e.getData(CommonDataKeys.EDITOR); - if (editor == null) - return; - if (!editor.isColumnMode()) { - e.getPresentation().setEnabledAndVisible(false); - } else { - super.update(e); - } - } - - private static class Handler extends EditorActionHandler { - Handler() { - super(false); - } - - public void doExecute(@NotNull Editor editor, @Nullable Caret caret, DataContext dataContext) { - if (editor instanceof EditorEx && editor.isColumnMode()) { - ((EditorEx) editor).setColumnMode(false); - } - } + EditorEx editor = getEditor(e); + boolean enabled = editor != null && !editor.isOneLineMode() && editor.isColumnMode(); + e.getPresentation().setEnabledAndVisible(enabled); } } diff --git a/src/main/java/io/github/srizzo/codebuddy/columnmode/ToSingleCaretAction.java b/src/main/java/io/github/srizzo/codebuddy/columnmode/ToSingleCaretAction.java index 31aeaab..5e51275 100644 --- a/src/main/java/io/github/srizzo/codebuddy/columnmode/ToSingleCaretAction.java +++ b/src/main/java/io/github/srizzo/codebuddy/columnmode/ToSingleCaretAction.java @@ -1,63 +1,44 @@ package io.github.srizzo.codebuddy.columnmode; -import com.intellij.ide.IdeEventQueue; -import com.intellij.openapi.actionSystem.ActionPlaces; -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.actionSystem.EditorActionHandler; -import com.intellij.openapi.editor.actions.TextComponentEditorAction; +import com.intellij.openapi.editor.Caret; import com.intellij.openapi.editor.ex.EditorEx; -import io.github.srizzo.codebuddy.settings.CodeBuddySettingsState; -import io.github.srizzo.codebuddy.util.RunActionUtil; +import com.intellij.openapi.project.DumbAwareAction; +import io.github.srizzo.codebuddy.util.BlockSelectionUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.awt.*; -import java.awt.event.KeyEvent; +public class ToSingleCaretAction extends DumbAwareAction { -public class ToSingleCaretAction extends TextComponentEditorAction { - public static final boolean DONT_STOP = false; - private static final String ACTION_EXIT_COLUMN_MODE_ACTION = ToSingleCaretAction.class.getName(); - - static { - IdeEventQueue.getInstance().addDispatcher(event -> handleEvent(event), ApplicationManager.getApplication()); + private static EditorEx getEditor(@NotNull AnActionEvent e) { + Editor editor = e.getData(CommonDataKeys.EDITOR); + return editor instanceof EditorEx ? (EditorEx) editor : null; } - public ToSingleCaretAction() { - super(new ToSingleCaretAction.Handler()); - } + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + EditorEx editor = getEditor(e); + if (editor == null) return; - private static boolean handleEvent(AWTEvent event) { - if (!CodeBuddySettingsState.getInstance().arrowUpAndDownReturnToSingleCursorStatus) return DONT_STOP; - if (!(event instanceof KeyEvent)) return DONT_STOP; - - KeyEvent keyEvent = (KeyEvent) event; - - if (keyEvent.getID() == KeyEvent.KEY_PRESSED && keyEvent.getModifiersEx() == 0) { - switch (keyEvent.getKeyCode()) { - case KeyEvent.VK_UP: - case KeyEvent.VK_DOWN: - RunActionUtil.runAction(keyEvent, ACTION_EXIT_COLUMN_MODE_ACTION, ActionPlaces.KEYBOARD_SHORTCUT); + // Remove all carets except the primary one + for (Caret caret : editor.getCaretModel().getAllCarets()) { + if (!caret.isUpToDate()) { + editor.getCaretModel().removeCaret(caret); } } - - return DONT_STOP; - } - - private static class Handler extends EditorActionHandler { - Handler() { - super(false); + + // Ensure we have block selection converted to regular selection + if (BlockSelectionUtil.editorHasBlockSelection(editor)) { + BlockSelectionUtil.blockSelectionToSelection(editor); } + } - public void doExecute(@NotNull Editor editor, @Nullable Caret caret, DataContext dataContext) { - if (!(editor instanceof EditorEx)) return; - - if (!editor.isColumnMode()) { - editor.getCaretModel().removeSecondaryCarets(); - ((EditorEx) editor).setColumnMode(false); - } - } + @Override + public void update(@NotNull AnActionEvent e) { + EditorEx editor = getEditor(e); + boolean enabled = editor != null && !editor.isOneLineMode() && + editor.getCaretModel().getAllCarets().size() > 1; + e.getPresentation().setEnabledAndVisible(enabled); } } diff --git a/src/main/java/io/github/srizzo/codebuddy/columnmode/ToggleBlockSelectionAction.java b/src/main/java/io/github/srizzo/codebuddy/columnmode/ToggleBlockSelectionAction.java index ada9fa2..9b1f052 100644 --- a/src/main/java/io/github/srizzo/codebuddy/columnmode/ToggleBlockSelectionAction.java +++ b/src/main/java/io/github/srizzo/codebuddy/columnmode/ToggleBlockSelectionAction.java @@ -1,73 +1,27 @@ package io.github.srizzo.codebuddy.columnmode; -import com.intellij.ide.IdeEventQueue; -import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.ToggleAction; -import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.project.DumbAware; -import io.github.srizzo.codebuddy.settings.CodeBuddySettingsState; import io.github.srizzo.codebuddy.util.BlockSelectionUtil; -import io.github.srizzo.codebuddy.util.RunActionUtil; import org.jetbrains.annotations.NotNull; -import java.awt.*; -import java.awt.event.KeyEvent; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; - public class ToggleBlockSelectionAction extends ToggleAction implements DumbAware { - private static final String ACTION_TOGGLE_MULTI_CARET_ACTION = ToggleBlockSelectionAction.class.getName(); - - private static final AtomicBoolean actionStarted = new AtomicBoolean(false); - private static final AtomicLong actionStartedAt = new AtomicLong(0); - private static final boolean DONT_STOP = false; - - static { - IdeEventQueue.getInstance().addDispatcher(event -> handle(event), ApplicationManager.getApplication()); - } - - public ToggleBlockSelectionAction() { - this.setEnabledInModalContext(true); - } - - private static boolean handle(AWTEvent event) { - if (!CodeBuddySettingsState.getInstance().toggleBlockSelectionOnModifierKeyPressStatus) return DONT_STOP; - - if (event instanceof KeyEvent) { - KeyEvent keyEvent = (KeyEvent) event; - if (keyEvent.getKeyCode() == BlockSelectionUtil.getMultiCaretActionKeyCode()) { - switch (keyEvent.getID()) { - case KeyEvent.KEY_PRESSED: - if (keyEvent.getModifiers() != BlockSelectionUtil.getMultiCaretActionKeyModifier()) break; - actionStarted.set(true); - actionStartedAt.set(keyEvent.getWhen()); - break; - case KeyEvent.KEY_RELEASED: - if (keyEvent.getModifiers() != 0) break; - if (actionStarted.get() && keyEvent.getWhen() - actionStartedAt.get() < 200) { - RunActionUtil.runAction(keyEvent, ACTION_TOGGLE_MULTI_CARET_ACTION, - ActionPlaces.KEYBOARD_SHORTCUT); - } - } - } else { - actionStarted.set(false); - } - } - - return DONT_STOP; - } private static EditorEx getEditor(@NotNull AnActionEvent e) { - return (EditorEx) e.getData(CommonDataKeys.EDITOR); + Editor editor = e.getData(CommonDataKeys.EDITOR); + return editor instanceof EditorEx ? (EditorEx) editor : null; } @Override public void setSelected(@NotNull AnActionEvent e, boolean state) { EditorEx editor = getEditor(e); - if (!editor.getCaretModel().supportsMultipleCarets()) return; + if (editor == null || !editor.getCaretModel().supportsMultipleCarets()) { + return; + } if (state) { BlockSelectionUtil.selectionToBlockSelection(editor); @@ -78,17 +32,21 @@ public void setSelected(@NotNull AnActionEvent e, boolean state) { @Override public boolean isSelected(@NotNull AnActionEvent e) { - return BlockSelectionUtil.editorHasBlockSelection(getEditor(e)); + EditorEx editor = getEditor(e); + return editor != null && BlockSelectionUtil.editorHasBlockSelection(editor); } @Override public void update(@NotNull AnActionEvent e) { EditorEx editor = getEditor(e); if (editor != null && !editor.isOneLineMode()) { - e.getPresentation().setEnabledAndVisible(BlockSelectionUtil.editorHasSelection(editor) || BlockSelectionUtil.editorHasBlockSelection(editor)); + boolean hasSelection = BlockSelectionUtil.editorHasSelection(editor) || + BlockSelectionUtil.editorHasBlockSelection(editor); + e.getPresentation().setEnabledAndVisible(hasSelection); super.update(e); } else { e.getPresentation().setEnabledAndVisible(false); + super.update(e); } } } diff --git a/src/main/java/io/github/srizzo/codebuddy/findreplace/UseSelectionForFindAction.java b/src/main/java/io/github/srizzo/codebuddy/findreplace/UseSelectionForFindAction.java index a9879ec..4ed5e90 100644 --- a/src/main/java/io/github/srizzo/codebuddy/findreplace/UseSelectionForFindAction.java +++ b/src/main/java/io/github/srizzo/codebuddy/findreplace/UseSelectionForFindAction.java @@ -24,7 +24,8 @@ private static boolean isRegularExpressionsSearch(FindManager findManager) { @Override public void actionPerformed(@NotNull AnActionEvent event) { - final @NotNull Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); + final Editor editor = event.getData(CommonDataKeys.EDITOR); + if (editor == null) return; final @Nullable FindManager findManager = FindAndReplaceUtil.getFindManager(editor); if (findManager == null) return; diff --git a/src/main/java/io/github/srizzo/codebuddy/findreplace/UseSelectionForReplaceAction.java b/src/main/java/io/github/srizzo/codebuddy/findreplace/UseSelectionForReplaceAction.java index 718342a..37be05d 100644 --- a/src/main/java/io/github/srizzo/codebuddy/findreplace/UseSelectionForReplaceAction.java +++ b/src/main/java/io/github/srizzo/codebuddy/findreplace/UseSelectionForReplaceAction.java @@ -14,7 +14,8 @@ public class UseSelectionForReplaceAction extends AnAction { public void actionPerformed(@NotNull AnActionEvent event) { // TODO fails on terminal - final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); + final Editor editor = event.getData(CommonDataKeys.EDITOR); + if (editor == null) return; final String replacement = StringUtil.defaultIfEmpty(editor.getSelectionModel().getSelectedText(), ""); FindAndReplaceUtil.getAllFindModels(editor).stream().forEach(findModel -> { diff --git a/src/main/java/io/github/srizzo/codebuddy/twiddle/TwiddleAction.java b/src/main/java/io/github/srizzo/codebuddy/twiddle/TwiddleAction.java index 7d22a51..55b8702 100644 --- a/src/main/java/io/github/srizzo/codebuddy/twiddle/TwiddleAction.java +++ b/src/main/java/io/github/srizzo/codebuddy/twiddle/TwiddleAction.java @@ -16,7 +16,8 @@ public class TwiddleAction extends AnAction { @Override public void actionPerformed(@NotNull AnActionEvent event) { - final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); + final Editor editor = event.getData(CommonDataKeys.EDITOR); + if (editor == null) return; WriteCommandAction.runWriteCommandAction(editor.getProject(), () -> { if (!editor.getCaretModel().supportsMultipleCarets()) return; diff --git a/src/main/java/io/github/srizzo/codebuddy/util/BlockSelectionUtil.java b/src/main/java/io/github/srizzo/codebuddy/util/BlockSelectionUtil.java index b083aed..33dcea3 100644 --- a/src/main/java/io/github/srizzo/codebuddy/util/BlockSelectionUtil.java +++ b/src/main/java/io/github/srizzo/codebuddy/util/BlockSelectionUtil.java @@ -118,6 +118,11 @@ public static int getMultiCaretActionKeyCode() { } public static int getMultiCaretActionKeyModifier() { - return SystemInfo.isMac ? InputEvent.ALT_MASK : InputEvent.CTRL_MASK; + return SystemInfo.isMac ? InputEvent.ALT_DOWN_MASK : InputEvent.CTRL_DOWN_MASK; + } + + public static boolean isBlockSelectionActive() { + // For now, always return true when handler is set - we'll track this properly later + return true; } } diff --git a/src/main/java/io/github/srizzo/codebuddy/util/RunActionUtil.java b/src/main/java/io/github/srizzo/codebuddy/util/RunActionUtil.java index 349c98b..f0ab4b2 100644 --- a/src/main/java/io/github/srizzo/codebuddy/util/RunActionUtil.java +++ b/src/main/java/io/github/srizzo/codebuddy/util/RunActionUtil.java @@ -1,13 +1,8 @@ package io.github.srizzo.codebuddy.util; import com.intellij.ide.DataManager; -import com.intellij.ide.actions.ActionsCollector; -import com.intellij.lang.Language; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.AnActionResult; -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher; import com.intellij.openapi.wm.IdeFocusManager; import org.jetbrains.annotations.NotNull; @@ -17,10 +12,17 @@ public class RunActionUtil { public static boolean runAction(KeyEvent event, String actionId, String actionPlace) { - ActionManagerEx ex = ActionManagerEx.getInstanceEx(); - AnAction action = ex.getAction(actionId); - if (action == null) return false; + if (!ApplicationManager.getApplication().isDispatchThread()) { + return false; + } + + ActionManager actionManager = ActionManager.getInstance(); + AnAction action = actionManager.getAction(actionId); + if (action == null) { + return false; + } + // Check modal context if (!action.isEnabledInModalContext()) { Window focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow(); if (focusedWindow != null && IdeKeyEventDispatcher.isModalContext(focusedWindow)) { @@ -28,22 +30,41 @@ public static boolean runAction(KeyEvent event, String actionId, String actionPl } } - AnActionEvent anActionEvent = AnActionEvent.createFromAnAction(action, event, actionPlace, getDataContext()); - action.update(anActionEvent); - if (!anActionEvent.getPresentation().isEnabled()) return false; + // Get proper data context + DataContext dataContext = getDataContext(); + + // Create AnActionEvent using the deprecated but functional API + // TODO: Replace with modern API when available + AnActionEvent anActionEvent = AnActionEvent.createFromAnAction( + action, event, actionPlace, dataContext); - ex.fireBeforeActionPerformed(action, anActionEvent); - action.actionPerformed(anActionEvent); - ex.fireAfterActionPerformed(action, anActionEvent, AnActionResult.PERFORMED); + // Update action presentation + action.update(anActionEvent); + if (!anActionEvent.getPresentation().isEnabled()) { + return false; + } - return true; + // Execute action using ActionManager's modern execution mechanism + try { + actionManager.tryToExecute(action, event, null, actionPlace, true); + return true; + } catch (Exception e) { + // Fallback: direct execution + try { + action.actionPerformed(anActionEvent); + return true; + } catch (Exception ex) { + return false; + } + } } - private static @NotNull - DataContext getDataContext() { + private static @NotNull DataContext getDataContext() { IdeFocusManager focusManager = IdeFocusManager.findInstance(); Component focusedComponent = focusManager.getFocusOwner(); - return DataManager.getInstance().getDataContext(focusedComponent); + if (focusedComponent != null) { + return DataManager.getInstance().getDataContext(focusedComponent); + } + return DataContext.EMPTY_CONTEXT; } - } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 295dd41..bc36a39 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -15,6 +15,9 @@ id="io.github.srizzo.codebuddy.settings.CodeBuddySettingsConfigurable" displayName="CodeBuddy" /> + + + diff --git a/src/test/java/io/github/srizzo/codebuddy/columnmode/ToggleBlockSelectionActionTest.java b/src/test/java/io/github/srizzo/codebuddy/columnmode/ToggleBlockSelectionActionTest.java new file mode 100644 index 0000000..1d42d04 --- /dev/null +++ b/src/test/java/io/github/srizzo/codebuddy/columnmode/ToggleBlockSelectionActionTest.java @@ -0,0 +1,28 @@ +package io.github.srizzo.codebuddy.columnmode; + +import junit.framework.TestCase; + +public class ToggleBlockSelectionActionTest extends TestCase { + + private ToggleBlockSelectionAction action; + + @Override + protected void setUp() throws Exception { + super.setUp(); + action = new ToggleBlockSelectionAction(); + } + + public void testActionCanBeInstantiated() { + assertNotNull("ToggleBlockSelectionAction should be instantiable", action); + } + + public void testActionImplementsDumbAware() { + assertTrue("ToggleBlockSelectionAction should implement DumbAware", + action instanceof com.intellij.openapi.project.DumbAware); + } + + public void testActionExtendsToggleAction() { + assertTrue("ToggleBlockSelectionAction should extend ToggleAction", + action instanceof com.intellij.openapi.actionSystem.ToggleAction); + } +} diff --git a/src/test/java/io/github/srizzo/codebuddy/util/BlockSelectionUtilTest.java b/src/test/java/io/github/srizzo/codebuddy/util/BlockSelectionUtilTest.java new file mode 100644 index 0000000..2efa158 --- /dev/null +++ b/src/test/java/io/github/srizzo/codebuddy/util/BlockSelectionUtilTest.java @@ -0,0 +1,44 @@ +package io.github.srizzo.codebuddy.util; + +import com.intellij.openapi.util.SystemInfo; +import junit.framework.TestCase; + +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +public class BlockSelectionUtilTest extends TestCase { + + public void testGetMultiCaretActionKeyCodeReturnsAltOnMac() { + if (SystemInfo.isMac) { + assertEquals("On Mac, multi-caret key code should be VK_ALT", + KeyEvent.VK_ALT, BlockSelectionUtil.getMultiCaretActionKeyCode()); + } + } + + public void testGetMultiCaretActionKeyCodeReturnsCtrlOnNonMac() { + if (!SystemInfo.isMac) { + assertEquals("On non-Mac, multi-caret key code should be VK_CONTROL", + KeyEvent.VK_CONTROL, BlockSelectionUtil.getMultiCaretActionKeyCode()); + } + } + + public void testGetMultiCaretActionKeyModifierReturnsAltDownMaskOnMac() { + if (SystemInfo.isMac) { + assertEquals("On Mac, multi-caret modifier should be ALT_DOWN_MASK", + InputEvent.ALT_DOWN_MASK, BlockSelectionUtil.getMultiCaretActionKeyModifier()); + } + } + + public void testGetMultiCaretActionKeyModifierReturnsCtrlDownMaskOnNonMac() { + if (!SystemInfo.isMac) { + assertEquals("On non-Mac, multi-caret modifier should be CTRL_DOWN_MASK", + InputEvent.CTRL_DOWN_MASK, BlockSelectionUtil.getMultiCaretActionKeyModifier()); + } + } + + public void testModifierMaskIsCompatibleWithGetModifiersEx() { + int modifier = BlockSelectionUtil.getMultiCaretActionKeyModifier(); + assertTrue("Modifier should be a valid extended modifier mask (>= 64)", + modifier >= InputEvent.SHIFT_DOWN_MASK); + } +}