├── .clj-kondo ├── babashka │ └── fs │ │ └── config.edn ├── config.edn ├── http-kit │ └── http-kit │ │ ├── config.edn │ │ └── httpkit │ │ └── with_channel.clj └── rewrite-clj │ └── rewrite-clj │ └── config.edn ├── .dir-locals.el ├── .gitignore ├── .lsp └── config.edn ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bb.edn ├── build.gradle.kts ├── deps.edn ├── doc └── intellij-plugin-development.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pom.properties ├── pom.xml ├── settings.gradle.kts └── src ├── main ├── clojure │ └── com │ │ └── github │ │ └── ericdallo │ │ └── clj4intellij │ │ ├── action.clj │ │ ├── app_manager.clj │ │ ├── config.clj │ │ ├── extension.clj │ │ ├── extension │ │ └── nrepl_startup.clj │ │ ├── listener │ │ └── class_loader.clj │ │ ├── logger.clj │ │ ├── tasks.clj │ │ ├── test.clj │ │ └── util.clj ├── java │ └── com │ │ └── github │ │ └── ericdallo │ │ └── clj4intellij │ │ └── ClojureClassLoader.java └── resources │ ├── clj-kondo.exports │ └── ericdallo │ │ └── clj4intellij │ │ ├── clj4intellij │ │ └── proxy_plus.clj │ │ └── config.edn │ └── clojure-lsp.exports │ └── ericdallo │ └── clj4intellij │ └── config.edn └── scripts └── ci.clj /.clj-kondo/babashka/fs/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {babashka.fs/with-temp-dir clojure.core/let}} 2 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:config-paths ["ericdallo/clj4intellij"]} 2 | -------------------------------------------------------------------------------- /.clj-kondo/http-kit/http-kit/config.edn: -------------------------------------------------------------------------------- 1 | 2 | {:hooks 3 | {:analyze-call {org.httpkit.server/with-channel httpkit.with-channel/with-channel}}} 4 | -------------------------------------------------------------------------------- /.clj-kondo/http-kit/http-kit/httpkit/with_channel.clj: -------------------------------------------------------------------------------- 1 | (ns httpkit.with-channel 2 | (:require [clj-kondo.hooks-api :as api])) 3 | 4 | (defn with-channel [{node :node}] 5 | (let [[request channel & body] (rest (:children node))] 6 | (when-not (and request channel) (throw (ex-info "No request or channel provided" {}))) 7 | (when-not (api/token-node? channel) (throw (ex-info "Missing channel argument" {}))) 8 | (let [new-node 9 | (api/list-node 10 | (list* 11 | (api/token-node 'let) 12 | (api/vector-node [channel (api/vector-node [])]) 13 | request 14 | body))] 15 | 16 | {:node new-node}))) 17 | -------------------------------------------------------------------------------- /.clj-kondo/rewrite-clj/rewrite-clj/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as 2 | {rewrite-clj.zip/subedit-> clojure.core/-> 3 | rewrite-clj.zip/subedit->> clojure.core/->> 4 | rewrite-clj.zip/edit-> clojure.core/-> 5 | rewrite-clj.zip/edit->> clojure.core/->>}} 6 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((clojure-mode 2 | (cider-clojure-cli-aliases . "dev"))) 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | build 4 | bin 5 | .DS_Store 6 | .cpcache 7 | .classpath 8 | /.nrepl-port 9 | .lsp/.cache 10 | .clj-kondo 11 | !.clj-kondo/config.edn 12 | .project 13 | .settings 14 | target 15 | -------------------------------------------------------------------------------- /.lsp/config.edn: -------------------------------------------------------------------------------- 1 | {:cljfmt {:indents {proxy+ [[:block 2] [:inner 1]]}} 2 | :project-specs [{:classpath-cmd ["./gradlew" "-q" "classpath"] 3 | :project-path "build.gradle.kts"} 4 | {:classpath-cmd ["clojure" "-Spath"] 5 | :project-path "deps.edn"}]} 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## 0.8.0 6 | 7 | - Drop support of older IntelliJ versions (2021/2022). Now requires minimum IntelliJ 2023.3 (Build 233) 8 | - Bump JAVA min version to 17 9 | - Add support for tests. 10 | 11 | ## 0.7.1 12 | 13 | - Fix clojure-lsp hook 14 | 15 | ## 0.7.0 16 | 17 | - Create `def-extension` to create plugin.xml extension points easily and more idiomatic. 18 | 19 | ## 0.6.3 20 | 21 | - Add clj-kondo hook for proxy+. 22 | 23 | ## 0.6.0 24 | 25 | - Add unregister-action! and improve register-action! 26 | 27 | ## 0.5.4 28 | 29 | - Fix keyboard shortcut side effect from 0.5.3 change. 30 | 31 | ## 0.5.3 32 | 33 | - Fix keyboard shortcut register to not always add the default shortcut. 34 | 35 | ## 0.5.2 36 | 37 | - Register actions only if not registered before. 38 | 39 | ## 0.5.1 40 | 41 | - Support multiple keystrokes on action keymap. 42 | 43 | ## 0.5.0 44 | 45 | - Add `com.github.ericdallo.clj4intellij.action` ns to register actions dynamically. 46 | 47 | ## 0.4.0 48 | 49 | - Add tasks and util namespace. 50 | 51 | ## 0.3.7 52 | 53 | ## 0.3.6 54 | 55 | ## 0.3.5 56 | 57 | ## 0.3.4 58 | 59 | - Allow skip nrepl server setup if nrepl key is nullable. 60 | 61 | ## 0.3.3 62 | 63 | - Fix prod jar 64 | 65 | ## 0.3.2 66 | 67 | - Include clojure code to jar. 68 | 69 | ## 0.3.1 70 | 71 | - Support custom nrepl ports 72 | 73 | ## 0.3.0 74 | 75 | - Add NREPL and logger support. 76 | 77 | ## 0.2.1 78 | 79 | - Include clojure source to jar. 80 | 81 | ## 0.2.0 82 | 83 | - Add `com.github.ericdallo.clj4intellij.app-manager` namespace with functions to access `ApplicationManager`. 84 | 85 | ## 0.1.3 86 | 87 | - First release 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eric Dallo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Clojars Project](https://img.shields.io/clojars/v/com.github.ericdallo/clj4intellij.svg)](https://clojars.org/com.github.ericdallo/clj4intellij) 2 | 3 | # clj4intellij 4 | 5 | Library for create IntelliJ plugins with Clojure. 6 | 7 | ## How to use it? 8 | 9 | Configure [clojurephant](https://clojurephant.dev) first: 10 | 11 | `build.gradle.kts` 12 | ```kotlin 13 | plugins { 14 | ... 15 | id("dev.clojurephant.clojure") version "VERSION" 16 | } 17 | clojure.builds.named("main") { 18 | classpath.from(sourceSets.main.get().runtimeClasspath.asPath) 19 | checkAll() 20 | aotAll() 21 | reflection.set("fail") 22 | } 23 | ``` 24 | 25 | Add clj4intellij as dependency: 26 | 27 | `build.gradle.kts` 28 | ```kotlin 29 | repositories { 30 | ... 31 | maven { 32 | name = "Clojars" 33 | url = uri("https://repo.clojars.org") 34 | } 35 | } 36 | dependencies { 37 | ... 38 | implementation ("com.github.ericdallo:clj4intellij:VERSION") 39 | } 40 | ``` 41 | 42 | Add an application listener that will change the classloader on IntelliJ startup to load your plugin Clojure code: 43 | 44 | `src/main/resources/META-INF/plugin.xml` 45 | ```xml 46 | 47 | 49 | 50 | ``` 51 | 52 | Now you can create clojure namespaces in your sourcepath (ex `src/main/clojure`), use clj4intellij helpers to create extensions like `def-extension` or implement yourself extensions using Clojure's `gen-class`. 53 | 54 | ### Repl support 55 | 56 | Add this extension and after startup a random port will be logged in IntelliJ's log, then you can connect from any editor to that port to development: 57 | 58 | `src/main/resources/META-INF/plugin.xml` 59 | ```xml 60 | 61 | 62 | 63 | ``` 64 | 65 | You can specify a port to always be used in the clj4intellij config file: 66 | 67 | `src/main/resources/META-INF/clj4intellij.edn` 68 | ```xml 69 | {:nrepl {:port 9876}} 70 | ``` 71 | 72 | ### Logging 73 | 74 | There is the `com.github.ericdallo.clj4intellij.logger` ns which can be used to log messages to intelliJ's log via Clojure. 75 | 76 | ## How it works? 77 | 78 | This plugin has classes required to make possible code in Clojure a Intellij plugin changing the classloader at IntelliJ's startup. 79 | 80 | Also contains useful functions for a more Clojure idiomatic development avoid the directly use of java or Intellij API. 81 | 82 | > For more information about plugin development, read the [IntelliJ Plugin development](./doc/intellij-plugin-development.md) guide. 83 | 84 | ## Useful namespaces 85 | 86 | - `com.github.ericdallo.clj4intellij.app-manager` to handle `Application` calls. 87 | - `com.github.ericdallo.clj4intellij.action` to register actions dynamically. 88 | 89 | ## Plugins using clj4intellij 90 | 91 | - [clojure-lsp-intellij](https://github.com/clojure-lsp/clojure-lsp-intellij) 92 | - [clojure-repl-intellij](https://github.com/afucher/clojure-repl-intellij) 93 | 94 | # Support 95 | 96 | Consider support the work of this project [here](https://github.com/sponsors/ericdallo) ❤️ 97 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/scripts"] 2 | :tasks {tag ci/tag 3 | clean (shell "./gradlew clean") 4 | install ci/install 5 | deploy ci/deploy 6 | build ci/build}} 7 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | fun properties(key: String) = project.findProperty(key).toString() 2 | 3 | plugins { 4 | `maven-publish` 5 | id("org.jetbrains.kotlin.jvm") version "1.9.0" 6 | id("dev.clojurephant.clojure") version "0.7.0" 7 | id("org.jetbrains.intellij") version "1.15.0" 8 | } 9 | 10 | group = properties("group") 11 | version = properties("version") 12 | 13 | publishing { 14 | repositories { 15 | maven { 16 | // change to point to your repo, e.g. http://my.org/repo 17 | url = uri("https://repo.clojars.org") 18 | credentials { 19 | username = System.getenv("CLOJARS_USERNAME") 20 | password = System.getenv("CLOJARS_PASSWORD") 21 | } 22 | } 23 | } 24 | publications { 25 | create("maven") { 26 | from(components["java"]) 27 | pom { 28 | licenses { 29 | license { 30 | name.set("MIT License") 31 | url.set("http://www.opensource.org/licenses/mit-license.php") 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | repositories { 40 | mavenLocal() 41 | mavenCentral() 42 | maven { 43 | name = "Clojars" 44 | url = uri("https://repo.clojars.org") 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation ("org.clojure:clojure:1.11.1") 50 | implementation ("com.rpl:proxy-plus:0.0.9") 51 | implementation ("camel-snake-kebab:camel-snake-kebab:0.4.3") 52 | implementation ("nrepl:nrepl:1.0.0") 53 | } 54 | 55 | sourceSets { 56 | main { 57 | java.setSrcDirs(listOf("src/main/java")) 58 | resources.setSrcDirs(listOf("src/main/resources")) 59 | } 60 | } 61 | 62 | // Useful to override another IC platforms from env 63 | val platformVersion = System.getenv("PLATFORM_VERSION") ?: properties("platformVersion") 64 | val platformPlugins = System.getenv("PLATFORM_PLUGINS") ?: properties("platformPlugins") 65 | 66 | intellij { 67 | pluginName.set(properties("name")) 68 | version.set(platformVersion) 69 | type.set(properties("platformType")) 70 | updateSinceUntilBuild.set(false) 71 | } 72 | 73 | java { 74 | targetCompatibility = JavaVersion.VERSION_17 75 | sourceCompatibility = JavaVersion.VERSION_17 76 | } 77 | 78 | 79 | tasks.register("classpath") { 80 | doFirst { 81 | println(sourceSets["main"].compileClasspath.asPath) 82 | } 83 | } 84 | 85 | tasks { 86 | compileKotlin { 87 | kotlinOptions { 88 | jvmTarget = "11" 89 | apiVersion = "1.5" 90 | languageVersion = "1.5" 91 | freeCompilerArgs = listOf("-Xjvm-default=all") 92 | } 93 | } 94 | 95 | wrapper { 96 | gradleVersion = properties("gradleVersion") 97 | } 98 | 99 | buildSearchableOptions { 100 | enabled = false 101 | } 102 | } 103 | 104 | clojure.builds.named("main") { 105 | classpath.from(sourceSets.main.get().runtimeClasspath.asPath) 106 | checkAll() 107 | aotAll() 108 | reflection.set("fail") 109 | } 110 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main/clojure" "target/classes" "resources"]} 2 | -------------------------------------------------------------------------------- /doc/intellij-plugin-development.md: -------------------------------------------------------------------------------- 1 | # IntelliJ plugin development 2 | 3 | This docs covers the basic concepts of IntelliJ plugin development. 4 | 5 | 6 | ## Overview about IntelliJ 7 | 8 | This doc covers some common terms and short descriptions about key concepts. To go deeper and learn more, IntelliJ has a official documentation about [plugin development](https://plugins.jetbrains.com/docs/intellij/welcome.html). 9 | 10 | In some cases just the docs are not enough, so you can check the [IntelliJ community source code](https://github.com/JetBrains/intellij-community), to see the implementation code, and also find some useful examples of usage. 11 | 12 | ## IntelliJ Concepts 13 | 14 | ### Project 15 | 16 | A project encapsulates all of a project's source code, libraries, and build instructions into a single organizational unit. 17 | 18 | > Everything in the IntelliJ Platform SDK is done within the context of a project. 19 | 20 | A project defines collections referred to as Modules and Libraries. Depending on the project's logical and functional requirements, a single-module or a multi-module project can be created. 21 | 22 | Reference: [Project | IntelliJ Platform Plugin SDK](https://plugins.jetbrains.com/docs/intellij/project-model.html#project) 23 | 24 | ### Editor 25 | 26 | Represents an instance of a text editor. 27 | 28 | Once you have the Editor you can get information about Project, File and Document (handles the text of the editor). 29 | 30 | ### Actions 31 | 32 | The Action System allows plugins to add their items to IntelliJ Platform-based IDE menus and toolbars. 33 | Actions in the IntelliJ Platform require a code implementation and must be registered. The action implementation determines the contexts in which an action is available and its functionality when selected in the UI. Registration determines where an action appears in the IDE UI. Once implemented and registered, an action receives callbacks from the IntelliJ Platform in response to user gestures. 34 | 35 | Reference: [Action System | IntelliJ Platform Plugin SDK](https://plugins.jetbrains.com/docs/intellij/action-system.html) 36 | 37 | ### Run Configurations 38 | 39 | The IntelliJ Platform Execution API allows running external processes from within the IDE, e.g., the REPL connection. 40 | 41 | The name of that concept is Configurations (or Run Configurations), where the user can create/edit configurations using the UI or that can be run based on implementations created from the code. 42 | 43 | ## Clojure and Java/Kotlin 44 | 45 | IntelliJ is written in Java and Kotlin, so to develop a plugin and extends behaviors from IntelliJ you must write clases to be instantiated by IntelliJ. 46 | Since Clojure has interop with Java, the plugin development relays a lot in the interoperability: 47 | - working with IntelliJ Java/Kotlin classes; 48 | - generating classes using `:gen-class`; 49 | - making use of `reify`, `proxy` and `proxy+` to extend/instantiate classes; 50 | 51 | [clj4intellij](https://github.com/ericdallo/clj4intellij) was created to abstract some of those needs 52 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group = com.github.ericdallo 2 | name = clj4intellij 3 | version = 0.8.0 4 | 5 | # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties 6 | platformType = IC 7 | platformVersion = 2023.3 8 | 9 | # Gradle Releases -> https://github.com/gradle/gradle/releases 10 | gradleVersion = 7.6.1 11 | 12 | # Opt-out flag for bundling Kotlin standard library. 13 | # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. 14 | # suppress inspection "UnusedProperty" 15 | kotlin.stdlib.default.dependency = false 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdallo/clj4intellij/3ea9054586cbfde94f4bf53840c491aea74459cc/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-7.6.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 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pom.properties: -------------------------------------------------------------------------------- 1 | # Generated by org.clojure/tools.build 2 | # Thu Sep 07 15:58:07 BRT 2023 3 | version=0.1.2 4 | groupId=com.github.ericdallo 5 | artifactId=clj4intellij -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | jar 5 | com.github.ericdallo 6 | clj4intellij 7 | 0.1.2 8 | clj4intellij 9 | 10 | 11 | org.clojure 12 | clojure 13 | 1.11.1 14 | 15 | 16 | com.rpl 17 | proxy-plus 18 | 0.0.9 19 | 20 | 21 | 22 | src 23 | 24 | 25 | resources 26 | 27 | 28 | 29 | 30 | 31 | clojars 32 | https://repo.clojars.org/ 33 | 34 | 35 | intellij-1 36 | https://cache-redirector.jetbrains.com/intellij-dependencies 37 | 38 | 39 | intellij-2 40 | https://www.jetbrains.com/intellij-repository/releases 41 | 42 | 43 | 44 | 0.1.2 45 | 46 | 47 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "clj4intellij" 2 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/ericdallo/clj4intellij/action.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.ericdallo.clj4intellij.action 2 | (:require 3 | [com.rpl.proxy-plus :refer [proxy+]]) 4 | (:import 5 | [com.intellij.openapi.actionSystem 6 | ActionManager 7 | Anchor 8 | Constraints 9 | DefaultActionGroup 10 | KeyboardShortcut] 11 | [com.intellij.openapi.keymap KeymapManager] 12 | [com.intellij.openapi.project DumbAwareAction] 13 | [javax.swing Icon KeyStroke])) 14 | 15 | (set! *warn-on-reflection* true) 16 | 17 | (defn register-action! 18 | "Dynamically register an action if not already registered." 19 | [& {:keys [id title description icon use-shortcut-of keyboard-shortcut on-performed action] 20 | :or {action #_{:clj-kondo/ignore [:destructured-or-binding-of-same-map]} 21 | (proxy+ 22 | [^String title ^String description ^Icon icon] 23 | DumbAwareAction 24 | (actionPerformed [_ event] (on-performed event)))}}] 25 | (let [manager (ActionManager/getInstance) 26 | keymap-manager (KeymapManager/getInstance) 27 | keymap (.getActiveKeymap keymap-manager)] 28 | (when-not (.getAction manager id) 29 | (.registerAction manager id action) 30 | (when use-shortcut-of 31 | (.addShortcut keymap 32 | id 33 | (first (.getShortcuts (.getShortcutSet (.getAction manager use-shortcut-of)))))) 34 | (when keyboard-shortcut 35 | (let [k-shortcut (KeyboardShortcut. (KeyStroke/getKeyStroke ^String (:first keyboard-shortcut)) 36 | (some-> ^String (:second keyboard-shortcut) KeyStroke/getKeyStroke))] 37 | (when (empty? (.getShortcuts keymap id)) 38 | (.addShortcut keymap id k-shortcut)) 39 | (when (:replace-all keyboard-shortcut) 40 | (doseq [[conflict-action-id shortcuts] (.getConflicts keymap id k-shortcut)] 41 | (doseq [shortcut shortcuts] 42 | (.removeShortcut keymap conflict-action-id shortcut)))))) 43 | action))) 44 | 45 | (defn unregister-action [id] 46 | (let [manager (ActionManager/getInstance)] 47 | (when (.getAction manager id) 48 | (.unregisterAction manager id)))) 49 | 50 | (defn ^:private ->constraint ^Constraints [anchor relative-to] 51 | (Constraints. (case anchor 52 | :first Anchor/FIRST 53 | :last Anchor/LAST 54 | :before Anchor/BEFORE 55 | :after Anchor/AFTER) relative-to)) 56 | 57 | (defn register-group! 58 | "Dynamically register an action group if not registered yet." 59 | [& {:keys [id popup ^String text icon children]}] 60 | (let [group (DefaultActionGroup.) 61 | manager (ActionManager/getInstance)] 62 | (when-not (.getAction manager id) 63 | (when popup 64 | (.setPopup group popup)) 65 | (when text 66 | (.setText (.getTemplatePresentation group) text)) 67 | (when icon 68 | (.setIcon (.getTemplatePresentation group) icon)) 69 | (.registerAction manager id group) 70 | (doseq [{:keys [type group-id anchor relative-to ref]} children] 71 | (case type 72 | :add-to-group (.add ^DefaultActionGroup (.getAction manager group-id) group (->constraint anchor relative-to)) 73 | :reference (.add group (.getAction manager ref)) 74 | :separator (.addSeparator group))) 75 | group))) 76 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/ericdallo/clj4intellij/app_manager.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.ericdallo.clj4intellij.app-manager 2 | (:import 3 | [com.intellij.openapi.application ApplicationManager ModalityState] 4 | [com.intellij.openapi.command CommandProcessor WriteCommandAction] 5 | [com.intellij.openapi.command UndoConfirmationPolicy] 6 | [com.intellij.openapi.project Project] 7 | [com.intellij.openapi.util Computable] 8 | [com.intellij.util ThrowableRunnable])) 9 | 10 | (set! *warn-on-reflection* true) 11 | 12 | (defn invoke-later! 13 | "API for `Application/invokeLater`. 14 | Returns a promise which can be `deref` to await the result of `invoke-fn` execution. 15 | 16 | ref: https://github.com/JetBrains/intellij-community/blob/master/platform/core-api/src/com/intellij/openapi/application/Application.java#L373" 17 | [{:keys [invoke-fn ^ModalityState modality-state] 18 | :or {modality-state (ModalityState/defaultModalityState)}}] 19 | (let [p (promise)] 20 | (.invokeLater 21 | (ApplicationManager/getApplication) 22 | (fn [] 23 | (deliver p (invoke-fn))) 24 | modality-state) 25 | p)) 26 | 27 | (defn read-action! 28 | "API for `Application/runReadAction`. 29 | Returns a promise which can be `deref` to await the result of `run-fn` execution. 30 | 31 | ref: https://github.com/JetBrains/intellij-community/blob/master/platform/core-api/src/com/intellij/openapi/application/Application.java#L112" 32 | [{:keys [run-fn]}] 33 | (let [p (promise)] 34 | (.runReadAction 35 | (ApplicationManager/getApplication) 36 | (reify Computable 37 | (compute [_] 38 | (let [result (run-fn)] 39 | (deliver p result) 40 | result)))) 41 | p)) 42 | 43 | (defn write-action! 44 | "API for `Application/runWriteAction`. 45 | Returns a promise which can be `deref` to await the result of `run-fn` execution. 46 | 47 | ref: https://github.com/JetBrains/intellij-community/blob/master/platform/core-api/src/com/intellij/openapi/application/Application.java#L157" 48 | [{:keys [run-fn]}] 49 | (let [p (promise)] 50 | (.runWriteAction 51 | (ApplicationManager/getApplication) 52 | (reify Computable 53 | (compute [_] 54 | (let [result (run-fn)] 55 | (deliver p result) 56 | result)))) 57 | p)) 58 | 59 | (defn execute-command! 60 | "API for `CommandProcessor/executeCommand`. 61 | Returns a promise which can be `deref` to await the result of `command-fn` execution. 62 | 63 | ref: https://github.com/JetBrains/intellij-community/blob/master/platform/core-api/src/com/intellij/openapi/command/CommandProcessor.java" 64 | [{:keys [^String name group-id ^Project project command-fn 65 | ^UndoConfirmationPolicy undo-confirmation-policy 66 | ^Boolean record-command-for-active-document?] 67 | :or {undo-confirmation-policy UndoConfirmationPolicy/DEFAULT 68 | record-command-for-active-document? false}}] 69 | (let [p (promise)] 70 | (.executeCommand 71 | (CommandProcessor/getInstance) 72 | project 73 | (fn [] 74 | (let [result (command-fn)] 75 | (deliver p result) 76 | result)) 77 | name 78 | group-id 79 | undo-confirmation-policy 80 | record-command-for-active-document?) 81 | p)) 82 | 83 | (defn write-command-action 84 | "API for `WriteCommandAction/writeCommandAction`." 85 | [^Project project run-fn] 86 | (.run (WriteCommandAction/writeCommandAction project) 87 | (reify ThrowableRunnable 88 | (run [_] 89 | (run-fn))))) 90 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/ericdallo/clj4intellij/config.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.ericdallo.clj4intellij.config 2 | (:require 3 | [clojure.edn :as edn] 4 | [clojure.java.io :as io])) 5 | 6 | (defonce ^:private cache* (atom {})) 7 | 8 | (defn ^:private memoize-if-not-nil [f] 9 | (fn [] 10 | (if-let [cached-result (get @cache* f)] 11 | cached-result 12 | (if-let [result (f)] 13 | (do (swap! cache* assoc f result) 14 | result) 15 | nil)))) 16 | 17 | (defn ^:private config* [] 18 | (try 19 | (edn/read-string (slurp (io/resource "META-INF/clj4intellij.edn"))) 20 | (catch Exception _ nil))) 21 | 22 | (def ^:private config (memoize-if-not-nil config*)) 23 | 24 | (defn ^:private plugin* [] 25 | (try 26 | (slurp (io/resource "META-INF/plugin.xml")) 27 | (catch Exception _ nil))) 28 | 29 | (def ^:private plugin (memoize-if-not-nil plugin*)) 30 | 31 | (defn ^:private plugin-name* [] 32 | (when-let [plugin (plugin)] 33 | (last (re-find #"(.+)" plugin)))) 34 | 35 | (defn nrepl-support? [] 36 | (-> (config) :nrepl)) 37 | 38 | (defn nrepl-port [] 39 | (-> (config) :nrepl :port)) 40 | 41 | (def plugin-name (memoize-if-not-nil plugin-name*)) 42 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/ericdallo/clj4intellij/extension.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.ericdallo.clj4intellij.extension 2 | (:require 3 | [camel-snake-kebab.core :as csk])) 4 | 5 | (set! *warn-on-reflection* true) 6 | 7 | (defn ^:private flatten-1 [coll] 8 | (mapcat #(if (sequential? %) % [%]) coll)) 9 | 10 | (defn ^:private interface? [clazz] 11 | (and (class? clazz) (.isInterface ^Class clazz))) 12 | 13 | (defmacro def-extension 14 | "Defines a extension for plugin.xml, using `gen-class` to create a class 15 | that extends or implements the provided `super-class`. 16 | 17 | The generated class is only generated at compile time but the methods 18 | can be re-defined. 19 | 20 | Example: 21 | ```clojure 22 | (def-extension ClojureBraceMatcher [] 23 | PairedBraceMatcher 24 | (getPairs [this _] 25 | [,,,]) 26 | (isPairedBracesAllowedBeforeType [this _ context-type] 27 | (boolean context-type))) 28 | ```" 29 | [name super-args super-class & methods] 30 | (let [super-class-resolved (resolve super-class) 31 | prefix (str name "_") 32 | interface? (interface? super-class-resolved) 33 | init-method (when (seq super-args) 34 | (list 'defn (symbol (str prefix "init")) [] [super-args nil])) 35 | gen-class-args (->> [:name (str (csk/->snake_case (str *ns*)) "." name) 36 | :prefix prefix 37 | (when (seq super-args) [(symbol "init") (str prefix "init")]) 38 | (if interface? 39 | [:implements [super-class-resolved]] 40 | [:extends super-class-resolved])] 41 | (remove nil?) 42 | (flatten-1)) 43 | method-defs (map (fn [[name args & body]] 44 | (concat ['defn (symbol (str prefix name)) args] body)) 45 | methods)] 46 | `(do 47 | ~init-method 48 | ~@method-defs 49 | (gen-class ~@gen-class-args)))) 50 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/ericdallo/clj4intellij/extension/nrepl_startup.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.ericdallo.clj4intellij.extension.nrepl-startup 2 | (:gen-class 3 | :name com.github.ericdallo.clj4intellij.extension.NREPLStartup 4 | :implements [com.intellij.openapi.startup.StartupActivity 5 | com.intellij.openapi.project.DumbAware]) 6 | (:require 7 | [com.github.ericdallo.clj4intellij.config :as plugin] 8 | [com.github.ericdallo.clj4intellij.logger :as logger]) 9 | (:import 10 | [com.github.ericdallo.clj4intellij ClojureClassLoader] 11 | [com.intellij.openapi.project Project] 12 | [java.net ServerSocket])) 13 | 14 | (set! *warn-on-reflection* true) 15 | 16 | (defn get-open-port [] 17 | (with-open [socket (ServerSocket. 0)] 18 | (.getLocalPort socket))) 19 | 20 | (defn -runActivity [_this ^Project _] 21 | (ClojureClassLoader/bind) 22 | (if (plugin/nrepl-support?) 23 | (let [port (or (plugin/nrepl-port) 24 | (get-open-port))] 25 | (logger/info "Starting nrepl server on port" port "...") 26 | (try 27 | ((requiring-resolve 'nrepl.server/start-server) 28 | :port port) 29 | (logger/info "Started nrepl server at port" port) 30 | (catch Exception e 31 | (logger/warn "Could not start nrepl server, error:" e)))) 32 | (logger/info "Skipping nrepl server start, no config found."))) 33 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/ericdallo/clj4intellij/listener/class_loader.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.ericdallo.clj4intellij.listener.class-loader 2 | (:gen-class 3 | :name com.github.ericdallo.clj4intellij.listener.ClojureClassLoaderListener 4 | :extends com.github.ericdallo.clj4intellij.ClojureClassLoader 5 | :implements [com.intellij.ide.AppLifecycleListener]) 6 | (:require 7 | [clojure.java.io :as io]) 8 | (:import 9 | [com.github.ericdallo.clj4intellij ClojureClassLoader])) 10 | 11 | (set! *warn-on-reflection* true) 12 | 13 | (defn -appFrameCreated [_ _] 14 | (ClojureClassLoader/bind) 15 | (last (re-find #"(.+)" (slurp (io/resource "META-INF/plugin.xml"))))) 16 | 17 | (defn -welcomeScreenDisplayed [_]) 18 | (defn -appStarted [_]) 19 | (defn -projectFrameClosed [_]) 20 | (defn -projectOpenFailed [_]) 21 | (defn -appStarting [_ _]) 22 | (defn -appClosing [_]) 23 | (defn -appWillBeClosed [_ _]) 24 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/ericdallo/clj4intellij/logger.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.ericdallo.clj4intellij.logger 2 | (:require 3 | [clojure.string :as string] 4 | [com.github.ericdallo.clj4intellij.config :as plugin]) 5 | (:import 6 | [com.github.ericdallo.clj4intellij ClojureClassLoader] 7 | [com.intellij.openapi.diagnostic Logger])) 8 | 9 | (set! *warn-on-reflection* true) 10 | 11 | (defonce ^Logger logger (Logger/getInstance ClojureClassLoader)) 12 | 13 | (defn ^:private build-msg ^String [messages] 14 | (format "[%s] %s" 15 | (plugin/plugin-name) 16 | (string/join " " (mapv str messages)))) 17 | 18 | (defn info [& messages] 19 | (.info logger (build-msg messages))) 20 | 21 | (defn warn [& messages] 22 | (.warn logger (build-msg messages))) 23 | 24 | (defn error [& messages] 25 | (.error logger (build-msg messages))) 26 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/ericdallo/clj4intellij/tasks.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.ericdallo.clj4intellij.tasks 2 | (:require 3 | [com.rpl.proxy-plus :refer [proxy+]]) 4 | (:import 5 | [com.intellij.openapi.progress ProgressIndicator ProgressManager Task$Backgroundable])) 6 | 7 | (defn run-background-task! [project title run-fn] 8 | (.run (ProgressManager/getInstance) 9 | (proxy+ 10 | [project title] 11 | Task$Backgroundable 12 | (run [_ ^ProgressIndicator indicator] 13 | (run-fn indicator))))) 14 | 15 | (defn set-progress 16 | ([^ProgressIndicator indicator text] 17 | (.setText indicator text) 18 | (.setIndeterminate indicator true)) 19 | ([^ProgressIndicator indicator text percentage] 20 | (.setText indicator text) 21 | (.setIndeterminate indicator false) 22 | (.setFraction indicator (double (/ percentage 100))))) 23 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/ericdallo/clj4intellij/test.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.ericdallo.clj4intellij.test 2 | "Test utilities for clj4intellij" 3 | (:import 4 | [com.intellij.testFramework EdtTestUtil LightProjectDescriptor] 5 | [com.intellij.testFramework.fixtures CodeInsightTestFixture IdeaTestFixtureFactory TestFixtureBuilder] 6 | [com.intellij.testFramework.fixtures.impl IdeaTestFixtureFactoryImpl] 7 | [com.intellij.util ThrowableRunnable] 8 | [com.intellij.util.ui UIUtil])) 9 | 10 | 11 | (set! *warn-on-reflection* true) 12 | 13 | (defn setup 14 | "Setup fixture factory, with an empty project and return an instance of CodeInsightTestFixture 15 | 16 | ref: https://github.com/JetBrains/intellij-community/blob/2766d0bf1cec76c0478244f6ad5309af527c245e/platform/testFramework/src/com/intellij/testFramework/fixtures/CodeInsightTestFixture.java" 17 | ^CodeInsightTestFixture 18 | [project-name] 19 | (let [factory ^IdeaTestFixtureFactoryImpl (IdeaTestFixtureFactory/getFixtureFactory) 20 | raw-fixture (-> factory 21 | ^TestFixtureBuilder (.createLightFixtureBuilder LightProjectDescriptor/EMPTY_PROJECT_DESCRIPTOR project-name) 22 | (.getFixture)) 23 | fixture (.createCodeInsightFixture factory raw-fixture)] 24 | (.setUp fixture) 25 | fixture)) 26 | 27 | (defn dispatch-all 28 | "API for `UIUtil/dispatchAllInvocationEvents`. 29 | 30 | ref:https://github.com/JetBrains/intellij-community/blob/2766d0bf1cec76c0478244f6ad5309af527c245e/platform/util/ui/src/com/intellij/util/ui/UIUtil.java#L1450" 31 | [] 32 | (EdtTestUtil/runInEdtAndWait 33 | (reify ThrowableRunnable 34 | (run [_] 35 | (UIUtil/dispatchAllInvocationEvents))))) 36 | 37 | (defn dispatch-all-until 38 | "Dispatch all events in the EDT until condition is met. 39 | Returns a promise which can be `deref` to await the the condition to be met. 40 | 41 | Receives a map with the following keys: 42 | - `:cond-fn` - a function that returns true when the condition is met. 43 | - `:millis` - the time to wait between dispatches (default: 100) 44 | 45 | See `dispatch-all` for more information." 46 | [{:keys [cond-fn millis] 47 | :or {millis 100}}] 48 | (let [p (promise)] 49 | (future 50 | (loop [] 51 | (if (cond-fn) 52 | (deliver p true) 53 | (do 54 | (dispatch-all) 55 | (Thread/sleep millis) 56 | (recur))))) 57 | p)) 58 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/ericdallo/clj4intellij/util.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.ericdallo.clj4intellij.util 2 | (:require 3 | [clojure.java.io :as io]) 4 | (:import 5 | [com.intellij.openapi.editor Editor] 6 | [com.intellij.openapi.fileEditor FileEditorManager OpenFileDescriptor TextEditor] 7 | [com.intellij.openapi.project Project] 8 | [com.intellij.openapi.util.text StringUtil] 9 | [com.intellij.openapi.vfs LocalFileSystem VirtualFile])) 10 | 11 | (defn editor->cursor-position [^Editor editor] 12 | (let [offset (.. editor getCaretModel getCurrentCaret getOffset) 13 | text (.getCharsSequence (.getDocument editor)) 14 | line-col (StringUtil/offsetToLineColumn text offset)] 15 | [(.line line-col) (.column line-col)])) 16 | 17 | (defn v-file->editor ^Editor [^VirtualFile v-file ^Project project ^Boolean focus] 18 | (let [file-manager (FileEditorManager/getInstance project) 19 | editor (if (.isFileOpen file-manager v-file) 20 | (.getEditor ^TextEditor (first (.getAllEditors file-manager v-file))) 21 | (.openTextEditor file-manager (OpenFileDescriptor. project v-file) focus))] 22 | editor)) 23 | 24 | (defn uri->v-file ^VirtualFile [^String uri] 25 | (.findFileByIoFile (LocalFileSystem/getInstance) 26 | (io/file (java.net.URI. uri)))) 27 | 28 | (defn uri->editor ^Editor [^String uri ^Project project ^Boolean focus?] 29 | (let [v-file (uri->v-file uri)] 30 | (v-file->editor v-file project focus?))) 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/ericdallo/clj4intellij/ClojureClassLoader.java: -------------------------------------------------------------------------------- 1 | package com.github.ericdallo.clj4intellij; 2 | 3 | public class ClojureClassLoader { 4 | static { 5 | bind(); 6 | } 7 | public static void bind() { 8 | Thread.currentThread().setContextClassLoader(ClojureClassLoader.class.getClassLoader()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/clj-kondo.exports/ericdallo/clj4intellij/clj4intellij/proxy_plus.clj: -------------------------------------------------------------------------------- 1 | (ns clj4intellij.proxy-plus 2 | (:require 3 | [clj-kondo.hooks-api :as hooks])) 4 | 5 | (defn proxy+ 6 | "Expands a proxy+ form into a reify form with a declared proxy name. 7 | 8 | Input: 9 | (proxy+ 10 | [^String title ^String description ^Icon icon] 11 | DumbAwareAction 12 | (actionPerformed [_ event] (on-performed event))) 13 | Output: 14 | (clojure.core/let [_ [title description icon]] 15 | (clojure.core/reify 16 | DumbAwareAction 17 | (actionPerformed [_ event] (on-performed event)))) 18 | " 19 | [{{:keys [children]} :node :as _context}] 20 | (let [[_proxy+ & args] children 21 | [_proxy-name-sym super-args impls] 22 | (if (symbol? (hooks/sexpr (first args))) 23 | [(first args) 24 | (first (rest args)) 25 | (rest (rest args))] 26 | [(gensym "proxy_plus") 27 | (first args) 28 | (rest args)]) 29 | new-node (hooks/list-node 30 | [(hooks/token-node 'clojure.core/let) 31 | (hooks/vector-node [(hooks/token-node '_) 32 | super-args]) 33 | (hooks/list-node 34 | (concat [(hooks/token-node 'clojure.core/reify)] 35 | impls))])] 36 | {:node new-node 37 | :defined-by 'com.rpl.proxy-plus/proxy+})) 38 | 39 | (comment 40 | ;; (require '[clojure.repl.deps :as repl]) 41 | ;; (repl/add-lib 'clj-kondo/clj-kondo {:mvn/version "2025.01.16"}) 42 | ;; I'd love if this worked, but it's returning: 43 | ;; ; Syntax error (IllegalArgumentException) compiling fn* at (clj_kondo/impl/analysis/java.clj:156:1). 44 | ;; ; Error - no matches found for static method ASM9 in class org.objectweb.asm.Opcodes 45 | ;; So, you need to add clj-kondo to the deps.edn file in order to evaluate 46 | ;; the following code: 47 | 48 | (->> "(proxy+ ClojureModuleType [\"CLOJURE_MODULE\"] ModuleType 49 | (getName [_] \"Clojure\") 50 | (getDescription [_] \"Create programs using the Clojure language.\") 51 | (getNodeIcon [_ _] Icons/CLOJURE))" 52 | hooks/parse-string 53 | (assoc {} :node) 54 | proxy+ 55 | :node 56 | str) 57 | ;; => "(clojure.core/let [_ [\"CLOJURE_MODULE\"]] (clojure.core/reify ModuleType (getName [_] \"Clojure\") (getDescription [_] \"Create programs using the Clojure language.\") (getNodeIcon [_ _] Icons/CLOJURE)))" 58 | 59 | (->> "(proxy+ 60 | [^String title ^String description ^Icon icon] 61 | DumbAwareAction 62 | (actionPerformed [_ event] (on-performed event)))" 63 | hooks/parse-string 64 | (assoc {} :node) 65 | proxy+ 66 | :node 67 | str) 68 | ;; => "(clojure.core/let [_ [title description icon]] (clojure.core/reify DumbAwareAction (actionPerformed [_ event] (on-performed event))))" 69 | ) 70 | -------------------------------------------------------------------------------- /src/main/resources/clj-kondo.exports/ericdallo/clj4intellij/config.edn: -------------------------------------------------------------------------------- 1 | {:hooks {:analyze-call {com.rpl.proxy-plus/proxy+ clj4intellij.proxy-plus/proxy+ 2 | com.github.ericdallo.clj4intellij.extension/def-extension clj4intellij.proxy-plus/proxy+}}} 3 | -------------------------------------------------------------------------------- /src/main/resources/clojure-lsp.exports/ericdallo/clj4intellij/config.edn: -------------------------------------------------------------------------------- 1 | {:cljfmt {:extra-indents {proxy+ [[:block 2] [:inner 1]] 2 | def-extension [[:block 2] [:inner 1]]}}} 3 | -------------------------------------------------------------------------------- /src/scripts/ci.clj: -------------------------------------------------------------------------------- 1 | (ns ci 2 | (:require 3 | [babashka.fs :as fs] 4 | [babashka.tasks :refer [shell]] 5 | [clojure.string :as string])) 6 | 7 | (defn ^:private replace-in-file [file regex content] 8 | (as-> (slurp file) $ 9 | (string/replace $ regex content) 10 | (spit file $))) 11 | 12 | (defn ^:private add-changelog-entry [tag comment] 13 | (replace-in-file "CHANGELOG.md" 14 | #"## \[Unreleased\]" 15 | (if comment 16 | (format "## [Unreleased]\n\n## %s\n\n- %s" tag comment) 17 | (format "## [Unreleased]\n\n## %s" tag)))) 18 | 19 | (defn ^:private replace-tag [tag] 20 | (replace-in-file "gradle.properties" 21 | #"pluginVersion = [0-9]+.[0-9]+.[0-9]+.*" 22 | (format "pluginVersion = %s" tag)) 23 | (replace-in-file "gradle.properties" 24 | #"version = [0-9]+.[0-9]+.[0-9]+.*" 25 | (format "version = %s" tag))) 26 | 27 | #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} 28 | (defn tag [& [tag]] 29 | (shell "git fetch origin") 30 | (shell "git pull origin HEAD") 31 | (replace-tag tag) 32 | (add-changelog-entry tag nil) 33 | (shell "git add gradle.properties CHANGELOG.md") 34 | (shell (format "git commit -m \"Release: %s\"" tag)) 35 | (shell (str "git tag " tag)) 36 | (shell "git push origin HEAD") 37 | (shell "git push origin --tags")) 38 | 39 | (defn install [& _] 40 | (shell "./gradlew clean") 41 | (fs/copy-tree "src/main/clojure/" "src/main/resources/") 42 | (shell "./gradlew build publishToMavenLocal") 43 | (fs/delete-tree "src/main/resources/com/github/ericdallo/clj4intellij")) 44 | 45 | (defn build [& _] 46 | (shell "./gradlew clean") 47 | (fs/copy-tree "src/main/clojure/" "src/main/resources/") 48 | (shell "./gradlew build") 49 | (fs/delete-tree "src/main/resources/com/github/ericdallo/clj4intellij")) 50 | 51 | (defn deploy [& _] 52 | (shell "./gradlew clean") 53 | (fs/copy-tree "src/main/clojure/" "src/main/resources/") 54 | (shell "./gradlew build publish") 55 | (fs/delete-tree "src/main/resources/com/github/ericdallo/clj4intellij")) 56 | --------------------------------------------------------------------------------