├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main ├── java └── com │ └── github │ └── anonfunc │ └── vcidea │ ├── VoicecodePlugin.java │ └── commands │ ├── CloneLineCommand.java │ ├── ExtendCommand.java │ ├── FindCommand.java │ ├── GivenActionCommand.java │ ├── GotoCommand.java │ ├── LocationCommand.java │ ├── RangeCommand.java │ ├── StructureCommand.java │ └── VcCommand.java └── resources └── META-INF └── plugin.xml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for testing and preparing the plugin release in following steps: 2 | # - validate Gradle Wrapper, 3 | # - run 'test' and 'verifyPlugin' tasks, 4 | # - run 'buildPlugin' task and prepare artifact for the further tests, 5 | # - run 'runPluginVerifier' task, 6 | # - create a draft release. 7 | # 8 | # Workflow is triggered on push and pull_request events. 9 | # 10 | # GitHub Actions reference: https://help.github.com/en/actions 11 | # 12 | ## JBIJPPTPL 13 | 14 | name: Build 15 | on: 16 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests) 17 | push: 18 | branches: [master] 19 | # Trigger the workflow on any pull request 20 | pull_request: 21 | 22 | jobs: 23 | 24 | # Run Gradle Wrapper Validation Action to verify the wrapper's checksum 25 | # Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks 26 | # Build plugin and provide the artifact for the next workflow jobs 27 | build: 28 | name: Build 29 | runs-on: ubuntu-latest 30 | outputs: 31 | version: ${{ steps.properties.outputs.version }} 32 | changelog: ${{ steps.properties.outputs.changelog }} 33 | steps: 34 | 35 | # Check out current repository 36 | - name: Fetch Sources 37 | uses: actions/checkout@v2.4.0 38 | 39 | # Validate wrapper 40 | - name: Gradle Wrapper Validation 41 | uses: gradle/wrapper-validation-action@v1.0.4 42 | 43 | # Setup Java 17 environment for the next steps 44 | - name: Setup Java 45 | uses: actions/setup-java@v2 46 | with: 47 | distribution: zulu 48 | java-version: 17 49 | cache: gradle 50 | 51 | # Set environment variables 52 | - name: Export Properties 53 | id: properties 54 | shell: bash 55 | run: | 56 | PROPERTIES="$(./gradlew properties --console=plain -q)" 57 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 58 | NAME="$(echo "$PROPERTIES" | grep "^pluginName:" | cut -f2- -d ' ')" 59 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 60 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 61 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 62 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 63 | 64 | echo "::set-output name=version::$VERSION" 65 | echo "::set-output name=name::$NAME" 66 | echo "::set-output name=changelog::$CHANGELOG" 67 | echo "::set-output name=pluginVerifierHomeDir::~/.pluginVerifier" 68 | 69 | ./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier 70 | 71 | # Run tests 72 | - name: Run Tests 73 | run: ./gradlew test 74 | 75 | # Collect Tests Result of failed tests 76 | - name: Collect Tests Result 77 | if: ${{ failure() }} 78 | uses: actions/upload-artifact@v2 79 | with: 80 | name: tests-result 81 | path: ${{ github.workspace }}/build/reports/tests 82 | # 83 | # # Cache Plugin Verifier IDEs 84 | # - name: Setup Plugin Verifier IDEs Cache 85 | # uses: actions/cache@v2.1.6 86 | # with: 87 | # path: ${{ steps.properties.outputs.pluginVerifierHomeDir }}/ides 88 | # key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} 89 | # 90 | # # Run Verify Plugin task and IntelliJ Plugin Verifier tool 91 | # - name: Run Plugin Verification tasks 92 | # run: ./gradlew runPluginVerifier -Pplugin.verifier.home.dir=${{ steps.properties.outputs.pluginVerifierHomeDir }} 93 | # 94 | # # Collect Plugin Verifier Result 95 | # - name: Collect Plugin Verifier Result 96 | # if: ${{ always() }} 97 | # uses: actions/upload-artifact@v2 98 | # with: 99 | # name: pluginVerifier-result 100 | # path: ${{ github.workspace }}/build/reports/pluginVerifier 101 | 102 | - name: Build Plugin Artifact 103 | shell: bash 104 | run: ./gradlew buildPlugin 105 | # Prepare plugin archive content for creating artifact 106 | - name: Prepare Plugin Artifact 107 | id: artifact 108 | shell: bash 109 | run: | 110 | cd ${{ github.workspace }}/build/distributions 111 | FILENAME=`ls *.zip` 112 | unzip "$FILENAME" -d content 113 | 114 | echo "::set-output name=filename::$FILENAME" 115 | 116 | # Store already-built plugin as an artifact for downloading 117 | - name: Upload artifact 118 | uses: actions/upload-artifact@v2.2.4 119 | with: 120 | name: ${{ steps.artifact.outputs.filename }} 121 | path: ./build/distributions/content/*/* 122 | 123 | # Prepare a draft release for GitHub Releases page for the manual verification 124 | # If accepted and published, release workflow would be triggered 125 | releaseDraft: 126 | name: Release Draft 127 | if: github.event_name != 'pull_request' 128 | needs: build 129 | runs-on: ubuntu-latest 130 | steps: 131 | 132 | # Check out current repository 133 | - name: Fetch Sources 134 | uses: actions/checkout@v2.4.0 135 | 136 | # Remove old release drafts by using the curl request for the available releases with draft flag 137 | - name: Remove Old Release Drafts 138 | env: 139 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 140 | run: | 141 | gh api repos/{owner}/{repo}/releases \ 142 | --jq '.[] | select(.draft == true) | .id' \ 143 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 144 | 145 | # Create new release draft - which is not publicly visible and requires manual acceptance 146 | - name: Create Release Draft 147 | env: 148 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 149 | run: | 150 | gh release create v${{ needs.build.outputs.version }} \ 151 | --draft \ 152 | --title "v${{ needs.build.outputs.version }}" \ 153 | --notes "$(cat << 'EOM' 154 | ${{ needs.build.outputs.changelog }} 155 | EOM 156 | )" 157 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared 2 | # with the Build workflow. Running the publishPlugin task requires the PUBLISH_TOKEN secret provided. 3 | 4 | name: Release 5 | on: 6 | release: 7 | types: [prereleased, released] 8 | 9 | jobs: 10 | 11 | # Prepare and publish the plugin to the Marketplace repository 12 | release: 13 | name: Publish Plugin 14 | runs-on: ubuntu-latest 15 | steps: 16 | 17 | # Check out current repository 18 | - name: Fetch Sources 19 | uses: actions/checkout@v2.4.0 20 | with: 21 | ref: ${{ github.event.release.tag_name }} 22 | 23 | # Setup Java 17 environment for the next steps 24 | - name: Setup Java 25 | uses: actions/setup-java@v2 26 | with: 27 | distribution: zulu 28 | java-version: 17 29 | cache: gradle 30 | 31 | # Set environment variables 32 | - name: Export Properties 33 | id: properties 34 | shell: bash 35 | run: | 36 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 37 | ${{ github.event.release.body }} 38 | EOM 39 | )" 40 | 41 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 42 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 43 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 44 | 45 | echo "::set-output name=changelog::$CHANGELOG" 46 | 47 | # Update Unreleased section with the current release note 48 | - name: Patch Changelog 49 | if: ${{ steps.properties.outputs.changelog != '' }} 50 | env: 51 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 52 | run: | 53 | ./gradlew patchChangelog --release-note="$CHANGELOG" 54 | 55 | # Publish the plugin to the Marketplace 56 | - name: Publish Plugin 57 | env: 58 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 59 | run: ./gradlew publishPlugin 60 | 61 | # Upload artifact as a release asset 62 | - name: Upload Release Asset 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 66 | 67 | # Create pull request 68 | - name: Create Pull Request 69 | if: ${{ steps.properties.outputs.changelog != '' }} 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | run: | 73 | VERSION="${{ github.event.release.tag_name }}" 74 | BRANCH="changelog-update-$VERSION" 75 | 76 | git config user.email "action@github.com" 77 | git config user.name "GitHub Action" 78 | 79 | git checkout -b $BRANCH 80 | git commit -am "Changelog update - $VERSION" 81 | git push --set-upstream origin $BRANCH 82 | 83 | gh pr create \ 84 | --title "Changelog update - \`$VERSION\`" \ 85 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 86 | --base master \ 87 | --head $BRANCH 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea/ 3 | /build/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Changelog 4 | 5 | ## Unreleased 6 | 7 | ## [0.0.17] 8 | - Update pluginUntilBuild to support all future builds 9 | 10 | ## 0.0.16 - 2023-05-06 11 | - Update pluginUntilBuild to support 2022.1 12 | 13 | ## 0.0.13 14 | - Bump platformVersion to 2022.1 to support latest intellij 15 | 16 | ## 0.0.12 17 | 18 | ### Added 19 | - No longer modifies setting for whole-line copy on empty selection. 20 | - Recreated build from JetBrains/intellij-plugin-template 21 | 22 | ## 0.0.11 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IntelliJ support for VoiceCode (IDE plugin) 2 | 3 | This plugin adds a small web server which supports a Talon module, but can be used generically as an 4 | HTTP based RPC driven by any system on the same machine.t 5 | 6 | 7 | This adds [VoiceCode](https://voicecode.io) support for IntelliJ, in tandem with [this VoiceCode package](https://github.com/anonfunc/voicecode-intellij). 8 | 9 | This should support: 10 | 11 | - Android Studio 12 | - AppCode 13 | - CLion 14 | - DataGrip 15 | - GoLand 16 | - MPS 17 | - PhpStorm 18 | - PyCharm (Professional and Community editions) 19 | - RubyMine 20 | - WebStorm 21 | 22 | However, my primary use case is currently Java and IntelliJ CE. I will not be able to debug or reproduce issues occuring outside of the freely available IDEs. (Pull requests are welcome. :smile:) Support for these IDEs is contingent on their menu items remaining very similar, see *Limitations*. 23 | 24 | ## Manual Installation 25 | 26 | ### Download release zip, install plugin from zip. 27 | 28 | ## Testing uncommitted changes 29 | 30 | ./gradlew runIde -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.markdownToHTML 2 | 3 | fun properties(key: String) = project.findProperty(key).toString() 4 | 5 | plugins { 6 | // Java support 7 | id("java") 8 | // Gradle IntelliJ Plugin 9 | id("org.jetbrains.intellij") version "1.15.0" 10 | // Gradle Changelog Plugin 11 | id("org.jetbrains.changelog") version "2.0.0" 12 | } 13 | 14 | group = properties("pluginGroup") 15 | version = properties("pluginVersion") 16 | 17 | // Configure project's dependencies 18 | repositories { 19 | mavenCentral() 20 | } 21 | 22 | // Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin 23 | intellij { 24 | pluginName.set(properties("pluginName")) 25 | version.set(properties("platformVersion")) 26 | type.set(properties("platformType")) 27 | 28 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. 29 | plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty)) 30 | } 31 | 32 | // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin 33 | changelog { 34 | version.set(properties("pluginVersion")) 35 | groups.set(emptyList()) 36 | } 37 | 38 | 39 | tasks { 40 | // Set the JVM compatibility versions 41 | properties("javaVersion").let { 42 | withType { 43 | sourceCompatibility = it 44 | targetCompatibility = it 45 | } 46 | } 47 | 48 | buildSearchableOptions { 49 | enabled = false 50 | } 51 | 52 | wrapper { 53 | gradleVersion = properties("gradleVersion") 54 | } 55 | 56 | intellij.updateSinceUntilBuild.set(false) 57 | 58 | patchPluginXml { 59 | version.set(properties("pluginVersion")) 60 | sinceBuild.set(properties("pluginSinceBuild")) 61 | 62 | // Extract the section from README.md and provide for the plugin's manifest 63 | pluginDescription.set( 64 | projectDir.resolve("README.md").readText().lines().run { 65 | val start = "" 66 | val end = "" 67 | 68 | if (!containsAll(listOf(start, end))) { 69 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 70 | } 71 | subList(indexOf(start) + 1, indexOf(end)) 72 | }.joinToString("\n").run { markdownToHTML(this) } 73 | ) 74 | 75 | // Get the latest available change notes from the changelog file 76 | changeNotes.set(provider { 77 | changelog.run { 78 | getOrNull(properties("pluginVersion")) ?: getLatest() 79 | }.toHTML() 80 | }) 81 | } 82 | 83 | // Configure UI tests plugin 84 | // Read more: https://github.com/JetBrains/intellij-ui-test-robot 85 | runIdeForUiTests { 86 | systemProperty("robot-server.port", "8082") 87 | systemProperty("ide.mac.message.dialogs.as.sheets", "false") 88 | systemProperty("jb.privacy.policy.text", "") 89 | systemProperty("jb.consents.confirmation.enabled", "false") 90 | } 91 | 92 | signPlugin { 93 | certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) 94 | privateKey.set(System.getenv("PRIVATE_KEY")) 95 | password.set(System.getenv("PRIVATE_KEY_PASSWORD")) 96 | } 97 | 98 | publishPlugin { 99 | dependsOn("patchChangelog") 100 | token.set(System.getenv("PUBLISH_TOKEN")) 101 | // pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 102 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 103 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 104 | channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first())) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories 2 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 3 | 4 | pluginGroup = com.github.anonfunc.vcidea 5 | pluginName = Voice Code IDEA 6 | # SemVer format -> https://semver.org 7 | pluginVersion = 0.0.17 8 | 9 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 10 | # for insight into build numbers and IntelliJ Platform versions. 11 | pluginSinceBuild = 222 12 | 13 | # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties 14 | platformType = IC 15 | platformVersion = 2022.2 16 | 17 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 18 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 19 | platformPlugins = 20 | 21 | # Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 22 | javaVersion = 17 23 | 24 | # Gradle Releases -> https://github.com/gradle/gradle/releases 25 | gradleVersion = 8.1.1 26 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anonfunc/intellij-voicecode/93da702842e16701f970f49ce78f90cac968fbf1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 134 | 135 | Please set the JAVA_HOME variable in your environment to match the 136 | location of your Java installation." 137 | fi 138 | 139 | # Increase the maximum file descriptors if we can. 140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 141 | case $MAX_FD in #( 142 | max*) 143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 144 | # shellcheck disable=SC3045 145 | MAX_FD=$( ulimit -H -n ) || 146 | warn "Could not query maximum file descriptor limit" 147 | esac 148 | case $MAX_FD in #( 149 | '' | soft) :;; #( 150 | *) 151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 152 | # shellcheck disable=SC3045 153 | ulimit -n "$MAX_FD" || 154 | warn "Could not set maximum file descriptor limit to $MAX_FD" 155 | esac 156 | fi 157 | 158 | # Collect all arguments for the java command, stacking in reverse order: 159 | # * args from the command line 160 | # * the main class name 161 | # * -classpath 162 | # * -D...appname settings 163 | # * --module-path (only if needed) 164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 165 | 166 | # For Cygwin or MSYS, switch paths to Windows format before running java 167 | if "$cygwin" || "$msys" ; then 168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 170 | 171 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 172 | 173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 174 | for arg do 175 | if 176 | case $arg in #( 177 | -*) false ;; # don't mess with options #( 178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 179 | [ -e "$t" ] ;; #( 180 | *) false ;; 181 | esac 182 | then 183 | arg=$( cygpath --path --ignore --mixed "$arg" ) 184 | fi 185 | # Roll the args list around exactly as many times as the number of 186 | # args, so each arg winds up back in the position where it started, but 187 | # possibly modified. 188 | # 189 | # NB: a `for` loop captures its iteration list before it begins, so 190 | # changing the positional parameters here affects neither the number of 191 | # iterations, nor the values presented in `arg`. 192 | shift # remove old arg 193 | set -- "$@" "$arg" # push replacement arg 194 | done 195 | fi 196 | 197 | 198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 200 | 201 | # Collect all arguments for the java command; 202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 203 | # shell script including quotes and variable substitutions, so put them in 204 | # double quotes to make sure that they get re-expanded; and 205 | # * put everything else in single quotes, so that it's not re-expanded. 206 | 207 | set -- \ 208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 209 | -classpath "$CLASSPATH" \ 210 | org.gradle.wrapper.GradleWrapperMain \ 211 | "$@" 212 | 213 | # Stop when "xargs" is not available. 214 | if ! command -v xargs >/dev/null 2>&1 215 | then 216 | die "xargs is not available" 217 | fi 218 | 219 | # Use "xargs" to parse quoted args. 220 | # 221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 222 | # 223 | # In Bash we could simply go: 224 | # 225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 226 | # set -- "${ARGS[@]}" "$@" 227 | # 228 | # but POSIX shell has neither arrays nor command substitution, so instead we 229 | # post-process each arg (as a line of input to sed) to backslash-escape any 230 | # character that might be a shell metacharacter, then use eval to reverse 231 | # that process (while maintaining the separation between arguments), and wrap 232 | # the whole thing up as a single "set" statement. 233 | # 234 | # This will of course break if any of these variables contains a newline or 235 | # an unmatched quote. 236 | # 237 | 238 | eval "set -- $( 239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 240 | xargs -n1 | 241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 242 | tr '\n' ' ' 243 | )" '"$@"' 244 | 245 | exec "$JAVACMD" "$@" 246 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "IntelliJ Platform Plugin Template" 2 | -------------------------------------------------------------------------------- /src/main/java/com/github/anonfunc/vcidea/VoicecodePlugin.java: -------------------------------------------------------------------------------- 1 | package com.github.anonfunc.vcidea; 2 | 3 | import com.github.anonfunc.vcidea.commands.VcCommand; 4 | import com.intellij.ide.AppLifecycleListener; 5 | import com.intellij.notification.Notification; 6 | import com.intellij.notification.NotificationType; 7 | import com.intellij.notification.Notifications; 8 | import com.intellij.openapi.diagnostic.Logger; 9 | import com.intellij.util.PlatformUtils; 10 | import com.sun.net.httpserver.HttpExchange; 11 | import com.sun.net.httpserver.HttpHandler; 12 | import com.sun.net.httpserver.HttpServer; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.io.OutputStream; 17 | import java.net.InetAddress; 18 | import java.net.InetSocketAddress; 19 | import java.nio.file.FileSystems; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | import java.security.SecureRandom; 23 | import java.util.*; 24 | 25 | 26 | public class VoicecodePlugin implements HttpHandler, AppLifecycleListener { 27 | 28 | public static final int DEFAULT_PORT = 8652; 29 | 30 | private static final Map PLATFORM_TO_PORT = new HashMap<>(); 31 | private static final Logger LOG = Logger.getInstance(VoicecodePlugin.class); 32 | 33 | private Path pathToNonce; 34 | private HttpServer server; 35 | 36 | static { 37 | PLATFORM_TO_PORT.put(PlatformUtils.IDEA_PREFIX, 8653); 38 | PLATFORM_TO_PORT.put(PlatformUtils.IDEA_CE_PREFIX, 8654); 39 | PLATFORM_TO_PORT.put(PlatformUtils.APPCODE_PREFIX, 8655); 40 | PLATFORM_TO_PORT.put(PlatformUtils.CLION_PREFIX, 8657); 41 | PLATFORM_TO_PORT.put(PlatformUtils.PYCHARM_PREFIX, 8658); 42 | PLATFORM_TO_PORT.put(PlatformUtils.PYCHARM_CE_PREFIX, 8658); 43 | PLATFORM_TO_PORT.put(PlatformUtils.PYCHARM_EDU_PREFIX, 8658); 44 | PLATFORM_TO_PORT.put(PlatformUtils.RUBY_PREFIX, 8661); 45 | PLATFORM_TO_PORT.put(PlatformUtils.PHP_PREFIX, 8662); 46 | PLATFORM_TO_PORT.put(PlatformUtils.WEB_PREFIX, 8663); 47 | PLATFORM_TO_PORT.put(PlatformUtils.DBE_PREFIX, 8664); 48 | PLATFORM_TO_PORT.put(PlatformUtils.RIDER_PREFIX, 8660); 49 | PLATFORM_TO_PORT.put(PlatformUtils.GOIDE_PREFIX, 8659); 50 | } 51 | 52 | @Override 53 | public void appStarted() { 54 | LOG.info("Starting Voicecode plugin..."); 55 | SecureRandom random = new SecureRandom(); 56 | byte[] bytes = new byte[20]; 57 | random.nextBytes(bytes); 58 | String nonce = new String(Base64.getUrlEncoder().encode(bytes)); 59 | // String nonce = "localdev"; 60 | Integer port = PLATFORM_TO_PORT.getOrDefault(PlatformUtils.getPlatformPrefix(), DEFAULT_PORT); 61 | try { 62 | pathToNonce = FileSystems.getDefault().getPath(System.getProperty("java.io.tmpdir"), "vcidea_" + port.toString()); 63 | Files.write(pathToNonce, nonce.getBytes()); 64 | } catch (IOException e) { 65 | LOG.error("Failed to write nonce file", e); 66 | } 67 | 68 | Notification notification = new Notification("vc-idea", 69 | "Voicecode Plugin","Listening on http://localhost:" + port + "/" + nonce, 70 | NotificationType.INFORMATION); 71 | Notifications.Bus.notify(notification); 72 | 73 | // https://stackoverflow.com/questions/3732109/simple-http-server-in-java-using-only-java-se-api#3732328 74 | final InetSocketAddress loopbackSocket = new InetSocketAddress(InetAddress.getLoopbackAddress(), port); 75 | try { 76 | server = HttpServer.create(loopbackSocket, -1); 77 | } catch (IOException e) { 78 | LOG.error("Failed to start server to listen for commands", e); 79 | return; 80 | } 81 | server.createContext("/" + nonce, this); 82 | server.setExecutor(null); // creates a default executor 83 | server.start(); 84 | } 85 | 86 | @Override 87 | public void appWillBeClosed(boolean isRestart) { 88 | try { 89 | if(pathToNonce != null) { 90 | Files.delete(pathToNonce); 91 | } 92 | } catch (IOException e) { 93 | LOG.error("Failed to cleanup nonce file", e); 94 | } 95 | 96 | if(server != null) { 97 | server.stop(1); 98 | } 99 | LOG.info("Completed cleanup of Voicecode plugin"); 100 | } 101 | 102 | @Override 103 | public void handle(final HttpExchange httpExchange) throws IOException { 104 | try { 105 | LOG.debug("Handling " + httpExchange.getRequestURI().toString() + httpExchange.getRequestMethod()); 106 | final InputStream is = httpExchange.getRequestBody(); 107 | final Scanner s = new Scanner(is).useDelimiter("\\A"); 108 | var response = VcCommand.fromRequestUri(httpExchange.getRequestURI()) 109 | .map(VcCommand::run) 110 | .map(resp -> new VoicePluginResponse(200, resp)) 111 | .orElse(new VoicePluginResponse(502, "BAD")); 112 | httpExchange.sendResponseHeaders(response.responseCode(), response.response().length()); 113 | OutputStream os = httpExchange.getResponseBody(); 114 | os.write(response.response().getBytes()); 115 | os.close(); 116 | } catch (Exception e) { 117 | LOG.error("Failed to process command... ", e); 118 | final String response = e.toString(); 119 | httpExchange.sendResponseHeaders(500, response.length()); 120 | OutputStream os = httpExchange.getResponseBody(); 121 | os.write(response.getBytes()); 122 | os.close(); 123 | } 124 | } 125 | 126 | record VoicePluginResponse(int responseCode, String response) { 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/github/anonfunc/vcidea/commands/CloneLineCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.anonfunc.vcidea.commands; 2 | 3 | import com.intellij.openapi.application.Application; 4 | import com.intellij.openapi.application.ApplicationManager; 5 | import com.intellij.openapi.command.CommandProcessor; 6 | import com.intellij.openapi.diagnostic.Logger; 7 | import com.intellij.openapi.editor.Document; 8 | import com.intellij.openapi.editor.Editor; 9 | import com.intellij.openapi.project.Project; 10 | import com.intellij.openapi.util.TextRange; 11 | 12 | 13 | 14 | public class CloneLineCommand extends VcCommand { 15 | 16 | private static final Logger LOG = Logger.getInstance(StructureCommand.class); 17 | private int sourceLine; 18 | 19 | public CloneLineCommand(final int sourceLine) { 20 | this.sourceLine = sourceLine - 1; 21 | } 22 | 23 | @Override 24 | public String run() { 25 | final Application application = ApplicationManager.getApplication(); 26 | final CommandProcessor cp = CommandProcessor.getInstance(); 27 | final Project p = VcCommand.getProject(); 28 | try { 29 | application.invokeAndWait(() -> { 30 | final Editor e = VcCommand.getEditor(); 31 | final Document document = e.getDocument(); 32 | document.setReadOnly(false); 33 | final int startOffset = document.getLineStartOffset(sourceLine); 34 | final int endOffset = document.getLineEndOffset(sourceLine); 35 | final String text = document.getText(new TextRange(startOffset, endOffset)).trim(); 36 | 37 | application.runWriteAction(() -> { 38 | final int originalOffset = e.getCaretModel().getOffset(); 39 | cp.executeCommand(p, () -> document.insertString(originalOffset, text), "clone", "cloneGroup"); 40 | }); 41 | }); 42 | } catch (Exception ex) { 43 | LOG.error("Failed to run clone line command", ex); 44 | return null; 45 | } 46 | return "OK"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/anonfunc/vcidea/commands/ExtendCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.anonfunc.vcidea.commands; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.editor.Editor; 5 | import com.intellij.openapi.editor.LogicalPosition; 6 | import com.intellij.openapi.editor.ScrollType; 7 | import com.intellij.openapi.editor.SelectionModel; 8 | import com.intellij.openapi.wm.IdeFocusManager; 9 | 10 | public class ExtendCommand extends VcCommand { 11 | private int targetLine; 12 | 13 | public ExtendCommand(int line) { 14 | this.targetLine = line - 1; 15 | } 16 | 17 | @Override 18 | public String run() { 19 | 20 | ApplicationManager.getApplication().invokeAndWait(() -> { 21 | final Editor e = VcCommand.getEditor(); 22 | final SelectionModel selection = e.getSelectionModel(); 23 | final LogicalPosition current = e.getCaretModel().getLogicalPosition(); 24 | 25 | int startLine = Math.min(current.line, targetLine); 26 | int endLine = Math.max(current.line, targetLine); 27 | 28 | e.getCaretModel().moveToLogicalPosition(new LogicalPosition(startLine, 0)); 29 | int startOffset = e.getCaretModel().getOffset(); 30 | e.getCaretModel().moveToLogicalPosition(new LogicalPosition(endLine + 1, 0)); 31 | int endOffset = e.getCaretModel().getOffset() - 1; 32 | selection.setSelection(startOffset, endOffset); 33 | e.getScrollingModel().scrollToCaret(ScrollType.CENTER); 34 | IdeFocusManager.getGlobalInstance().requestFocus(e.getContentComponent(), true); 35 | }); 36 | 37 | return "OK"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/anonfunc/vcidea/commands/FindCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.anonfunc.vcidea.commands; 2 | 3 | import com.intellij.find.FindManager; 4 | import com.intellij.find.FindModel; 5 | import com.intellij.find.FindResult; 6 | import com.intellij.openapi.application.ApplicationManager; 7 | import com.intellij.openapi.editor.Document; 8 | import com.intellij.openapi.editor.Editor; 9 | import com.intellij.openapi.editor.ScrollType; 10 | import com.intellij.openapi.editor.SelectionModel; 11 | import com.intellij.openapi.project.Project; 12 | import com.intellij.openapi.project.ProjectManager; 13 | import com.intellij.openapi.wm.IdeFocusManager; 14 | 15 | public class FindCommand extends VcCommand { 16 | private String direction; 17 | private String searchTerm; 18 | 19 | public FindCommand(final String direction, final String searchTerm) { 20 | this.direction = direction; 21 | this.searchTerm = searchTerm; 22 | } 23 | 24 | @Override 25 | 26 | public String run() { 27 | ApplicationManager.getApplication().invokeAndWait(() -> { 28 | final Editor e = VcCommand.getEditor(); 29 | final Document document = e.getDocument(); 30 | final SelectionModel selection = e.getSelectionModel(); 31 | final Project project = ProjectManager.getInstance().getOpenProjects()[0]; 32 | FindManager findManager = FindManager.getInstance(project); 33 | final FindModel findModel = new FindModel(); 34 | findModel.setStringToFind(searchTerm); 35 | findModel.setCaseSensitive(false); 36 | findModel.setRegularExpressions(true); 37 | findModel.setForward(direction.equals("next")); 38 | final FindResult result = findManager.findString( 39 | document.getCharsSequence(), 40 | e.getCaretModel().getOffset(), 41 | findModel); 42 | 43 | if (result.isStringFound()) { 44 | if (direction.equals("next")) { 45 | e.getCaretModel().moveToOffset(result.getEndOffset()); 46 | } else { 47 | e.getCaretModel().moveToOffset(result.getStartOffset()); 48 | } 49 | selection.setSelection(result.getStartOffset(), result.getEndOffset()); 50 | e.getScrollingModel().scrollToCaret(ScrollType.CENTER); 51 | IdeFocusManager.getGlobalInstance().requestFocus(e.getContentComponent(), true); 52 | } 53 | }); 54 | return "OK"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/anonfunc/vcidea/commands/GivenActionCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.anonfunc.vcidea.commands; 2 | 3 | import com.intellij.openapi.actionSystem.ActionManager; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.application.ApplicationManager; 6 | import com.intellij.openapi.ui.playback.commands.ActionCommand; 7 | import com.intellij.openapi.wm.ToolWindow; 8 | 9 | import java.awt.Component; 10 | import java.awt.event.InputEvent; 11 | 12 | // Not sure if this works. XXX 13 | public class GivenActionCommand extends VcCommand { 14 | private String actionId; 15 | 16 | public GivenActionCommand(final String actionId) { 17 | this.actionId = actionId; 18 | } 19 | 20 | @Override 21 | public String run() { 22 | ApplicationManager.getApplication().invokeAndWait(() -> { 23 | AnAction action = ActionManager.getInstance().getAction(actionId); 24 | InputEvent event = ActionCommand.getInputEvent(actionId); 25 | final ToolWindow e = VcCommand.getToolWindow(); 26 | Component component = null; 27 | if (e != null) { 28 | component = e.getComponent(); 29 | } 30 | ActionManager.getInstance().tryToExecute(action, event, component, null, true); 31 | }); 32 | return "OK"; 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/anonfunc/vcidea/commands/GotoCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.anonfunc.vcidea.commands; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.editor.Editor; 5 | import com.intellij.openapi.editor.LogicalPosition; 6 | import com.intellij.openapi.editor.ScrollType; 7 | import com.intellij.openapi.wm.IdeFocusManager; 8 | 9 | public class GotoCommand extends VcCommand { 10 | private int line; 11 | private int column; 12 | 13 | public GotoCommand(final int line, final int column) { 14 | // Both count from 0, so adjust. 15 | this.line = Math.max(line - 1, 0); 16 | this.column = Math.max(column - 1, 0); 17 | } 18 | 19 | @Override 20 | public String run() { 21 | final LogicalPosition pos = new LogicalPosition(line, column); 22 | ApplicationManager.getApplication().invokeAndWait(() -> { 23 | final Editor e = VcCommand.getEditor(); 24 | e.getCaretModel().removeSecondaryCarets(); 25 | e.getCaretModel().moveToLogicalPosition(pos); 26 | e.getScrollingModel().scrollToCaret(ScrollType.CENTER); 27 | e.getSelectionModel().removeSelection(); 28 | IdeFocusManager.getGlobalInstance().requestFocus(e.getContentComponent(), true); 29 | }); 30 | return "OK"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/anonfunc/vcidea/commands/LocationCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.anonfunc.vcidea.commands; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.editor.Editor; 5 | import com.intellij.openapi.editor.LogicalPosition; 6 | 7 | public class LocationCommand extends VcCommand { 8 | 9 | public LocationCommand() { 10 | } 11 | 12 | @Override 13 | public String run() { 14 | final Editor e[] = new Editor[1]; 15 | ApplicationManager.getApplication().invokeAndWait(() -> { 16 | e[0] = VcCommand.getEditor(); 17 | }); 18 | LogicalPosition logicalPosition = e[0].getCaretModel().getLogicalPosition(); 19 | return String.format("%d %d", logicalPosition.line + 1, logicalPosition.column + 1); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/anonfunc/vcidea/commands/RangeCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.anonfunc.vcidea.commands; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.editor.Editor; 5 | import com.intellij.openapi.editor.LogicalPosition; 6 | import com.intellij.openapi.editor.ScrollType; 7 | import com.intellij.openapi.editor.SelectionModel; 8 | import com.intellij.openapi.wm.IdeFocusManager; 9 | 10 | public class RangeCommand extends VcCommand { 11 | 12 | private int startLine; 13 | private int endLine; 14 | 15 | public RangeCommand(int startLine, int endLine) { 16 | this.startLine = startLine - 1; 17 | this.endLine = endLine - 1; 18 | } 19 | 20 | @Override 21 | public String run() { 22 | ApplicationManager.getApplication().invokeAndWait(() -> { 23 | final Editor e = VcCommand.getEditor(); 24 | final SelectionModel selection = e.getSelectionModel(); 25 | e.getCaretModel().moveToLogicalPosition(new LogicalPosition(startLine, 0)); 26 | int startOffset = e.getCaretModel().getOffset(); 27 | e.getCaretModel().moveToLogicalPosition(new LogicalPosition(endLine + 1, 0)); 28 | int endOffset = e.getCaretModel().getOffset() - 1; 29 | selection.setSelection(startOffset, endOffset); 30 | e.getScrollingModel().scrollToCaret(ScrollType.CENTER); 31 | IdeFocusManager.getGlobalInstance().requestFocus(e.getContentComponent(), true); 32 | }); 33 | 34 | return "OK"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/anonfunc/vcidea/commands/StructureCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.anonfunc.vcidea.commands; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.diagnostic.Logger; 5 | import com.intellij.openapi.editor.Editor; 6 | import com.intellij.openapi.editor.ScrollType; 7 | import com.intellij.openapi.util.TextRange; 8 | import com.intellij.openapi.wm.IdeFocusManager; 9 | import com.intellij.psi.PsiElement; 10 | import com.intellij.psi.PsiFile; 11 | import com.intellij.psi.util.PsiUtilBase; 12 | import java.util.ArrayDeque; 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | import java.util.Comparator; 16 | import java.util.HashSet; 17 | import java.util.List; 18 | import java.util.Queue; 19 | import java.util.Set; 20 | 21 | public class StructureCommand extends VcCommand { 22 | 23 | private static final Logger LOG = Logger.getInstance(StructureCommand.class); 24 | 25 | private final String cursorMovement; 26 | private final String startNavType; 27 | private final String[] classes; 28 | 29 | public StructureCommand(String cursorMovement, String[] classes) { 30 | // psi start/end/range containing/previous/next Psi Classes... 31 | this.cursorMovement = cursorMovement; 32 | this.startNavType = classes[0]; 33 | this.classes = Arrays.copyOfRange(classes, 1, classes.length); 34 | } 35 | 36 | @Override 37 | public String run() { 38 | LOG.debug("psi " + this.cursorMovement + " " + this.startNavType + " " + String 39 | .join(" ", this.classes)); 40 | try { 41 | ApplicationManager.getApplication().invokeAndWait(() -> { 42 | final Editor e = VcCommand.getEditor(); 43 | final PsiFile psiFile = VcCommand.getPsiFile(); 44 | final int startingOffset = e.getCaretModel().getOffset(); 45 | PsiElement currentElement = PsiUtilBase 46 | .getElementAtOffset(psiFile, startingOffset); 47 | printHierarchy(currentElement); 48 | currentElement = parentElementOfNavType(currentElement, startNavType); 49 | LOG.debug("Containing element? " + (currentElement != null)); 50 | 51 | if (currentElement == null) { 52 | return; 53 | } 54 | 55 | for (String clazz : this.classes) { 56 | currentElement = childElementOfNavType(currentElement, clazz, startingOffset); 57 | LOG.debug("Next element(" + clazz + ") found? " + (currentElement != null)); 58 | if (currentElement == null) { 59 | return; 60 | } 61 | } 62 | // CurrentElement is where we want to go 63 | 64 | final TextRange result = currentElement.getTextRange(); 65 | if (cursorMovement.equals("start")) { 66 | e.getCaretModel().moveToOffset(result.getStartOffset()); 67 | } else if (cursorMovement.equals("end")) { 68 | e.getCaretModel().moveToOffset(result.getEndOffset()); 69 | } else { 70 | e.getCaretModel().moveToOffset(result.getStartOffset()); 71 | e.getSelectionModel().setSelection(result.getStartOffset(), result.getEndOffset()); 72 | } 73 | e.getScrollingModel().scrollToCaret(ScrollType.CENTER); 74 | IdeFocusManager.getGlobalInstance().requestFocus(e.getContentComponent(), true); 75 | 76 | }); 77 | } catch (Exception e) { 78 | LOG.error("Structure command failed", e); 79 | return null; 80 | } 81 | 82 | return "OK"; 83 | } 84 | 85 | private void printHierarchy(PsiElement element) { 86 | while (element != null && element.getNode() != null) { 87 | LOG.debug(element.toString() + " " + element.getClass().getName() + " " + element 88 | .getNavigationElement().toString() + " " + element.getNode().getElementType().toString()); 89 | 90 | element = element.getParent(); 91 | } 92 | } 93 | 94 | private PsiElement parentElementOfNavType(PsiElement element, String specifier) { 95 | final String navType = specifier.split("#")[0]; 96 | while (element != null && element.getNode() != null) { 97 | // LOG.debug(element.toString() + " " + element.getClass().getName() + " " + element 98 | // .getNavigationElement().toString() + " " + element.getNode().getElementType().toString()); 99 | if (matches(navType, element.getNode().getElementType().toString())) { 100 | return element; 101 | } 102 | element = element.getParent(); 103 | } 104 | return null; 105 | } 106 | 107 | private PsiElement childElementOfNavType(PsiElement element, String specifier, int offset) { 108 | Queue toSearch = new ArrayDeque<>(); 109 | final String[] split = specifier.split("#"); 110 | final String navType; 111 | String direction = null; 112 | int index; 113 | if (split.length == 2) { 114 | navType = split[0]; 115 | try { 116 | index = Integer.parseInt(split[1]); 117 | } catch (NumberFormatException e) { 118 | direction = split[1]; 119 | index = 0; 120 | } 121 | } else { 122 | navType = specifier; 123 | index = 0; 124 | } 125 | List results = new ArrayList<>(); 126 | Set seen = new HashSet<>(); 127 | toSearch.add(element); 128 | while (toSearch.peek() != null) { 129 | final PsiElement current = toSearch.remove(); 130 | if (current.getNode() != null && matches(navType, 131 | current.getNode().getElementType().toString())) { 132 | results.add(current); 133 | // continue; 134 | } 135 | PsiElement child = current.getFirstChild(); 136 | while (child != null) { 137 | if (!seen.contains(child)) { 138 | seen.add(child); 139 | toSearch.add(child); 140 | } 141 | child = child.getNextSibling(); 142 | } 143 | } 144 | if (results.size() == 0) { 145 | return null; 146 | } 147 | LOG.debug("Results " + results.toString()); 148 | if (direction == null) { 149 | if (index < 0) { 150 | // LOG.debug("Negative index bump " + index); 151 | index += results.size(); 152 | // LOG.debug("Negative index bump " + index); 153 | } 154 | return results.get(index); 155 | } else if (direction.equals("next")) { 156 | results.sort(Comparator.comparingInt(PsiElement::getTextOffset)); 157 | PsiElement best = null; 158 | int distance = 9999; 159 | for (PsiElement result : results) { 160 | LOG.debug("Result " + result + " offset: " + result.getTextRange().getStartOffset() + " > " + offset); 161 | final int dist = result.getTextRange().getStartOffset() - offset; 162 | if (dist > 0 && dist <= distance) { 163 | best = result; 164 | distance = dist; 165 | } 166 | } 167 | return best; 168 | } else if (direction.equals("last")) { 169 | results.sort((o1, o2) -> -Integer.compare(o1.getTextRange().getEndOffset(), o2.getTextRange().getEndOffset())); 170 | PsiElement best = null; 171 | int distance = 9999; 172 | for (PsiElement result : results) { 173 | LOG.debug( 174 | "Result " + result + " offset: " + result.getTextRange().getEndOffset() + " < " 175 | + offset); 176 | final int dist = offset - result.getTextRange().getEndOffset(); 177 | if (dist > 0 && dist <= distance) { 178 | best = result; 179 | distance = dist; 180 | } 181 | } 182 | return best; 183 | } else if (direction.equals("this")) { 184 | PsiElement best = null; 185 | int size = 999999; 186 | for (PsiElement result : results) { 187 | 188 | if (result.getTextRange().contains(offset) && result.getTextLength() < size) { 189 | best = result; 190 | size = result.getTextLength(); 191 | } 192 | } 193 | return best; 194 | } 195 | return null; 196 | } 197 | 198 | private boolean matches(String navType, String type) { 199 | final String[] matchingTypes = navType.split("\\|"); 200 | for (String matchingType : matchingTypes) { 201 | // LOG.debug("type "+ type +" matches " + matchingType + "? " + type.matches(matchingType)); 202 | if (!matchingType.startsWith("^")) { 203 | matchingType = ".*"+ matchingType; 204 | } 205 | if (type.matches(matchingType)) { 206 | return true; 207 | } 208 | } 209 | return false; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main/java/com/github/anonfunc/vcidea/commands/VcCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.anonfunc.vcidea.commands; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import com.intellij.openapi.editor.Editor; 5 | import com.intellij.openapi.fileEditor.FileEditorManager; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.openapi.wm.IdeFocusManager; 8 | import com.intellij.openapi.wm.ToolWindow; 9 | import com.intellij.openapi.wm.ToolWindowManager; 10 | import com.intellij.psi.PsiDocumentManager; 11 | import com.intellij.psi.PsiFile; 12 | import java.io.UnsupportedEncodingException; 13 | import java.net.URI; 14 | import java.net.URLDecoder; 15 | import java.util.Arrays; 16 | import java.util.Optional; 17 | 18 | public abstract class VcCommand { 19 | 20 | private static final Logger LOG = Logger.getInstance(VcCommand.class); 21 | 22 | public static Optional fromRequestUri(URI requestURI) { 23 | 24 | String[] split; 25 | try { 26 | String decode = URLDecoder.decode(requestURI.toString().substring(1), "UTF-8"); 27 | split = decode.split("/"); 28 | // XXX For debugging 29 | // Notification notification =new Notification("vc-idea", "Voicecode Plugin", decode, 30 | // NotificationType.INFORMATION); 31 | // Notifications.Bus.notify(notification); 32 | split = split[1].split(" "); 33 | } catch (UnsupportedEncodingException e) { 34 | LOG.error("Failed to parse request URI", e); 35 | return Optional.empty(); 36 | 37 | } 38 | 39 | String command = split[0]; 40 | if (command.equals("goto")) { 41 | return Optional.of(new GotoCommand(Integer.parseInt(split[1]), Integer.parseInt(split[2]))); 42 | } 43 | if (command.equals("range")) { 44 | return Optional.of(new RangeCommand(Integer.parseInt(split[1]), Integer.parseInt(split[2]))); 45 | } 46 | if (command.equals("extend")) { 47 | return Optional.of(new ExtendCommand(Integer.parseInt(split[1]))); 48 | } 49 | if (command.equals("clone")) { 50 | return Optional.of(new CloneLineCommand(Integer.parseInt(split[1]))); 51 | } 52 | if (command.equals("action")) { 53 | return Optional.of(new GivenActionCommand(split[1])); 54 | } 55 | if (command.equals("location")) { 56 | return Optional.of(new LocationCommand()); 57 | } 58 | if (command.equals("find")) { 59 | return Optional.of(new FindCommand(split[1], String.join(" ", Arrays.copyOfRange(split, 2, split.length)))); 60 | } 61 | if (command.equals("psi")) { 62 | return Optional.of(new StructureCommand(split[1], String.join(" ", Arrays.copyOfRange(split, 2, split.length)).split(","))); 63 | } 64 | return Optional.empty(); 65 | } 66 | 67 | static Editor getEditor() { 68 | Project currentProject = getProject(); 69 | Editor e = FileEditorManager.getInstance(currentProject).getSelectedTextEditor(); 70 | if (e == null) { 71 | LOG.debug("No selected editor?"); 72 | } 73 | return e; 74 | } 75 | 76 | static ToolWindow getToolWindow() { 77 | Project currentProject = getProject(); 78 | ToolWindowManager twm = ToolWindowManager.getInstance(currentProject); 79 | ToolWindow tw = twm.getToolWindow(twm.getActiveToolWindowId()); 80 | if (tw == null) { 81 | LOG.debug("No selected tool window?"); 82 | } 83 | return tw; 84 | } 85 | 86 | static PsiFile getPsiFile() { 87 | Project currentProject = getProject(); 88 | Editor e = FileEditorManager.getInstance(currentProject).getSelectedTextEditor(); 89 | final PsiFile psiFile = PsiDocumentManager.getInstance(currentProject) 90 | .getPsiFile(e.getDocument()); 91 | return psiFile; 92 | } 93 | 94 | static Project getProject() { 95 | return IdeFocusManager.findInstance().getLastFocusedFrame().getProject(); 96 | } 97 | 98 | public abstract String run(); 99 | } 100 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.github.anonfunc.vcidea 3 | Voice Code Idea 4 | dummy 5 | anonfunc 6 | 7 | This plugin adds a small web server which supports a Talon module, but can be used generically as an 9 | HTTP based RPC driven by any system on the same machine.

10 | 11 |

For an example integration, see the author's Talon files.

12 | 13 |

Support, if any, provided in the #jetbrains channel of the Talon slack. See the Talon homepage for 14 | more information about Talon.

15 | ]]>
16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | com.intellij.modules.platform 29 | com.intellij.modules.lang 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 43 |
44 | --------------------------------------------------------------------------------