From eca5e0309056ddb74bab294342521aec2cc7a7ce Mon Sep 17 00:00:00 2001 From: "Patrick K." Date: Wed, 19 Aug 2026 23:27:20 +0200 Subject: [PATCH 1/9] updated all dependencies and versions to make it compatible with newest intellij versions --- .gitattributes | 2 + .github/workflows/build.yml | 46 +++ .github/workflows/release.yml | 35 +++ .gitignore | 1 + LICENSE | 201 ++++++++++++ README.md | 28 +- build.gradle | 41 --- build.gradle.kts | 65 ++++ gradle.properties | 2 + gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 47505 bytes gradle/wrapper/gradle-wrapper.properties | 10 +- gradlew | 297 +++++++++++------- gradlew.bat | 96 +++--- settings.gradle | 10 - settings.gradle.kts | 1 + src/main/java/QuickSwitchAction.java | 84 ----- .../QuickSwitchAction.java | 97 ++++++ src/main/resources/META-INF/plugin.xml | 20 +- src/main/resources/META-INF/pluginIcon.svg | 5 + .../QuickSwitchActionTest.java | 68 ++++ 20 files changed, 795 insertions(+), 314 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml create mode 100644 LICENSE delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 gradle.properties delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts delete mode 100755 src/main/java/QuickSwitchAction.java create mode 100644 src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java create mode 100644 src/main/resources/META-INF/pluginIcon.svg create mode 100644 src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java diff --git a/.gitattributes b/.gitattributes index 00a51af..9a335a8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,8 @@ # # https://help.github.com/articles/dealing-with-line-endings/ # +* text=auto eol=lf + # These are explicitly windows files and should use crlf *.bat text eol=crlf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8e7429d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,46 @@ +name: Build + +on: + push: + branches: [master] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Fetch sources + uses: actions/checkout@v6 + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Test and build + run: ./gradlew check buildPlugin + + - name: Verify plugin compatibility + run: ./gradlew verifyPlugin + + - name: Upload plugin distribution + uses: actions/upload-artifact@v7 + with: + name: angular-cli-quick-switch + path: build/distributions/*.zip + + - name: Upload verification reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: plugin-verifier-reports + path: build/reports/pluginVerifier + if-no-files-found: ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2bc55ca --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: Publish + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + environment: marketplace + steps: + - name: Fetch sources + uses: actions/checkout@v6 + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Test and verify + run: ./gradlew check verifyPlugin + + - name: Sign and publish + run: ./gradlew publishPlugin + env: + CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} + PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} diff --git a/.gitignore b/.gitignore index 4ddb3dd..2c7d09a 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea/ .gradle/ +.intellijPlatform/ *.iws *.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 32e9a14..7020fa8 100755 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ IntelliJ Plugin for switching amongst Angular CLI component files efficiently (e.g. ts -> html -> css) by just using one shortcut (**Alt+S**). +Version 2.0 supports IntelliJ Platform 2024.2 and newer across standalone JetBrains IDEs. It requires Java 21 to build. + For example let's say we have a structure like this foo/ @@ -33,4 +35,28 @@ Further information: If the default shortcut **Alt + S** does not fit your needs, you can of course easily adjust it in the keymap settings by replacing it for **QuickSwitch**. Due to the nature of this plugin, it will of course also **work with other projects**, that have not been generated by Angular CLI - or are -not even Angular projects at all - as long as your project structure follows similar patterns. \ No newline at end of file +not even Angular projects at all - as long as your project structure follows similar patterns. + +## Development + +Build and test the plugin with: + +```shell +./gradlew check buildPlugin +``` + +Run the plugin in a sandbox IDE with: + +```shell +./gradlew runIde +``` + +Verify binary compatibility with supported IntelliJ Platform releases with: + +```shell +./gradlew verifyPlugin +``` + +The plugin ZIP is written to `build/distributions`. + +Marketplace publishing uses the `CERTIFICATE_CHAIN`, `PRIVATE_KEY`, `PRIVATE_KEY_PASSWORD`, and `PUBLISH_TOKEN` environment variables. Publishing is available through the manual GitHub Actions release workflow. diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 18de48f..0000000 --- a/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -/* - * This file was generated by the Gradle 'init' task. - * - * This is a general purpose Gradle build. - * Learn how to create Gradle builds at https://guides.gradle.org/creating-new-gradle-builds - */ - -plugins { - id 'java' - id 'idea' - id 'org.jetbrains.intellij' version '0.4.21' -} - -group 'patrick.kelleter' -version '1.3.1' - -apply plugin: 'java' - -java { - sourceCompatibility = JavaVersion.VERSION_11 -} - -repositories { - mavenCentral() -} - -dependencies { - testCompileOnly 'junit:junit:4.12' -} - -intellij { - updateSinceUntilBuild false -} -patchPluginXml { - changeNotes """ - - Added support for php, haml, jade, pug, slim, less, styl files.
- Updated docs
""" -} - -task wrapper(type: Wrapper) { - gradleVersion = '6.5' -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..29644da --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,65 @@ +import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + +plugins { + java + id("org.jetbrains.intellij.platform") version "2.18.1" +} + +group = "patrick.kelleter" +version = "2.0.0" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() + intellijPlatform { + defaultRepositories() + } +} + +dependencies { + testImplementation("junit:junit:4.13.2") + + intellijPlatform { + intellijIdeaCommunity("2024.2.6") + testFramework(TestFrameworkType.Platform) + } +} + +tasks.withType().configureEach { + options.release = 21 +} + +intellijPlatform { + buildSearchableOptions = false + + pluginConfiguration { + changeNotes = """ + + """.trimIndent() + + ideaVersion { + sinceBuild = "242" + untilBuild = provider { null } + } + } + + pluginVerification { + ides { + current() + recommended() + create(IntelliJPlatformType.IntellijIdea, "2025.3.6.1") + create(IntelliJPlatformType.IntellijIdea, "2026.1.5") + create(IntelliJPlatformType.IntellijIdea, "2026.2.1") + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..6f78d3f --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.caching=true +org.gradle.configuration-cache=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053550b91381bbd28b1afc82d634bf73a8a..eddabd2eef8d94a5437d6168ff9c87a78ff725b3 100755 GIT binary patch literal 47505 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ad635e5..760c459 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,10 @@ -#Wed Jun 03 13:15:19 CEST 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 32f5a93..249efbb 100755 --- a/gradlew +++ b/gradlew @@ -1,79 +1,128 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# 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. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -82,88 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 85baee8..a51ec4f 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,15 +1,34 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @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=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -24,65 +43,40 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +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:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +if exist "%JAVA_EXE%" goto execute -goto fail +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +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 -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* +"%COMSPEC%" /c exit 1 :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 %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 -:mainEnd -if "%OS%"=="Windows_NT" endlocal +@rem Execute gradlew +@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 -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 9ced24a..0000000 --- a/settings.gradle +++ /dev/null @@ -1,10 +0,0 @@ -/* - * This file was generated by the Gradle 'init' task. - * - * The settings file is used to specify which projects to include in your build. - * - * Detailed information about configuring a multi-project build in Gradle can be found - * in the user manual at https://docs.gradle.org/6.5/userguide/multi_project_builds.html - */ - -rootProject.name = 'angular-cli-quick-switch-plugin' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..29d909a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "angular-cli-quick-switch-plugin" diff --git a/src/main/java/QuickSwitchAction.java b/src/main/java/QuickSwitchAction.java deleted file mode 100755 index 9e0563b..0000000 --- a/src/main/java/QuickSwitchAction.java +++ /dev/null @@ -1,84 +0,0 @@ -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.fileEditor.OpenFileDescriptor; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VirtualFile; - -import java.util.Arrays; -import java.util.List; - -public class QuickSwitchAction extends AnAction { - - private AnActionEvent latestEvent; - - private List extensions = Arrays.asList( - "ts", "js", - "html", "php", "haml", "jade", "pug", "slim", - "css", "sass", "scss", "less", "styl" - ); - - public QuickSwitchAction() { - super("QuickSwitch"); - } - - @Override - public void actionPerformed(AnActionEvent event) { - this.latestEvent = event; - - VirtualFile currentFile = (VirtualFile) event.getDataContext().getData("virtualFile"); - if (currentFile == null) return; - - String currentFilePath = currentFile.getCanonicalPath(); - if (currentFilePath == null) return; - - String basePath = this.removeFilePathExtension(currentFilePath); - String extension = this.getFilePathExtension(currentFilePath); - if (basePath == null || extension == null) return; - - String newPath = this.cycleExtension(basePath, extension); - this.switchFile(currentFilePath, newPath); - } - - private String getFilePathExtension(String filePath) { - int extensionPosition = filePath.lastIndexOf("."); - if (extensionPosition == -1) return null; - return filePath.substring(extensionPosition + 1); - } - - private String removeFilePathExtension(String filePath) { - int extensionPosition = filePath.lastIndexOf("."); - if (extensionPosition == -1) return filePath; - return filePath.substring(0, extensionPosition); - } - - private String cycleExtension(String basePath, String currentExtension) { - String currentPath = basePath + "." + currentExtension; - int currentIndex = this.extensions.indexOf(currentExtension); - if (currentIndex == -1) return currentPath; - for (int i = 0; i < this.extensions.size(); i++) { - int newIndex = ((currentIndex + 1 + i) % this.extensions.size()); - String newExtension = this.extensions.get(newIndex); - String newPath = basePath + "." + newExtension; - VirtualFile newFile = this.getFileByPath(newPath); - if (newFile != null && newFile.exists()) return newPath; - } - return currentPath; - } - - private void switchFile(String currentFilePath, String newFilePath) { - Project project = this.latestEvent.getProject(); - if (project == null) return; - VirtualFile currentFile = this.getFileByPath(currentFilePath); - VirtualFile newFile = this.getFileByPath(newFilePath); - if (!currentFile.exists() || !newFile.exists()) return; - FileEditorManager.getInstance(project).closeFile(currentFile); - new OpenFileDescriptor(project, newFile).navigate(true); - } - - private VirtualFile getFileByPath(String path) { - return LocalFileSystem.getInstance().findFileByPath(path); - } - -} diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java new file mode 100644 index 0000000..5f415ef --- /dev/null +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java @@ -0,0 +1,97 @@ +package patrick.kelleter.angularcliquickswitch; + +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Locale; + +public final class QuickSwitchAction extends DumbAwareAction { + private static final List EXTENSIONS = List.of( + "ts", "js", + "html", "php", "haml", "jade", "pug", "slim", + "css", "sass", "scss", "less", "styl" + ); + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + @Override + public void update(@NotNull AnActionEvent event) { + Project project = event.getProject(); + VirtualFile file = event.getData(CommonDataKeys.VIRTUAL_FILE); + event.getPresentation().setEnabled(project != null && !project.isDisposed() && isSwitchable(file)); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + Project project = event.getProject(); + VirtualFile currentFile = event.getData(CommonDataKeys.VIRTUAL_FILE); + if (project == null || project.isDisposed() || currentFile == null) { + return; + } + + VirtualFile targetFile = findTargetFile(currentFile); + if (targetFile == null) { + return; + } + + OpenFileDescriptor descriptor = new OpenFileDescriptor(project, targetFile) + .setUseCurrentWindow(true); + FileEditorManager.getInstance(project).openEditor(descriptor, true); + } + + static @Nullable VirtualFile findTargetFile(@NotNull VirtualFile currentFile) { + if (!currentFile.isValid() || currentFile.isDirectory()) { + return null; + } + + VirtualFile parent = currentFile.getParent(); + String extension = currentFile.getExtension(); + if (parent == null || extension == null) { + return null; + } + + String currentExtension = extension.toLowerCase(Locale.ROOT); + int currentIndex = EXTENSIONS.indexOf(currentExtension); + if (currentIndex < 0) { + return null; + } + + String baseName = currentFile.getNameWithoutExtension(); + VirtualFile[] siblings = parent.getChildren(); + + for (int offset = 1; offset < EXTENSIONS.size(); offset++) { + String targetExtension = EXTENSIONS.get((currentIndex + offset) % EXTENSIONS.size()); + for (VirtualFile sibling : siblings) { + if (sibling.isValid() + && !sibling.isDirectory() + && baseName.equals(sibling.getNameWithoutExtension()) + && targetExtension.equalsIgnoreCase(sibling.getExtension())) { + return sibling; + } + } + } + + return null; + } + + private static boolean isSwitchable(@Nullable VirtualFile file) { + if (file == null || !file.isValid() || file.isDirectory() || file.getParent() == null) { + return false; + } + + String extension = file.getExtension(); + return extension != null && EXTENSIONS.contains(extension.toLowerCase(Locale.ROOT)); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 5fb7547..e73edc4 100755 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -4,25 +4,15 @@ Patrick Kelleter - - Updated docs
- ]]> -
- - - - com.intellij.modules.lang - - - + com.intellij.modules.platform - - + - \ No newline at end of file + diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..fa6fbe2 --- /dev/null +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java new file mode 100644 index 0000000..f11dea7 --- /dev/null +++ b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java @@ -0,0 +1,68 @@ +package patrick.kelleter.angularcliquickswitch; + +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; + +public final class QuickSwitchActionTest extends BasePlatformTestCase { + public void testCyclesThroughExistingCounterparts() { + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile html = addFile("example.component.html"); + VirtualFile stylesheet = addFile("example.component.scss"); + + assertTarget(html, QuickSwitchAction.findTargetFile(typescript)); + assertTarget(stylesheet, QuickSwitchAction.findTargetFile(html)); + assertTarget(typescript, QuickSwitchAction.findTargetFile(stylesheet)); + } + + public void testUsesConfiguredExtensionOrder() { + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile javascript = addFile("example.component.js"); + addFile("example.component.html"); + + assertTarget(javascript, QuickSwitchAction.findTargetFile(typescript)); + } + + public void testReturnsNullForUnsupportedExtension() { + VirtualFile markdown = addFile("example.component.md"); + addFile("example.component.ts"); + + assertNull(QuickSwitchAction.findTargetFile(markdown)); + } + + public void testReturnsNullWithoutCounterpart() { + VirtualFile typescript = addFile("example.component.ts"); + + assertNull(QuickSwitchAction.findTargetFile(typescript)); + } + + public void testSupportsUppercaseExtensions() { + VirtualFile typescript = addFile("example.component.TS"); + VirtualFile html = addFile("example.component.HTML"); + + assertTarget(html, QuickSwitchAction.findTargetFile(typescript)); + } + + public void testHandlesDotsInParentDirectory() { + VirtualFile typescript = addFile("feature.v2/example.component.ts"); + VirtualFile html = addFile("feature.v2/example.component.html"); + + assertTarget(html, QuickSwitchAction.findTargetFile(typescript)); + } + + public void testSkipsDirectoriesNamedLikeCounterparts() { + VirtualFile typescript = addFile("example.component.ts"); + myFixture.addFileToProject("example.component.html/nested.txt", ""); + VirtualFile stylesheet = addFile("example.component.css"); + + assertTarget(stylesheet, QuickSwitchAction.findTargetFile(typescript)); + } + + private VirtualFile addFile(String path) { + return myFixture.addFileToProject(path, "").getVirtualFile(); + } + + private void assertTarget(VirtualFile expected, VirtualFile actual) { + assertNotNull(actual); + assertEquals(expected.getPath(), actual.getPath()); + } +} From dd46c49bf4ef26172b18b06ad617527aa9f18115 Mon Sep 17 00:00:00 2001 From: "Patrick K." Date: Wed, 19 Aug 2026 23:47:43 +0200 Subject: [PATCH 2/9] add settings menu and option for keeping other tabs open --- README.md | 2 + build.gradle.kts | 5 +- .../QuickSwitchAction.java | 21 ++++++- .../QuickSwitchConfigurable.java | 58 +++++++++++++++++++ .../QuickSwitchSettings.java | 47 +++++++++++++++ src/main/resources/META-INF/plugin.xml | 8 +++ .../QuickSwitchActionTest.java | 37 ++++++++++++ .../QuickSwitchSettingsTest.java | 30 ++++++++++ 8 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java create mode 100644 src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java create mode 100644 src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java diff --git a/README.md b/README.md index 7020fa8..6425fdd 100755 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ Further information: If the default shortcut **Alt + S** does not fit your needs, you can of course easily adjust it in the keymap settings by replacing it for **QuickSwitch**. +By default, switched files remain open to preserve tab position, pinned state, and editor splits. To close the previous tab after a successful switch, enable **Close the previous tab after switching** under **Settings | Tools | Angular CLI QuickSwitch**. + Due to the nature of this plugin, it will of course also **work with other projects**, that have not been generated by Angular CLI - or are not even Angular projects at all - as long as your project structure follows similar patterns. diff --git a/build.gradle.kts b/build.gradle.kts index 29644da..3a23b7a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,13 +36,12 @@ tasks.withType().configureEach { } intellijPlatform { - buildSearchableOptions = false - pluginConfiguration { changeNotes = """
  • Updated for IntelliJ Platform 2024.2 and newer.
  • -
  • Fixed switching in editor splits and removed tab-closing side effects.
  • +
  • Added a settings page with optional previous-tab closing.
  • +
  • Fixed switching in editor splits and removed tab-closing side effects by default.
  • Improved support for virtual files and modern IntelliJ action APIs.
""".trimIndent() diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java index 5f415ef..df333a7 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java @@ -46,9 +46,28 @@ public void actionPerformed(@NotNull AnActionEvent event) { return; } + navigate( + project, + currentFile, + targetFile, + QuickSwitchSettings.getInstance().isClosePreviousTab() + ); + } + + static void navigate( + @NotNull Project project, + @NotNull VirtualFile currentFile, + @NotNull VirtualFile targetFile, + boolean closePreviousTab + ) { OpenFileDescriptor descriptor = new OpenFileDescriptor(project, targetFile) .setUseCurrentWindow(true); - FileEditorManager.getInstance(project).openEditor(descriptor, true); + FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); + boolean targetOpened = !fileEditorManager.openEditor(descriptor, true).isEmpty(); + + if (targetOpened && closePreviousTab && currentFile.isValid() && !currentFile.equals(targetFile)) { + fileEditorManager.closeFile(currentFile); + } } static @Nullable VirtualFile findTargetFile(@NotNull VirtualFile currentFile) { diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java new file mode 100644 index 0000000..7371bbf --- /dev/null +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java @@ -0,0 +1,58 @@ +package patrick.kelleter.angularcliquickswitch; + +import com.intellij.openapi.options.Configurable; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JPanel; +import java.awt.BorderLayout; + +public final class QuickSwitchConfigurable implements Configurable { + private JCheckBox closePreviousTabCheckBox; + + @Override + public @Nls String getDisplayName() { + return "Angular CLI QuickSwitch"; + } + + @Override + public @NotNull JComponent createComponent() { + closePreviousTabCheckBox = new JCheckBox("Close the previous tab after switching"); + closePreviousTabCheckBox.setToolTipText( + "The target opens first; IntelliJ may also close copies of the previous file in other editor splits." + ); + JPanel panel = new JPanel(new BorderLayout()); + panel.add(closePreviousTabCheckBox, BorderLayout.NORTH); + return panel; + } + + @Override + public boolean isModified() { + return closePreviousTabCheckBox != null + && Configurable.isCheckboxModified( + closePreviousTabCheckBox, + QuickSwitchSettings.getInstance().isClosePreviousTab() + ); + } + + @Override + public void apply() { + if (closePreviousTabCheckBox != null) { + QuickSwitchSettings.getInstance().setClosePreviousTab(closePreviousTabCheckBox.isSelected()); + } + } + + @Override + public void reset() { + if (closePreviousTabCheckBox != null) { + closePreviousTabCheckBox.setSelected(QuickSwitchSettings.getInstance().isClosePreviousTab()); + } + } + + @Override + public void disposeUIResources() { + closePreviousTabCheckBox = null; + } +} diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java new file mode 100644 index 0000000..58d2e6d --- /dev/null +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java @@ -0,0 +1,47 @@ +package patrick.kelleter.angularcliquickswitch; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.components.SettingsCategory; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import org.jetbrains.annotations.NotNull; + +@Service(Service.Level.APP) +@State( + name = "AngularCliQuickSwitchSettings", + storages = @Storage("angularCliQuickSwitch.xml"), + category = SettingsCategory.PLUGINS +) +public final class QuickSwitchSettings implements PersistentStateComponent { + public static final class SettingsState { + public boolean closePreviousTab; + } + + private volatile SettingsState state = new SettingsState(); + + public static @NotNull QuickSwitchSettings getInstance() { + return ApplicationManager.getApplication().getService(QuickSwitchSettings.class); + } + + @Override + public @NotNull SettingsState getState() { + return state; + } + + @Override + public void loadState(@NotNull SettingsState state) { + this.state = state; + } + + public boolean isClosePreviousTab() { + return state.closePreviousTab; + } + + public void setClosePreviousTab(boolean closePreviousTab) { + SettingsState newState = new SettingsState(); + newState.closePreviousTab = closePreviousTab; + state = newState; + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e73edc4..721a235 100755 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -9,6 +9,14 @@ com.intellij.modules.platform + + + + diff --git a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java index f11dea7..e402a3d 100644 --- a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java +++ b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java @@ -1,8 +1,11 @@ package patrick.kelleter.angularcliquickswitch; +import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import java.util.Arrays; + public final class QuickSwitchActionTest extends BasePlatformTestCase { public void testCyclesThroughExistingCounterparts() { VirtualFile typescript = addFile("example.component.ts"); @@ -57,6 +60,30 @@ public void testSkipsDirectoriesNamedLikeCounterparts() { assertTarget(stylesheet, QuickSwitchAction.findTargetFile(typescript)); } + public void testKeepsPreviousTabOpen() { + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile html = addFile("example.component.html"); + FileEditorManager fileEditorManager = FileEditorManager.getInstance(getProject()); + fileEditorManager.openFile(typescript, true); + + QuickSwitchAction.navigate(getProject(), typescript, html, false); + + assertOpen(fileEditorManager, typescript); + assertOpen(fileEditorManager, html); + } + + public void testClosesPreviousTabAfterOpeningTarget() { + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile html = addFile("example.component.html"); + FileEditorManager fileEditorManager = FileEditorManager.getInstance(getProject()); + fileEditorManager.openFile(typescript, true); + + QuickSwitchAction.navigate(getProject(), typescript, html, true); + + assertNotOpen(fileEditorManager, typescript); + assertOpen(fileEditorManager, html); + } + private VirtualFile addFile(String path) { return myFixture.addFileToProject(path, "").getVirtualFile(); } @@ -65,4 +92,14 @@ private void assertTarget(VirtualFile expected, VirtualFile actual) { assertNotNull(actual); assertEquals(expected.getPath(), actual.getPath()); } + + private void assertOpen(FileEditorManager fileEditorManager, VirtualFile file) { + assertTrue(Arrays.stream(fileEditorManager.getOpenFiles()) + .anyMatch(openFile -> openFile.getPath().equals(file.getPath()))); + } + + private void assertNotOpen(FileEditorManager fileEditorManager, VirtualFile file) { + assertFalse(Arrays.stream(fileEditorManager.getOpenFiles()) + .anyMatch(openFile -> openFile.getPath().equals(file.getPath()))); + } } diff --git a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java new file mode 100644 index 0000000..930c903 --- /dev/null +++ b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java @@ -0,0 +1,30 @@ +package patrick.kelleter.angularcliquickswitch; + +import junit.framework.TestCase; + +public final class QuickSwitchSettingsTest extends TestCase { + public void testKeepsTabsOpenByDefault() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + + assertFalse(settings.isClosePreviousTab()); + } + + public void testLoadsPersistedClosePreference() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + QuickSwitchSettings.SettingsState state = new QuickSwitchSettings.SettingsState(); + state.closePreviousTab = true; + + settings.loadState(state); + + assertTrue(settings.isClosePreviousTab()); + } + + public void testUpdatesClosePreference() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + + settings.setClosePreviousTab(true); + + assertTrue(settings.isClosePreviousTab()); + assertTrue(settings.getState().closePreviousTab); + } +} From c615afb5b75e4013f8bced5dbe7d8b1c2fb0dca7 Mon Sep 17 00:00:00 2001 From: "Patrick K." Date: Thu, 20 Aug 2026 00:20:12 +0200 Subject: [PATCH 3/9] add settings to exclude certain extensions from cycling; add support for spec files --- README.md | 3 + build.gradle.kts | 1 + .../QuickSwitchAction.java | 71 +++++++----- .../QuickSwitchConfigurable.java | 103 ++++++++++++++++-- .../QuickSwitchFileType.java | 61 +++++++++++ .../QuickSwitchSettings.java | 45 +++++++- src/main/resources/META-INF/plugin.xml | 2 +- .../QuickSwitchActionTest.java | 49 +++++++++ .../QuickSwitchSettingsTest.java | 34 ++++++ 9 files changed, 330 insertions(+), 39 deletions(-) create mode 100644 src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchFileType.java diff --git a/README.md b/README.md index 6425fdd..d52a98d 100755 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Explicitly whitelisted extensions are: - js, ts - html, php, haml, jade, pug, slim - css, sass, scss, less, styl +- spec.ts, spec.js (disabled by default) Note: For switching between your code and test/spec files, you can simply use the built in default shortcut from IntelliJ (**Ctrl+Shift+T**) @@ -36,6 +37,8 @@ If the default shortcut **Alt + S** does not fit your needs, you can of course e By default, switched files remain open to preserve tab position, pinned state, and editor splits. To close the previous tab after a successful switch, enable **Close the previous tab after switching** under **Settings | Tools | Angular CLI QuickSwitch**. +The same settings page lets you enable or disable individual file types grouped by controller, template, style, and tests. Disabled file types are skipped while cycling. + Due to the nature of this plugin, it will of course also **work with other projects**, that have not been generated by Angular CLI - or are not even Angular projects at all - as long as your project structure follows similar patterns. diff --git a/build.gradle.kts b/build.gradle.kts index 3a23b7a..6e450b8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,7 @@ intellijPlatform {
  • Updated for IntelliJ Platform 2024.2 and newer.
  • Added a settings page with optional previous-tab closing.
  • +
  • Added grouped file-type controls and optional spec.ts/spec.js test cycling.
  • Fixed switching in editor splits and removed tab-closing side effects by default.
  • Improved support for virtual files and modern IntelliJ action APIs.
diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java index df333a7..62d6dd9 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java @@ -11,15 +11,15 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Comparator; import java.util.List; -import java.util.Locale; +import java.util.function.Predicate; public final class QuickSwitchAction extends DumbAwareAction { - private static final List EXTENSIONS = List.of( - "ts", "js", - "html", "php", "haml", "jade", "pug", "slim", - "css", "sass", "scss", "less", "styl" - ); + private static final List FILE_TYPES = List.of(QuickSwitchFileType.values()); + private static final List FILE_TYPES_BY_SUFFIX_LENGTH = FILE_TYPES.stream() + .sorted(Comparator.comparingInt((QuickSwitchFileType fileType) -> fileType.suffix().length()).reversed()) + .toList(); @Override public @NotNull ActionUpdateThread getActionUpdateThread() { @@ -41,7 +41,8 @@ public void actionPerformed(@NotNull AnActionEvent event) { return; } - VirtualFile targetFile = findTargetFile(currentFile); + QuickSwitchSettings settings = QuickSwitchSettings.getInstance(); + VirtualFile targetFile = findTargetFile(currentFile, settings::isFileTypeEnabled); if (targetFile == null) { return; } @@ -50,7 +51,7 @@ public void actionPerformed(@NotNull AnActionEvent event) { project, currentFile, targetFile, - QuickSwitchSettings.getInstance().isClosePreviousTab() + settings.isClosePreviousTab() ); } @@ -71,32 +72,37 @@ static void navigate( } static @Nullable VirtualFile findTargetFile(@NotNull VirtualFile currentFile) { + return findTargetFile(currentFile, QuickSwitchFileType::enabledByDefault); + } + + static @Nullable VirtualFile findTargetFile( + @NotNull VirtualFile currentFile, + @NotNull Predicate isEnabled + ) { if (!currentFile.isValid() || currentFile.isDirectory()) { return null; } VirtualFile parent = currentFile.getParent(); - String extension = currentFile.getExtension(); - if (parent == null || extension == null) { + FileMatch currentMatch = matchFile(currentFile); + if (parent == null || currentMatch == null) { return null; } - String currentExtension = extension.toLowerCase(Locale.ROOT); - int currentIndex = EXTENSIONS.indexOf(currentExtension); - if (currentIndex < 0) { - return null; - } - - String baseName = currentFile.getNameWithoutExtension(); + int currentIndex = FILE_TYPES.indexOf(currentMatch.fileType()); VirtualFile[] siblings = parent.getChildren(); - for (int offset = 1; offset < EXTENSIONS.size(); offset++) { - String targetExtension = EXTENSIONS.get((currentIndex + offset) % EXTENSIONS.size()); + for (int offset = 1; offset < FILE_TYPES.size(); offset++) { + QuickSwitchFileType targetType = FILE_TYPES.get((currentIndex + offset) % FILE_TYPES.size()); + if (!isEnabled.test(targetType)) { + continue; + } + + String targetName = currentMatch.baseName() + "." + targetType.suffix(); for (VirtualFile sibling : siblings) { if (sibling.isValid() && !sibling.isDirectory() - && baseName.equals(sibling.getNameWithoutExtension()) - && targetExtension.equalsIgnoreCase(sibling.getExtension())) { + && targetName.equalsIgnoreCase(sibling.getName())) { return sibling; } } @@ -106,11 +112,24 @@ static void navigate( } private static boolean isSwitchable(@Nullable VirtualFile file) { - if (file == null || !file.isValid() || file.isDirectory() || file.getParent() == null) { - return false; - } + return file != null + && file.isValid() + && !file.isDirectory() + && file.getParent() != null + && matchFile(file) != null; + } - String extension = file.getExtension(); - return extension != null && EXTENSIONS.contains(extension.toLowerCase(Locale.ROOT)); + private static @Nullable FileMatch matchFile(@NotNull VirtualFile file) { + String fileName = file.getName(); + for (QuickSwitchFileType fileType : FILE_TYPES_BY_SUFFIX_LENGTH) { + String suffix = "." + fileType.suffix(); + int suffixStart = fileName.length() - suffix.length(); + if (suffixStart > 0 && fileName.regionMatches(true, suffixStart, suffix, 0, suffix.length())) { + return new FileMatch(fileType, fileName.substring(0, suffixStart)); + } + } + return null; } + + private record FileMatch(QuickSwitchFileType fileType, String baseName) { } } diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java index 7371bbf..270a010 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java @@ -4,13 +4,24 @@ import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; import javax.swing.JCheckBox; import javax.swing.JComponent; +import javax.swing.JLabel; import javax.swing.JPanel; -import java.awt.BorderLayout; +import javax.swing.border.Border; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridLayout; +import java.util.EnumMap; +import java.util.Map; public final class QuickSwitchConfigurable implements Configurable { private JCheckBox closePreviousTabCheckBox; + private Map fileTypeCheckBoxes; @Override public @Nls String getDisplayName() { @@ -19,40 +30,110 @@ public final class QuickSwitchConfigurable implements Configurable { @Override public @NotNull JComponent createComponent() { + JPanel rootPanel = new JPanel(); + rootPanel.setLayout(new BoxLayout(rootPanel, BoxLayout.Y_AXIS)); + + JPanel generalPanel = createSectionPanel("General"); closePreviousTabCheckBox = new JCheckBox("Close the previous tab after switching"); closePreviousTabCheckBox.setToolTipText( "The target opens first; IntelliJ may also close copies of the previous file in other editor splits." ); - JPanel panel = new JPanel(new BorderLayout()); - panel.add(closePreviousTabCheckBox, BorderLayout.NORTH); - return panel; + generalPanel.add(closePreviousTabCheckBox); + allowHorizontalStretch(generalPanel); + rootPanel.add(generalPanel); + rootPanel.add(Box.createVerticalStrut(12)); + + fileTypeCheckBoxes = new EnumMap<>(QuickSwitchFileType.class); + JPanel extensionsPanel = createSectionPanel("Supported Extensions"); + JPanel categoriesPanel = new JPanel(new GridLayout( + 1, + QuickSwitchFileType.Category.values().length, + 24, + 0 + )); + for (QuickSwitchFileType.Category category : QuickSwitchFileType.Category.values()) { + JPanel categoryPanel = new JPanel(); + categoryPanel.setLayout(new BoxLayout(categoryPanel, BoxLayout.Y_AXIS)); + JLabel categoryLabel = new JLabel(category.displayName()); + categoryLabel.setFont(categoryLabel.getFont().deriveFont(Font.BOLD)); + categoryPanel.add(categoryLabel); + categoryPanel.add(Box.createVerticalStrut(3)); + + for (QuickSwitchFileType fileType : QuickSwitchFileType.values()) { + if (fileType.category() == category) { + JCheckBox checkBox = new JCheckBox("." + fileType.suffix()); + fileTypeCheckBoxes.put(fileType, checkBox); + categoryPanel.add(checkBox); + } + } + + categoriesPanel.add(categoryPanel); + } + + allowHorizontalStretch(categoriesPanel); + extensionsPanel.add(categoriesPanel); + allowHorizontalStretch(extensionsPanel); + rootPanel.add(extensionsPanel); + rootPanel.add(Box.createVerticalGlue()); + return rootPanel; } @Override public boolean isModified() { - return closePreviousTabCheckBox != null - && Configurable.isCheckboxModified( - closePreviousTabCheckBox, - QuickSwitchSettings.getInstance().isClosePreviousTab() - ); + QuickSwitchSettings settings = QuickSwitchSettings.getInstance(); + if (closePreviousTabCheckBox != null + && Configurable.isCheckboxModified(closePreviousTabCheckBox, settings.isClosePreviousTab())) { + return true; + } + + return fileTypeCheckBoxes != null && fileTypeCheckBoxes.entrySet().stream() + .anyMatch(entry -> Configurable.isCheckboxModified( + entry.getValue(), + settings.isFileTypeEnabled(entry.getKey()) + )); } @Override public void apply() { + QuickSwitchSettings settings = QuickSwitchSettings.getInstance(); if (closePreviousTabCheckBox != null) { - QuickSwitchSettings.getInstance().setClosePreviousTab(closePreviousTabCheckBox.isSelected()); + settings.setClosePreviousTab(closePreviousTabCheckBox.isSelected()); + } + if (fileTypeCheckBoxes != null) { + fileTypeCheckBoxes.forEach((fileType, checkBox) -> + settings.setFileTypeEnabled(fileType, checkBox.isSelected())); } } @Override public void reset() { + QuickSwitchSettings settings = QuickSwitchSettings.getInstance(); if (closePreviousTabCheckBox != null) { - closePreviousTabCheckBox.setSelected(QuickSwitchSettings.getInstance().isClosePreviousTab()); + closePreviousTabCheckBox.setSelected(settings.isClosePreviousTab()); + } + if (fileTypeCheckBoxes != null) { + fileTypeCheckBoxes.forEach((fileType, checkBox) -> + checkBox.setSelected(settings.isFileTypeEnabled(fileType))); } } @Override public void disposeUIResources() { closePreviousTabCheckBox = null; + fileTypeCheckBoxes = null; + } + + private static JPanel createSectionPanel(String title) { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + Border titleBorder = BorderFactory.createTitledBorder(title); + Border padding = BorderFactory.createEmptyBorder(8, 12, 10, 12); + panel.setBorder(BorderFactory.createCompoundBorder(titleBorder, padding)); + return panel; + } + + private static void allowHorizontalStretch(JComponent component) { + component.setAlignmentX(Component.LEFT_ALIGNMENT); + component.setMaximumSize(new Dimension(Integer.MAX_VALUE, component.getPreferredSize().height)); } } diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchFileType.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchFileType.java new file mode 100644 index 0000000..ffa9225 --- /dev/null +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchFileType.java @@ -0,0 +1,61 @@ +package patrick.kelleter.angularcliquickswitch; + +enum QuickSwitchFileType { + TYPESCRIPT(Category.CONTROLLER, "ts", true), + JAVASCRIPT(Category.CONTROLLER, "js", true), + + HTML(Category.TEMPLATE, "html", true), + PHP(Category.TEMPLATE, "php", true), + HAML(Category.TEMPLATE, "haml", true), + JADE(Category.TEMPLATE, "jade", true), + PUG(Category.TEMPLATE, "pug", true), + SLIM(Category.TEMPLATE, "slim", true), + + CSS(Category.STYLE, "css", true), + SASS(Category.STYLE, "sass", true), + SCSS(Category.STYLE, "scss", true), + LESS(Category.STYLE, "less", true), + STYLUS(Category.STYLE, "styl", true), + + TYPESCRIPT_SPEC(Category.TESTS, "spec.ts", false), + JAVASCRIPT_SPEC(Category.TESTS, "spec.js", false); + + enum Category { + CONTROLLER("Controller"), + TEMPLATE("Template"), + STYLE("Style"), + TESTS("Tests"); + + private final String displayName; + + Category(String displayName) { + this.displayName = displayName; + } + + String displayName() { + return displayName; + } + } + + private final Category category; + private final String suffix; + private final boolean enabledByDefault; + + QuickSwitchFileType(Category category, String suffix, boolean enabledByDefault) { + this.category = category; + this.suffix = suffix; + this.enabledByDefault = enabledByDefault; + } + + Category category() { + return category; + } + + String suffix() { + return suffix; + } + + boolean enabledByDefault() { + return enabledByDefault; + } +} diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java index 58d2e6d..06be157 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java @@ -8,6 +8,9 @@ import com.intellij.openapi.components.Storage; import org.jetbrains.annotations.NotNull; +import java.util.HashSet; +import java.util.Set; + @Service(Service.Level.APP) @State( name = "AngularCliQuickSwitchSettings", @@ -17,6 +20,8 @@ public final class QuickSwitchSettings implements PersistentStateComponent { public static final class SettingsState { public boolean closePreviousTab; + public Set disabledFileTypes = new HashSet<>(); + public Set enabledOptInFileTypes = new HashSet<>(); } private volatile SettingsState state = new SettingsState(); @@ -32,6 +37,12 @@ public static final class SettingsState { @Override public void loadState(@NotNull SettingsState state) { + if (state.disabledFileTypes == null) { + state.disabledFileTypes = new HashSet<>(); + } + if (state.enabledOptInFileTypes == null) { + state.enabledOptInFileTypes = new HashSet<>(); + } this.state = state; } @@ -40,8 +51,40 @@ public boolean isClosePreviousTab() { } public void setClosePreviousTab(boolean closePreviousTab) { - SettingsState newState = new SettingsState(); + SettingsState newState = copyState(); newState.closePreviousTab = closePreviousTab; state = newState; } + + boolean isFileTypeEnabled(@NotNull QuickSwitchFileType fileType) { + SettingsState currentState = state; + return fileType.enabledByDefault() + ? !currentState.disabledFileTypes.contains(fileType.name()) + : currentState.enabledOptInFileTypes.contains(fileType.name()); + } + + void setFileTypeEnabled(@NotNull QuickSwitchFileType fileType, boolean enabled) { + SettingsState newState = copyState(); + if (fileType.enabledByDefault()) { + if (enabled) { + newState.disabledFileTypes.remove(fileType.name()); + } else { + newState.disabledFileTypes.add(fileType.name()); + } + } else if (enabled) { + newState.enabledOptInFileTypes.add(fileType.name()); + } else { + newState.enabledOptInFileTypes.remove(fileType.name()); + } + state = newState; + } + + private SettingsState copyState() { + SettingsState currentState = state; + SettingsState newState = new SettingsState(); + newState.closePreviousTab = currentState.closePreviousTab; + newState.disabledFileTypes = new HashSet<>(currentState.disabledFileTypes); + newState.enabledOptInFileTypes = new HashSet<>(currentState.enabledOptInFileTypes); + return newState; + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 721a235..ae50ffa 100755 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -4,7 +4,7 @@ Patrick Kelleter com.intellij.modules.platform diff --git a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java index e402a3d..89f6cf3 100644 --- a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java +++ b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java @@ -60,6 +60,55 @@ public void testSkipsDirectoriesNamedLikeCounterparts() { assertTarget(stylesheet, QuickSwitchAction.findTargetFile(typescript)); } + public void testSkipsDisabledFileType() { + VirtualFile html = addFile("example.component.html"); + addFile("example.component.scss"); + VirtualFile typescript = addFile("example.component.ts"); + + VirtualFile target = QuickSwitchAction.findTargetFile( + html, + fileType -> fileType.enabledByDefault() && fileType != QuickSwitchFileType.SCSS + ); + + assertTarget(typescript, target); + } + + public void testCanSwitchAwayFromDisabledFileType() { + VirtualFile stylesheet = addFile("example.component.scss"); + VirtualFile typescript = addFile("example.component.ts"); + + VirtualFile target = QuickSwitchAction.findTargetFile( + stylesheet, + fileType -> fileType.enabledByDefault() && fileType != QuickSwitchFileType.SCSS + ); + + assertTarget(typescript, target); + } + + public void testSkipsSpecFileByDefault() { + VirtualFile typescript = addFile("example.component.ts"); + addFile("example.component.spec.ts"); + VirtualFile html = addFile("example.component.html"); + + assertTarget(html, QuickSwitchAction.findTargetFile(typescript)); + } + + public void testCyclesThroughEnabledSpecFile() { + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile spec = addFile("example.component.spec.ts"); + + VirtualFile target = QuickSwitchAction.findTargetFile( + typescript, + fileType -> fileType.enabledByDefault() || fileType == QuickSwitchFileType.TYPESCRIPT_SPEC + ); + + assertTarget(spec, target); + assertTarget(typescript, QuickSwitchAction.findTargetFile( + spec, + fileType -> fileType.enabledByDefault() || fileType == QuickSwitchFileType.TYPESCRIPT_SPEC + )); + } + public void testKeepsPreviousTabOpen() { VirtualFile typescript = addFile("example.component.ts"); VirtualFile html = addFile("example.component.html"); diff --git a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java index 930c903..704ac48 100644 --- a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java +++ b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java @@ -9,6 +9,14 @@ public void testKeepsTabsOpenByDefault() { assertFalse(settings.isClosePreviousTab()); } + public void testUsesFileTypeDefaults() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + + for (QuickSwitchFileType fileType : QuickSwitchFileType.values()) { + assertEquals(fileType.suffix(), fileType.enabledByDefault(), settings.isFileTypeEnabled(fileType)); + } + } + public void testLoadsPersistedClosePreference() { QuickSwitchSettings settings = new QuickSwitchSettings(); QuickSwitchSettings.SettingsState state = new QuickSwitchSettings.SettingsState(); @@ -21,10 +29,36 @@ public void testLoadsPersistedClosePreference() { public void testUpdatesClosePreference() { QuickSwitchSettings settings = new QuickSwitchSettings(); + settings.setFileTypeEnabled(QuickSwitchFileType.SCSS, false); settings.setClosePreviousTab(true); assertTrue(settings.isClosePreviousTab()); assertTrue(settings.getState().closePreviousTab); + assertFalse(settings.isFileTypeEnabled(QuickSwitchFileType.SCSS)); + } + + public void testPersistsFileTypeOverrides() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + + settings.setFileTypeEnabled(QuickSwitchFileType.SCSS, false); + settings.setFileTypeEnabled(QuickSwitchFileType.TYPESCRIPT_SPEC, true); + + assertFalse(settings.isFileTypeEnabled(QuickSwitchFileType.SCSS)); + assertTrue(settings.isFileTypeEnabled(QuickSwitchFileType.TYPESCRIPT_SPEC)); + assertTrue(settings.getState().disabledFileTypes.contains(QuickSwitchFileType.SCSS.name())); + assertTrue(settings.getState().enabledOptInFileTypes.contains(QuickSwitchFileType.TYPESCRIPT_SPEC.name())); + } + + public void testLoadsPersistedFileTypeOverrides() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + QuickSwitchSettings.SettingsState state = new QuickSwitchSettings.SettingsState(); + state.disabledFileTypes.add(QuickSwitchFileType.HTML.name()); + state.enabledOptInFileTypes.add(QuickSwitchFileType.JAVASCRIPT_SPEC.name()); + + settings.loadState(state); + + assertFalse(settings.isFileTypeEnabled(QuickSwitchFileType.HTML)); + assertTrue(settings.isFileTypeEnabled(QuickSwitchFileType.JAVASCRIPT_SPEC)); } } From 11806a4cb5464d73218e03bd3312034a8da21718 Mon Sep 17 00:00:00 2001 From: "Patrick K." Date: Thu, 20 Aug 2026 00:35:37 +0200 Subject: [PATCH 4/9] add new shortcuts for direct access of controller/style/template --- README.md | 9 +++ build.gradle.kts | 1 + .../QuickSwitchAction.java | 59 +++++++++++++++- .../QuickSwitchCategoryAction.java | 52 ++++++++++++++ .../QuickSwitchControllersAction.java | 7 ++ .../QuickSwitchMarkupAction.java | 7 ++ .../QuickSwitchStylesAction.java | 7 ++ .../QuickSwitchTestsAction.java | 7 ++ src/main/resources/META-INF/plugin.xml | 18 ++++- .../QuickSwitchActionTest.java | 70 +++++++++++++++++++ 10 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchCategoryAction.java create mode 100644 src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchControllersAction.java create mode 100644 src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchMarkupAction.java create mode 100644 src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchStylesAction.java create mode 100644 src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchTestsAction.java diff --git a/README.md b/README.md index d52a98d..25c7bfc 100755 --- a/README.md +++ b/README.md @@ -17,6 +17,13 @@ By pressing the assigned shortcut repeatedly, the IDE will cycle amongst the fil foo.component.ts -> foo.component.html -> foo.component.css -> foo.component.ts -> ... +Dedicated shortcuts jump directly to a file category and then cycle only within that category: + +- **Alt+T**: tests (`spec.ts`, `spec.js`) +- **Alt+X**: controllers (`ts`, `js`) +- **Alt+C**: styles (`css`, `sass`, `scss`, `less`, `styl`) +- **Alt+V**: markup (`html`, `php`, `haml`, `jade`, `pug`, `slim`) + Whilst cycling, unknown filenames (like bar.info) and extensions (like foo.md) will be skipped on purpose. @@ -39,6 +46,8 @@ By default, switched files remain open to preserve tab position, pinned state, a The same settings page lets you enable or disable individual file types grouped by controller, template, style, and tests. Disabled file types are skipped while cycling. +File-type checkboxes control the main **Alt+S** cycle. Dedicated category shortcuts always search every file type in their category. + Due to the nature of this plugin, it will of course also **work with other projects**, that have not been generated by Angular CLI - or are not even Angular projects at all - as long as your project structure follows similar patterns. diff --git a/build.gradle.kts b/build.gradle.kts index 6e450b8..f67d73e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,6 +42,7 @@ intellijPlatform {
  • Updated for IntelliJ Platform 2024.2 and newer.
  • Added a settings page with optional previous-tab closing.
  • Added grouped file-type controls and optional spec.ts/spec.js test cycling.
  • +
  • Added dedicated shortcuts for tests, controllers, styles, and markup.
  • Fixed switching in editor splits and removed tab-closing side effects by default.
  • Improved support for virtual files and modern IntelliJ action APIs.
  • diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java index 62d6dd9..f04b640 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java @@ -111,7 +111,48 @@ static void navigate( return null; } - private static boolean isSwitchable(@Nullable VirtualFile file) { + static @Nullable VirtualFile findTargetFile( + @NotNull VirtualFile currentFile, + @NotNull QuickSwitchFileType.Category category + ) { + if (!currentFile.isValid() || currentFile.isDirectory()) { + return null; + } + + VirtualFile parent = currentFile.getParent(); + FileMatch currentMatch = matchFile(currentFile); + if (parent == null || currentMatch == null) { + return null; + } + + List categoryTypes = FILE_TYPES.stream() + .filter(fileType -> fileType.category() == category) + .toList(); + int currentIndex = categoryTypes.indexOf(currentMatch.fileType()); + VirtualFile[] siblings = parent.getChildren(); + + if (currentIndex < 0) { + for (QuickSwitchFileType targetType : categoryTypes) { + VirtualFile target = findSibling(currentMatch.baseName(), targetType, siblings); + if (target != null) { + return target; + } + } + return null; + } + + for (int offset = 1; offset < categoryTypes.size(); offset++) { + QuickSwitchFileType targetType = categoryTypes.get((currentIndex + offset) % categoryTypes.size()); + VirtualFile target = findSibling(currentMatch.baseName(), targetType, siblings); + if (target != null) { + return target; + } + } + + return null; + } + + static boolean isSwitchable(@Nullable VirtualFile file) { return file != null && file.isValid() && !file.isDirectory() @@ -119,6 +160,22 @@ private static boolean isSwitchable(@Nullable VirtualFile file) { && matchFile(file) != null; } + private static @Nullable VirtualFile findSibling( + @NotNull String baseName, + @NotNull QuickSwitchFileType targetType, + VirtualFile @NotNull [] siblings + ) { + String targetName = baseName + "." + targetType.suffix(); + for (VirtualFile sibling : siblings) { + if (sibling.isValid() + && !sibling.isDirectory() + && targetName.equalsIgnoreCase(sibling.getName())) { + return sibling; + } + } + return null; + } + private static @Nullable FileMatch matchFile(@NotNull VirtualFile file) { String fileName = file.getName(); for (QuickSwitchFileType fileType : FILE_TYPES_BY_SUFFIX_LENGTH) { diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchCategoryAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchCategoryAction.java new file mode 100644 index 0000000..c40b2eb --- /dev/null +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchCategoryAction.java @@ -0,0 +1,52 @@ +package patrick.kelleter.angularcliquickswitch; + +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +public abstract class QuickSwitchCategoryAction extends DumbAwareAction { + private final QuickSwitchFileType.Category category; + + QuickSwitchCategoryAction(@NotNull QuickSwitchFileType.Category category) { + this.category = category; + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + @Override + public void update(@NotNull AnActionEvent event) { + Project project = event.getProject(); + VirtualFile file = event.getData(CommonDataKeys.VIRTUAL_FILE); + event.getPresentation().setEnabled( + project != null && !project.isDisposed() && QuickSwitchAction.isSwitchable(file) + ); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + Project project = event.getProject(); + VirtualFile currentFile = event.getData(CommonDataKeys.VIRTUAL_FILE); + if (project == null || project.isDisposed() || currentFile == null) { + return; + } + + VirtualFile targetFile = QuickSwitchAction.findTargetFile(currentFile, category); + if (targetFile == null) { + return; + } + + QuickSwitchAction.navigate( + project, + currentFile, + targetFile, + QuickSwitchSettings.getInstance().isClosePreviousTab() + ); + } +} diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchControllersAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchControllersAction.java new file mode 100644 index 0000000..829f51f --- /dev/null +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchControllersAction.java @@ -0,0 +1,7 @@ +package patrick.kelleter.angularcliquickswitch; + +public final class QuickSwitchControllersAction extends QuickSwitchCategoryAction { + public QuickSwitchControllersAction() { + super(QuickSwitchFileType.Category.CONTROLLER); + } +} diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchMarkupAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchMarkupAction.java new file mode 100644 index 0000000..8d8dc8c --- /dev/null +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchMarkupAction.java @@ -0,0 +1,7 @@ +package patrick.kelleter.angularcliquickswitch; + +public final class QuickSwitchMarkupAction extends QuickSwitchCategoryAction { + public QuickSwitchMarkupAction() { + super(QuickSwitchFileType.Category.TEMPLATE); + } +} diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchStylesAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchStylesAction.java new file mode 100644 index 0000000..8ffc29b --- /dev/null +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchStylesAction.java @@ -0,0 +1,7 @@ +package patrick.kelleter.angularcliquickswitch; + +public final class QuickSwitchStylesAction extends QuickSwitchCategoryAction { + public QuickSwitchStylesAction() { + super(QuickSwitchFileType.Category.STYLE); + } +} diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchTestsAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchTestsAction.java new file mode 100644 index 0000000..f2f8fdd --- /dev/null +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchTestsAction.java @@ -0,0 +1,7 @@ +package patrick.kelleter.angularcliquickswitch; + +public final class QuickSwitchTestsAction extends QuickSwitchCategoryAction { + public QuickSwitchTestsAction() { + super(QuickSwitchFileType.Category.TESTS); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index ae50ffa..00924a2 100755 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -18,9 +18,25 @@ - + + + + + + + + + + + + diff --git a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java index 89f6cf3..59daa3c 100644 --- a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java +++ b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java @@ -109,6 +109,76 @@ public void testCyclesThroughEnabledSpecFile() { )); } + public void testJumpsDirectlyToFirstTestFile() { + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile typescriptSpec = addFile("example.component.spec.ts"); + addFile("example.component.spec.js"); + + VirtualFile target = QuickSwitchAction.findTargetFile( + typescript, + QuickSwitchFileType.Category.TESTS + ); + + assertTarget(typescriptSpec, target); + } + + public void testCyclesOnlyWithinTestFiles() { + VirtualFile typescriptSpec = addFile("example.component.spec.ts"); + VirtualFile javascriptSpec = addFile("example.component.spec.js"); + addFile("example.component.ts"); + + assertTarget(javascriptSpec, QuickSwitchAction.findTargetFile( + typescriptSpec, + QuickSwitchFileType.Category.TESTS + )); + assertTarget(typescriptSpec, QuickSwitchAction.findTargetFile( + javascriptSpec, + QuickSwitchFileType.Category.TESTS + )); + } + + public void testJumpsDirectlyToFirstControllerFile() { + VirtualFile html = addFile("example.component.html"); + VirtualFile typescript = addFile("example.component.ts"); + addFile("example.component.js"); + + assertTarget(typescript, QuickSwitchAction.findTargetFile( + html, + QuickSwitchFileType.Category.CONTROLLER + )); + } + + public void testJumpsDirectlyToFirstStyleFile() { + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile css = addFile("example.component.css"); + addFile("example.component.scss"); + + assertTarget(css, QuickSwitchAction.findTargetFile( + typescript, + QuickSwitchFileType.Category.STYLE + )); + } + + public void testJumpsDirectlyToFirstMarkupFile() { + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile html = addFile("example.component.html"); + addFile("example.component.pug"); + + assertTarget(html, QuickSwitchAction.findTargetFile( + typescript, + QuickSwitchFileType.Category.TEMPLATE + )); + } + + public void testCategoryShortcutDoesNotReopenOnlyCurrentFile() { + VirtualFile typescript = addFile("example.component.ts"); + + assertNull(QuickSwitchAction.findTargetFile( + typescript, + QuickSwitchFileType.Category.CONTROLLER + )); + } + public void testKeepsPreviousTabOpen() { VirtualFile typescript = addFile("example.component.ts"); VirtualFile html = addFile("example.component.html"); From df0e8b8fd34d769ab1fb00d1821eec6d5acffa5a Mon Sep 17 00:00:00 2001 From: "Patrick K." Date: Thu, 20 Aug 2026 00:47:36 +0200 Subject: [PATCH 5/9] update readme --- README.md | 96 +++++++++++++++++++++++++++---------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 25c7bfc..8366ecc 100755 --- a/README.md +++ b/README.md @@ -1,76 +1,76 @@ -# angular-cli-quick-switch-plugin +# Angular CLI QuickSwitch -IntelliJ Plugin for switching amongst Angular CLI component files efficiently (e.g. ts -> html -> css) by just using one shortcut (**Alt+S**). +Quickly switch between related component files in IntelliJ-based IDEs. Use one shortcut to cycle through a component, or jump directly to its controller, markup, style, or test files. -Version 2.0 supports IntelliJ Platform 2024.2 and newer across standalone JetBrains IDEs. It requires Java 21 to build. +Given these related files: -For example let's say we have a structure like this - - foo/ - bar.info - foo.component.css - foo.component.html - **foo.component.ts** - foo.md +```text +foo/ + foo.component.css + foo.component.html + foo.component.spec.ts + foo.component.ts +``` -By pressing the assigned shortcut repeatedly, the IDE will cycle amongst the files as follows: +Press **Alt+S** repeatedly to cycle through the enabled file types: - foo.component.ts -> foo.component.html -> foo.component.css -> foo.component.ts -> ... +```text +foo.component.ts -> foo.component.html -> foo.component.css -> foo.component.ts +``` -Dedicated shortcuts jump directly to a file category and then cycle only within that category: +Test files are excluded from the main cycle by default. Press **Alt+T** to jump directly to `foo.component.spec.ts`, or enable test files in the plugin settings to include them in the main cycle. -- **Alt+T**: tests (`spec.ts`, `spec.js`) -- **Alt+X**: controllers (`ts`, `js`) -- **Alt+C**: styles (`css`, `sass`, `scss`, `less`, `styl`) -- **Alt+V**: markup (`html`, `php`, `haml`, `jade`, `pug`, `slim`) +## Shortcuts +| Action | Default | Behavior | +|---|---|---| +| QuickSwitch | **Alt+S** | Cycles through all enabled related file types. | +| QuickSwitch: Tests | **Alt+T** | Jumps to and cycles between `.spec.ts` and `.spec.js`. | +| QuickSwitch: Controllers | **Alt+X** | Jumps to and cycles between `.ts` and `.js`. | +| QuickSwitch: Styles | **Alt+C** | Jumps to and cycles between supported style files. | +| QuickSwitch: Markup | **Alt+V** | Jumps to and cycles between supported markup files. | -Whilst cycling, unknown filenames (like bar.info) and extensions (like foo.md) will be skipped on purpose. +All shortcuts can be changed under the IDE's **Settings | Keymap** page by searching for `QuickSwitch`. -Explicitly whitelisted extensions are: -- js, ts -- html, php, haml, jade, pug, slim -- css, sass, scss, less, styl -- spec.ts, spec.js (disabled by default) +## Settings -Note: -For switching between your code and test/spec files, you can simply use the built in default shortcut from IntelliJ (**Ctrl+Shift+T**) +Open **Settings | Tools | Angular CLI QuickSwitch** to configure navigation behavior and the main cycle. +### General +**Close the previous tab after switching** is disabled by default. -Further information: +When disabled, the previous file remains open. This provides the smoothest behavior and preserves tab order, pinned state, and editor splits. -If the default shortcut **Alt + S** does not fit your needs, you can of course easily adjust it in the keymap settings by replacing it for **QuickSwitch**. +When enabled, QuickSwitch opens the target in the active editor group first and closes the source only after the target opens successfully. Opening first minimizes the tab collapse and flicker caused by the old close-first behavior. -By default, switched files remain open to preserve tab position, pinned state, and editor splits. To close the previous tab after a successful switch, enable **Close the previous tab after switching** under **Settings | Tools | Angular CLI QuickSwitch**. +IntelliJ does not expose stable public APIs for transferring pinned state or closing only one occurrence of a file in a specific split. Consequently, close-on-switch mode can lose pinning and may close other occurrences of the source file in sibling editor groups. -The same settings page lets you enable or disable individual file types grouped by controller, template, style, and tests. Disabled file types are skipped while cycling. +### Supported Extensions -File-type checkboxes control the main **Alt+S** cycle. Dedicated category shortcuts always search every file type in their category. +The settings page groups file types by purpose: -Due to the nature of this plugin, it will of course also **work with other projects**, that have not been generated by Angular CLI - or are -not even Angular projects at all - as long as your project structure follows similar patterns. +| Group | File types | Main-cycle default | +|---|---|---| +| Controller | `.ts`, `.js` | Enabled | +| Template | `.html`, `.php`, `.haml`, `.jade`, `.pug`, `.slim` | Enabled | +| Style | `.css`, `.sass`, `.scss`, `.less`, `.styl` | Enabled | +| Tests | `.spec.ts`, `.spec.js` | Disabled | -## Development +Each checkbox controls whether that file type participates in the main **Alt+S** cycle. This makes workflows such as cycling directly between TypeScript and HTML possible by disabling the style types. -Build and test the plugin with: +The dedicated category shortcuts intentionally ignore these checkboxes. For example, **Alt+T** can always find a related test file even when tests are excluded from the main cycle. -```shell -./gradlew check buildPlugin -``` +If the current file type is disabled, QuickSwitch can still switch away from it to the next enabled counterpart. Disabled types are only excluded as main-cycle targets. -Run the plugin in a sandbox IDE with: +## Compatibility -```shell -./gradlew runIde -``` +Version 2.0 targets IntelliJ Platform build `242` and newer, corresponding to IntelliJ IDEA 2024.2 and compatible releases of WebStorm, PhpStorm, PyCharm, GoLand, Rider, CLion, Android Studio, and other standalone IntelliJ-based IDEs. -Verify binary compatibility with supported IntelliJ Platform releases with: +The plugin is compiled with Java 21. End users do not need to install a separate JDK because supported IDEs provide the required runtime. Java 21 is required when building the plugin from source. -```shell -./gradlew verifyPlugin -``` +Compatibility is checked with JetBrains Plugin Verifier against platform versions from 2024.2 through 2026.2. -The plugin ZIP is written to `build/distributions`. +## License -Marketplace publishing uses the `CERTIFICATE_CHAIN`, `PRIVATE_KEY`, `PRIVATE_KEY_PASSWORD`, and `PUBLISH_TOKEN` environment variables. Publishing is available through the manual GitHub Actions release workflow. +Licensed under the [Apache License 2.0](LICENSE). From 6bfc7b089fbcfc2465dd7565daca42be6c0ff610 Mon Sep 17 00:00:00 2001 From: "Patrick K." Date: Thu, 20 Aug 2026 01:12:10 +0200 Subject: [PATCH 6/9] fix shortcut order; add support for custom extensions --- README.md | 14 +- .../QuickSwitchAction.java | 226 +++++++++++++----- .../QuickSwitchCategoryAction.java | 9 +- .../QuickSwitchConfigurable.java | 113 ++++++++- .../QuickSwitchSettings.java | 40 ++++ src/main/resources/META-INF/plugin.xml | 8 +- .../QuickSwitchActionTest.java | 55 +++++ .../QuickSwitchSettingsTest.java | 32 +++ 8 files changed, 415 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 8366ecc..0ff864b 100755 --- a/README.md +++ b/README.md @@ -22,13 +22,13 @@ Test files are excluded from the main cycle by default. Press **Alt+T** to jump ## Shortcuts -| Action | Default | Behavior | -|---|---|---| +| Action | Default | Behavior | +|---|-----------|---| | QuickSwitch | **Alt+S** | Cycles through all enabled related file types. | -| QuickSwitch: Tests | **Alt+T** | Jumps to and cycles between `.spec.ts` and `.spec.js`. | | QuickSwitch: Controllers | **Alt+X** | Jumps to and cycles between `.ts` and `.js`. | -| QuickSwitch: Styles | **Alt+C** | Jumps to and cycles between supported style files. | -| QuickSwitch: Markup | **Alt+V** | Jumps to and cycles between supported markup files. | +| QuickSwitch: Markup | **Alt+C** | Jumps to and cycles between supported markup files. | +| QuickSwitch: Styles | **Alt+V** | Jumps to and cycles between supported style files. | +| QuickSwitch: Tests | **Alt+T** | Jumps to and cycles between `.spec.ts` and `.spec.js`. | All shortcuts can be changed under the IDE's **Settings | Keymap** page by searching for `QuickSwitch`. @@ -61,7 +61,9 @@ Each checkbox controls whether that file type participates in the main **Alt+S** The dedicated category shortcuts intentionally ignore these checkboxes. For example, **Alt+T** can always find a related test file even when tests are excluded from the main cycle. -If the current file type is disabled, QuickSwitch can still switch away from it to the next enabled counterpart. Disabled types are only excluded as main-cycle targets. +### Custom Extensions + +Each group supports one optional custom suffix, such as `.tsx` or `.module.scss`; enable its checkbox and enter a unique suffix with or without a leading dot. Custom suffixes are appended to their group and participate in both the main cycle and dedicated category shortcut only while enabled. ## Compatibility diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java index f04b640..ee9cd38 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchAction.java @@ -3,22 +3,25 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Predicate; public final class QuickSwitchAction extends DumbAwareAction { - private static final List FILE_TYPES = List.of(QuickSwitchFileType.values()); - private static final List FILE_TYPES_BY_SUFFIX_LENGTH = FILE_TYPES.stream() - .sorted(Comparator.comparingInt((QuickSwitchFileType fileType) -> fileType.suffix().length()).reversed()) + private static final List BUILT_IN_TYPES = List.of(QuickSwitchFileType.values()); + private static final List BUILT_IN_DEFINITIONS = BUILT_IN_TYPES.stream() + .map(fileType -> new FileTypeDefinition(fileType.category(), fileType.suffix(), fileType)) .toList(); @Override @@ -30,7 +33,11 @@ public final class QuickSwitchAction extends DumbAwareAction { public void update(@NotNull AnActionEvent event) { Project project = event.getProject(); VirtualFile file = event.getData(CommonDataKeys.VIRTUAL_FILE); - event.getPresentation().setEnabled(project != null && !project.isDisposed() && isSwitchable(file)); + event.getPresentation().setEnabled( + project != null + && !project.isDisposed() + && isSwitchable(file, QuickSwitchSettings.getInstance()) + ); } @Override @@ -42,17 +49,12 @@ public void actionPerformed(@NotNull AnActionEvent event) { } QuickSwitchSettings settings = QuickSwitchSettings.getInstance(); - VirtualFile targetFile = findTargetFile(currentFile, settings::isFileTypeEnabled); + VirtualFile targetFile = findTargetFile(currentFile, settings); if (targetFile == null) { return; } - navigate( - project, - currentFile, - targetFile, - settings.isClosePreviousTab() - ); + navigate(project, currentFile, targetFile, settings.isClosePreviousTab()); } static void navigate( @@ -79,61 +81,93 @@ static void navigate( @NotNull VirtualFile currentFile, @NotNull Predicate isEnabled ) { - if (!currentFile.isValid() || currentFile.isDirectory()) { - return null; - } + return findTargetFile( + currentFile, + BUILT_IN_DEFINITIONS, + definition -> isEnabled.test(definition.builtInType()) + ); + } - VirtualFile parent = currentFile.getParent(); - FileMatch currentMatch = matchFile(currentFile); - if (parent == null || currentMatch == null) { + static @Nullable VirtualFile findTargetFile( + @NotNull VirtualFile currentFile, + @NotNull QuickSwitchSettings settings + ) { + List definitions = createFileTypeDefinitions(settings); + return findTargetFile(currentFile, definitions, definition -> isEnabled(definition, settings)); + } + + static @Nullable VirtualFile findTargetFile( + @NotNull VirtualFile currentFile, + @NotNull QuickSwitchFileType.Category category + ) { + return findTargetFile(currentFile, category, BUILT_IN_DEFINITIONS, definition -> true); + } + + static @Nullable VirtualFile findTargetFile( + @NotNull VirtualFile currentFile, + @NotNull QuickSwitchFileType.Category category, + @NotNull QuickSwitchSettings settings + ) { + List definitions = createFileTypeDefinitions(settings); + return findTargetFile( + currentFile, + category, + definitions, + definition -> definition.builtInType() != null || isEnabled(definition, settings) + ); + } + + static boolean isSwitchable(@Nullable VirtualFile file, @NotNull QuickSwitchSettings settings) { + return isSwitchable(file, createFileTypeDefinitions(settings)); + } + + private static @Nullable VirtualFile findTargetFile( + @NotNull VirtualFile currentFile, + @NotNull List definitions, + @NotNull Predicate isEnabled + ) { + FileContext context = getFileContext(currentFile, definitions); + if (context == null) { return null; } - int currentIndex = FILE_TYPES.indexOf(currentMatch.fileType()); - VirtualFile[] siblings = parent.getChildren(); - - for (int offset = 1; offset < FILE_TYPES.size(); offset++) { - QuickSwitchFileType targetType = FILE_TYPES.get((currentIndex + offset) % FILE_TYPES.size()); + int currentIndex = definitions.indexOf(context.currentType()); + for (int offset = 1; offset < definitions.size(); offset++) { + FileTypeDefinition targetType = definitions.get((currentIndex + offset) % definitions.size()); if (!isEnabled.test(targetType)) { continue; } - String targetName = currentMatch.baseName() + "." + targetType.suffix(); - for (VirtualFile sibling : siblings) { - if (sibling.isValid() - && !sibling.isDirectory() - && targetName.equalsIgnoreCase(sibling.getName())) { - return sibling; - } + VirtualFile target = findSibling(context.baseName(), targetType, context.siblings()); + if (target != null) { + return target; } } - return null; } - static @Nullable VirtualFile findTargetFile( + private static @Nullable VirtualFile findTargetFile( @NotNull VirtualFile currentFile, - @NotNull QuickSwitchFileType.Category category + @NotNull QuickSwitchFileType.Category category, + @NotNull List definitions, + @NotNull Predicate isEnabled ) { - if (!currentFile.isValid() || currentFile.isDirectory()) { + FileContext context = getFileContext(currentFile, definitions); + if (context == null) { return null; } - VirtualFile parent = currentFile.getParent(); - FileMatch currentMatch = matchFile(currentFile); - if (parent == null || currentMatch == null) { - return null; - } - - List categoryTypes = FILE_TYPES.stream() - .filter(fileType -> fileType.category() == category) + List categoryTypes = definitions.stream() + .filter(definition -> definition.category() == category) .toList(); - int currentIndex = categoryTypes.indexOf(currentMatch.fileType()); - VirtualFile[] siblings = parent.getChildren(); + int currentIndex = categoryTypes.indexOf(context.currentType()); if (currentIndex < 0) { - for (QuickSwitchFileType targetType : categoryTypes) { - VirtualFile target = findSibling(currentMatch.baseName(), targetType, siblings); + for (FileTypeDefinition targetType : categoryTypes) { + if (!isEnabled.test(targetType)) { + continue; + } + VirtualFile target = findSibling(context.baseName(), targetType, context.siblings()); if (target != null) { return target; } @@ -142,27 +176,63 @@ static void navigate( } for (int offset = 1; offset < categoryTypes.size(); offset++) { - QuickSwitchFileType targetType = categoryTypes.get((currentIndex + offset) % categoryTypes.size()); - VirtualFile target = findSibling(currentMatch.baseName(), targetType, siblings); + FileTypeDefinition targetType = categoryTypes.get((currentIndex + offset) % categoryTypes.size()); + if (!isEnabled.test(targetType)) { + continue; + } + VirtualFile target = findSibling(context.baseName(), targetType, context.siblings()); if (target != null) { return target; } } - return null; } - static boolean isSwitchable(@Nullable VirtualFile file) { - return file != null - && file.isValid() - && !file.isDirectory() - && file.getParent() != null - && matchFile(file) != null; + private static boolean isSwitchable( + @Nullable VirtualFile file, + @NotNull List definitions + ) { + return file != null && getFileContext(file, definitions) != null; + } + + private static @Nullable FileContext getFileContext( + @NotNull VirtualFile file, + @NotNull List definitions + ) { + if (!file.isValid() || file.isDirectory()) { + return null; + } + + VirtualFile parent = file.getParent(); + FileMatch match = matchFile(file, definitions); + if (parent == null || match == null) { + return null; + } + return new FileContext(match.fileType(), match.baseName(), parent.getChildren()); + } + + private static @Nullable FileMatch matchFile( + @NotNull VirtualFile file, + @NotNull List definitions + ) { + String fileName = file.getName(); + List longestSuffixFirst = definitions.stream() + .sorted(Comparator.comparingInt((FileTypeDefinition definition) -> definition.suffix().length()).reversed()) + .toList(); + + for (FileTypeDefinition definition : longestSuffixFirst) { + String suffix = "." + definition.suffix(); + int suffixStart = fileName.length() - suffix.length(); + if (suffixStart > 0 && fileName.regionMatches(true, suffixStart, suffix, 0, suffix.length())) { + return new FileMatch(definition, fileName.substring(0, suffixStart)); + } + } + return null; } private static @Nullable VirtualFile findSibling( @NotNull String baseName, - @NotNull QuickSwitchFileType targetType, + @NotNull FileTypeDefinition targetType, VirtualFile @NotNull [] siblings ) { String targetName = baseName + "." + targetType.suffix(); @@ -176,17 +246,43 @@ static boolean isSwitchable(@Nullable VirtualFile file) { return null; } - private static @Nullable FileMatch matchFile(@NotNull VirtualFile file) { - String fileName = file.getName(); - for (QuickSwitchFileType fileType : FILE_TYPES_BY_SUFFIX_LENGTH) { - String suffix = "." + fileType.suffix(); - int suffixStart = fileName.length() - suffix.length(); - if (suffixStart > 0 && fileName.regionMatches(true, suffixStart, suffix, 0, suffix.length())) { - return new FileMatch(fileType, fileName.substring(0, suffixStart)); + private static List createFileTypeDefinitions(QuickSwitchSettings settings) { + List definitions = new ArrayList<>(); + Set usedSuffixes = new HashSet<>(); + for (QuickSwitchFileType.Category category : QuickSwitchFileType.Category.values()) { + for (QuickSwitchFileType fileType : BUILT_IN_TYPES) { + if (fileType.category() == category) { + definitions.add(new FileTypeDefinition(category, fileType.suffix(), fileType)); + usedSuffixes.add(fileType.suffix().toLowerCase(java.util.Locale.ROOT)); + } + } + + String customSuffix = settings.getCustomSuffix(category); + if (!customSuffix.isBlank() + && usedSuffixes.add(customSuffix.toLowerCase(java.util.Locale.ROOT))) { + definitions.add(new FileTypeDefinition(category, customSuffix, null)); } } - return null; + return definitions; } - private record FileMatch(QuickSwitchFileType fileType, String baseName) { } + private static boolean isEnabled(FileTypeDefinition definition, QuickSwitchSettings settings) { + return definition.builtInType() != null + ? settings.isFileTypeEnabled(definition.builtInType()) + : settings.isCustomSuffixEnabled(definition.category()); + } + + private record FileTypeDefinition( + QuickSwitchFileType.Category category, + String suffix, + @Nullable QuickSwitchFileType builtInType + ) { } + + private record FileMatch(FileTypeDefinition fileType, String baseName) { } + + private record FileContext( + FileTypeDefinition currentType, + String baseName, + VirtualFile[] siblings + ) { } } diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchCategoryAction.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchCategoryAction.java index c40b2eb..2bbc4fe 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchCategoryAction.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchCategoryAction.java @@ -25,7 +25,9 @@ public void update(@NotNull AnActionEvent event) { Project project = event.getProject(); VirtualFile file = event.getData(CommonDataKeys.VIRTUAL_FILE); event.getPresentation().setEnabled( - project != null && !project.isDisposed() && QuickSwitchAction.isSwitchable(file) + project != null + && !project.isDisposed() + && QuickSwitchAction.isSwitchable(file, QuickSwitchSettings.getInstance()) ); } @@ -37,7 +39,8 @@ public void actionPerformed(@NotNull AnActionEvent event) { return; } - VirtualFile targetFile = QuickSwitchAction.findTargetFile(currentFile, category); + QuickSwitchSettings settings = QuickSwitchSettings.getInstance(); + VirtualFile targetFile = QuickSwitchAction.findTargetFile(currentFile, category, settings); if (targetFile == null) { return; } @@ -46,7 +49,7 @@ public void actionPerformed(@NotNull AnActionEvent event) { project, currentFile, targetFile, - QuickSwitchSettings.getInstance().isClosePreviousTab() + settings.isClosePreviousTab() ); } } diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java index 270a010..5c50306 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java @@ -1,6 +1,8 @@ package patrick.kelleter.angularcliquickswitch; import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.ui.components.JBTextField; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -11,17 +13,23 @@ import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JTextField; import javax.swing.border.Border; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.GridLayout; import java.util.EnumMap; +import java.util.HashSet; +import java.util.Locale; import java.util.Map; +import java.util.Set; public final class QuickSwitchConfigurable implements Configurable { private JCheckBox closePreviousTabCheckBox; private Map fileTypeCheckBoxes; + private Map customEnabledCheckBoxes; + private Map customSuffixFields; @Override public @Nls String getDisplayName() { @@ -44,6 +52,8 @@ public final class QuickSwitchConfigurable implements Configurable { rootPanel.add(Box.createVerticalStrut(12)); fileTypeCheckBoxes = new EnumMap<>(QuickSwitchFileType.class); + customEnabledCheckBoxes = new EnumMap<>(QuickSwitchFileType.Category.class); + customSuffixFields = new EnumMap<>(QuickSwitchFileType.Category.class); JPanel extensionsPanel = createSectionPanel("Supported Extensions"); JPanel categoriesPanel = new JPanel(new GridLayout( 1, @@ -59,14 +69,40 @@ public final class QuickSwitchConfigurable implements Configurable { categoryPanel.add(categoryLabel); categoryPanel.add(Box.createVerticalStrut(3)); + JPanel extensionListPanel = new JPanel(); + extensionListPanel.setLayout(new BoxLayout(extensionListPanel, BoxLayout.Y_AXIS)); + for (QuickSwitchFileType fileType : QuickSwitchFileType.values()) { if (fileType.category() == category) { JCheckBox checkBox = new JCheckBox("." + fileType.suffix()); + checkBox.setAlignmentX(Component.LEFT_ALIGNMENT); fileTypeCheckBoxes.put(fileType, checkBox); - categoryPanel.add(checkBox); + extensionListPanel.add(checkBox); } } + extensionListPanel.add(Box.createVerticalStrut(4)); + JPanel customPanel = new JPanel(); + customPanel.setLayout(new BoxLayout(customPanel, BoxLayout.X_AXIS)); + customPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + JCheckBox customEnabledCheckBox = new JCheckBox(); + customEnabledCheckBox.getAccessibleContext().setAccessibleName( + "Enable custom " + category.displayName().toLowerCase(Locale.ROOT) + " suffix" + ); + JBTextField customSuffixField = new JBTextField(7); + customSuffixField.getEmptyText().setText(".custom"); + customSuffixField.setToolTipText("Enter a suffix such as tsx or component.html"); + customEnabledCheckBox.addActionListener(event -> + customSuffixField.setEnabled(customEnabledCheckBox.isSelected())); + customEnabledCheckBoxes.put(category, customEnabledCheckBox); + customSuffixFields.put(category, customSuffixField); + customPanel.add(customEnabledCheckBox); + customPanel.add(customSuffixField); + customPanel.setMaximumSize(customPanel.getPreferredSize()); + extensionListPanel.add(customPanel); + extensionListPanel.setMaximumSize(extensionListPanel.getPreferredSize()); + categoryPanel.add(extensionListPanel); + categoriesPanel.add(categoryPanel); } @@ -86,15 +122,33 @@ public boolean isModified() { return true; } - return fileTypeCheckBoxes != null && fileTypeCheckBoxes.entrySet().stream() + if (fileTypeCheckBoxes != null && fileTypeCheckBoxes.entrySet().stream() .anyMatch(entry -> Configurable.isCheckboxModified( entry.getValue(), settings.isFileTypeEnabled(entry.getKey()) - )); + ))) { + return true; + } + + if (customEnabledCheckBoxes == null || customSuffixFields == null) { + return false; + } + + for (QuickSwitchFileType.Category category : QuickSwitchFileType.Category.values()) { + if (Configurable.isCheckboxModified( + customEnabledCheckBoxes.get(category), + settings.isCustomSuffixEnabled(category) + ) || !normalizeSuffix(customSuffixFields.get(category).getText()) + .equals(settings.getCustomSuffix(category))) { + return true; + } + } + return false; } @Override - public void apply() { + public void apply() throws ConfigurationException { + Map customSuffixes = validateCustomSuffixes(); QuickSwitchSettings settings = QuickSwitchSettings.getInstance(); if (closePreviousTabCheckBox != null) { settings.setClosePreviousTab(closePreviousTabCheckBox.isSelected()); @@ -103,6 +157,12 @@ public void apply() { fileTypeCheckBoxes.forEach((fileType, checkBox) -> settings.setFileTypeEnabled(fileType, checkBox.isSelected())); } + if (customEnabledCheckBoxes != null && customSuffixFields != null) { + customSuffixes.forEach((category, suffix) -> { + settings.setCustomSuffix(category, suffix, customEnabledCheckBoxes.get(category).isSelected()); + customSuffixFields.get(category).setText(suffix); + }); + } } @Override @@ -115,12 +175,22 @@ public void reset() { fileTypeCheckBoxes.forEach((fileType, checkBox) -> checkBox.setSelected(settings.isFileTypeEnabled(fileType))); } + if (customEnabledCheckBoxes != null && customSuffixFields != null) { + for (QuickSwitchFileType.Category category : QuickSwitchFileType.Category.values()) { + boolean enabled = settings.isCustomSuffixEnabled(category); + customEnabledCheckBoxes.get(category).setSelected(enabled); + customSuffixFields.get(category).setText(settings.getCustomSuffix(category)); + customSuffixFields.get(category).setEnabled(enabled); + } + } } @Override public void disposeUIResources() { closePreviousTabCheckBox = null; fileTypeCheckBoxes = null; + customEnabledCheckBoxes = null; + customSuffixFields = null; } private static JPanel createSectionPanel(String title) { @@ -136,4 +206,39 @@ private static void allowHorizontalStretch(JComponent component) { component.setAlignmentX(Component.LEFT_ALIGNMENT); component.setMaximumSize(new Dimension(Integer.MAX_VALUE, component.getPreferredSize().height)); } + + private Map validateCustomSuffixes() throws ConfigurationException { + Map normalizedSuffixes = + new EnumMap<>(QuickSwitchFileType.Category.class); + Set usedSuffixes = new HashSet<>(); + for (QuickSwitchFileType fileType : QuickSwitchFileType.values()) { + usedSuffixes.add(fileType.suffix().toLowerCase(Locale.ROOT)); + } + + for (QuickSwitchFileType.Category category : QuickSwitchFileType.Category.values()) { + String suffix = normalizeSuffix(customSuffixFields.get(category).getText()); + boolean enabled = customEnabledCheckBoxes.get(category).isSelected(); + if (enabled && suffix.isEmpty()) { + throw new ConfigurationException(category.displayName() + " custom suffix cannot be empty."); + } + if (!suffix.isEmpty() && !suffix.matches("[a-z0-9_-]+(?:\\.[a-z0-9_-]+)*")) { + throw new ConfigurationException( + category.displayName() + " custom suffix contains invalid characters." + ); + } + if (!suffix.isEmpty() && !usedSuffixes.add(suffix)) { + throw new ConfigurationException("Custom suffix ." + suffix + " is already configured."); + } + normalizedSuffixes.put(category, suffix); + } + return normalizedSuffixes; + } + + private static String normalizeSuffix(String value) { + String suffix = value.trim().toLowerCase(Locale.ROOT); + while (suffix.startsWith(".")) { + suffix = suffix.substring(1); + } + return suffix; + } } diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java index 06be157..45b916a 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettings.java @@ -9,6 +9,8 @@ import org.jetbrains.annotations.NotNull; import java.util.HashSet; +import java.util.HashMap; +import java.util.Map; import java.util.Set; @Service(Service.Level.APP) @@ -22,6 +24,8 @@ public static final class SettingsState { public boolean closePreviousTab; public Set disabledFileTypes = new HashSet<>(); public Set enabledOptInFileTypes = new HashSet<>(); + public Map customSuffixes = new HashMap<>(); + public Set enabledCustomCategories = new HashSet<>(); } private volatile SettingsState state = new SettingsState(); @@ -43,6 +47,12 @@ public void loadState(@NotNull SettingsState state) { if (state.enabledOptInFileTypes == null) { state.enabledOptInFileTypes = new HashSet<>(); } + if (state.customSuffixes == null) { + state.customSuffixes = new HashMap<>(); + } + if (state.enabledCustomCategories == null) { + state.enabledCustomCategories = new HashSet<>(); + } this.state = state; } @@ -79,12 +89,42 @@ void setFileTypeEnabled(@NotNull QuickSwitchFileType fileType, boolean enabled) state = newState; } + boolean isCustomSuffixEnabled(@NotNull QuickSwitchFileType.Category category) { + return state.enabledCustomCategories.contains(category.name()); + } + + @NotNull String getCustomSuffix(@NotNull QuickSwitchFileType.Category category) { + return state.customSuffixes.getOrDefault(category.name(), ""); + } + + void setCustomSuffix( + @NotNull QuickSwitchFileType.Category category, + @NotNull String suffix, + boolean enabled + ) { + SettingsState newState = copyState(); + if (suffix.isEmpty()) { + newState.customSuffixes.remove(category.name()); + } else { + newState.customSuffixes.put(category.name(), suffix); + } + + if (enabled) { + newState.enabledCustomCategories.add(category.name()); + } else { + newState.enabledCustomCategories.remove(category.name()); + } + state = newState; + } + private SettingsState copyState() { SettingsState currentState = state; SettingsState newState = new SettingsState(); newState.closePreviousTab = currentState.closePreviousTab; newState.disabledFileTypes = new HashSet<>(currentState.disabledFileTypes); newState.enabledOptInFileTypes = new HashSet<>(currentState.enabledOptInFileTypes); + newState.customSuffixes = new HashMap<>(currentState.customSuffixes); + newState.enabledCustomCategories = new HashSet<>(currentState.enabledCustomCategories); return newState; } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 00924a2..42fb32c 100755 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -30,12 +30,12 @@ text="QuickSwitch: Controllers" description="Switches directly to and between related controller files">
    - - - + + +
    diff --git a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java index 59daa3c..602fca5 100644 --- a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java +++ b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchActionTest.java @@ -179,6 +179,61 @@ public void testCategoryShortcutDoesNotReopenOnlyCurrentFile() { )); } + public void testMainCycleIncludesEnabledCustomSuffix() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + settings.setCustomSuffix(QuickSwitchFileType.Category.CONTROLLER, "tsx", true); + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile tsx = addFile("example.component.tsx"); + + assertTarget(tsx, QuickSwitchAction.findTargetFile(typescript, settings)); + } + + public void testMainCycleSkipsDisabledCustomSuffix() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + settings.setCustomSuffix(QuickSwitchFileType.Category.CONTROLLER, "tsx", false); + VirtualFile typescript = addFile("example.component.ts"); + addFile("example.component.tsx"); + VirtualFile html = addFile("example.component.html"); + + assertTarget(html, QuickSwitchAction.findTargetFile(typescript, settings)); + } + + public void testCanSwitchAwayFromDisabledCustomSuffix() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + settings.setCustomSuffix(QuickSwitchFileType.Category.CONTROLLER, "tsx", false); + VirtualFile tsx = addFile("example.component.tsx"); + VirtualFile html = addFile("example.component.html"); + + assertTarget(html, QuickSwitchAction.findTargetFile(tsx, settings)); + } + + public void testCategoryShortcutIncludesEnabledCustomSuffix() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + settings.setCustomSuffix(QuickSwitchFileType.Category.STYLE, "pcss", true); + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile pcss = addFile("example.component.pcss"); + + assertTarget(pcss, QuickSwitchAction.findTargetFile( + typescript, + QuickSwitchFileType.Category.STYLE, + settings + )); + } + + public void testCompoundCustomSuffixUsesComponentBaseName() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + settings.setCustomSuffix(QuickSwitchFileType.Category.TEMPLATE, "view.html", true); + VirtualFile typescript = addFile("example.component.ts"); + VirtualFile customTemplate = addFile("example.component.view.html"); + + assertTarget(customTemplate, QuickSwitchAction.findTargetFile( + typescript, + QuickSwitchFileType.Category.TEMPLATE, + settings + )); + assertTarget(typescript, QuickSwitchAction.findTargetFile(customTemplate, settings)); + } + public void testKeepsPreviousTabOpen() { VirtualFile typescript = addFile("example.component.ts"); VirtualFile html = addFile("example.component.html"); diff --git a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java index 704ac48..c94f8f5 100644 --- a/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java +++ b/src/test/java/patrick/kelleter/angularcliquickswitch/QuickSwitchSettingsTest.java @@ -17,6 +17,15 @@ public void testUsesFileTypeDefaults() { } } + public void testCustomSuffixesAreDisabledAndEmptyByDefault() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + + for (QuickSwitchFileType.Category category : QuickSwitchFileType.Category.values()) { + assertFalse(settings.isCustomSuffixEnabled(category)); + assertEquals("", settings.getCustomSuffix(category)); + } + } + public void testLoadsPersistedClosePreference() { QuickSwitchSettings settings = new QuickSwitchSettings(); QuickSwitchSettings.SettingsState state = new QuickSwitchSettings.SettingsState(); @@ -61,4 +70,27 @@ public void testLoadsPersistedFileTypeOverrides() { assertFalse(settings.isFileTypeEnabled(QuickSwitchFileType.HTML)); assertTrue(settings.isFileTypeEnabled(QuickSwitchFileType.JAVASCRIPT_SPEC)); } + + public void testPersistsCustomSuffix() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + + settings.setCustomSuffix(QuickSwitchFileType.Category.CONTROLLER, "tsx", true); + + assertTrue(settings.isCustomSuffixEnabled(QuickSwitchFileType.Category.CONTROLLER)); + assertEquals("tsx", settings.getCustomSuffix(QuickSwitchFileType.Category.CONTROLLER)); + assertEquals( + "tsx", + settings.getState().customSuffixes.get(QuickSwitchFileType.Category.CONTROLLER.name()) + ); + } + + public void testDisabledCustomSuffixRetainsItsValue() { + QuickSwitchSettings settings = new QuickSwitchSettings(); + settings.setCustomSuffix(QuickSwitchFileType.Category.STYLE, "pcss", true); + + settings.setCustomSuffix(QuickSwitchFileType.Category.STYLE, "pcss", false); + + assertFalse(settings.isCustomSuffixEnabled(QuickSwitchFileType.Category.STYLE)); + assertEquals("pcss", settings.getCustomSuffix(QuickSwitchFileType.Category.STYLE)); + } } From d4157a80f30374b5afb023edcd38dd9a48f5a8c9 Mon Sep 17 00:00:00 2001 From: "Patrick K." Date: Thu, 20 Aug 2026 01:17:00 +0200 Subject: [PATCH 7/9] validate input for custom fields to start with dot --- README.md | 8 ++--- .../QuickSwitchConfigurable.java | 29 +++++++++++++------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0ff864b..a339900 100755 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Test files are excluded from the main cycle by default. Press **Alt+T** to jump ## Shortcuts -| Action | Default | Behavior | -|---|-----------|---| +| Action | Default | Behavior | +|---|---|---| | QuickSwitch | **Alt+S** | Cycles through all enabled related file types. | | QuickSwitch: Controllers | **Alt+X** | Jumps to and cycles between `.ts` and `.js`. | | QuickSwitch: Markup | **Alt+C** | Jumps to and cycles between supported markup files. | @@ -57,13 +57,13 @@ The settings page groups file types by purpose: | Style | `.css`, `.sass`, `.scss`, `.less`, `.styl` | Enabled | | Tests | `.spec.ts`, `.spec.js` | Disabled | -Each checkbox controls whether that file type participates in the main **Alt+S** cycle. This makes workflows such as cycling directly between TypeScript and HTML possible by disabling the style types. +**Alt+S** checks the groups in the listed order: Controller, Template, Style, then Tests. Each checkbox controls whether that file type participates in the main cycle, making workflows such as cycling directly between TypeScript and HTML possible by disabling the style types. The dedicated category shortcuts intentionally ignore these checkboxes. For example, **Alt+T** can always find a related test file even when tests are excluded from the main cycle. ### Custom Extensions -Each group supports one optional custom suffix, such as `.tsx` or `.module.scss`; enable its checkbox and enter a unique suffix with or without a leading dot. Custom suffixes are appended to their group and participate in both the main cycle and dedicated category shortcut only while enabled. +Each group supports one optional custom suffix, such as `.tsx` or `.module.scss`; enable its checkbox and enter a unique suffix starting with a dot. Enabled custom suffixes are appended to their group and used as targets by both the main cycle and dedicated category shortcut. ## Compatibility diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java index 5c50306..4438199 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java @@ -91,7 +91,7 @@ public final class QuickSwitchConfigurable implements Configurable { ); JBTextField customSuffixField = new JBTextField(7); customSuffixField.getEmptyText().setText(".custom"); - customSuffixField.setToolTipText("Enter a suffix such as tsx or component.html"); + customSuffixField.setToolTipText("Enter a suffix such as .tsx or .component.html"); customEnabledCheckBox.addActionListener(event -> customSuffixField.setEnabled(customEnabledCheckBox.isSelected())); customEnabledCheckBoxes.put(category, customEnabledCheckBox); @@ -138,8 +138,8 @@ public boolean isModified() { if (Configurable.isCheckboxModified( customEnabledCheckBoxes.get(category), settings.isCustomSuffixEnabled(category) - ) || !normalizeSuffix(customSuffixFields.get(category).getText()) - .equals(settings.getCustomSuffix(category))) { + ) || !customSuffixFields.get(category).getText().trim() + .equals(formatSuffix(settings.getCustomSuffix(category)))) { return true; } } @@ -160,7 +160,7 @@ public void apply() throws ConfigurationException { if (customEnabledCheckBoxes != null && customSuffixFields != null) { customSuffixes.forEach((category, suffix) -> { settings.setCustomSuffix(category, suffix, customEnabledCheckBoxes.get(category).isSelected()); - customSuffixFields.get(category).setText(suffix); + customSuffixFields.get(category).setText(formatSuffix(suffix)); }); } } @@ -179,7 +179,7 @@ public void reset() { for (QuickSwitchFileType.Category category : QuickSwitchFileType.Category.values()) { boolean enabled = settings.isCustomSuffixEnabled(category); customEnabledCheckBoxes.get(category).setSelected(enabled); - customSuffixFields.get(category).setText(settings.getCustomSuffix(category)); + customSuffixFields.get(category).setText(formatSuffix(settings.getCustomSuffix(category))); customSuffixFields.get(category).setEnabled(enabled); } } @@ -216,12 +216,19 @@ private Map validateCustomSuffixes() throw } for (QuickSwitchFileType.Category category : QuickSwitchFileType.Category.values()) { - String suffix = normalizeSuffix(customSuffixFields.get(category).getText()); + String enteredSuffix = customSuffixFields.get(category).getText().trim().toLowerCase(Locale.ROOT); + String suffix = normalizeSuffix(enteredSuffix); boolean enabled = customEnabledCheckBoxes.get(category).isSelected(); - if (enabled && suffix.isEmpty()) { + if (enabled && enteredSuffix.isEmpty()) { throw new ConfigurationException(category.displayName() + " custom suffix cannot be empty."); } - if (!suffix.isEmpty() && !suffix.matches("[a-z0-9_-]+(?:\\.[a-z0-9_-]+)*")) { + if (!enteredSuffix.isEmpty() && !enteredSuffix.startsWith(".")) { + throw new ConfigurationException( + category.displayName() + " custom suffix must start with a dot." + ); + } + if (!enteredSuffix.isEmpty() + && !enteredSuffix.matches("\\.[a-z0-9_-]+(?:\\.[a-z0-9_-]+)*")) { throw new ConfigurationException( category.displayName() + " custom suffix contains invalid characters." ); @@ -236,9 +243,13 @@ private Map validateCustomSuffixes() throw private static String normalizeSuffix(String value) { String suffix = value.trim().toLowerCase(Locale.ROOT); - while (suffix.startsWith(".")) { + if (suffix.startsWith(".")) { suffix = suffix.substring(1); } return suffix; } + + private static String formatSuffix(String suffix) { + return suffix.isEmpty() ? "" : "." + suffix; + } } From 650ee6e3cdf4de0a21d9b8975e40c4bbce1b3922 Mon Sep 17 00:00:00 2001 From: "Patrick K." Date: Thu, 20 Aug 2026 01:48:14 +0200 Subject: [PATCH 8/9] split build to support non-angular theming for plugin --- README.md | 18 +++ build.gradle.kts | 113 +++++++++++++++++- gradle.properties | 1 + .../QuickSwitchConfigurable.java | 26 +++- .../QuickSwitchFileType.java | 18 +-- .../{resources/META-INF => plugin}/plugin.xml | 21 ++-- .../QuickSwitchBranding.properties | 5 + 7 files changed, 171 insertions(+), 31 deletions(-) rename src/main/{resources/META-INF => plugin}/plugin.xml (61%) mode change 100755 => 100644 create mode 100644 src/main/resources/patrick/kelleter/angularcliquickswitch/QuickSwitchBranding.properties diff --git a/README.md b/README.md index a339900..9d715c2 100755 --- a/README.md +++ b/README.md @@ -73,6 +73,24 @@ The plugin is compiled with Java 21. End users do not need to install a separate Compatibility is checked with JetBrains Plugin Verifier against platform versions from 2024.2 through 2026.2. +## Build Variants + +The shared codebase produces two mutually exclusive plugin variants: + +| Variant | Plugin name | Plugin ID | +|---|---|---| +| `angular` (default) | Angular CLI QuickSwitch | `patrick.kelleter.angular-cli-quick-switch` | +| `file` | File QuickSwitch | `patrick.kelleter.file-quick-switch` | + +Build either variant with Java 21: + +```shell +./gradlew buildPlugin -PpluginVariant=angular +./gradlew buildPlugin -PpluginVariant=file +``` + +The ZIP files are written to `build/distributions` with variant-specific names. Both variants retain the same internal action and settings IDs so keymaps and preferences survive switching between them, but they cannot be installed simultaneously. + ## License Licensed under the [Apache License 2.0](LICENSE). diff --git a/build.gradle.kts b/build.gradle.kts index f67d73e..06403b3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,14 +1,85 @@ +import org.apache.tools.ant.filters.ReplaceTokens +import org.gradle.api.tasks.bundling.Zip import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType import org.jetbrains.intellij.platform.gradle.TestFrameworkType +import org.jetbrains.intellij.platform.gradle.tasks.ComposedJarTask +import org.jetbrains.intellij.platform.gradle.tasks.PatchPluginXmlTask plugins { java id("org.jetbrains.intellij.platform") version "2.18.1" } +data class PluginBranding( + val id: String, + val name: String, + val description: String, + val actionPrefix: String, + val controllerCategoryName: String, + val controllerActionName: String, + val controllerActionDescription: String, + val templateCategoryName: String, + val cycleActionDescription: String, + val incompatiblePluginId: String, + val archiveBaseName: String +) + +val pluginVariantName = providers.gradleProperty("pluginVariant") + .orElse("angular") + .get() + .lowercase() +val pluginBranding = when (pluginVariantName) { + "angular" -> PluginBranding( + id = "patrick.kelleter.angular-cli-quick-switch", + name = "Angular CLI QuickSwitch", + description = "Angular CLI QuickSwitch cycles between related component files with one shortcut (default Alt + S). Supported file types are js, ts, html, php, haml, jade, pug, slim, css, sass, scss, less, styl, optional spec.ts/spec.js tests, and custom suffixes.", + actionPrefix = "QuickSwitch", + controllerCategoryName = "Controller", + controllerActionName = "Controllers", + controllerActionDescription = "Switches directly to and between related controller files", + templateCategoryName = "Template", + cycleActionDescription = "Quickly switches between Angular CLI files for one component, e.g. ts, html, css", + incompatiblePluginId = "patrick.kelleter.file-quick-switch", + archiveBaseName = "angular-cli-quick-switch" + ) + "file" -> PluginBranding( + id = "patrick.kelleter.file-quick-switch", + name = "File QuickSwitch", + description = "File QuickSwitch cycles between related sibling files with one shortcut (default Alt + S). Supported file types are js, ts, html, php, haml, jade, pug, slim, css, sass, scss, less, styl, optional spec.ts/spec.js tests, and custom suffixes.", + actionPrefix = "File QuickSwitch", + controllerCategoryName = "Scripts", + controllerActionName = "Scripts", + controllerActionDescription = "Switches directly to and between related script files", + templateCategoryName = "Markup", + cycleActionDescription = "Quickly switches between related files with the same base name, e.g. ts, html, css", + incompatiblePluginId = "patrick.kelleter.angular-cli-quick-switch", + archiveBaseName = "file-quick-switch" + ) + else -> throw GradleException( + "Unknown pluginVariant '$pluginVariantName'. Supported variants are 'angular' and 'file'." + ) +} + +val brandingTokens = mapOf( + "PLUGIN_ID" to pluginBranding.id, + "PLUGIN_NAME" to pluginBranding.name, + "PLUGIN_DESCRIPTION" to pluginBranding.description, + "ACTION_PREFIX" to pluginBranding.actionPrefix, + "CONTROLLER_CATEGORY_NAME" to pluginBranding.controllerCategoryName, + "CONTROLLER_ACTION_NAME" to pluginBranding.controllerActionName, + "CONTROLLER_ACTION_DESCRIPTION" to pluginBranding.controllerActionDescription, + "TEMPLATE_CATEGORY_NAME" to pluginBranding.templateCategoryName, + "CYCLE_ACTION_DESCRIPTION" to pluginBranding.cycleActionDescription, + "INCOMPATIBLE_PLUGIN_ID" to pluginBranding.incompatiblePluginId +) + group = "patrick.kelleter" version = "2.0.0" +base { + archivesName = pluginBranding.archiveBaseName +} + java { toolchain { languageVersion = JavaLanguageVersion.of(21) @@ -35,14 +106,54 @@ tasks.withType().configureEach { options.release = 21 } +val generatedPluginXml = layout.buildDirectory.file("generated/pluginXml/plugin.xml") +val generatePluginXml = tasks.register("generatePluginXml") { + inputs.property("pluginVariant", pluginVariantName) + inputs.properties(brandingTokens) + from(layout.projectDirectory.file("src/main/plugin/plugin.xml")) + into(generatedPluginXml.map { it.asFile.parentFile }) + filteringCharset = "UTF-8" + filter("tokens" to brandingTokens) +} + +tasks.named("patchPluginXml") { + dependsOn(generatePluginXml) + inputFile = generatedPluginXml +} + +tasks.processResources { + inputs.property("pluginVariant", pluginVariantName) + inputs.properties(brandingTokens) + filteringCharset = "UTF-8" + filter("tokens" to brandingTokens) +} + +tasks.named("buildPlugin") { + archiveBaseName = pluginBranding.archiveBaseName +} + +tasks.named("composedJar") { + archiveBaseName = pluginBranding.archiveBaseName +} + +tasks.check { + dependsOn(tasks.named("verifyPluginProjectConfiguration")) +} + intellijPlatform { + buildSearchableOptions = true + pluginConfiguration { + id = pluginBranding.id + name = pluginBranding.name + description = pluginBranding.description changeNotes = """
    • Updated for IntelliJ Platform 2024.2 and newer.
    • Added a settings page with optional previous-tab closing.
    • Added grouped file-type controls and optional spec.ts/spec.js test cycling.
    • -
    • Added dedicated shortcuts for tests, controllers, styles, and markup.
    • +
    • Added one validated custom file suffix per category.
    • +
    • Added dedicated shortcuts for tests, ${pluginBranding.controllerActionName.lowercase()}, styles, and markup.
    • Fixed switching in editor splits and removed tab-closing side effects by default.
    • Improved support for virtual files and modern IntelliJ action APIs.
    diff --git a/gradle.properties b/gradle.properties index 6f78d3f..bd042d8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ org.gradle.caching=true org.gradle.configuration-cache=true +org.jetbrains.intellij.platform.forceBuildSearchableOptions=true diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java index 4438199..c7beaa3 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchConfigurable.java @@ -23,9 +23,14 @@ import java.util.HashSet; import java.util.Locale; import java.util.Map; +import java.util.ResourceBundle; import java.util.Set; public final class QuickSwitchConfigurable implements Configurable { + private static final ResourceBundle BRANDING = ResourceBundle.getBundle( + "patrick.kelleter.angularcliquickswitch.QuickSwitchBranding" + ); + private JCheckBox closePreviousTabCheckBox; private Map fileTypeCheckBoxes; private Map customEnabledCheckBoxes; @@ -33,7 +38,7 @@ public final class QuickSwitchConfigurable implements Configurable { @Override public @Nls String getDisplayName() { - return "Angular CLI QuickSwitch"; + return BRANDING.getString("pluginName"); } @Override @@ -64,7 +69,7 @@ public final class QuickSwitchConfigurable implements Configurable { for (QuickSwitchFileType.Category category : QuickSwitchFileType.Category.values()) { JPanel categoryPanel = new JPanel(); categoryPanel.setLayout(new BoxLayout(categoryPanel, BoxLayout.Y_AXIS)); - JLabel categoryLabel = new JLabel(category.displayName()); + JLabel categoryLabel = new JLabel(categoryDisplayName(category)); categoryLabel.setFont(categoryLabel.getFont().deriveFont(Font.BOLD)); categoryPanel.add(categoryLabel); categoryPanel.add(Box.createVerticalStrut(3)); @@ -87,7 +92,7 @@ public final class QuickSwitchConfigurable implements Configurable { customPanel.setAlignmentX(Component.LEFT_ALIGNMENT); JCheckBox customEnabledCheckBox = new JCheckBox(); customEnabledCheckBox.getAccessibleContext().setAccessibleName( - "Enable custom " + category.displayName().toLowerCase(Locale.ROOT) + " suffix" + "Enable custom " + categoryDisplayName(category).toLowerCase(Locale.ROOT) + " suffix" ); JBTextField customSuffixField = new JBTextField(7); customSuffixField.getEmptyText().setText(".custom"); @@ -220,17 +225,17 @@ private Map validateCustomSuffixes() throw String suffix = normalizeSuffix(enteredSuffix); boolean enabled = customEnabledCheckBoxes.get(category).isSelected(); if (enabled && enteredSuffix.isEmpty()) { - throw new ConfigurationException(category.displayName() + " custom suffix cannot be empty."); + throw new ConfigurationException(categoryDisplayName(category) + " custom suffix cannot be empty."); } if (!enteredSuffix.isEmpty() && !enteredSuffix.startsWith(".")) { throw new ConfigurationException( - category.displayName() + " custom suffix must start with a dot." + categoryDisplayName(category) + " custom suffix must start with a dot." ); } if (!enteredSuffix.isEmpty() && !enteredSuffix.matches("\\.[a-z0-9_-]+(?:\\.[a-z0-9_-]+)*")) { throw new ConfigurationException( - category.displayName() + " custom suffix contains invalid characters." + categoryDisplayName(category) + " custom suffix contains invalid characters." ); } if (!suffix.isEmpty() && !usedSuffixes.add(suffix)) { @@ -252,4 +257,13 @@ private static String normalizeSuffix(String value) { private static String formatSuffix(String suffix) { return suffix.isEmpty() ? "" : "." + suffix; } + + private static String categoryDisplayName(QuickSwitchFileType.Category category) { + return switch (category) { + case CONTROLLER -> BRANDING.getString("category.controller"); + case TEMPLATE -> BRANDING.getString("category.template"); + case STYLE -> BRANDING.getString("category.style"); + case TESTS -> BRANDING.getString("category.tests"); + }; + } } diff --git a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchFileType.java b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchFileType.java index ffa9225..a1a2a98 100644 --- a/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchFileType.java +++ b/src/main/java/patrick/kelleter/angularcliquickswitch/QuickSwitchFileType.java @@ -21,20 +21,10 @@ enum QuickSwitchFileType { JAVASCRIPT_SPEC(Category.TESTS, "spec.js", false); enum Category { - CONTROLLER("Controller"), - TEMPLATE("Template"), - STYLE("Style"), - TESTS("Tests"); - - private final String displayName; - - Category(String displayName) { - this.displayName = displayName; - } - - String displayName() { - return displayName; - } + CONTROLLER, + TEMPLATE, + STYLE, + TESTS } private final Category category; diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/plugin/plugin.xml old mode 100755 new mode 100644 similarity index 61% rename from src/main/resources/META-INF/plugin.xml rename to src/main/plugin/plugin.xml index 42fb32c..89aad17 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/plugin/plugin.xml @@ -1,41 +1,42 @@ - patrick.kelleter.angular-cli-quick-switch - Angular CLI QuickSwitch + @PLUGIN_ID@ + @PLUGIN_NAME@ Patrick Kelleter com.intellij.modules.platform + @INCOMPATIBLE_PLUGIN_ID@ + displayName="@PLUGIN_NAME@"/> - + + text="@ACTION_PREFIX@: Tests" description="Switches directly to and between related test files"> + text="@ACTION_PREFIX@: @CONTROLLER_ACTION_NAME@" description="@CONTROLLER_ACTION_DESCRIPTION@"> + text="@ACTION_PREFIX@: Markup" description="Switches directly to and between related markup files"> + text="@ACTION_PREFIX@: Styles" description="Switches directly to and between related style files"> diff --git a/src/main/resources/patrick/kelleter/angularcliquickswitch/QuickSwitchBranding.properties b/src/main/resources/patrick/kelleter/angularcliquickswitch/QuickSwitchBranding.properties new file mode 100644 index 0000000..983e0bf --- /dev/null +++ b/src/main/resources/patrick/kelleter/angularcliquickswitch/QuickSwitchBranding.properties @@ -0,0 +1,5 @@ +pluginName=@PLUGIN_NAME@ +category.controller=@CONTROLLER_CATEGORY_NAME@ +category.template=@TEMPLATE_CATEGORY_NAME@ +category.style=Style +category.tests=Tests From 312b6b6a289b0a0b5f5af215ce17a7a742c3d269 Mon Sep 17 00:00:00 2001 From: "Patrick K." Date: Thu, 20 Aug 2026 02:00:42 +0200 Subject: [PATCH 9/9] adjust descriptions --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 06403b3..d72456c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,7 +32,7 @@ val pluginBranding = when (pluginVariantName) { "angular" -> PluginBranding( id = "patrick.kelleter.angular-cli-quick-switch", name = "Angular CLI QuickSwitch", - description = "Angular CLI QuickSwitch cycles between related component files with one shortcut (default Alt + S). Supported file types are js, ts, html, php, haml, jade, pug, slim, css, sass, scss, less, styl, optional spec.ts/spec.js tests, and custom suffixes.", + description = "Switch between related Angular component files with configurable shortcuts. Cycle through controllers, templates, styles, and tests, jump directly to a category, and add custom suffixes.", actionPrefix = "QuickSwitch", controllerCategoryName = "Controller", controllerActionName = "Controllers", @@ -45,7 +45,7 @@ val pluginBranding = when (pluginVariantName) { "file" -> PluginBranding( id = "patrick.kelleter.file-quick-switch", name = "File QuickSwitch", - description = "File QuickSwitch cycles between related sibling files with one shortcut (default Alt + S). Supported file types are js, ts, html, php, haml, jade, pug, slim, css, sass, scss, less, styl, optional spec.ts/spec.js tests, and custom suffixes.", + description = "Switch between related sibling files with configurable shortcuts. Cycle through scripts, markup, styles, and tests, jump directly to a category, and add custom suffixes.", actionPrefix = "File QuickSwitch", controllerCategoryName = "Scripts", controllerActionName = "Scripts",