├── .clj-kondo └── config.edn ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── build_plugin.yaml │ ├── publish_plugin.yaml │ └── test.yaml ├── .gitignore ├── .lsp └── config.edn ├── .projectile ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bb.edn ├── build.gradle.kts ├── deps.edn ├── doc ├── CONTRIBUTING.md ├── developing.md ├── eval-and-ns.md ├── how-to-use.md ├── shortcuts.md └── troubleshooting.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── configuration-add-new.png ├── configuration-run.png ├── configuration-settings.png ├── connected-repl.png ├── demo.png └── logo.svg ├── settings.gradle.kts └── src ├── main ├── clojure │ └── com │ │ └── github │ │ └── clojure_repl │ │ └── intellij │ │ ├── action │ │ ├── eval.clj │ │ └── test.clj │ │ ├── actions.clj │ │ ├── app_info.clj │ │ ├── config.clj │ │ ├── configuration │ │ ├── factory │ │ │ ├── base.clj │ │ │ ├── local.clj │ │ │ └── remote.clj │ │ ├── repl_type.clj │ │ └── settings_editor │ │ │ ├── local.clj │ │ │ └── remote.clj │ │ ├── db.clj │ │ ├── editor.clj │ │ ├── extension │ │ ├── color_settings_page.clj │ │ ├── init_db_startup.clj │ │ ├── inlay_esc_handler.clj │ │ ├── inlay_listener_startup.clj │ │ ├── register_actions_startup.clj │ │ └── run_test_line_marker_provider.clj │ │ ├── interop.clj │ │ ├── keyboard_manager.clj │ │ ├── nrepl.clj │ │ ├── parser.clj │ │ ├── project.clj │ │ ├── repl_command.clj │ │ ├── tests.clj │ │ ├── tool_window │ │ └── repl_test.clj │ │ └── ui │ │ ├── color.clj │ │ ├── components.clj │ │ ├── font.clj │ │ ├── hint.clj │ │ ├── inlay_hint.clj │ │ ├── repl.clj │ │ └── text.clj ├── dev-resources │ └── META-INF │ │ ├── clj4intellij.edn │ │ └── clojure-repl-intellij.edn ├── kotlin │ ├── icons.kt │ └── run-configuration-options.kt └── resources │ ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg │ └── icons │ ├── clojure-mono.svg │ ├── clojure.svg │ └── clojure_repl.svg ├── scripts └── scripts.clj └── test └── clojure └── com └── github └── clojure_repl └── intellij ├── repl_command_test.clj ├── repl_eval_test.clj └── ui └── repl_test.clj /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {com.rpl.proxy-plus/proxy+ clojure.core/reify} 2 | :linters {:warn-on-reflection {:level :info 3 | :warn-only-on-interop true} 4 | :unresolved-symbol {:exclude [(com.rpl.proxy-plus/proxy+)]}}} 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - IntelliJ Version: [e.g. IDEA 2023.2.5 (Community Edition)] 28 | - OS [e.g. MacOS, Windows, Linux] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ## Checklist 3 | - [ ] Added changes in the `Unreleased` section of CHANGELOG.md 4 | -------------------------------------------------------------------------------- /.github/workflows/build_plugin.yaml: -------------------------------------------------------------------------------- 1 | name: Build Plugin zip 2 | on: 3 | push: 4 | 5 | jobs: 6 | build-plugin: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | fetch-depth: 0 12 | 13 | - name: Gradle wrapper validate 14 | uses: gradle/actions/wrapper-validation@v4 15 | 16 | - name: Setup Java 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: 'temurin' 20 | java-version: 17 21 | cache: 'gradle' 22 | 23 | - name: Install Babashka 24 | uses: DeLaGuardo/setup-clojure@master 25 | with: 26 | bb: '1.12.196' 27 | 28 | - name: Build plugin 29 | run: bb build-plugin 30 | 31 | - name: Prepare Plugin Artifact 32 | id: artifact 33 | shell: bash 34 | run: | 35 | cd ${{ github.workspace }}/build/distributions 36 | FILENAME=`ls *.zip` 37 | unzip "$FILENAME" -d content 38 | 39 | echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT 40 | 41 | - name: Upload artifact 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: ${{ steps.artifact.outputs.filename }} 45 | path: ./build/distributions/content/*/* 46 | -------------------------------------------------------------------------------- /.github/workflows/publish_plugin.yaml: -------------------------------------------------------------------------------- 1 | name: Publish plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | jobs: 9 | publish-plugin: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Prepare java 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: 'temurin' 20 | java-version: 17 21 | cache: 'gradle' 22 | 23 | - name: Setup Gradle 24 | uses: gradle/actions/setup-gradle@v4 25 | 26 | - name: Install Babashka 27 | uses: DeLaGuardo/setup-clojure@master 28 | with: 29 | bb: '1.12.196' 30 | 31 | - name: Publish to Jetbrains 32 | env: 33 | JETBRAINS_TOKEN: ${{ secrets.JETBRAINS_TOKEN }} 34 | run: bb publish-plugin 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | 5 | permissions: 6 | contents: read 7 | checks: write 8 | pull-requests: write 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Setup Java 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'temurin' 22 | java-version: 17 23 | cache: 'gradle' 24 | 25 | - name: Install Clojure 26 | uses: DeLaGuardo/setup-clojure@master 27 | with: 28 | bb: '1.12.196' 29 | cli: 1.12.0.1530 30 | 31 | - name: Run tests 32 | run: ./gradlew test 33 | 34 | - name: Publish Test Report 35 | uses: mikepenz/action-junit-report@v5 36 | if: success() || failure() # always run even if the previous step fails 37 | with: 38 | report_paths: '**/build/test-results/test/TEST-*.xml' 39 | simplified_summary: true 40 | comment: false 41 | -------------------------------------------------------------------------------- /.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 | output.calva-repl 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 | -------------------------------------------------------------------------------- /.projectile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afucher/clojure-repl-intellij/a481e4c29a5e1ef7aaa61fc4d3e154d8c60f6c76/.projectile -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## 2.5.3 6 | 7 | - Add client info to clone op for metrics. 8 | - Bump nrepl + cider-nrepl version on local repl. 9 | 10 | ## 2.5.2 11 | 12 | - Disable "clear REPL" action when REPL is not connected. #126 13 | - Bump clj4intellij to 0.8.0 14 | - Configure project with IntelliJ integration tests (headless) 15 | 16 | ## 2.5.1 17 | 18 | - Fix REPL window horizontal scrollbar not working. 19 | - Fix REPL window broken after making any change to its layout. #144 20 | 21 | ## 2.5.0 22 | 23 | - Enhance REPL evaluations. #108 24 | - Isolate ns from REPL windows and file editors 25 | - Evaluate ns form from file automatically to avoid namespace-not-found errors. 26 | 27 | ## 2.4.0 28 | 29 | - Send to REPL eval results. #92 30 | - Fix repl input when evaluated the same input of last eval. 31 | - Fix history navigation via shortcut not working after 2.0.0. 32 | 33 | ## 2.3.0 34 | 35 | - Update repl window ns after switching ns. 36 | - Fix exception on settings page. 37 | - Fix special form evaluations. #135 38 | - Add support for JVM args on local REPL configuration. #124 39 | 40 | ## 2.2.0 41 | 42 | - Fix `eval defun at cursor` action error. #121 43 | - Create view error on test error. #128 44 | - Block backspace on repl input. 45 | 46 | ## 2.1.0 47 | 48 | - Add default name for RunConfigurations instead of save as Unnamed. #123 49 | - Add REPL syntax highlight. #18 50 | 51 | ## 2.0.0 52 | 53 | - Add icons of REPL commands to REPL window (clear and entry history navigation). #99 54 | - Drop support of older IntelliJ versions (2021/2022). Now requires minimum IntelliJ 2023.3 (Build 233) 55 | - Fix namespace-not-found error handling. Now shows a message to the user. #107 56 | - Add eval inlay hint support. #106 57 | - Add action to interrupt evals on the REPL session (`shift alt R` + `shift alt S`). #104 58 | - Add color settings page for customization of some tokens. 59 | 60 | ## 1.6.2 61 | 62 | - Fix remote config running wrongly as repl-file. 63 | 64 | ## 1.6.1 65 | 66 | - Fix error when running tests after open multiple projects 67 | 68 | ## 1.6.0 69 | 70 | - Fix shortcuts not being added after 1.5.1. 71 | - Add Re-run last test action. #93 72 | 73 | ## 1.5.1 74 | 75 | - Fix entries history navigation in REPL. 76 | - Fix Local repl configuration setup when more than one project is opened. 77 | - Fix default shortcuts being added for already customized shortcuts. #94 78 | 79 | ## 1.5.0 80 | 81 | - Add support for env vars on local repl. 82 | - Fix support for windows. #85 83 | 84 | ## 1.4.2 85 | 86 | - Fix ignore namespace metadata in REPL 87 | - add dynamic background color to REPL 88 | - add instructions to download babashka 89 | 90 | ## 1.4.1 91 | 92 | - Fix REPL input parse of functions with `>` char. #79 93 | 94 | ## 1.4.0 95 | 96 | - Fix freezing when running actions. 97 | - Add action to clear REPL output. #78 98 | - Add action to refresh all namespaces. #80 99 | - Add action to refresh changed namespaces. #81 100 | 101 | ## 1.3.1 102 | 103 | - Fix regression on 1.2.0: unable to run old local REPL configurations. 104 | 105 | ## 1.3.0 106 | 107 | - Add entry history navigation with `ctrl + PG_UP` and `ctrl + PG_DOWN` 108 | 109 | ## 1.2.0 110 | 111 | - Fix remote run configuration wrong state after open a previously saved configuration. 112 | - Add support alias for Clojure and Lein project types 113 | - Add support choosing project type for local repl instead of only guess, now we guess when creating the run configuration but let user choose a different one. 114 | 115 | ## 1.1.2 116 | 117 | - Fix ANSI chars in console removing it. #70 118 | 119 | ## 1.1.1 120 | 121 | - Fix minor regression exception on 1.1.0. 122 | 123 | ## 1.1.0 124 | 125 | - Fixes Prints to stdout/stderr do not show on REPL when happens async (tests, async threads) #65 126 | - Fix a exception that happens after some seconds of repl running 127 | - Add new eval defun action. 128 | 129 | ## 1.0.4 130 | 131 | - Fix print failing tests. #62 132 | 133 | ## 1.0.3 134 | 135 | - Fix Load file action not showing the error in case of eval error. 136 | - Fix Override configuration when editing multiple configs in same project. #42 137 | 138 | ## 1.0.2 139 | 140 | - Fix noisy exceptions introduced on 1.0.1 when opening multiple projects. 141 | 142 | ## 1.0.1 143 | 144 | - Fix support for IntelliJ 2024.1 145 | 146 | ## 1.0.0 147 | 148 | - Improve success test report message UI. 149 | - Support multiple opened projects. #51 150 | - Fix eval not using same session as load-file. #52 151 | 152 | ## 0.1.7 153 | 154 | - Use cider-nrepl middleware to support more features. 155 | - Add test support. #46 156 | - Fix freeze on evaluation. #48 157 | 158 | ## 0.1.6 159 | 160 | ## 0.1.5 161 | 162 | - Fix cwd of the spawned repl process to be the project dir. 163 | 164 | ## 0.1.4 165 | 166 | - Add support for starting REPL from inside IntelliJ. #40 167 | - Add support to read .nrepl-port file to connect to a running nREPL process. #5 168 | 169 | ## 0.1.3 170 | 171 | - Fix repl output duplicated. 172 | - Clear repl state on repl disconnection. 173 | 174 | ## 0.1.2 175 | 176 | ## 0.1.1 177 | 178 | ## 0.1.0 179 | 180 | ## 0.1.0 181 | 182 | - Connect to an existing nREPL process via host and port 183 | - Load file to REPL 184 | - Eval code at point 185 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Arthur Fücher 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![JetBrains Plugin Version](https://img.shields.io/jetbrains/plugin/v/com.github.clojure-repl?style=flat-square&labelColor=91B6FB&color=93DA52&link=https%3A%2F%2Fplugins.jetbrains.com%2Fplugin%2F23073-clojure-repl)](https://plugins.jetbrains.com/plugin/23073-clojure-repl) 2 | [![Slack community](https://img.shields.io/badge/Slack-chat-blue?style=flat-square&labelColor=91B6FB&color=93DA52)](https://clojurians.slack.com/archives/C06DZSPDCPJ) 3 | 4 | 5 | 6 | # clojure-repl-intellij 7 | 8 | 9 | 10 | Free OpenSource IntelliJ plugin for Clojure REPL development. 11 | 12 | Checkout all available [features](https://github.com/afucher/clojure-repl-intellij#features) 13 | 14 | 15 | 16 | ![Clojure LSP Intellij](images/demo.png) 17 | 18 | --- 19 | ## Getting Started 20 | After installing [the plugin](https://plugins.jetbrains.com/plugin/23073-clojure-repl) in IntelliJ, you can add a REPL to your Run 21 | configurations. 22 | 23 | ### Local: Start a nREPL server from IntelliJ 24 | 1. Go to `Run` > `Edit Configurations` 25 | 2. If you don't have any existing configurations, click `Add new...` or `Add new run configuration`. Otherwise, click the `+` ("Add New Configuration"). 26 | 3. Select `Clojure REPL` > `Local` 27 | 4. Optional: Name your Configuration (e.g. "Local REPL") 28 | 5. Click `OK` 29 | 30 | 31 | ### Remote: Connecting to an existing nREPL process 32 | 1. Ensure you have an existing nREPL process running outside IntelliJ 33 | 2. Within Intellij, go to `Run` > `Edit Configurations` 34 | 3. If you don't have any existing configurations, click `Add new...` or `Add new run configuration`. Otherwise, click the `+` ("Add New Configuration"). 35 | 4. Select `Clojure REPL` > `Remote` 36 | 5. In the boxes for `Host` and `Port` copy and paste the values from your existing nREPL process 37 | 6. Optional: Name your Configuration (e.g. "Remote REPL") 38 | 7. Click `OK` 39 | 40 | ## Features 41 | 42 | - Start a nREPL server from IntelliJ 43 | - Connect to an existing nREPL process 44 | - Load file to REPL (`alt/opt + shift + L`) 45 | - Eval code at point (`alt/opt + shift + E`) 46 | - Eval defun at point (`alt/opt + shift + D`) 47 | - Run ns tests (`alt/opt + shift + T` `alt/opt + shift + N`) 48 | - Run test at cursor (`alt/opt + shift + T` `alt/opt + shift + T`) 49 | - Re-run last test (`alt/opt + shift + T` `alt/opt + shift + A`) 50 | - Switch to file namespace (`alt/opt + shift + N`) 51 | - Clear REPL output (`alt/opt + shift + R` `alt/opt + shift + C`) 52 | - Refresh all ns (`alt/opt + shift + R` `alt/opt + shift + A`) 53 | - Refresh changed ns (`alt/opt + shift + R` `alt/opt + shift + R`) 54 | - Stop (interrupt) evaluation of REPL session (`alt/opt + shift + R` `alt/opt + shift + S`) 55 | - Entry history navigation in REPL (`ctrl + PAGE_UP` or `ctrl + PAGE_DOWN`) 56 | - Isolate REPL window and file editors namespace. [Read more about evaluation and namespace](https://github.com/afucher/clojure-repl-intellij/blob/master/doc/eval-and-ns.md) 57 | 58 | 59 | ## Contributing 60 | 61 | Contributions are very welcome, check the [issues page](https://github.com/afucher/clojure-repl-intellij/issues) for more information about what are good first issues or open an issue describing the desired support. 62 | 63 | 64 | ## Developing 65 | Check [developing doc](./doc/developing.md). 66 | 67 | ## Release 68 | 69 | 1. `bb tag x.y.z` to tag and push the new tag 70 | 2. `bb publish-plugin` to publish to Jetbrains Marketplace (requires JETBRAINS_TOKEN on env). 71 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/scripts"] 2 | :tasks {tag scripts/tag 3 | build-plugin scripts/build-plugin 4 | test scripts/tests 5 | install-plugin scripts/install-plugin 6 | run-ide scripts/run-ide 7 | publish-plugin scripts/publish-plugin}} 8 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.markdownToHTML 2 | 3 | fun properties(key: String) = project.findProperty(key).toString() 4 | 5 | plugins { 6 | id("org.jetbrains.kotlin.jvm") version "1.9.0" 7 | id("dev.clojurephant.clojure") version "0.7.0" 8 | id("org.jetbrains.intellij") version "1.15.0" 9 | id("org.jetbrains.changelog") version "1.3.1" 10 | id("org.jetbrains.grammarkit") version "2021.2.2" 11 | } 12 | 13 | group = properties("pluginGroup") 14 | version = properties("pluginVersion") 15 | 16 | repositories { 17 | mavenLocal() 18 | mavenCentral() 19 | maven { 20 | name = "Clojars" 21 | url = uri("https://repo.clojars.org") 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation ("org.clojure:clojure:1.12.0") 27 | implementation ("org.clojure:core.async:1.5.648") { 28 | because("issue https://clojure.atlassian.net/browse/ASYNC-248") 29 | } 30 | implementation ("com.github.ericdallo:clj4intellij:0.8.0") 31 | implementation ("babashka:fs:0.5.22") 32 | implementation ("com.rpl:proxy-plus:0.0.9") 33 | implementation ("seesaw:seesaw:1.5.0") 34 | implementation ("rewrite-clj:rewrite-clj:1.1.47") 35 | implementation ("nrepl:nrepl:1.3.1") 36 | 37 | testImplementation("junit:junit:latest.release") 38 | testImplementation("org.junit.platform:junit-platform-launcher:latest.release") 39 | testRuntimeOnly ("dev.clojurephant:jovial:0.4.2") 40 | } 41 | 42 | sourceSets { 43 | main { 44 | java.srcDirs("src/main", "src/gen") 45 | if (project.gradle.startParameter.taskNames.contains("buildPlugin") || 46 | project.gradle.startParameter.taskNames.contains("clojureRepl") || 47 | project.gradle.startParameter.taskNames.contains("runIde")) { 48 | resources.srcDirs("src/main/dev-resources") 49 | } 50 | } 51 | test { 52 | java.srcDirs("src/test") 53 | } 54 | } 55 | 56 | // Useful to override another IC platforms from env 57 | val platformVersion = System.getenv("PLATFORM_VERSION") ?: properties("platformVersion") 58 | val platformPlugins = System.getenv("PLATFORM_PLUGINS") ?: properties("platformPlugins") 59 | 60 | intellij { 61 | pluginName.set(properties("pluginName")) 62 | version.set(platformVersion) 63 | type.set(properties("platformType")) 64 | plugins.set(platformPlugins.split(',').map(String::trim).filter(String::isNotEmpty)) 65 | updateSinceUntilBuild.set(false) 66 | } 67 | 68 | changelog { 69 | version.set(properties("pluginVersion")) 70 | groups.set(emptyList()) 71 | } 72 | 73 | java { 74 | targetCompatibility = JavaVersion.VERSION_17 75 | sourceCompatibility = JavaVersion.VERSION_17 76 | } 77 | 78 | tasks.register("classpath") { 79 | doFirst { 80 | println(sourceSets["main"].compileClasspath.asPath) 81 | } 82 | } 83 | 84 | tasks { 85 | compileKotlin { 86 | kotlinOptions { 87 | jvmTarget = "17" 88 | apiVersion = "1.9" 89 | languageVersion = "1.9" 90 | freeCompilerArgs = listOf("-Xjvm-default=all") 91 | } 92 | } 93 | 94 | wrapper { 95 | gradleVersion = properties("gradleVersion") 96 | } 97 | 98 | patchPluginXml { 99 | version.set(properties("pluginVersion")) 100 | sinceBuild.set(properties("pluginSinceBuild")) 101 | 102 | // Extract the section from README.md and provide for the plugin's manifest 103 | pluginDescription.set( 104 | projectDir.resolve("README.md").readText().lines().run { 105 | val start = "" 106 | val end = "" 107 | 108 | if (!containsAll(listOf(start, end))) { 109 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 110 | } 111 | subList(indexOf(start) + 1, indexOf(end)) 112 | }.joinToString("\n").run { markdownToHTML(this) } 113 | ) 114 | 115 | // Get the latest available change notes from the changelog file 116 | changeNotes.set(provider { 117 | changelog.run { 118 | getOrNull(properties("pluginVersion")) ?: getLatest() 119 | }.toHTML() 120 | }) 121 | } 122 | 123 | runPluginVerifier { 124 | ideVersions.set(properties("pluginVerifierIdeVersions").split(',').map(String::trim).filter(String::isNotEmpty)) 125 | } 126 | 127 | // Configure UI tests plugin 128 | // Read more: https://github.com/JetBrains/intellij-ui-test-robot 129 | runIdeForUiTests { 130 | systemProperty("robot-server.port", "8082") 131 | systemProperty("ide.mac.message.dialogs.as.sheets", "false") 132 | systemProperty("jb.privacy.policy.text", "") 133 | systemProperty("jb.consents.confirmation.enabled", "false") 134 | } 135 | 136 | test { 137 | systemProperty("idea.mimic.jar.url.connection", "true") 138 | } 139 | 140 | signPlugin { 141 | certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) 142 | privateKey.set(System.getenv("PRIVATE_KEY")) 143 | password.set(System.getenv("PRIVATE_KEY_PASSWORD")) 144 | } 145 | 146 | publishPlugin { 147 | dependsOn("patchChangelog") 148 | token.set(System.getenv("JETBRAINS_TOKEN")) 149 | // pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 150 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 151 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 152 | channels.set(listOf("default")) 153 | } 154 | 155 | buildSearchableOptions { 156 | enabled = false 157 | } 158 | 159 | clojureRepl { 160 | dependsOn("compileClojure") 161 | classpath.from(sourceSets.main.get().runtimeClasspath 162 | + file("build/classes/kotlin/main") 163 | + file("build/clojure/main") 164 | ) 165 | // doFirst { 166 | // println(classpath.asPath) 167 | // } 168 | forkOptions.jvmArgs = listOf("--add-opens=java.desktop/java.awt=ALL-UNNAMED", 169 | "--add-opens=java.desktop/java.awt.event=ALL-UNNAMED", 170 | "--add-opens=java.desktop/sun.awt=ALL-UNNAMED", 171 | "--add-opens=java.desktop/sun.font=ALL-UNNAMED", 172 | "--add-opens=java.base/java.lang=ALL-UNNAMED", 173 | "-Djava.system.class.loader=com.intellij.util.lang.PathClassLoader", 174 | "-Didea.mimic.jar.url.connection=true", 175 | "-Didea.force.use.core.classloader=true" 176 | ) 177 | } 178 | } 179 | 180 | tasks.withType().configureEach { 181 | useJUnitPlatform() 182 | } 183 | 184 | grammarKit { 185 | jflexRelease.set("1.7.0-1") 186 | grammarKitRelease.set("2021.1.2") 187 | intellijRelease.set("203.7717.81") 188 | } 189 | 190 | clojure.builds.named("main") { 191 | classpath.from(sourceSets.main.get().runtimeClasspath.asPath + "build/classes/kotlin/main") 192 | checkAll() 193 | aotAll() 194 | reflection.set("fail") 195 | } 196 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main/clojure"] 2 | :aliases {:test {:extra-paths ["src/test/clojure"]}}} 3 | -------------------------------------------------------------------------------- /doc/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to clojure-repl-intellij 2 | 3 | First off, thanks for taking the time to contribute! 4 | 5 | If you like the project, but just don’t have time to contribute, that’s fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 6 | 7 | - Star the project 8 | - Talk about it on social media 9 | - Refer this project in your blog posts 10 | - Mention the project at local meetups and tell your friends/colleagues 11 | 12 | 13 | ## Do 14 | 15 | - remember that a gift, while appreciated, is also a burden. We value your input but start with an issue to propose your change before investing your valuable time in a PR. 16 | - read the [clojure-repl-intellij developing guide](./developing.md). 17 | - include/update tests for your change. 18 | - ensure that the Continuous Integration checks pass. 19 | - feel free to pester the project maintainers about your PR if it hasn't been responded to. Sometimes notifications can be missed. 20 | 21 | 22 | ## Don't 23 | 24 | - include more than one feature or fix in a single PR. 25 | - include changes unrelated to the purpose of the PR. This includes changing the project version number, adding lines to the .gitignore file, or changing the indentation or formatting. 26 | - open a new PR if changes are requested. Just push to the same branch and the PR will be updated. 27 | - overuse vertical whitespace; avoid multiple sequential blank lines. 28 | 29 | Inspired by [rewrite-clj](https://github.com/clj-commons/rewrite-clj/blob/main/CONTRIBUTING.md) 30 | -------------------------------------------------------------------------------- /doc/developing.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | ⚠️ Before start developing, if you are not familiar with developing IntelliJ plugins, is recommended to read the [IntelliJ Plugin Development](https://github.com/ericdallo/clj4intellij/blob/master/doc/intellij-plugin-development.md) doc to onboard you. 4 | 5 | The flow to develop the plugin is usually: 6 | 1. build locally; 7 | 2. install in a local IntelliJ; 8 | 3. connect REPl and evaluate your changes; 9 | 10 | ## Prerequisites 11 | 12 | Java JDK 17 or above 13 | [Babashka](https://github.com/babashka/babashka#installation): Used to make run required tasks easier during development. 14 | 15 | 16 | ## Build and run locally 17 | 18 | There is some ways to build and run this plugin inside the InteliJ locally, they are listed below in the order that we consider that is more efficient when developing: 19 | 20 | `bb install-plugin` to builds the plugin, and install it from disk in IntelliJ automatically, then restart your IntelliJ. You need to pass the IntelliJ plugins path: 21 | e.g: ```bb install-plugin /home/youruser/.local/share/JetBrains/IdeaIC2024.3``` 22 | 23 | or 24 | 25 | `bb build-plugin` to build the plugin, then install it manually from disk in IntelliJ, the zip should be on `./build/distributions/*.zip`. IntelliJ will ask to restart to get the new version. 26 | 27 | or 28 | 29 | `bb run-ide` to spawn a new IntelliJ session with the plugin. 30 | 31 | ## NREPL 32 | 33 | Unless you need to edit some generated extension file or kotlin file, mostly clojure code is editable via repl while your plugin is running! 34 | 35 | NREPL is included in the plugin during development, so you can jack in and edit most of the plugin behavior while running it. 36 | 37 | It runs on port `7770`, you can configure the port in the [clj4intellij.edn](../src/main/dev-resources/META-INF/clj4intellij.edn) file. 38 | -------------------------------------------------------------------------------- /doc/eval-and-ns.md: -------------------------------------------------------------------------------- 1 | # Eval and ns 2 | 3 | This document describes how this plugin handle evaluation and namespaces for different buffers (files and REPL window). 4 | 5 | ## REPL window 6 | The REPL window uses by default the `user` namespace. 7 | If you want to change the namespace from it you can use the `Switch REPL namespace (alt + shift + N)` action or use the `in-ns` function. 8 | The window shows the namespace in the last line, where you can type to eval your code: 9 | ``` 10 | user> *ns* 11 | => #namespace[user] 12 | user> 13 | ``` 14 | 15 | ## Files 16 | Each file uses by default its own namespace for evaluation, if they do not have a ns form, in this case the default is `user`. 17 | The only exception is the `ns` form, that uses the `user` namespace to avoid `namespace-not-found` error. 18 | When evaluating a code for the first time in a namespace, the `ns` form is evaluated first to also avoid `namespace-not-found` error. 19 | If you want to change the namespace from it you can use the `in-ns` function, after that all evaluations from this file will start to use the new namespace. 20 | -------------------------------------------------------------------------------- /doc/how-to-use.md: -------------------------------------------------------------------------------- 1 | # How to use 2 | 3 | ## Starting the REPL 4 | 5 | ### Configuration 6 | 7 | Create a new Run Configuration of type `Clojure REPL` 8 | 9 | ![Run Configuration](../images/configuration-add-new.png) 10 | 11 | Fill in the host and port of your REPL server. 12 | Also, check if the selected project is the one you want to use. 13 | 14 | ![Configuration settings](../images/configuration-settings.png) 15 | 16 | ### Running 17 | 18 | Run the configuration you just created. 19 | 20 | ![Run Configuration](../images/configuration-run.png) 21 | 22 | A new REPL window should appear, and you should be able to evaluate code in it. 23 | 24 | ![REPL window](../images/connected-repl.png) 25 | -------------------------------------------------------------------------------- /doc/shortcuts.md: -------------------------------------------------------------------------------- 1 | # Shortcuts 2 | 3 | ## Actions 4 | 5 | | Shortcut | Description | 6 | |--------------------------------------------|----------------------------| 7 | | `alt/opt + shift + E` | Evaluate last sexp | 8 | | `alt/opt +shift + D` | Evaluate defun at cursor | 9 | | `alt/opt +shift + L` | Load file | 10 | | `alt/opt +shift + T` `alt/opt + shift + T` | Run test at cursor | 11 | | `alt/opt +shift + T` `alt/opt + shift + N` | Run ns tests | 12 | | `alt/opt +shift + T` `alt/opt + shift + A` | Re-run last test | 13 | | `alt/opt +shift + N` | Switch namespace | 14 | | `alt/opt +shift + R` `alt/opt + shift + C` | Clear REPL output | 15 | | `alt/opt +shift + R` `alt/opt + shift + A` | Refresh all namespaces | 16 | | `alt/opt +shift + R` `alt/opt + shift + R` | Refresh changed namespaces | 17 | | `alt/opt +shift + R` `alt/opt + shift + S` | Interrupt session eval | 18 | | `ctrl + PAGE_UP` or `ctrl + PAGE_DOWN` | REPL history navigation | 19 | -------------------------------------------------------------------------------- /doc/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## I can't connect to the REPL 4 | 5 | Please check the Configuration settings, the host and port should match the ones from your REPL server. 6 | 7 | ## Shortcuts are not working 8 | 9 | Shortcuts in IntelliJ can not work for different reasons, like Conflicting shortcuts of different plugins or keystroke not reaching IDE due to an external program. 10 | Follow the [Keyboard Shortcuts Troubleshooting](https://www.jetbrains.com/help/idea/keyboard-shortcuts-troubleshooting.html) guide to solve your issue. If they do not work, please open [an issue](https://github.com/afucher/clojure-repl-intellij/issues) 11 | 12 | ## Finding logs 13 | 14 | You can access the IntelliJ logs from the menu `Help > Show Log in Finder` or `Help > Show Log in Explorer` depending on your OS. 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories 2 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 3 | 4 | pluginGroup = com.github.clojure-repl-intellij 5 | pluginName = clojure-repl 6 | pluginVersion = 2.5.3 7 | 8 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 9 | # for insight into build numbers and IntelliJ Platform versions. 10 | pluginSinceBuild = 233 11 | 12 | # Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl 13 | # See https://jb.gg/intellij-platform-builds-list for available build versions. 14 | pluginVerifierIdeVersions = 2023.3 15 | 16 | # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties 17 | platformType = IC 18 | platformVersion = 2023.3 19 | 20 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 21 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 22 | platformPlugins = 23 | 24 | # Gradle Releases -> https://github.com/gradle/gradle/releases 25 | gradleVersion = 7.6.1 26 | 27 | # Opt-out flag for bundling Kotlin standard library. 28 | # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. 29 | # suppress inspection "UnusedProperty" 30 | kotlin.stdlib.default.dependency = false 31 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afucher/clojure-repl-intellij/a481e4c29a5e1ef7aaa61fc4d3e154d8c60f6c76/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 | -------------------------------------------------------------------------------- /images/configuration-add-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afucher/clojure-repl-intellij/a481e4c29a5e1ef7aaa61fc4d3e154d8c60f6c76/images/configuration-add-new.png -------------------------------------------------------------------------------- /images/configuration-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afucher/clojure-repl-intellij/a481e4c29a5e1ef7aaa61fc4d3e154d8c60f6c76/images/configuration-run.png -------------------------------------------------------------------------------- /images/configuration-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afucher/clojure-repl-intellij/a481e4c29a5e1ef7aaa61fc4d3e154d8c60f6c76/images/configuration-settings.png -------------------------------------------------------------------------------- /images/connected-repl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afucher/clojure-repl-intellij/a481e4c29a5e1ef7aaa61fc4d3e154d8c60f6c76/images/connected-repl.png -------------------------------------------------------------------------------- /images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afucher/clojure-repl-intellij/a481e4c29a5e1ef7aaa61fc4d3e154d8c60f6c76/images/demo.png -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | > 54 | _ 66 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "clojure-repl-intellij" 2 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/action/eval.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.action.eval 2 | (:require 3 | [clojure.string :as string] 4 | [com.github.clojure-repl.intellij.actions :as actions] 5 | [com.github.clojure-repl.intellij.db :as db] 6 | [com.github.clojure-repl.intellij.nrepl :as nrepl] 7 | [com.github.clojure-repl.intellij.parser :as parser] 8 | [com.github.clojure-repl.intellij.ui.hint :as ui.hint] 9 | [com.github.clojure-repl.intellij.ui.inlay-hint :as ui.inlay-hint] 10 | [com.github.clojure-repl.intellij.ui.repl :as ui.repl] 11 | [com.github.ericdallo.clj4intellij.app-manager :as app-manager] 12 | [com.github.ericdallo.clj4intellij.tasks :as tasks] 13 | [com.github.ericdallo.clj4intellij.util :as util] 14 | [rewrite-clj.zip :as z]) 15 | (:import 16 | [com.intellij.openapi.actionSystem CommonDataKeys] 17 | [com.intellij.openapi.actionSystem AnActionEvent] 18 | [com.intellij.openapi.editor Editor] 19 | [com.intellij.openapi.vfs VirtualFile])) 20 | 21 | (set! *warn-on-reflection* true) 22 | 23 | (defn ^:private send-result-to-repl [^AnActionEvent event text prefix?] 24 | (ui.repl/append-output 25 | (actions/action-event->project event) 26 | (str "\n" (if prefix? "=> " "") text))) 27 | 28 | (defn ^:private eval-action 29 | [& {:keys [^AnActionEvent event loading-msg eval-fn success-msg-fn post-success-fn inlay-hint-feedback?] 30 | :or {post-success-fn identity}}] 31 | (when-let [editor ^Editor (.getData event CommonDataKeys/EDITOR_EVEN_IF_INACTIVE)] 32 | (let [project (.getProject editor)] 33 | (if (db/get-in project [:current-nrepl :session-id]) 34 | (tasks/run-background-task! 35 | project 36 | loading-msg 37 | (fn [_indicator] 38 | (let [{:keys [status err] :as response} (eval-fn editor)] 39 | (app-manager/invoke-later! 40 | {:invoke-fn 41 | (fn [] 42 | (cond 43 | (and (contains? status "eval-error") err) 44 | (ui.hint/show-repl-error :message err :editor editor) 45 | 46 | (contains? status "namespace-not-found") 47 | (ui.hint/show-error {:message (str "Namespace not found: " (:ns response)) :editor editor}) 48 | 49 | :else 50 | (do 51 | (if inlay-hint-feedback? 52 | (ui.inlay-hint/show-code (success-msg-fn response) editor) 53 | (ui.hint/show-repl-info :message (success-msg-fn response) :editor editor)) 54 | (post-success-fn response))))})))) 55 | (ui.hint/show-error :message "No REPL connected" :editor editor))))) 56 | 57 | (defn load-file-action [^AnActionEvent event] 58 | (when-let [virtual-file ^VirtualFile (.getData event CommonDataKeys/VIRTUAL_FILE)] 59 | (let [msg (str "Loaded file " (.getPath ^VirtualFile virtual-file))] 60 | (eval-action 61 | :event event 62 | :loading-msg "REPL: Loading file" 63 | :eval-fn (fn [^Editor editor] 64 | (nrepl/load-file (.getProject editor) editor virtual-file)) 65 | :success-msg-fn (fn [_response] msg) 66 | :post-success-fn (fn [_response] 67 | (send-result-to-repl event (str ";; " msg) false)))))) 68 | 69 | (defn eval-last-sexpr-action [^AnActionEvent event] 70 | (when-let [editor ^Editor (.getData event CommonDataKeys/EDITOR_EVEN_IF_INACTIVE)] 71 | (let [[row col] (util/editor->cursor-position editor)] 72 | (eval-action 73 | :event event 74 | :loading-msg "REPL: Evaluating" 75 | :eval-fn (fn [^Editor editor] 76 | (let [text (.getText (.getDocument editor)) 77 | root-zloc (z/of-string text) 78 | zloc (parser/find-form-at-pos root-zloc (inc row) col) 79 | special-form? (contains? #{:quote :syntax-quote :var :reader-macro} (-> zloc z/up z/tag)) 80 | code (if special-form? 81 | (-> zloc z/up z/string) 82 | (z/string zloc))] 83 | (nrepl/eval-from-editor {:editor editor :code code}))) 84 | :success-msg-fn (fn [response] 85 | (string/join "\n" (:value response))) 86 | :post-success-fn (fn [response] 87 | (send-result-to-repl event (string/join "\n" (:value response)) true)) 88 | :inlay-hint-feedback? true)))) 89 | 90 | (defn interrupt [^AnActionEvent event] 91 | (-> event 92 | actions/action-event->project 93 | nrepl/interrupt)) 94 | 95 | (defn eval-defun-action [^AnActionEvent event] 96 | (when-let [editor ^Editor (.getData event CommonDataKeys/EDITOR_EVEN_IF_INACTIVE)] 97 | (let [[row col] (util/editor->cursor-position editor)] 98 | (eval-action 99 | :event event 100 | :loading-msg "REPL: Evaluating" 101 | :eval-fn (fn [^Editor editor] 102 | (let [text (.getText (.getDocument editor)) 103 | root-zloc (z/of-string text) 104 | zloc (-> (parser/find-form-at-pos root-zloc (inc row) col) 105 | parser/to-top) 106 | code (z/string zloc)] 107 | (nrepl/eval-from-editor {:editor editor :code code}))) 108 | :success-msg-fn (fn [response] 109 | (string/join "\n" (:value response))) 110 | :post-success-fn (fn [response] 111 | (send-result-to-repl event (string/join "\n" (:value response)) true)) 112 | :inlay-hint-feedback? true)))) 113 | 114 | (defn clear-repl-output-action [^AnActionEvent event] 115 | (let [project (actions/action-event->project event)] 116 | (ui.repl/clear-repl project))) 117 | 118 | (defn history-up-action [^AnActionEvent event] 119 | (-> event 120 | actions/action-event->project 121 | ui.repl/history-up)) 122 | 123 | (defn history-down-action [^AnActionEvent event] 124 | (-> event 125 | actions/action-event->project 126 | ui.repl/history-down)) 127 | 128 | (defn switch-ns-action [^AnActionEvent event] 129 | (eval-action 130 | :event event 131 | :loading-msg "REPL: Switching ns" 132 | :eval-fn (fn [^Editor editor] 133 | (let [text (.getText (.getDocument editor)) 134 | root-zloc (z/of-string text) 135 | zloc (parser/find-namespace root-zloc) 136 | namespace (parser/remove-metadata (z/string zloc))] 137 | (nrepl/switch-ns {:project (.getProject editor) :ns namespace}))) 138 | :success-msg-fn (fn [response] 139 | (string/join "\n" (:value response))) 140 | :post-success-fn (fn [_response] 141 | (ui.repl/clear-input (actions/action-event->project event))))) 142 | 143 | (defn refresh-all-action [^AnActionEvent event] 144 | (let [msg "Refreshed all sucessfully"] 145 | (eval-action 146 | :event event 147 | :loading-msg "REPL: Refreshing all ns" 148 | :eval-fn (fn [^Editor editor] 149 | (nrepl/refresh-all (.getProject editor))) 150 | :success-msg-fn (fn [response] 151 | (if (contains? (:status response) "ok") 152 | msg 153 | (str "Refresh failed:\n" (:error response)))) 154 | :post-success-fn (fn [_response] 155 | (send-result-to-repl event (str ";; " msg) false))))) 156 | 157 | (defn refresh-changed-action [^AnActionEvent event] 158 | (let [msg "Refreshed sucessfully"] 159 | (eval-action 160 | :event event 161 | :loading-msg "REPL: Refreshing changed ns" 162 | :eval-fn (fn [^Editor editor] 163 | (nrepl/refresh (.getProject editor))) 164 | :success-msg-fn (fn [response] 165 | (if (contains? (:status response) "ok") 166 | msg 167 | (str "Refresh failed:\n" (:error response)))) 168 | :post-success-fn (fn [_response] 169 | (send-result-to-repl event (str ";; " msg) false))))) 170 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/action/test.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.action.test 2 | (:require 3 | [com.github.clojure-repl.intellij.tests :as tests]) 4 | (:import 5 | [com.intellij.openapi.actionSystem CommonDataKeys] 6 | [com.intellij.openapi.actionSystem AnActionEvent] 7 | [com.intellij.openapi.editor Editor])) 8 | 9 | (set! *warn-on-reflection* true) 10 | 11 | (defn run-ns-tests-action [^AnActionEvent event] 12 | (when-let [editor ^Editor (.getData event CommonDataKeys/EDITOR_EVEN_IF_INACTIVE)] 13 | (tests/run-ns-tests editor))) 14 | 15 | (defn run-cursor-test-action [^AnActionEvent event] 16 | (when-let [editor ^Editor (.getData event CommonDataKeys/EDITOR_EVEN_IF_INACTIVE)] 17 | (tests/run-at-cursor editor))) 18 | 19 | (defn re-run-test-action [^AnActionEvent event] 20 | (when-let [editor ^Editor (.getData event CommonDataKeys/EDITOR_EVEN_IF_INACTIVE)] 21 | (tests/re-run-test editor))) 22 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/actions.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.actions 2 | (:import 3 | [com.intellij.openapi.actionSystem AnActionEvent CommonDataKeys] 4 | [com.intellij.openapi.editor Editor] 5 | [com.intellij.openapi.project Project])) 6 | 7 | (set! *warn-on-reflection* true) 8 | 9 | (defn action-event->project ^Project [^AnActionEvent event] 10 | (let [editor ^Editor (.getData event CommonDataKeys/EDITOR_EVEN_IF_INACTIVE) 11 | project ^Project (or (.getData event CommonDataKeys/PROJECT) 12 | (.getProject editor))] 13 | project)) 14 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/app_info.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.app-info 2 | (:import 3 | [com.intellij.openapi.application ApplicationInfo] 4 | [com.intellij.openapi.util BuildNumber])) 5 | 6 | (set! *warn-on-reflection* true) 7 | 8 | (defn at-least-version? [version] 9 | (>= (.compareTo (.withoutProductCode (.getBuild (ApplicationInfo/getInstance))) 10 | (BuildNumber/fromString version)) 11 | 0)) 12 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/config.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.config 2 | (:require 3 | [clojure.edn :as edn] 4 | [clojure.java.io :as io]) 5 | (:import 6 | [com.intellij.ide.plugins PluginManagerCore] 7 | [com.intellij.openapi.extensions PluginId])) 8 | 9 | (set! *warn-on-reflection* true) 10 | 11 | (defonce ^:private cache* (atom {})) 12 | 13 | (defn ^:private memoize-if-not-nil [f] 14 | (fn [] 15 | (if-let [cached-result (get @cache* f)] 16 | cached-result 17 | (if-let [result (f)] 18 | (do (swap! cache* assoc f result) 19 | result) 20 | nil)))) 21 | 22 | (defn ^:private config* [] 23 | (try 24 | (edn/read-string (slurp (io/resource "META-INF/clojure-repl-intellij.edn"))) 25 | (catch Exception _ nil))) 26 | 27 | (def ^:private config (memoize-if-not-nil config*)) 28 | 29 | (defn nrepl-debug? [] 30 | (-> (config) :nrepl-debug)) 31 | 32 | (defn plugin-version* [] 33 | (.getVersion (PluginManagerCore/getPlugin (PluginId/getId "com.github.clojure-repl")))) 34 | 35 | (def plugin-version (memoize plugin-version*)) 36 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/configuration/factory/base.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.configuration.factory.base 2 | (:require 3 | [com.github.clojure-repl.intellij.db :as db] 4 | [com.github.clojure-repl.intellij.nrepl :as nrepl] 5 | [com.github.clojure-repl.intellij.ui.repl :as ui.repl] 6 | [com.rpl.proxy-plus :refer [proxy+]]) 7 | (:import 8 | [com.intellij.execution.ui ConsoleView] 9 | [com.intellij.ide ActivityTracker] 10 | [com.intellij.ide.plugins PluginManagerCore] 11 | [com.intellij.openapi.actionSystem ActionManager AnAction] 12 | [com.intellij.openapi.extensions PluginId] 13 | [com.intellij.openapi.project Project])) 14 | 15 | (set! *warn-on-reflection* true) 16 | 17 | (defn ^:private initial-repl-text [project] 18 | (let [{:keys [clojure java nrepl]} (db/get-in project [:current-nrepl :versions])] 19 | (str (format ";; Connected to nREPL server - nrepl://%s:%s\n" 20 | (db/get-in project [:current-nrepl :nrepl-host]) 21 | (db/get-in project [:current-nrepl :nrepl-port])) 22 | (format ";; Clojure REPL Intellij %s\n" 23 | (.getVersion 24 | (PluginManagerCore/getPlugin (PluginId/getId "com.github.clojure-repl")))) 25 | (format ";; Clojure %s, Java %s, nREPL %s" 26 | (:version-string clojure) 27 | (:version-string java) 28 | (:version-string nrepl))))) 29 | 30 | (defn ^:private build-console-actions 31 | [] 32 | (let [manager (ActionManager/getInstance) 33 | clear-repl (.getAction manager "ClojureREPL.ClearReplOutput") 34 | history-up (.getAction manager "ClojureREPL.HistoryUp") 35 | history-down (.getAction manager "ClojureREPL.HistoryDown") 36 | interrupt (.getAction manager "ClojureREPL.Interrupt")] 37 | [clear-repl history-up history-down interrupt])) 38 | 39 | (defn build-console-view [project loading-text] 40 | (db/assoc-in! project [:console :process-handler] nil) 41 | (db/assoc-in! project [:console :ui] (ui.repl/build-console 42 | project 43 | {:on-eval (fn [code] 44 | (nrepl/eval-from-repl 45 | {:project project 46 | :code code 47 | :ns (db/get-in project [:current-nrepl :ns])}))})) 48 | (ui.repl/append-output project loading-text) 49 | (proxy+ [] ConsoleView 50 | (getComponent [_] (db/get-in project [:console :ui])) 51 | (getPreferredFocusableComponent [_] (db/get-in project [:console :ui])) 52 | (dispose [_]) 53 | (print [_ _ _]) 54 | (clear [_]) 55 | (scrollTo [_ _]) 56 | (attachToProcess [_ _]) 57 | (setOutputPaused [_ _]) 58 | (isOutputPaused [_] false) 59 | (hasDeferredOutput [_] false) 60 | (performWhenNoDeferredOutput [_ _]) 61 | (setHelpId [_ _]) 62 | (addMessageFilter [_ _]) 63 | (printHyperlink [_ _ _]) 64 | (getContentSize [_] 0) 65 | (canPause [_] false) 66 | (createConsoleActions [_] (into-array AnAction (build-console-actions))) 67 | (allowHeavyFilters [_]))) 68 | 69 | (defn ^:private on-repl-evaluated [project {:keys [out err]}] 70 | (when err 71 | (ui.repl/append-output project (str "\n" err))) 72 | (when out 73 | (ui.repl/append-output project (str "\n" out)))) 74 | 75 | (defn ^:private on-ns-changed [project _] 76 | (ui.repl/clear-input project)) 77 | 78 | (defn ^:private trigger-ui-update 79 | "IntelliJ actions status (visibility/enable) depend on IntelliJ calls an update of the UI 80 | but the call of update is not guaranteed. This function triggers the update of the UI. 81 | @see https://github.com/JetBrains/intellij-community/blob/08d00166f92aaf0eedfa6fc9c147ef10ea86da27/platform/editor-ui-api/src/com/intellij/openapi/actionSystem/AnAction.java#L361" 82 | [_ _] 83 | (.inc (ActivityTracker/getInstance))) 84 | 85 | (defn repl-disconnected [^Project project] 86 | (ui.repl/close-console project (db/get-in project [:console :ui])) 87 | (db/assoc-in! project [:console :process-handler] nil) 88 | (db/assoc-in! project [:console :ui] nil) 89 | (db/assoc-in! project [:current-nrepl] nil) 90 | (db/update-in! project [:on-repl-evaluated-fns] (fn [fns] (remove #(contains? #{on-repl-evaluated trigger-ui-update} %) fns)))) 91 | 92 | (defn repl-started [project extra-initial-text] 93 | (nrepl/start-client! 94 | :project project 95 | :on-receive-async-message (fn [msg] 96 | (when (:out msg) 97 | (ui.repl/append-output project (:out msg))))) 98 | (nrepl/clone-session project) 99 | (let [description (nrepl/describe project)] 100 | (when (:out-subscribe (:ops description)) 101 | (nrepl/out-subscribe project)) 102 | (db/assoc-in! project [:current-nrepl :ops] (:ops description)) 103 | (db/assoc-in! project [:current-nrepl :versions] (:versions description)) 104 | (db/assoc-in! project [:current-nrepl :entry-history] '()) 105 | (db/assoc-in! project [:current-nrepl :entry-index] -1) 106 | (db/assoc-in! project [:current-nrepl :ns] "user") 107 | (db/assoc-in! project [:file->ns] {}) 108 | (ui.repl/set-repl-started-initial-text project 109 | (db/get-in project [:console :ui]) 110 | (str (initial-repl-text project) extra-initial-text)) 111 | (db/update-in! project [:on-repl-evaluated-fns] #(conj % on-repl-evaluated trigger-ui-update)) 112 | (db/update-in! project [:on-ns-changed-fns] #(conj % on-ns-changed trigger-ui-update)))) 113 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/configuration/factory/local.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.configuration.factory.local 2 | (:require 3 | [clojure.string :as string] 4 | [com.github.clojure-repl.intellij.configuration.factory.base :as config.factory.base] 5 | [com.github.clojure-repl.intellij.db :as db] 6 | [com.github.clojure-repl.intellij.interop :as interop] 7 | [com.github.clojure-repl.intellij.project :as project] 8 | [com.github.clojure-repl.intellij.repl-command :as repl-command] 9 | [com.github.clojure-repl.intellij.ui.repl :as ui.repl] 10 | [com.github.ericdallo.clj4intellij.logger :as logger] 11 | [com.rpl.proxy-plus :refer [proxy+]]) 12 | (:import 13 | [com.github.clojure_repl.intellij.configuration ReplLocalRunOptions] 14 | [com.intellij.execution.configurations 15 | CommandLineState 16 | ConfigurationFactory 17 | ConfigurationType 18 | GeneralCommandLine 19 | RunConfigurationBase] 20 | [com.intellij.execution.process ColoredProcessHandler ProcessEvent ProcessListener] 21 | [com.intellij.execution.runners ExecutionEnvironment] 22 | [com.intellij.openapi.project Project ProjectManager] 23 | [com.intellij.util.io BaseOutputReader$Options] 24 | [java.nio.charset Charset])) 25 | 26 | (set! *warn-on-reflection* false) 27 | (defn ^:private project-name [configuration] (.getProject (.getOptions configuration))) 28 | (defn ^:private project-type [configuration] (keyword (.getProjectType (.getOptions configuration)))) 29 | (defn ^:private aliases [configuration] (seq (.getAliases (.getOptions configuration)))) 30 | (defn ^:private env-vars [configuration] 31 | (into {} 32 | (mapv #(string/split % #"=") 33 | (seq (.getEnvVars (.getOptions configuration)))))) 34 | (defn ^:private jvm-args [configuration] 35 | (into {} 36 | (mapv #(string/split % #"=") 37 | (seq (.getJvmArgs (.getOptions configuration)))))) 38 | (set! *warn-on-reflection* true) 39 | 40 | (def ^:private options-class ReplLocalRunOptions) 41 | 42 | (defn ^:private repl-started-initial-text [command] 43 | (str "\n;; Startup: " command)) 44 | 45 | (defn ^:private process-output->nrepl-uri [text] 46 | (or (when-let [[_ port] (re-find #"nREPL server started on port (\d+)" text)] 47 | ["localhost" (parse-long port)]) 48 | (when-let [[_ host port] (re-find #"Started nREPL server at (\w+):(\d+)" text)] 49 | [host (parse-long port)]))) 50 | 51 | (defn ^:private setup-process [^Project project command env-vars] 52 | (let [command-str (string/join " " command) 53 | command-line (doto (GeneralCommandLine. ^java.util.List command) 54 | (.setCharset (Charset/forName "UTF-8")) 55 | (.setWorkDirectory (.getBasePath project)) 56 | (.withEnvironment env-vars)) 57 | handler (proxy [ColoredProcessHandler] [command-line] 58 | (readerOptions [] 59 | (BaseOutputReader$Options/forMostlySilentProcess)))] 60 | (db/assoc-in! project [:console :process-handler] handler) 61 | (.addProcessListener handler 62 | (proxy+ [] ProcessListener 63 | (onTextAvailable [_ ^ProcessEvent event _key] 64 | (if-let [[host port] (process-output->nrepl-uri (.getText event))] 65 | (do 66 | (db/assoc-in! project [:current-nrepl :nrepl-host] host) 67 | (db/assoc-in! project [:current-nrepl :nrepl-port] port) 68 | (config.factory.base/repl-started project (repl-started-initial-text command-str))) 69 | (ui.repl/append-output project (.getText event)))) 70 | (processWillTerminate [_ _ _] (config.factory.base/repl-disconnected project)))) 71 | (logger/info "Starting nREPL process:" command-str) 72 | handler)) 73 | 74 | (defn configuration-factory ^ConfigurationFactory [^ConfigurationType type] 75 | (proxy [ConfigurationFactory] [type] 76 | (getId [] "clojure-repl-local") 77 | (getName [] "Local") 78 | (getOptionsClass [] options-class) 79 | (createTemplateConfiguration 80 | ([project _] 81 | (.createTemplateConfiguration ^ConfigurationFactory this project)) 82 | ([^Project project] 83 | (proxy [RunConfigurationBase] [project this "Start a local nREPL process"] 84 | (getConfigurationEditor [] 85 | (interop/new-class "com.github.clojure_repl.intellij.configuration.settings_editor.Local")) 86 | 87 | (getState 88 | ([]) 89 | ([executor ^ExecutionEnvironment env] 90 | (let [project ^Project (->> (ProjectManager/getInstance) 91 | .getOpenProjects 92 | (filter #(= (project-name this) (.getName ^Project %))) 93 | first) 94 | config-project-type (project-type this) 95 | project-type (if (contains? project/known-project-types config-project-type) 96 | config-project-type 97 | (project/project->project-type project)) 98 | command (repl-command/project->repl-start-command project-type (aliases this) (jvm-args this)) 99 | env-vars (env-vars this)] 100 | (proxy [CommandLineState] [env] 101 | (createConsole [_] 102 | (config.factory.base/build-console-view project "Starting nREPL server via: ")) 103 | (startProcess [] 104 | (setup-process project command env-vars))))))))))) 105 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/configuration/factory/remote.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.configuration.factory.remote 2 | (:require 3 | [clojure.java.io :as io] 4 | [com.github.clojure-repl.intellij.configuration.factory.base :as config.factory.base] 5 | [com.github.clojure-repl.intellij.db :as db] 6 | [com.github.clojure-repl.intellij.interop :as interop] 7 | [com.github.ericdallo.clj4intellij.logger :as logger] 8 | [com.rpl.proxy-plus :refer [proxy+]]) 9 | (:import 10 | [com.github.clojure_repl.intellij.configuration ReplRemoteRunOptions] 11 | [com.intellij.execution.configurations 12 | CommandLineState 13 | ConfigurationFactory 14 | ConfigurationType 15 | RunConfigurationBase] 16 | [com.intellij.execution.process NopProcessHandler ProcessListener] 17 | [com.intellij.execution.runners ExecutionEnvironment] 18 | [com.intellij.openapi.project Project])) 19 | 20 | (set! *warn-on-reflection* false) 21 | (defn ^:private nrepl-host [configuration] (.getNreplHost (.getOptions configuration))) 22 | (defn ^:private nrepl-port [configuration] (.getNreplPort (.getOptions configuration))) 23 | (defn ^:private mode [configuration] (keyword (.getMode (.getOptions configuration)))) 24 | (set! *warn-on-reflection* true) 25 | 26 | (def ^:private options-class ReplRemoteRunOptions) 27 | 28 | (defn ^:private read-port-file? [configuration] 29 | (= (mode configuration) :file-config)) 30 | 31 | (defn ^:private setup-process [configuration project] 32 | (logger/info "Connecting to nREPL process...") 33 | (let [host (nrepl-host configuration) 34 | port (nrepl-port configuration)] 35 | (db/assoc-in! project [:current-nrepl :nrepl-host] host) 36 | (db/assoc-in! project [:current-nrepl :nrepl-port] (parse-long port)) 37 | (when (read-port-file? configuration) 38 | (let [base-path (.getBasePath ^Project project) 39 | repl-file (io/file base-path ".nrepl-port") 40 | ;TODO: handle when file does not exist 41 | port (slurp repl-file)] 42 | (db/assoc-in! project [:current-nrepl :nrepl-port] (parse-long port)))) 43 | 44 | (let [handler (NopProcessHandler.)] 45 | (db/assoc-in! project [:console :process-handler] handler) 46 | (.addProcessListener handler 47 | (proxy+ [] ProcessListener 48 | (startNotified [_ _] (config.factory.base/repl-started project "")) 49 | (processWillTerminate [_ _ _] (config.factory.base/repl-disconnected project)))) 50 | handler))) 51 | 52 | (defn configuration-factory ^ConfigurationFactory [^ConfigurationType type] 53 | (proxy [ConfigurationFactory] [type] 54 | (getId [] "clojure-repl-remote") 55 | (getName [] "Remote") 56 | (getOptionsClass [] options-class) 57 | (createTemplateConfiguration 58 | ([project _] 59 | (.createTemplateConfiguration ^ConfigurationFactory this project)) 60 | ([^Project project] 61 | (proxy [RunConfigurationBase] [project this "Connect to an existing nREPL process"] 62 | (getConfigurationEditor [] 63 | (interop/new-class "com.github.clojure_repl.intellij.configuration.settings_editor.Remote")) 64 | 65 | (getState 66 | ([]) 67 | ([executor ^ExecutionEnvironment env] 68 | (proxy+ [env] CommandLineState 69 | (createConsole [_ _] 70 | (config.factory.base/build-console-view project "Connecting to nREPL server...")) 71 | (startProcess [_] 72 | (setup-process this project)))))))))) 73 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/configuration/repl_type.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.configuration.repl-type 2 | (:gen-class 3 | :init init 4 | :constructors {[] [String String String com.intellij.openapi.util.NotNullLazyValue]} 5 | :name com.github.clojure_repl.intellij.configuration.ReplRunConfigurationType 6 | :extends com.intellij.execution.configurations.ConfigurationTypeBase) 7 | (:require 8 | [com.github.clojure-repl.intellij.configuration.factory.local :as config.factory.local] 9 | [com.github.clojure-repl.intellij.configuration.factory.remote :as config.factory.remote]) 10 | (:import 11 | [com.github.clojure_repl.intellij Icons] 12 | [com.intellij.execution.configurations 13 | ConfigurationFactory] 14 | [com.intellij.openapi.util NotNullFactory NotNullLazyValue])) 15 | 16 | (set! *warn-on-reflection* true) 17 | 18 | (defn -init [] 19 | [["clojure-repl" 20 | "Clojure REPL" 21 | "Connect to a local or remote REPL" 22 | (NotNullLazyValue/createValue 23 | (reify NotNullFactory (create [_] Icons/CLOJURE)))] nil]) 24 | 25 | (defn -getConfigurationFactories [this] 26 | (into-array ConfigurationFactory [(config.factory.local/configuration-factory this) 27 | (config.factory.remote/configuration-factory this)])) 28 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/configuration/settings_editor/local.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.configuration.settings-editor.local 2 | (:gen-class 3 | :state state 4 | :init init 5 | :name com.github.clojure_repl.intellij.configuration.settings_editor.Local 6 | :extends com.intellij.openapi.options.SettingsEditor) 7 | (:require 8 | [com.github.clojure-repl.intellij.project :as project] 9 | [com.github.clojure-repl.intellij.ui.components :as ui.components] 10 | [seesaw.core :as seesaw] 11 | [seesaw.layout] 12 | [seesaw.mig :as mig]) 13 | (:import 14 | [com.github.clojure_repl.intellij.configuration ReplLocalRunOptions] 15 | [com.intellij.execution.configurations RunConfigurationBase] 16 | [com.intellij.openapi.project Project ProjectManager] 17 | [com.intellij.ui IdeBorderFactory])) 18 | 19 | (set! *warn-on-reflection* true) 20 | 21 | (defn ^:private build-editor-ui [] 22 | (let [opened-projects (.getOpenProjects (ProjectManager/getInstance)) 23 | project-type (name (or (project/project->project-type (first opened-projects)) :clojure))] 24 | (mig/mig-panel 25 | :border (IdeBorderFactory/createTitledBorder "nREPL connection") 26 | :items (->> [[(seesaw/label "Project") ""] 27 | [(seesaw/combobox :id :project 28 | :model (mapv #(.getName ^Project %) opened-projects)) "wrap"] 29 | [(seesaw/label "Type") ""] 30 | [(doto 31 | (seesaw/combobox :id :project-type 32 | :model (map name project/types)) 33 | (seesaw/selection! project-type)) "wrap"] 34 | (ui.components/multi-field 35 | :label (if (= "lein" project-type) "Profiles" "Aliases") 36 | :group-id :aliases-group 37 | :button-id :add-alias 38 | :prefix-id "alias-" 39 | :columns 10 40 | :initial-text "") 41 | (ui.components/multi-field 42 | :label "Env vars" 43 | :group-id :env-vars-group 44 | :button-id :add-env-var 45 | :prefix-id "env-var-" 46 | :columns 15 47 | :initial-text "EXAMPLE_VAR=foo") 48 | (ui.components/multi-field 49 | :label "JVM args" 50 | :group-id :jvm-args-group 51 | :button-id :add-jvm-arg 52 | :prefix-id "jvm-arg-" 53 | :columns 15 54 | :initial-text "-Dexample=foo")] 55 | flatten 56 | (remove nil?) 57 | (partition 2) 58 | vec)))) 59 | 60 | (defn -init [] 61 | [[] (atom (build-editor-ui))]) 62 | 63 | (defn ^:private update-configuration-name [^RunConfigurationBase configuration] 64 | (when (contains? #{"Unnamed" ""} (.getName configuration)) 65 | (.setName configuration "Local REPL"))) 66 | 67 | (set! *warn-on-reflection* false) 68 | 69 | (defn -createEditor [this] 70 | @(.state this)) 71 | 72 | (defn -applyEditorTo [this configuration] 73 | (update-configuration-name configuration) 74 | (let [ui @(.state this) 75 | options ^ReplLocalRunOptions (.getOptions configuration) 76 | project-path (seesaw/text (seesaw/select ui [:#project])) 77 | type (seesaw/text (seesaw/select ui [:#project-type])) 78 | aliases (ui.components/field-values-from-multi-field ui :aliases-group) 79 | env-vars (ui.components/field-values-from-multi-field ui :env-vars-group) 80 | jvm-args (ui.components/field-values-from-multi-field ui :jvm-args-group)] 81 | (.setProject options project-path) 82 | (.setProjectType options type) 83 | (.setAliases options aliases) 84 | (.setEnvVars options env-vars) 85 | (.setJvmArgs options jvm-args))) 86 | 87 | (defn -resetEditorFrom [this configuration] 88 | (update-configuration-name configuration) 89 | (let [ui @(.state this) 90 | options ^ReplLocalRunOptions (.getOptions configuration) 91 | project-name (or (not-empty (.getProject options)) 92 | (-> (ProjectManager/getInstance) .getOpenProjects first .getName)) 93 | project (->> (ProjectManager/getInstance) 94 | .getOpenProjects 95 | (filter #(= project-name (.getName ^Project %))) 96 | first) 97 | type (or (some-> options .getProjectType not-empty) 98 | (name (project/project->project-type project))) 99 | aliases (.getAliases options) 100 | env-vars (.getEnvVars options) 101 | jvm-args (.getJvmArgs options)] 102 | (seesaw/selection! (seesaw/select ui [:#project-type]) type) 103 | (seesaw/text! (seesaw/select ui [:#project]) project-name) 104 | (doseq [alias aliases] 105 | (ui.components/add-field-to-multi-field! ui :aliases-group alias)) 106 | (doseq [env-var env-vars] 107 | (ui.components/add-field-to-multi-field! ui :env-vars-group env-var)) 108 | (doseq [jvm-arg jvm-args] 109 | (ui.components/add-field-to-multi-field! ui :jvm-args-group jvm-arg)))) 110 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/configuration/settings_editor/remote.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.configuration.settings-editor.remote 2 | (:gen-class 3 | :state state 4 | :init init 5 | :name com.github.clojure_repl.intellij.configuration.settings_editor.Remote 6 | :extends com.intellij.openapi.options.SettingsEditor) 7 | (:require 8 | [seesaw.core :as seesaw] 9 | [seesaw.mig :as mig]) 10 | (:import 11 | [com.intellij.execution.configurations RunConfigurationBase] 12 | [com.intellij.openapi.project Project ProjectManager] 13 | [com.intellij.ui IdeBorderFactory] 14 | [javax.swing JRadioButton])) 15 | 16 | (set! *warn-on-reflection* false) 17 | 18 | (defn ^:private mode-id-key [repl-mode] 19 | (->> (seesaw/selection repl-mode) 20 | (seesaw/id-of))) 21 | 22 | (defn ^:private build-editor-ui [] 23 | (let [repl-mode-group (seesaw/button-group) 24 | panel (mig/mig-panel 25 | :border (IdeBorderFactory/createTitledBorder "NREPL connection") 26 | :items [[(seesaw/radio :text "Manual" 27 | :id :manual 28 | :group repl-mode-group 29 | :mnemonic \M 30 | :selected? true) "wrap"] 31 | [(seesaw/label "Host") ""] 32 | [(seesaw/text :id :nrepl-host 33 | :columns 20) "wrap"] 34 | [(seesaw/label "Port") ""] 35 | [(seesaw/text :id :nrepl-port 36 | :columns 8) "wrap"] 37 | [(seesaw/radio :text "Read from repl file" 38 | :id :repl-file 39 | :group repl-mode-group 40 | :mnemonic \R) "wrap"] 41 | [(seesaw/label "Project") ""] 42 | [(seesaw/combobox :id :project 43 | :model (->> (ProjectManager/getInstance) 44 | .getOpenProjects 45 | (map #(.getName ^Project %)))) "wrap"]])] 46 | (seesaw/listen repl-mode-group :action 47 | (fn [_e] 48 | (let [mode-key (mode-id-key repl-mode-group) 49 | manual? (= mode-key :manual)] 50 | (seesaw/config! (seesaw/select panel [:#nrepl-host]) :enabled? manual?) 51 | (seesaw/config! (seesaw/select panel [:#nrepl-port]) :enabled? manual?)))) 52 | 53 | panel)) 54 | 55 | (defn -init [] 56 | [[] (atom (build-editor-ui))]) 57 | 58 | (defn -createEditor [this] 59 | @(.state this)) 60 | 61 | (defn ^:private manual? [editor] 62 | (.isSelected ^JRadioButton (seesaw/select editor [:#manual]))) 63 | 64 | (defn ^:private update-configuration-name [^RunConfigurationBase configuration] 65 | (when (contains? #{"Unnamed" ""} (.getName configuration)) 66 | (.setName configuration "Remote REPL"))) 67 | 68 | (defn -applyEditorTo [this configuration] 69 | (update-configuration-name configuration) 70 | (let [ui @(.state this) 71 | host (seesaw/text (seesaw/select ui [:#nrepl-host])) 72 | project-path (seesaw/text (seesaw/select ui [:#project])) 73 | mode (if (manual? ui) :manual-config :file-config)] 74 | (.setNreplHost (.getOptions configuration) host) 75 | (.setMode (.getOptions configuration) (name mode)) 76 | (when-let [nrepl-port (parse-long (seesaw/text (seesaw/select ui [:#nrepl-port])))] 77 | (.setNreplPort (.getOptions configuration) (str nrepl-port)) 78 | (.setProject (.getOptions configuration) project-path)))) 79 | 80 | (defn -resetEditorFrom [this configuration] 81 | (update-configuration-name configuration) 82 | (let [ui @(.state this) 83 | options (.getOptions configuration) 84 | mode (keyword (.getMode options))] 85 | (if (= :manual-config mode) 86 | (do 87 | (seesaw/config! (seesaw/select ui [:#manual]) :selected? true) 88 | (seesaw/config! (seesaw/select ui [:#repl-file]) :selected? false) 89 | (seesaw/config! (seesaw/select ui [:#nrepl-host]) :enabled? true) 90 | (seesaw/config! (seesaw/select ui [:#nrepl-port]) :enabled? true)) 91 | (do 92 | (seesaw/config! (seesaw/select ui [:#manual]) :selected? false) 93 | (seesaw/config! (seesaw/select ui [:#repl-file]) :selected? true) 94 | (seesaw/config! (seesaw/select ui [:#nrepl-host]) :enabled? false) 95 | (seesaw/config! (seesaw/select ui [:#nrepl-port]) :enabled? false))) 96 | (seesaw/text! (seesaw/select ui [:#nrepl-host]) (.getNreplHost options)) 97 | (seesaw/text! (seesaw/select ui [:#nrepl-port]) (.getNreplPort options)) 98 | (seesaw/text! (seesaw/select ui [:#project]) (.getProject options)))) 99 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/db.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.db 2 | (:refer-clojure :exclude [get-in assoc-in update-in]) 3 | (:require 4 | [com.github.clojure-repl.intellij.db :as db]) 5 | (:import 6 | [com.intellij.openapi.project Project])) 7 | 8 | (set! *warn-on-reflection* true) 9 | 10 | (def ^:private empty-project 11 | {:project nil 12 | :console {:state {:last-output "" 13 | :last-input nil 14 | :initial-text nil 15 | :status nil} 16 | :process-handler nil 17 | :ui nil} 18 | :current-nrepl {:session-id nil 19 | :ns "user" 20 | :nrepl-port nil 21 | :nrepl-host nil 22 | :entry-history '() 23 | :entry-index -1 24 | :last-test nil} 25 | :file->ns {} 26 | :on-repl-file-loaded-fns [] 27 | :on-repl-evaluated-fns [] 28 | :on-ns-changed-fns [] 29 | :on-test-failed-fns-by-key {} 30 | :on-test-succeeded-fns-by-key {} 31 | :ops {}}) 32 | 33 | (defonce db* (atom {:projects {}})) 34 | 35 | (defn get-in 36 | ([project fields] 37 | (get-in project fields nil)) 38 | ([^Project project fields default] 39 | (clojure.core/get-in @db* (concat [:projects (.getBasePath project)] fields) default))) 40 | 41 | (defn assoc-in! [^Project project fields value] 42 | (swap! db* clojure.core/assoc-in (concat [:projects (.getBasePath project)] fields) value)) 43 | 44 | (defn update-in! [^Project project fields fn] 45 | (swap! db* clojure.core/update-in (concat [:projects (.getBasePath project)] fields) fn)) 46 | 47 | (defn init-db-for-project [^Project project] 48 | (swap! db* update :projects (fn [projects] 49 | (if (clojure.core/get-in projects [(.getBasePath project) :project]) 50 | projects 51 | (update projects (.getBasePath project) #(merge (assoc empty-project :project project) %)))))) 52 | 53 | (defn all-projects [] 54 | (remove nil? 55 | (mapv :project (vals (:projects @db*))))) 56 | 57 | (comment 58 | ;; Useful for db debugging 59 | (require '[clojure.pprint :as pp]) 60 | (defn db-p [] (second (first (second (first @db*))))) 61 | (def p (:project (db-p))) 62 | 63 | (:state (:console (db-p))) 64 | (:ns (:current-nrepl (db-p))) 65 | 66 | (assoc-in! p [:ops] {}) 67 | (assoc-in! p [:on-ns-changed-fns] []) 68 | (pp/pprint (:ops (db-p)))) 69 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/editor.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.editor 2 | (:require 3 | [com.github.clojure-repl.intellij.parser :as parser] 4 | [rewrite-clj.zip :as z]) 5 | (:import 6 | [com.intellij.openapi.editor Editor] 7 | [com.intellij.openapi.fileEditor FileDocumentManager])) 8 | 9 | (set! *warn-on-reflection* true) 10 | 11 | (defn editor->url [^Editor editor] 12 | ;; TODO sanitize URL, encode, etc 13 | (.getUrl (.getFile (FileDocumentManager/getInstance) (.getDocument editor)))) 14 | 15 | (defn ns-form [^Editor editor] 16 | (-> editor 17 | .getDocument 18 | .getText 19 | z/of-string 20 | parser/find-namespace 21 | z/up)) 22 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/extension/color_settings_page.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.extension.color-settings-page 2 | (:gen-class 3 | :name com.github.clojure_repl.intellij.extension.ColorSettingsPage 4 | :implements [com.intellij.openapi.options.colors.ColorSettingsPage]) 5 | (:require 6 | [clojure.string :as string] 7 | [com.github.clojure-repl.intellij.ui.color :as ui.color]) 8 | (:import 9 | [com.intellij.lang Language] 10 | [com.intellij.openapi.editor.colors TextAttributesKey] 11 | [com.intellij.openapi.fileTypes SyntaxHighlighterFactory] 12 | [com.intellij.openapi.options.colors AttributesDescriptor ColorDescriptor])) 13 | 14 | (set! *warn-on-reflection* true) 15 | 16 | (defn -getDisplayName [_] "Clojure REPL") 17 | 18 | (defn ^:private title-by-text-attribute [] 19 | (merge (reduce (fn [m k] 20 | (let [display-name (-> (name k) 21 | (string/replace "--" "//") 22 | (string/replace "-" " ") 23 | string/capitalize)] 24 | (assoc m k display-name))) 25 | {} 26 | (keys (ui.color/text-attributes))) 27 | {:repl-window "REPL window"})) 28 | 29 | (defn -getAttributeDescriptors [_] 30 | (->> (ui.color/text-attributes) 31 | (mapv #(AttributesDescriptor. ^String ((title-by-text-attribute) (first %)) ^TextAttributesKey (second %))) 32 | (into-array AttributesDescriptor))) 33 | 34 | (defn -getColorDescriptors [_] ColorDescriptor/EMPTY_ARRAY) 35 | 36 | (defn -getHighlighter [_] 37 | (let [language (or (Language/findLanguageByID "clojure") 38 | (Language/findLanguageByID "textmate"))] 39 | (SyntaxHighlighterFactory/getSyntaxHighlighter language nil nil))) 40 | 41 | (defn -getAdditionalHighlightingTagToDescriptorMap [_] 42 | (update-keys (ui.color/text-attributes) name)) 43 | 44 | (defn -getDemoText [_] 45 | " 46 | (+ 1 2) 3 47 | \"Hello World\" \"Hello World\" 48 | ") 49 | 50 | (defn -getPreviewEditorCustomizer [_]) 51 | (defn -customizeColorScheme [_ scheme] scheme) 52 | (defn -getAdditionalInlineElementToDescriptorMap [_]) 53 | (defn -getAdditionalHighlightingTagToColorKeyMap [_]) 54 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/extension/init_db_startup.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.extension.init-db-startup 2 | (:gen-class 3 | :name com.github.clojure_repl.intellij.extension.InitDBStartup 4 | :implements [com.intellij.openapi.startup.ProjectActivity 5 | com.intellij.openapi.project.DumbAware]) 6 | (:require 7 | [com.github.clojure-repl.intellij.db :as db]) 8 | (:import 9 | [com.intellij.openapi.project Project] 10 | [kotlinx.coroutines CoroutineScope])) 11 | 12 | (set! *warn-on-reflection* true) 13 | 14 | (defn -execute [_this ^Project project ^CoroutineScope _] 15 | (db/init-db-for-project project)) 16 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/extension/inlay_esc_handler.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.extension.inlay-esc-handler 2 | (:gen-class 3 | :name com.github.clojure_repl.intellij.extension.InlayEscHandler 4 | :extends com.intellij.openapi.editor.actionSystem.EditorActionHandler) 5 | (:require 6 | [com.github.clojure-repl.intellij.ui.inlay-hint :as ui.inlay-hint]) 7 | (:import 8 | [com.intellij.openapi.editor Caret Editor])) 9 | 10 | (set! *warn-on-reflection* true) 11 | 12 | (defn -doExecute [_ ^Editor editor ^Caret _caret _data-context] 13 | (ui.inlay-hint/remove-all editor)) 14 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/extension/inlay_listener_startup.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.extension.inlay-listener-startup 2 | (:gen-class 3 | :name com.github.clojure_repl.intellij.extension.InlayListenerStartup 4 | :implements [com.intellij.openapi.startup.StartupActivity 5 | com.intellij.openapi.project.DumbAware]) 6 | (:require 7 | [com.github.clojure-repl.intellij.ui.inlay-hint :as ui.inlay-hint] 8 | [com.rpl.proxy-plus :refer [proxy+]]) 9 | (:import 10 | [com.intellij.openapi.editor EditorFactory] 11 | [com.intellij.openapi.editor.event EditorMouseEvent EditorMouseListener EditorMouseMotionListener] 12 | [com.intellij.openapi.project Project])) 13 | 14 | (set! *warn-on-reflection* true) 15 | 16 | (defonce ^:private last-hovered-inlay* (atom nil)) 17 | 18 | (defn ^:private mouse-moved [^EditorMouseEvent event] 19 | (when @last-hovered-inlay* 20 | (when-not (= @last-hovered-inlay* (.getInlay event)) 21 | (ui.inlay-hint/mark-inlay-hover-status @last-hovered-inlay* false)) 22 | (reset! last-hovered-inlay* nil)) 23 | (when-let [inlay (.getInlay event)] 24 | (when (= (ui.inlay-hint/renderer-class) (class (.getRenderer inlay))) 25 | (ui.inlay-hint/mark-inlay-hover-status inlay true) 26 | (reset! last-hovered-inlay* inlay)))) 27 | 28 | (defn ^:private mouse-released [^EditorMouseEvent event] 29 | (when-let [inlay (.getInlay event)] 30 | (when (= (ui.inlay-hint/renderer-class) (class (.getRenderer inlay))) 31 | (ui.inlay-hint/toggle-expand-inlay-hint inlay)))) 32 | 33 | (defn -runActivity [_this ^Project _project] 34 | (doto (.getEventMulticaster (EditorFactory/getInstance)) 35 | (.addEditorMouseMotionListener 36 | (proxy+ [] EditorMouseMotionListener 37 | (mouseMoved [_ event] (mouse-moved event)))) 38 | (.addEditorMouseListener 39 | (proxy+ [] EditorMouseListener 40 | (mousePressed [_ ^EditorMouseEvent _event]) 41 | (mouseReleased [_ ^EditorMouseEvent event] (mouse-released event)))))) 42 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/extension/register_actions_startup.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.extension.register-actions-startup 2 | (:gen-class 3 | :name com.github.clojure_repl.intellij.extension.RegisterActionsStartup 4 | :implements [com.intellij.openapi.startup.ProjectActivity 5 | com.intellij.openapi.project.DumbAware]) 6 | (:require 7 | [com.github.clojure-repl.intellij.action.eval :as a.eval] 8 | [com.github.clojure-repl.intellij.action.test :as a.test] 9 | [com.github.clojure-repl.intellij.actions :as actions] 10 | [com.github.clojure-repl.intellij.nrepl :as nrepl] 11 | [com.github.ericdallo.clj4intellij.action :as action] 12 | [com.rpl.proxy-plus :refer [proxy+]]) 13 | (:import 14 | [com.github.clojure_repl.intellij Icons] 15 | [com.intellij.icons AllIcons$Actions] 16 | [com.intellij.openapi.actionSystem AnActionEvent] 17 | [com.intellij.openapi.project DumbAwareAction Project] 18 | [kotlinx.coroutines CoroutineScope])) 19 | 20 | (set! *warn-on-reflection* true) 21 | 22 | (defn -execute 23 | "Shortcuts: https://github.com/JetBrains/intellij-community/blob/master/platform/platform-resources/src/keymaps/%24default.xml" 24 | [_this ^Project _project ^CoroutineScope _] 25 | (action/register-action! :id "ClojureREPL.RunCursorTest" 26 | :title "Run test at cursor" 27 | :description "Run test at cursor" 28 | :icon Icons/CLOJURE_REPL 29 | :keyboard-shortcut {:first "shift alt T" :second "shift alt T" :replace-all true} 30 | :on-performed #'a.test/run-cursor-test-action) 31 | (action/register-action! :id "ClojureREPL.RunNsTests" 32 | :title "Run namespace tests" 33 | :description "Run all namespaces tests" 34 | :icon Icons/CLOJURE_REPL 35 | :keyboard-shortcut {:first "shift alt T" :second "shift alt N" :replace-all true} 36 | :on-performed #'a.test/run-ns-tests-action) 37 | (action/register-action! :id "ClojureREPL.ReRunTest" 38 | :title "Re-run last test" 39 | :description "Re-run last executed test" 40 | :icon Icons/CLOJURE_REPL 41 | :keyboard-shortcut {:first "shift alt T" :second "shift alt A" :replace-all true} 42 | :on-performed #'a.test/re-run-test-action) 43 | (action/register-action! :id "ClojureREPL.LoadFile" 44 | :title "Load file to REPL" 45 | :description "Load file to REPL" 46 | :icon Icons/CLOJURE_REPL 47 | :keyboard-shortcut {:first "shift alt L" :replace-all true} 48 | :on-performed #'a.eval/load-file-action) 49 | (action/register-action! :id "ClojureREPL.EvalLastSexp" 50 | :title "Eval last sexp" 51 | :description "Eval the expression preceding cursor" 52 | :icon Icons/CLOJURE_REPL 53 | :keyboard-shortcut {:first "shift alt E" :replace-all true} 54 | :on-performed #'a.eval/eval-last-sexpr-action) 55 | (action/register-action! :id "ClojureREPL.EvalDefun" 56 | :title "Eval defun at cursor" 57 | :description "Evaluate the current toplevel form" 58 | :icon Icons/CLOJURE_REPL 59 | :keyboard-shortcut {:first "shift alt D" :replace-all true} 60 | :on-performed #'a.eval/eval-defun-action) 61 | (action/register-action! :id "ClojureREPL.ClearReplOutput" 62 | :keyboard-shortcut {:first "shift alt R" :second "shift alt C" :replace-all true} 63 | :action (proxy+ 64 | ["Clear REPL output" "Clear REPL output" AllIcons$Actions/GC] 65 | DumbAwareAction 66 | (update 67 | [_ ^AnActionEvent event] 68 | (let [project (actions/action-event->project event)] 69 | (.setEnabled (.getPresentation event) (boolean (nrepl/active-client? project))))) 70 | (actionPerformed 71 | [_ event] 72 | (a.eval/clear-repl-output-action event)))) 73 | 74 | (action/register-action! :id "ClojureREPL.SwitchNs" 75 | :title "Switch REPL namespace" 76 | :description "Switch REPL namespace to current opened file namespace" 77 | :icon Icons/CLOJURE_REPL 78 | :keyboard-shortcut {:first "shift alt N" :replace-all true} 79 | :on-performed #'a.eval/switch-ns-action) 80 | (action/register-action! :id "ClojureREPL.RefreshAll" 81 | :title "Refresh all namespaces" 82 | :description "Refresh all namespaces" 83 | :icon Icons/CLOJURE_REPL 84 | :keyboard-shortcut {:first "shift alt R" :second "shift alt A" :replace-all true} 85 | :on-performed #'a.eval/refresh-all-action) 86 | (action/register-action! :id "ClojureREPL.RefreshChanged" 87 | :title "Refresh changed namespaces" 88 | :description "Refresh changed namespaces" 89 | :icon Icons/CLOJURE_REPL 90 | :keyboard-shortcut {:first "shift alt R" :second "shift alt R" :replace-all true} 91 | :on-performed #'a.eval/refresh-changed-action) 92 | (action/register-action! :id "ClojureREPL.HistoryUp" 93 | :title "Moves up in history" 94 | :description "Moves up in history" 95 | :icon AllIcons$Actions/PreviousOccurence 96 | :keyboard-shortcut {:first "control PAGE_UP" :replace-all true} 97 | :on-performed #'a.eval/history-up-action) 98 | (action/register-action! :id "ClojureREPL.HistoryDown" 99 | :title "Moves down in history" 100 | :description "Moves down in history" 101 | :icon AllIcons$Actions/NextOccurence 102 | :keyboard-shortcut {:first "control PAGE_DOWN" :replace-all true} 103 | :on-performed #'a.eval/history-down-action) 104 | (action/register-action! :id "ClojureREPL.Interrupt" 105 | :keyboard-shortcut {:first "shift alt R" :second "shift alt S" :replace-all true} 106 | :action (proxy+ 107 | ["Interrupts session evaluation" "Interrupts session evaluation" AllIcons$Actions/Cancel] 108 | DumbAwareAction 109 | (update 110 | [_ ^AnActionEvent event] 111 | (let [project (actions/action-event->project event)] 112 | (.setEnabled (.getPresentation event) (boolean (nrepl/evaluating? project))))) 113 | (actionPerformed 114 | [_ event] 115 | (a.eval/interrupt event)))) 116 | 117 | (action/register-group! :id "ClojureREPL.ReplActions" 118 | :popup true 119 | :text "Clojure REPL" 120 | :icon Icons/CLOJURE_REPL 121 | :children [{:type :add-to-group :group-id "ToolsMenu" :anchor :first} 122 | {:type :add-to-group :group-id "EditorPopupMenu" :anchor :before :relative-to "RefactoringMenu"} 123 | {:type :separator} 124 | {:type :reference :ref "ClojureREPL.RunCursorTest"} 125 | {:type :reference :ref "ClojureREPL.RunNsTests"} 126 | {:type :reference :ref "ClojureREPL.ReRunTest"} 127 | {:type :separator} 128 | {:type :reference :ref "ClojureREPL.LoadFile"} 129 | {:type :reference :ref "ClojureREPL.EvalLastSexp"} 130 | {:type :reference :ref "ClojureREPL.EvalDefun"} 131 | {:type :separator} 132 | {:type :reference :ref "ClojureREPL.ClearReplOutput"} 133 | {:type :reference :ref "ClojureREPL.RefreshAll"} 134 | {:type :reference :ref "ClojureREPL.RefreshChanged"} 135 | {:type :reference :ref "ClojureREPL.SwitchNs"} 136 | {:type :separator}])) 137 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/extension/run_test_line_marker_provider.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.extension.run-test-line-marker-provider 2 | (:gen-class 3 | :name com.github.clojure_repl.intellij.extension.RunTestLineMarkerProvider 4 | :extends com.intellij.codeInsight.daemon.LineMarkerProviderDescriptor) 5 | (:require 6 | [clojure.string :as string] 7 | [com.github.clojure-repl.intellij.tests :as tests]) 8 | (:import 9 | [com.github.clojure_repl.intellij Icons] 10 | [com.intellij.codeInsight.daemon GutterIconNavigationHandler LineMarkerInfo] 11 | [com.intellij.openapi.editor.markup GutterIconRenderer$Alignment] 12 | [com.intellij.openapi.fileEditor FileEditorManager] 13 | [com.intellij.psi PsiElement] 14 | [com.intellij.psi.impl.source.tree LeafPsiElement])) 15 | 16 | (set! *warn-on-reflection* true) 17 | 18 | (defn -getName [_] 19 | "Run test") 20 | 21 | (defn -getIcon [_] 22 | Icons/CLOJURE_REPL) 23 | 24 | (defn -getLineMarkerInfo [_ ^PsiElement element] 25 | (when (and (instance? LeafPsiElement element) 26 | (or (string/ends-with? (.getText element) "-test") 27 | (contains? #{"deftest" "ns"} (some-> element .getPrevSibling .getPrevSibling .getText)))) 28 | (LineMarkerInfo. 29 | element 30 | (.getTextRange element) 31 | Icons/CLOJURE_REPL 32 | nil 33 | (reify GutterIconNavigationHandler 34 | (navigate [_ _event element] 35 | (let [element ^PsiElement element 36 | project (.getProject element) 37 | editor (.getSelectedTextEditor (FileEditorManager/getInstance project))] 38 | (.moveToOffset (.getCaretModel editor) (.getTextOffset element)) 39 | (tests/run-at-cursor editor)))) 40 | GutterIconRenderer$Alignment/CENTER))) 41 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/interop.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.interop) 2 | 3 | (set! *warn-on-reflection* true) 4 | 5 | ;; TODO move to clj4intellij 6 | (defn new-class 7 | "Dynamically resolves the class-name in class loader and creates a new instance. 8 | Workaround to call generated java classes via gen-class from Clojure code itself." 9 | [class-name & args] 10 | (clojure.lang.Reflector/invokeConstructor (Class/forName class-name) (into-array Object args))) 11 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/keyboard_manager.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.keyboard-manager 2 | (:require 3 | [com.rpl.proxy-plus :refer [proxy+]]) 4 | (:import 5 | [com.intellij.openapi.editor Editor] 6 | [java.awt KeyEventDispatcher KeyboardFocusManager] 7 | [java.awt.event KeyEvent])) 8 | 9 | (set! *warn-on-reflection* true) 10 | 11 | (defonce ^:private key-listeners* (atom {})) 12 | 13 | (defn register-listener-for-editor! [{:keys [^Editor editor on-key-pressed]}] 14 | (let [dispatcher (proxy+ [] KeyEventDispatcher 15 | 16 | (dispatchKeyEvent [_ ^KeyEvent event] 17 | (if (and (= KeyEvent/KEY_PRESSED (.getID event)) 18 | (= (.getContentComponent editor) (.getComponent event))) 19 | (boolean (on-key-pressed event)) 20 | false)))] 21 | (swap! key-listeners* assoc editor dispatcher) 22 | (.addKeyEventDispatcher 23 | (KeyboardFocusManager/getCurrentKeyboardFocusManager) 24 | dispatcher))) 25 | 26 | (defn unregister-listener-for-editor! [^Editor editor-to-unregister] 27 | (doseq [[^Editor editor dispatcher] @key-listeners*] 28 | (when (= editor-to-unregister editor) 29 | (.removeKeyEventDispatcher (KeyboardFocusManager/getCurrentKeyboardFocusManager) dispatcher) 30 | (swap! key-listeners* dissoc editor)))) 31 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/nrepl.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.nrepl 2 | (:refer-clojure :exclude [eval load-file]) 3 | (:require 4 | [clojure.core.async :as async] 5 | [clojure.string :as string] 6 | [com.github.clojure-repl.intellij.config :as config] 7 | [com.github.clojure-repl.intellij.db :as db] 8 | [com.github.clojure-repl.intellij.editor :as editor] 9 | [com.github.clojure-repl.intellij.parser :as parser] 10 | [com.github.ericdallo.clj4intellij.logger :as logger] 11 | [nrepl.core :as nrepl.core] 12 | [nrepl.transport :as transport] 13 | [rewrite-clj.zip :as z]) 14 | (:import 15 | [com.intellij.openapi.editor Editor] 16 | [com.intellij.openapi.project Project] 17 | [com.intellij.openapi.vfs VirtualFile])) 18 | 19 | (set! *warn-on-reflection* true) 20 | 21 | (defprotocol REPLClient 22 | (start! [this]) 23 | (log! [this level arg1] [this level arg1 arg2]) 24 | (send-message! [this msg])) 25 | 26 | (defrecord ^:private NREPLClient [host port join sent-messages* received-messages* conn* on-receive-async-message] 27 | REPLClient 28 | (start! [this] 29 | (log! this :info "connecting...") 30 | (reset! conn* (nrepl.core/connect :host host :port port)) 31 | (async/thread 32 | (loop [] 33 | (if-let [{:keys [id status] :as resp} (transport/recv @conn*)] 34 | (do 35 | (when-not (:changed-namespaces resp) 36 | (log! this :info "received message:" resp) 37 | (if-let [sent-request (get @sent-messages* id)] 38 | (do 39 | (swap! received-messages* update id conj resp) 40 | (when (and status (contains? (set status) "done")) 41 | (let [responses (get @received-messages* id)] 42 | (swap! sent-messages* dissoc id) 43 | (swap! received-messages* dissoc id) 44 | (deliver sent-request (nrepl.core/combine-responses responses))))) 45 | (on-receive-async-message resp))) 46 | (recur)) 47 | (deliver join :done)))) 48 | (log! this :info "connected") 49 | join) 50 | (send-message! [this msg] 51 | (let [id (str (random-uuid)) 52 | p (promise) 53 | msg (assoc msg :id id)] 54 | (log! this :info "sending message:" msg) 55 | (swap! sent-messages* assoc id p) 56 | (transport/send @conn* msg) 57 | p)) 58 | (log! [this msg params] 59 | (log! this :info msg params)) 60 | (log! [_this _color msg params] 61 | (when (config/nrepl-debug?) 62 | (logger/info "[NREPLClient] " (string/join " " [msg params]))))) 63 | 64 | (defn ^:private nrepl-client [^Project project on-receive-async-message] 65 | (map->NREPLClient {:host (db/get-in project [:current-nrepl :nrepl-host]) 66 | :port (db/get-in project [:current-nrepl :nrepl-port]) 67 | :join (promise) 68 | :on-receive-async-message on-receive-async-message 69 | :conn* (atom nil) 70 | :sent-messages* (atom {}) 71 | :received-messages* (atom {})})) 72 | 73 | (defn start-client! [& {:keys [project on-receive-async-message]}] 74 | (let [client (nrepl-client project on-receive-async-message)] 75 | (db/assoc-in! project [:current-nrepl :client] client) 76 | (start! client))) 77 | 78 | (defn ^:private send-msg [project message] 79 | (let [client (db/get-in project [:current-nrepl :client])] 80 | (send-message! client message))) 81 | 82 | (defn ^:private ns-form-changed [project url ns-form] 83 | (not (= ns-form 84 | (db/get-in project [:file->ns url :form])))) 85 | (defn ^:private is-ns-form [form] 86 | (parser/find-namespace (z/of-string form))) 87 | 88 | (defn ^:private eval [^Project project ns code] 89 | (let [session (db/get-in project [:current-nrepl :session-id]) 90 | response @(send-msg project {:op "eval" :code code :ns ns :session session})] 91 | (doseq [fn (db/get-in project [:on-repl-evaluated-fns])] 92 | (fn project response)) 93 | response)) 94 | 95 | (defn prep-env-for-eval 96 | "When evaluating a form for the first time, 97 | need to evaluate the ns form first to avoid namespace-not-found error. 98 | Also evaluate the ns form when it has changed to keep the environment up-to-date." 99 | [^Editor editor form] 100 | (when-let [current-ns-form (editor/ns-form editor)] 101 | (let [url (editor/editor->url editor) 102 | project (.getProject editor) 103 | str-current-ns-form (z/string current-ns-form) 104 | ns (-> current-ns-form parser/find-namespace z/string parser/remove-metadata)] 105 | (when (and (not (is-ns-form form)) 106 | (ns-form-changed project url str-current-ns-form)) 107 | (eval project "user" str-current-ns-form) 108 | (when ns 109 | (db/assoc-in! project [:file->ns url] 110 | {:form str-current-ns-form 111 | :ns ns})))))) 112 | 113 | (defn ^:private cur-ns 114 | "Returns current ns to evaluate code in. 115 | If there is no ns in cache, it returns the ns from the file. 116 | If the ns form is not found in the editor, it default to 'user'." 117 | [^Editor editor] 118 | (let [project (.getProject editor) 119 | url (editor/editor->url editor) 120 | cur-ns (some-> (editor/ns-form editor) parser/find-namespace z/string parser/remove-metadata)] 121 | (or (db/get-in project [:file->ns url :ns]) 122 | cur-ns 123 | "user"))) 124 | 125 | (defn eval-from-editor 126 | "When evaluating code related to a file (editor) we need to: 127 | - prepare the environment by evaluating the ns form 128 | - evaluate the code 129 | - update the current ns in the project if it has changed" 130 | [& {:keys [^Editor editor ns code]}] 131 | (prep-env-for-eval editor code) 132 | (let [project (.getProject editor) 133 | url (editor/editor->url editor) 134 | ns (or ns 135 | (cur-ns editor)) 136 | {:keys [ns] :as response} (eval project ns code)] 137 | (when ns 138 | (db/assoc-in! project [:file->ns url :ns] ns)) 139 | response)) 140 | 141 | (defn eval-from-repl 142 | "When evaluating from repl window we do not have editor associate" 143 | [& {:keys [^Project project ns code]}] 144 | (let [ns (or ns 145 | (db/get-in project [:current-nrepl :ns]))] 146 | (eval project ns code))) 147 | 148 | (defn switch-ns [{:keys [project ns]}] 149 | (let [code (format "(in-ns '%s)" ns) 150 | response @(send-msg project {:op "eval" :code code :ns ns :session (db/get-in project [:current-nrepl :session-id])})] 151 | (when (and ns 152 | (not= ns (db/get-in project [:current-nrepl :ns]))) 153 | (db/assoc-in! project [:current-nrepl :ns] ns) 154 | (doseq [fn (db/get-in project [:on-ns-changed-fns])] 155 | (fn project response))) 156 | response)) 157 | 158 | (defn clone-session [^Project project] 159 | (let [msg {:op "clone" 160 | :client-name "clojure-repl-intellij" 161 | :client-version (config/plugin-version)}] 162 | (db/assoc-in! project [:current-nrepl :session-id] (:new-session @(send-msg project msg))))) 163 | 164 | (defn load-file [project ^Editor editor ^VirtualFile virtual-file] 165 | (let [response @(send-msg project {:op "load-file" 166 | :session (db/get-in project [:current-nrepl :session-id]) 167 | :file (.getText (.getDocument editor)) 168 | :file-path (some-> virtual-file .getPath) 169 | :file-name (some-> virtual-file .getName)})] 170 | (doseq [fn (db/get-in project [:on-repl-evaluated-fns])] 171 | (fn project response)) 172 | response)) 173 | 174 | (defn describe [^Project project] 175 | @(send-msg project {:op "describe"})) 176 | 177 | (defn out-subscribe [^Project project] 178 | @(send-msg project {:op "out-subscribe" :session (db/get-in project [:current-nrepl :session-id])})) 179 | 180 | (defn sym-info [^Project project ns sym] 181 | @(send-msg project {:op "info" :ns ns :sym sym :session (db/get-in project [:current-nrepl :session-id])})) 182 | 183 | (defn run-tests [^Project project {:keys [ns tests on-ns-not-found on-out on-err on-succeeded on-failed]}] 184 | (let [{:keys [summary results status out err] :as response} 185 | @(send-msg project {:op "test" 186 | :load? (str (boolean ns)) 187 | :session (db/get-in project [:current-nrepl :session-id]) 188 | :ns ns 189 | :tests (when (seq tests) tests) 190 | :fail-fast "true"})] 191 | (when (some #(= % "namespace-not-found") status) 192 | (on-ns-not-found ns)) 193 | (when out (on-out out)) 194 | (when err (on-err err)) 195 | (when results 196 | (if (zero? (+ (:error summary) (:fail summary))) 197 | (on-succeeded response) 198 | (on-failed response))))) 199 | 200 | (defn test-stacktrace [^Project project ns var] 201 | @(send-msg project {:op "test-stacktrace" 202 | :session (db/get-in project [:current-nrepl :session-id]) 203 | :ns ns 204 | :index 0 205 | :var var})) 206 | 207 | (defn refresh-all [^Project project] 208 | @(send-msg project {:op "refresh-all"})) 209 | 210 | (defn refresh [^Project project] 211 | @(send-msg project {:op "refresh"})) 212 | 213 | (defn interrupt [^Project project] 214 | @(send-msg project {:op "interrupt" :session (db/get-in project [:current-nrepl :session-id])})) 215 | 216 | (defn evaluating? [^Project project] 217 | (when-let [client (db/get-in project [:current-nrepl :client])] 218 | (seq @(:sent-messages* client)))) 219 | 220 | (defn active-client? [^Project project] 221 | (db/get-in project [:current-nrepl :client])) 222 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/parser.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.parser 2 | (:require 3 | [clojure.string :as string] 4 | [rewrite-clj.zip :as z])) 5 | 6 | (defn find-by-heritability 7 | "Find the leftmost deepest zloc from `start-zloc` that satisfies `inherits?`. 8 | `inherits?` must be a function such that if zloc satisifies it then so will 9 | all of its ancestors. 10 | 11 | If a parent node satisifies `inherits?` but none of its children do, then this 12 | returns the parent, on the assumption that the parent is the last in its 13 | lineage with the trait. 14 | 15 | If a parent node doesn't satisfy `inherits?` then none of its descendants will 16 | be inspected. Instead, the search will continue with its sibling to the 17 | z/right*. As such, this algoritihm can be much faster than ones based on 18 | z/next*, which must inspect all descendants, even if information in the parent 19 | excludes the entire family." 20 | [start-zloc inherits?] 21 | (loop [zloc (cond-> start-zloc 22 | (= :forms (z/tag start-zloc)) z/down*)] 23 | (if (z/end? zloc) 24 | zloc 25 | (if (inherits? zloc) 26 | (if-let [inner (some-> zloc z/down* (z/find z/right* inherits?))] 27 | (recur inner) 28 | zloc) 29 | (recur (z/right* zloc)))))) 30 | 31 | ;; From rewrite-cljs; very similar to the private function 32 | ;; rewrite-clj.zip.findz/position-in-range? but based on zloc meta, avoiding the 33 | ;; need for :track-position? 34 | (defn in-range? 35 | "True if b is contained within a." 36 | [{:keys [row col end-row end-col] :as _a} 37 | {r :row c :col er :end-row ec :end-col :as _b}] 38 | (and (>= r row) 39 | (<= er end-row) 40 | (if (= r row) (>= c col) true) 41 | (if (= er end-row) (< ec end-col) true))) 42 | 43 | (defn ^:private zloc-in-range? 44 | "Checks whether the `loc`s node is [[in-range?]] of the given `pos`." 45 | [loc pos] 46 | (some-> loc z/node meta (in-range? pos))) 47 | 48 | (defn find-form-at-pos 49 | "Find the deepest zloc whose node is at the given `row` and `col`, seeking 50 | from initial zipper location `zloc`. 51 | 52 | This is similar to z/find-last-by-pos, but is faster, and doesn't require 53 | {:track-position? true}." 54 | [zloc row col] 55 | (let [exact-position {:row row, :col col, :end-row row, :end-col col}] 56 | (find-by-heritability zloc #(zloc-in-range? % exact-position)))) 57 | 58 | (defn root? [loc] 59 | (identical? :forms (z/tag loc))) 60 | 61 | (defn top? [loc] 62 | (root? (z/up loc))) 63 | 64 | (defn to-top 65 | "Returns the loc for the top-level form above the loc, or the loc itself if it 66 | is top-level, or nil if the loc is at the `:forms` node." 67 | [loc] 68 | (z/find loc z/up top?)) 69 | 70 | (defn var-name-loc-from-op [loc] 71 | (cond 72 | (not loc) 73 | nil 74 | 75 | (= :map (-> loc z/next z/tag)) 76 | (-> loc z/next z/right) 77 | 78 | (and (= :meta (-> loc z/next z/tag)) 79 | (= :map (-> loc z/next z/next z/tag))) 80 | (-> loc z/next z/down z/rightmost) 81 | 82 | (= :meta (-> loc z/next z/tag)) 83 | (-> loc z/next z/next z/next) 84 | 85 | :else 86 | (z/next loc))) 87 | 88 | (defn find-var-at-pos 89 | [root-zloc row col] 90 | (let [loc (find-form-at-pos root-zloc row col)] 91 | (some-> loc to-top z/next var-name-loc-from-op))) 92 | 93 | (defn to-root 94 | "Returns the loc of the root `:forms` node." 95 | [loc] 96 | (z/find loc z/up root?)) 97 | 98 | (defn find-namespace [zloc] 99 | (-> (to-root zloc) 100 | (z/find-value z/next 'ns) 101 | z/next)) 102 | 103 | (defn remove-metadata [zloc] 104 | (-> (string/replace zloc #"(\^\S+\s+)+" "") 105 | (string/trim))) 106 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/project.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.project 2 | (:require 3 | [clojure.java.io :as io]) 4 | (:import 5 | [com.intellij.openapi.project Project])) 6 | 7 | (set! *warn-on-reflection* true) 8 | 9 | (def type-by-file 10 | {"project.clj" :lein 11 | "deps.edn" :clojure 12 | "bb.edn" :babashka 13 | "shadow-cljs.edn" :shadow-cljs 14 | "build.boot" :boot 15 | "nbb.edn" :nbb 16 | "build.gradle" :gradle 17 | "build.gradle.kts" :gradle}) 18 | 19 | (def known-project-types (set (vals type-by-file))) 20 | 21 | (def types 22 | (set (vals type-by-file))) 23 | 24 | (defn ^:private list-files [project-path] 25 | (.list (io/file project-path))) 26 | 27 | (defn project->project-type [^Project project] 28 | (some 29 | #(get type-by-file %) 30 | (list-files (.getBasePath project)))) 31 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/repl_command.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.repl-command 2 | (:require 3 | [babashka.fs :as fs] 4 | [clojure.java.shell :as shell] 5 | [clojure.string :as string])) 6 | 7 | (set! *warn-on-reflection* true) 8 | 9 | (def ^:private project-type->command 10 | {:lein ["lein"] 11 | :clojure ["clojure"] 12 | :babashka ["bb"] 13 | :shadow-cljs ["npx" "shadow-cljs"] 14 | :boot ["boot"] 15 | :nbb ["nbb"] 16 | :gradle ["./gradlew"]}) 17 | 18 | (def ^:private middleware-versions 19 | ;; TODO make version configurable in intellij settings 20 | {"nrepl/nrepl" "1.3.1" 21 | "cider/cider-nrepl" "0.55.7"}) 22 | 23 | (defn ^:private psh-cmd 24 | "Return the command vector that uses the PowerShell executable PSH to 25 | invoke CMD-AND-ARGS." 26 | ([psh & cmd-and-args] 27 | (into [psh "-NoProfile" "-Command"] cmd-and-args))) 28 | 29 | (defn ^:private locate-executable 30 | "Locate and return the full path to the EXECUTABLE." 31 | [executable] 32 | (some-> ^java.nio.file.Path (fs/which executable) .toString)) 33 | 34 | (defn ^:private shell 35 | [& cmd-and-args] 36 | (println cmd-and-args) 37 | (apply shell/sh cmd-and-args)) 38 | 39 | (def ^:private windows-os? 40 | (.contains (System/getProperty "os.name") "Windows")) 41 | 42 | (defn ^:private normalize-command 43 | "Return CLASSPATH-CMD, but with the EXEC expanded to its full path (if found). 44 | 45 | If the EXEC cannot be found, is one of clojure or lein and the 46 | program is running on MS-Windows, then, if possible, it tries to 47 | replace it with a PowerShell cmd invocation sequence in the 48 | following manner, while keeping ARGS the same. 49 | 50 | There could be two PowerShell executable available in the system 51 | path: powershell.exe (up to version 5.1, comes with windows) and/or 52 | pwsh.exe (versions 6 and beyond, can be installed on demand). 53 | 54 | If powershell.exe is available, it checks if the EXEC is installed 55 | in it as a module, and creates an invocation sequence as such to 56 | replace the EXEC. If not, it tries the same with pwsh.exe." 57 | [[exec & args :as cmd]] 58 | (if (and windows-os? 59 | (#{"clojure" "lein"} exec) 60 | (not (locate-executable exec))) 61 | (if-let [up (some #(when-let [ps (locate-executable %)] 62 | (when (= 0 (:exit (apply shell (psh-cmd ps "Get-Command" exec)))) 63 | (psh-cmd ps exec))) 64 | ["powershell" "pwsh"])] 65 | (into up args) 66 | cmd) 67 | cmd)) 68 | 69 | (defn ^:private project-type->parameters [project-type aliases jvm-args] 70 | (flatten 71 | (remove 72 | nil? 73 | (get 74 | {:lein ["update-in" ":dependencies" "conj" "[nrepl/nrepl \"%nrepl/nrepl%\"]" 75 | "--" "update-in" ":plugins" "conj" "[cider/cider-nrepl \"%cider/cider-nrepl%\"]" 76 | (when (seq jvm-args) 77 | ["--" "update-in" ":jvm-opts" "concat" (str (mapv #(str (first %) "=" (second %)) jvm-args))]) 78 | "--" (when (seq aliases) 79 | ["with-profile" (str "+" (string/join ",+" aliases))]) 80 | "repl" ":headless" ":host" "localhost"] 81 | :clojure [(when (seq jvm-args) 82 | (map #(str "-J" (first %) "=" (second %)) jvm-args)) 83 | "-Sdeps" 84 | "{:deps {nrepl/nrepl {:mvn/version \"%nrepl/nrepl%\"} cider/cider-nrepl {:mvn/version \"%cider/cider-nrepl%\"}} :aliases {:cider/nrepl {:main-opts [\"-m\" \"nrepl.cmdline\" \"--middleware\" \"[cider.nrepl/cider-middleware]\"]}}}" 85 | (str "-M:" (string/join ":" (conj aliases "cider/nrepl")))] 86 | :babashka ["nrepl-server" "localhost:0"] 87 | :shadow-cljs ["server"] 88 | :boot ["repl" "-s" "-b" "localhost" "wait"] 89 | :nbb ["nrepl-server"] 90 | :gradle ["-Pdev.clojurephant.jack-in.nrepl=nrepl:nrepl:%nrepl/nrepl%,cider:cider-nrepl:%cider/cider-nrepl%" 91 | "clojureRepl" 92 | "--middleware=cider.nrepl/cider-middleware"]} 93 | project-type)))) 94 | 95 | (defn ^:private replace-versions [middleware-versions parameters] 96 | (reduce 97 | (fn [params [middleware version]] 98 | (mapv #(string/replace % (str "%" middleware "%") version) params)) 99 | parameters 100 | middleware-versions)) 101 | 102 | (defn project->repl-start-command [project-type aliases jvm-args] 103 | (let [command (normalize-command (project-type->command project-type)) 104 | parameters (replace-versions middleware-versions (project-type->parameters project-type aliases jvm-args))] 105 | (flatten (concat command parameters)))) 106 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/tests.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.tests 2 | (:require 3 | [com.github.clojure-repl.intellij.db :as db] 4 | [com.github.clojure-repl.intellij.nrepl :as nrepl] 5 | [com.github.clojure-repl.intellij.parser :as parser] 6 | [com.github.clojure-repl.intellij.ui.hint :as ui.hint] 7 | [com.github.clojure-repl.intellij.ui.repl :as ui.repl] 8 | [com.github.ericdallo.clj4intellij.app-manager :as app-manager] 9 | [com.github.ericdallo.clj4intellij.tasks :as tasks] 10 | [com.github.ericdallo.clj4intellij.util :as util] 11 | [rewrite-clj.zip :as z]) 12 | (:import 13 | [com.intellij.openapi.editor Editor])) 14 | 15 | (set! *warn-on-reflection* true) 16 | 17 | (defn ^:private run [^Editor editor ns tests] 18 | (let [project (.getProject editor)] 19 | (if-not (db/get-in project [:current-nrepl :session-id]) 20 | (ui.hint/show-error :message "No REPL connected" :editor editor) 21 | ;; TODO save last result and summary 22 | (tasks/run-background-task! 23 | project 24 | "REPL: Running test" 25 | (fn [_indicator] 26 | (db/assoc-in! project [:current-nrepl :last-test] {:ns ns :tests tests}) 27 | (nrepl/run-tests 28 | project 29 | {:ns ns 30 | :tests tests 31 | :on-ns-not-found (fn [ns] 32 | (app-manager/invoke-later! 33 | {:invoke-fn 34 | (fn [] 35 | (ui.hint/show-error :message (format "No namespace '%s' found. Did you load the file?" ns) :editor editor))})) 36 | :on-out (fn [out] 37 | (ui.repl/append-output project out)) 38 | :on-err (fn [err] 39 | (ui.repl/append-output project err)) 40 | :on-failed (fn [result] 41 | ;; TODO highlight errors on editor 42 | (doseq [[key fns] (db/get-in project [:on-test-failed-fns-by-key])] 43 | (doseq [fn fns] 44 | (fn project key result)))) 45 | :on-succeeded (fn [{{:keys [ns test var fail error]} :summary results :results elapsed-time :elapsed-time :as response}] 46 | (app-manager/invoke-later! 47 | {:invoke-fn 48 | (fn [] 49 | (if (empty? results) 50 | (ui.hint/show-info :message "No assertions (or no tests) were run. Did you forget to use `is' in your tests?\n" :editor editor) 51 | (ui.hint/show-success :message (format "%s: Ran %s assertions, in %s test functions. %s failures, %s errors%s\n" 52 | (if (= 1 ns) 53 | (name (ffirst results)) 54 | (str ns " namespaces")) 55 | test 56 | var 57 | fail 58 | error 59 | (or (some->> (:ms elapsed-time) (format " in %s ms")) 60 | ".")) :editor editor)) 61 | (doseq [[key fns] (db/get-in project [:on-test-succeeded-fns-by-key])] 62 | (doseq [fn fns] 63 | (fn project key response))))}))})))))) 64 | 65 | (defn run-at-cursor [^Editor editor] 66 | (let [text (.getText (.getDocument editor)) 67 | root-zloc (z/of-string text) 68 | [row col] (util/editor->cursor-position editor) 69 | ns (-> (parser/find-namespace root-zloc) z/string parser/remove-metadata)] 70 | (if-let [test (some-> (parser/find-var-at-pos root-zloc (inc row) col) z/string)] 71 | (run editor ns [test]) 72 | (ui.hint/show-error :message "No test var found, did you eval the var?" :editor editor)))) 73 | 74 | (defn run-ns-tests [^Editor editor] 75 | (let [text (.getText (.getDocument editor)) 76 | root-zloc (z/of-string text) 77 | zloc (parser/find-namespace root-zloc) 78 | ns (parser/remove-metadata (z/string zloc))] 79 | (run editor ns nil))) 80 | 81 | (defn re-run-test [^Editor editor] 82 | (if-let [{:keys [ns tests]} (db/get-in (.getProject editor) [:current-nrepl :last-test])] 83 | (run editor ns tests) 84 | (ui.hint/show-error :message "No last test found" :editor editor))) 85 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/tool_window/repl_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.tool-window.repl-test 2 | (:gen-class 3 | :name com.github.clojure_repl.intellij.tool_window.ReplTestToolWindow 4 | :implements [com.intellij.openapi.wm.ToolWindowFactory 5 | com.intellij.openapi.project.DumbAware]) 6 | (:require 7 | [clojure.java.io :as io] 8 | [clojure.string :as string] 9 | [clojure.walk :as walk] 10 | [com.github.clojure-repl.intellij.db :as db] 11 | [com.github.clojure-repl.intellij.nrepl :as nrepl] 12 | [com.github.clojure-repl.intellij.ui.color :as ui.color] 13 | [com.github.clojure-repl.intellij.ui.components :as ui.components] 14 | [com.github.clojure-repl.intellij.ui.font :as ui.font] 15 | [com.github.clojure-repl.intellij.ui.text :as ui.text] 16 | [com.github.ericdallo.clj4intellij.app-manager :as app-manager] 17 | [com.github.ericdallo.clj4intellij.util :as util] 18 | [com.rpl.proxy-plus :refer [proxy+]] 19 | [seesaw.core :as seesaw] 20 | [seesaw.mig :as mig]) 21 | (:import 22 | [com.github.clojure_repl.intellij Icons] 23 | [com.intellij.openapi.editor Editor] 24 | [com.intellij.openapi.editor.impl EditorImpl] 25 | [com.intellij.openapi.fileEditor FileDocumentManager FileEditorManager] 26 | [com.intellij.openapi.project Project ProjectManager] 27 | [com.intellij.openapi.util.text StringUtil] 28 | [com.intellij.openapi.wm ToolWindow ToolWindowAnchor] 29 | [com.intellij.ui.components ActionLink] 30 | [com.intellij.ui.content ContentFactory$SERVICE] 31 | [com.intellij.util.ui JBFont] 32 | [java.io File] 33 | [javax.swing JComponent JScrollPane])) 34 | 35 | (set! *warn-on-reflection* true) 36 | 37 | (defn ^:private any-clj-files? [dir] 38 | (->> (io/file dir) 39 | (walk/postwalk 40 | (fn [^File f] 41 | (if (.isDirectory f) 42 | (file-seq f) 43 | [f]))) 44 | (some #(and (.isFile ^File %) (.endsWith (str %) ".clj"))) 45 | boolean)) 46 | 47 | (defn ^:private test-result-type->color [type] 48 | (get {:pass (.getForegroundColor (ui.color/test-result-pass)) 49 | :fail (.getForegroundColor (ui.color/test-result-fail)) 50 | :error (.getForegroundColor (ui.color/test-result-error))} 51 | type)) 52 | 53 | (defn ^:private label [key value] 54 | ;; TODO support ANSI colors for libs like matcher-combinators pretty prints. 55 | (let [code (ui.text/remove-ansi-color value) 56 | any-project (first (.getOpenProjects (ProjectManager/getInstance))) 57 | font (ui.font/code-font (ui.color/test-summary-code))] 58 | [[(seesaw/label :text key :font font :foreground (.getForegroundColor (ui.color/test-summary-label))) "alignx right, aligny top"] 59 | [(let [field (ui.components/clojure-text-field 60 | :editable? false 61 | :text code 62 | :font font 63 | :project any-project)] 64 | ;; We remove the border after the editor is built 65 | (app-manager/invoke-later! {:invoke-fn 66 | (fn [] 67 | (.setBorder ^JScrollPane (.getScrollPane ^EditorImpl (.getEditor field)) nil))}) 68 | field) "span"]])) 69 | 70 | (defn ^:private navigate-to-test [project {:keys [ns var]}] 71 | (let [{:keys [file line column]} (nrepl/sym-info project ns var) 72 | project (first (.getOpenProjects (ProjectManager/getInstance))) 73 | editor ^Editor (util/uri->editor file project true) 74 | offset (StringUtil/lineColToOffset (.getText (.getDocument editor)) (dec line) (dec column)) 75 | file-editor-manager (FileEditorManager/getInstance project) 76 | file-document-manager (FileDocumentManager/getInstance) 77 | v-file (.getFile file-document-manager (.getDocument editor))] 78 | (.openFile file-editor-manager v-file true) 79 | (.moveToOffset (.getCaretModel editor) offset))) 80 | 81 | (defn ^:private view-error [project ns var] 82 | (ui.components/collapsible 83 | :expanded-title "Hide error" 84 | :collapsed-title "View error" 85 | :title-font (.asBold (JBFont/h3)) 86 | :content (fn [] 87 | (let [{:keys [class stacktrace message data]} (nrepl/test-stacktrace project ns var) 88 | code-font (ui.font/code-font (ui.color/test-summary-code)) 89 | [max-file-length 90 | max-line-length] (reduce (fn [[max-f-size max-l-size] {:keys [file line]}] 91 | (let [c-lines (count (str line)) 92 | c-file (count file) 93 | file-l (if (> c-file max-f-size) c-file max-f-size) 94 | line-l (if (> c-lines max-l-size) c-lines max-l-size)] 95 | [file-l line-l])) [0 0] stacktrace) 96 | stacktrace-text (reduce (fn [text {:keys [fn ns name file line]}] 97 | (format (str "%s%" max-file-length "s %" max-line-length "s %s\n") 98 | text 99 | file 100 | line 101 | (if fn (str ns "/" fn) name))) "" stacktrace)] 102 | (mig/mig-panel 103 | :constraints ["insets 0, gap 8"] 104 | :items [[(seesaw/horizontal-panel 105 | :items [(seesaw/label :text "Exception: " :font (JBFont/regular)) 106 | (seesaw/label :text class 107 | :foreground (.getForegroundColor (ui.color/test-result-error)) 108 | :font code-font)]) "span"] 109 | [(ui.components/clojure-text-field :text (ui.text/remove-ansi-color message) 110 | :project project 111 | :font code-font 112 | :editable? false) "span"] 113 | [(ui.components/clojure-text-field :text (ui.text/pretty-printed-clojure-text data) 114 | :project project 115 | :font code-font 116 | :editable? false) "span"] 117 | ;; TODO improve this component to support clickable links for file-uri 118 | [(ui.components/clojure-text-field :text stacktrace-text 119 | :project project 120 | :font code-font 121 | :editable? false) "span"]]))))) 122 | 123 | (defn ^:private test-report-content ^JComponent [^Project project vars] 124 | (seesaw/scrollable 125 | (mig/mig-panel 126 | :items 127 | (remove 128 | nil? 129 | (for [[_var tests] vars] 130 | (let [non-passing (remove #(= "pass" (:type %)) tests)] 131 | (when (seq non-passing) 132 | [(mig/mig-panel 133 | :items 134 | (for [{:keys [ns var context type message expected actual diffs error gen-input] :as test} non-passing] 135 | [(mig/mig-panel 136 | :items 137 | (->> [[(seesaw/flow-panel 138 | :vgap 0 139 | :hgap 0 140 | :items 141 | [(seesaw/label :text (string/capitalize type) 142 | :foreground (test-result-type->color (keyword type))) 143 | (seesaw/label :text " in ") 144 | (ActionLink. ^String var (proxy+ [] java.awt.event.ActionListener 145 | (actionPerformed [_ _] (navigate-to-test project test))))]) "span"] 146 | (when (seq context) [(seesaw/label :text (str context)) "span"]) 147 | (when (seq message) [(seesaw/label :text (str message)) "span"]) 148 | (when (seq expected) 149 | (label "expected:" expected)) 150 | (if (seq diffs) 151 | (for [[actual [removed added]] diffs] 152 | [(label "actual:" actual) 153 | (label "diff:" (str "- " removed)) 154 | (label "" (str "+ " added))]) 155 | 156 | (when (seq actual) 157 | (label "actual:" actual))) 158 | (when (seq error) 159 | [(label "error:" error) 160 | [(view-error project ns var) "span"]]) 161 | (when (seq gen-input) 162 | (label "input: " gen-input))] 163 | flatten 164 | (remove nil?) 165 | (partition 2) 166 | vec)) "span"])) "span"]))))) 167 | :border nil)) 168 | 169 | (defn ^:private test-report-title-summary [summary elapsed-time] 170 | (let [failed (when (> (:fail summary) 0) (:fail summary)) 171 | error (when (> (:error summary) 0) (:error summary)) 172 | ms (:ms elapsed-time)] 173 | (cond-> "" 174 | failed 175 | (str failed " failed") 176 | 177 | (and failed error) 178 | (str ", ") 179 | 180 | error 181 | (str error " errors") 182 | 183 | ms 184 | (str " in " ms "ms")))) 185 | 186 | (defn ^:private on-test-failed [^Project project ^ToolWindow tool-window {:keys [results summary elapsed-time]}] 187 | (when-not (.isDisposed tool-window) 188 | (let [content-factory (ContentFactory$SERVICE/getInstance) 189 | content-manager (.getContentManager tool-window)] 190 | (app-manager/invoke-later! 191 | {:invoke-fn (fn [] 192 | (.removeAllContents content-manager false) 193 | (doseq [[_ vars] results] 194 | (.addContent content-manager 195 | (.createContent content-factory 196 | (test-report-content project vars) 197 | (test-report-title-summary summary elapsed-time) 198 | false))) 199 | (.setAvailable tool-window true) 200 | (.show tool-window))})))) 201 | 202 | (defn ^:private on-test-succeeded [^Project _project ^ToolWindow tool-window _] 203 | (when-not (.isDisposed tool-window) 204 | (.removeAllContents (.getContentManager tool-window) false) 205 | (.setAvailable tool-window false) 206 | (.hide tool-window))) 207 | 208 | (set! *warn-on-reflection* false) 209 | (defn -init [_ ^ToolWindow tool-window] 210 | (let [project (.getProject tool-window)] 211 | (db/update-in! project [:on-test-failed-fns-by-key tool-window] #(conj % #'on-test-failed)) 212 | (db/update-in! project [:on-test-succeeded-fns-by-key tool-window] #(conj % #'on-test-succeeded)))) 213 | (set! *warn-on-reflection* true) 214 | 215 | (defn -manager [_ _ _]) 216 | 217 | (defn -isApplicableAsync 218 | ([_ ^Project project] 219 | (any-clj-files? (.getBasePath project))) 220 | ([_ ^Project project _] 221 | (-isApplicableAsync _ project))) 222 | 223 | (def -isApplicable -isApplicableAsync) 224 | 225 | (defn -shouldBeAvailable [_ _] false) 226 | 227 | (defn -createToolWindowContent [_ _ _]) 228 | 229 | (defn -getIcon [_] Icons/CLOJURE_REPL) 230 | 231 | (defn -getAnchor [_] ToolWindowAnchor/BOTTOM) 232 | 233 | (defn -manage [_ _ _ _]) 234 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/ui/color.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.ui.color 2 | (:import 3 | [com.intellij.openapi.editor.colors EditorColorsManager TextAttributesKey] 4 | [com.intellij.openapi.editor.markup TextAttributes] 5 | [com.intellij.ui JBColor] 6 | [com.intellij.util.ui UIUtil] 7 | [com.intellij.xdebugger.ui DebuggerColors] 8 | [java.awt Color Font])) 9 | 10 | (set! *warn-on-reflection* true) 11 | 12 | (defn ^:private attr ^TextAttributesKey [& {:keys [key foreground background effect effect-type font-type inherit] 13 | :or {font-type Font/PLAIN}}] 14 | (let [inherit-attrs (some-> ^TextAttributesKey inherit (.getDefaultAttributes) (.clone)) 15 | ^TextAttributes attrs (or inherit-attrs (TextAttributes.))] 16 | (when background (.setBackgroundColor attrs ^Color background)) 17 | (when foreground (.setForegroundColor attrs ^Color foreground)) 18 | (when effect (.setEffectColor attrs ^Color effect)) 19 | (when effect-type (.setEffectType attrs effect-type)) 20 | (when font-type (.setFontType attrs font-type)) 21 | (TextAttributesKey/createTextAttributesKey ^String key attrs))) 22 | 23 | (defn text-attributes [] 24 | {:repl-window (attr :key "REPL_WINDOW" 25 | :background (JBColor/background)) 26 | :eval-inline-hint (attr :key "REPL_EVAL_INLINE_INLAY_HINT" 27 | :background (JBColor. (Color/decode "#c7e8fc") (Color/decode "#16598c")) 28 | :inherit DebuggerColors/INLINED_VALUES_EXECUTION_LINE) 29 | :test-summary--label (attr :key "REPL_TEST_SUMMARY_LABEL" 30 | :foreground JBColor/GRAY) 31 | :test-summary--code (attr :key "REPL_TEST_SUMMARY_CODE") 32 | 33 | :test-result--error (attr :key "REPL_TEST_RESULT_ERROR" 34 | :foreground JBColor/RED) 35 | :test-result--fail (attr :key "REPL_TEST_RESULT_FAIL" 36 | :foreground JBColor/RED) 37 | :test-result--pass (attr :key "REPL_TEST_RESULT_PASS" 38 | :foreground (UIUtil/getToolTipForeground))}) 39 | 40 | (defn ^:private global-attribute-for ^TextAttributes [key] 41 | (.. (EditorColorsManager/getInstance) 42 | getGlobalScheme 43 | (getAttributes (key (text-attributes))))) 44 | 45 | (defn test-result-pass ^TextAttributes [] (global-attribute-for :test-result--pass)) 46 | (defn test-result-fail ^TextAttributes [] (global-attribute-for :test-result--fail)) 47 | (defn test-result-error ^TextAttributes [] (global-attribute-for :test-result--error)) 48 | (defn test-summary-label ^TextAttributes [] (global-attribute-for :test-summary--label)) 49 | (defn test-summary-code ^TextAttributes [] (global-attribute-for :test-summary--code)) 50 | (defn repl-window ^TextAttributes [] (global-attribute-for :repl-window)) 51 | (defn eval-inline-hint ^TextAttributes [] (global-attribute-for :eval-inline-hint)) 52 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/ui/components.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.ui.components 2 | (:require 3 | [com.github.clojure-repl.intellij.keyboard-manager :as key-manager] 4 | [com.rpl.proxy-plus :refer [proxy+]] 5 | [seesaw.core :as seesaw] 6 | [seesaw.layout :as s.layout] 7 | [seesaw.mig :as mig]) 8 | (:import 9 | [com.intellij.icons AllIcons$General] 10 | [com.intellij.openapi.editor EditorFactory] 11 | [com.intellij.openapi.editor.ex EditorEx] 12 | [com.intellij.openapi.fileTypes FileTypeManager] 13 | [com.intellij.ui EditorSettingsProvider EditorTextField IdeBorderFactory] 14 | [java.awt Font] 15 | [java.awt.event ActionEvent] 16 | [javax.swing JButton JComponent])) 17 | 18 | (set! *warn-on-reflection* true) 19 | 20 | (defn clojure-text-field ^EditorTextField 21 | [& {:keys [id background-color text project editable? font on-key-pressed] 22 | :or {editable? true}}] 23 | (let [document (.createDocument (EditorFactory/getInstance) ^String text) 24 | clojure-file-type (.getStdFileType (FileTypeManager/getInstance) "clojure") 25 | editor-text-field (EditorTextField. document project clojure-file-type (not editable?) false)] 26 | (.addSettingsProvider 27 | editor-text-field 28 | (proxy+ [] EditorSettingsProvider 29 | (customizeSettings [_ ^EditorEx editor] 30 | (.setVerticalScrollbarVisible editor true) 31 | (.setHorizontalScrollbarVisible editor true) 32 | (key-manager/register-listener-for-editor! 33 | {:editor editor 34 | :on-key-pressed on-key-pressed})))) 35 | (when id (seesaw/config! editor-text-field :id id)) 36 | (when background-color (.setBackground editor-text-field background-color)) 37 | (when font (.setFont editor-text-field font)) 38 | (when on-key-pressed (.putClientProperty editor-text-field :on-key-pressed on-key-pressed)) 39 | editor-text-field)) 40 | 41 | (defn collapsible ^JComponent 42 | [& {:keys [collapsed-title expanded-title content ^Font title-font]}] 43 | (let [content-panel (seesaw/vertical-panel :visible? false :items []) 44 | toggle ^JButton (seesaw/toggle 45 | :text collapsed-title 46 | :icon AllIcons$General/ArrowRight 47 | :selected? false)] 48 | (when title-font (.setFont toggle title-font)) 49 | (.setBorder toggle (IdeBorderFactory/createBorder (.getBackground toggle))) 50 | (seesaw/listen toggle :action (fn [_] 51 | (let [toggled? (seesaw/config toggle :selected?) 52 | icon (if toggled? AllIcons$General/ArrowDown AllIcons$General/ArrowRight) 53 | text (if toggled? expanded-title collapsed-title)] 54 | (seesaw/config! toggle 55 | :icon icon 56 | :text text) 57 | (seesaw/config! content-panel :visible? toggled?) 58 | (when toggled? 59 | (seesaw/config! content-panel :items [(content)]))))) 60 | (mig/mig-panel :constraints ["insets 0"] 61 | :items [[toggle "span"] 62 | [content-panel "gapx 10"]]))) 63 | 64 | (defn add-field-to-multi-field! [ui group-id initial-text] 65 | (let [group ^JComponent (seesaw/select ui [(keyword (str "#" (name group-id)))]) 66 | prefix (.getClientProperty group :prefix-id) 67 | columns (.getClientProperty group :columns) 68 | new-id (.getComponentCount group) 69 | text-id (keyword (str prefix new-id)) 70 | text-ui (seesaw/text :id text-id :text initial-text :columns columns)] 71 | (s.layout/add-widget group text-ui) 72 | (s.layout/handle-structure-change group))) 73 | 74 | (defn multi-field ^JComponent 75 | [& {:keys [label group-id button-id prefix-id initial-text columns] 76 | :or {columns 15}}] 77 | (let [group-component ^JComponent (mig/mig-panel 78 | :constraints ["gap 0"] 79 | :id group-id 80 | :items [])] 81 | (.putClientProperty group-component :columns columns) 82 | (.putClientProperty group-component :prefix-id prefix-id) 83 | [[(seesaw/label label) ""] 84 | [group-component "gap 0"] 85 | [(seesaw/button 86 | :id button-id 87 | :size [36 :by 36] 88 | :text "+" 89 | :listen [:action (fn [^ActionEvent event] 90 | (let [button (.getSource event) 91 | ui (.getParent ^JComponent button)] 92 | (add-field-to-multi-field! ui group-id initial-text)))]) "gap 0, wrap"]])) 93 | 94 | (defn field-values-from-multi-field [ui group-id] 95 | (->> (.getComponents ^JComponent (seesaw/select ui [(keyword (str "#" (name group-id)))])) 96 | (mapv seesaw/text) 97 | (filterv not-empty))) 98 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/ui/font.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.ui.font 2 | (:import 3 | [com.intellij.openapi.editor Editor] 4 | [com.intellij.openapi.editor.colors EditorColorsManager EditorFontType] 5 | [com.intellij.openapi.editor.markup TextAttributes] 6 | [com.intellij.util.ui UIUtil] 7 | [java.awt Font])) 8 | 9 | (set! *warn-on-reflection* true) 10 | 11 | (defn code-font ^Font 12 | ([^TextAttributes base-text-attr] 13 | (code-font base-text-attr nil)) 14 | ([^TextAttributes base-text-attr ^Editor editor] 15 | (UIUtil/getFontWithFallback 16 | (.getFont (if editor 17 | (.getColorsScheme editor) 18 | (.getGlobalScheme (EditorColorsManager/getInstance))) 19 | (EditorFontType/forJavaStyle (.getFontType base-text-attr)))))) 20 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/ui/hint.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.ui.hint 2 | (:require 3 | [com.github.clojure-repl.intellij.app-info :as app-info]) 4 | (:import 5 | [com.intellij.codeInsight.hint 6 | HintManager] 7 | [com.intellij.openapi.editor Editor])) 8 | 9 | (set! *warn-on-reflection* true) 10 | 11 | (defn show-info 12 | [& {:keys [message editor prefix] 13 | :or {prefix ""}}] 14 | (.showInformationHint (HintManager/getInstance) ^Editor editor (str prefix message) HintManager/RIGHT)) 15 | 16 | (defn show-error 17 | [& {:keys [message editor prefix] 18 | :or {prefix ""}}] 19 | (.showErrorHint (HintManager/getInstance) ^Editor editor (str prefix message) HintManager/RIGHT)) 20 | 21 | (set! *warn-on-reflection* false) 22 | (defn show-success 23 | [& {:keys [message ^Editor editor prefix] 24 | :or {prefix ""}}] 25 | (if (app-info/at-least-version? "233.9802.14") 26 | (.showSuccessHint (HintManager/getInstance) ^Editor editor (str prefix message) HintManager/RIGHT) 27 | (.showInformationHint (HintManager/getInstance) ^Editor editor (str prefix message) HintManager/RIGHT))) 28 | (set! *warn-on-reflection* true) 29 | 30 | (defn show-repl-info [& {:as args}] 31 | (show-info (assoc args :prefix "=> " :message (or (:message args) "nil")))) 32 | 33 | (defn show-repl-error [& {:as args}] 34 | (show-error (assoc args :prefix "=> "))) 35 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/ui/inlay_hint.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.ui.inlay-hint 2 | (:require 3 | [com.github.clojure-repl.intellij.ui.color :as ui.color] 4 | [com.github.clojure-repl.intellij.ui.components :as ui.components] 5 | [com.github.clojure-repl.intellij.ui.font :as font] 6 | [com.github.clojure-repl.intellij.ui.text :as ui.text] 7 | [com.rpl.proxy-plus :refer [proxy+]] 8 | [seesaw.core :as seesaw]) 9 | (:import 10 | [com.intellij.ide.highlighter HighlighterFactory] 11 | [com.intellij.ide.ui.laf.darcula DarculaUIUtil] 12 | [com.intellij.lang Language] 13 | [com.intellij.openapi.editor Editor EditorCustomElementRenderer Inlay] 14 | [com.intellij.openapi.editor.colors EditorFontType] 15 | [com.intellij.openapi.editor.impl EditorImpl FontInfo] 16 | [com.intellij.openapi.editor.markup TextAttributes] 17 | [com.intellij.openapi.fileEditor FileDocumentManager] 18 | [com.intellij.openapi.fileTypes SyntaxHighlighterFactory] 19 | [com.intellij.openapi.ui.popup JBPopupFactory] 20 | [com.intellij.ui SimpleColoredText SimpleTextAttributes] 21 | [com.intellij.util.ui GraphicsUtil JBUI$CurrentTheme$ActionButton] 22 | [java.awt 23 | Cursor 24 | FontMetrics 25 | Graphics 26 | Graphics2D 27 | Rectangle 28 | RenderingHints] 29 | [java.awt.geom RoundRectangle2D$Float])) 30 | 31 | (set! *warn-on-reflection* true) 32 | 33 | (defonce ^:private inlays* (atom {})) 34 | 35 | (defn renderer-class [] (try (Class/forName "com.github.clojure_repl.intellij.ui.inlay_hint.EvalInlineInlayHintRenderer") (catch Exception _ nil))) 36 | 37 | (defn ^:private font+metrics [^Inlay inlay] 38 | (let [editor (.getEditor inlay) 39 | font (font/code-font (ui.color/eval-inline-hint) editor) 40 | metrics (FontInfo/getFontMetrics font (FontInfo/getFontRenderContext (.getContentComponent editor)))] 41 | [font metrics])) 42 | 43 | (defn ^:private clojure-colored-text ^SimpleColoredText 44 | [^Editor editor ^String text] 45 | (let [language (or (Language/findLanguageByID "clojure") 46 | (Language/findLanguageByID "textmate")) 47 | scheme (.getColorsScheme editor) 48 | v-file (.getFile (FileDocumentManager/getInstance) (.getDocument editor)) 49 | syntax-highlighter (SyntaxHighlighterFactory/getSyntaxHighlighter language (.getProject editor) v-file) 50 | highlighter (HighlighterFactory/createHighlighter syntax-highlighter scheme)] 51 | (.setText highlighter text) 52 | (let [colored-text (SimpleColoredText.) 53 | iterator (.createIterator highlighter 0)] 54 | (while (not (.atEnd iterator)) 55 | (let [start (.getStart iterator) 56 | end (.getEnd iterator) 57 | keys (.getTextAttributesKeys iterator) 58 | attributes (if (seq keys) 59 | (SimpleTextAttributes/fromTextAttributes (.getAttributes scheme (first keys))) 60 | SimpleTextAttributes/REGULAR_ATTRIBUTES) 61 | token-text (subs text start end)] 62 | (.append colored-text token-text attributes)) 63 | (.advance iterator)) 64 | colored-text))) 65 | 66 | (defn ^:private paint-clojure-text 67 | [^Graphics2D g ^String text ^Rectangle r ^Editor editor ^FontMetrics font-metrics {:keys [background-x margin]}] 68 | (let [text-x* (atom (int (+ background-x margin))) 69 | presentation (clojure-colored-text editor text)] 70 | (doall 71 | (for [i (range (count (.getTexts presentation))) 72 | :let [cur-text ^String (nth (.getTexts presentation) i) 73 | attr ^SimpleTextAttributes (nth (.getAttributes presentation) i)]] 74 | (do 75 | (.setColor g (.getFgColor attr)) 76 | (.drawString g cur-text ^Integer @text-x* (+ (.y r) (.getAscent editor))) 77 | (reset! text-x* (+ @text-x* (.stringWidth font-metrics cur-text)))))))) 78 | 79 | (defn ^:private remove-in-cur-line [^Editor editor] 80 | (let [inlays (.getAfterLineEndElementsForLogicalLine 81 | (.getInlayModel editor) 82 | (.. editor getCaretModel getLogicalPosition line))] 83 | (doseq [^Inlay inlay inlays] 84 | (.dispose inlay) 85 | (swap! inlays* dissoc (.getRenderer inlay))))) 86 | 87 | (defn ^:private remove-inlay [^Inlay inlay] 88 | (.dispose inlay) 89 | (swap! inlays* dissoc (.getRenderer inlay))) 90 | 91 | (defn ^:private paint-background-rect 92 | [^Graphics2D g {:keys [background-x background-y background-width background-height]}] 93 | (let [config (GraphicsUtil/setupAAPainting g)] 94 | (GraphicsUtil/paintWithAlpha g 0.55) 95 | (.setColor g (.getBackgroundColor (ui.color/eval-inline-hint))) 96 | (.fillRoundRect g 97 | background-x 98 | background-y 99 | background-width 100 | background-height 101 | 6 102 | 6) 103 | (.restore config))) 104 | 105 | (defn ^:private paint-hovered-background [^Graphics2D g {:keys [background-x background-y background-width background-height]}] 106 | (let [^Graphics2D g2 (.create g) 107 | arc (.getFloat DarculaUIUtil/BUTTON_ARC)] 108 | (.setRenderingHint g2 RenderingHints/KEY_ANTIALIASING RenderingHints/VALUE_ANTIALIAS_ON) 109 | (.setRenderingHint g2 RenderingHints/KEY_STROKE_CONTROL RenderingHints/VALUE_STROKE_NORMALIZE) 110 | (.setColor g2 (JBUI$CurrentTheme$ActionButton/hoverBackground)) 111 | (.fill g2 (RoundRectangle2D$Float. background-x background-y background-width background-height arc arc)) 112 | (.dispose g2))) 113 | 114 | (defn ^:private paint 115 | [^String text ^Inlay inlay ^Graphics2D g ^Rectangle r ^TextAttributes _text-attributes] 116 | (let [editor (.getEditor inlay) 117 | [font metrics] (font+metrics inlay) 118 | space (.charWidth ^FontMetrics metrics \s) 119 | gap 1 120 | ui-opts {:space space 121 | :gap gap 122 | :margin (/ space 3) 123 | :background-x (+ (.x r) space) 124 | :background-y (+ gap (.y r)) 125 | :background-width (- (.width r) space) 126 | :background-height (- (.height r) (* 2 gap))} 127 | 128 | {:keys [hovering?]} (get @inlays* (.getRenderer inlay))] 129 | (.setFont g font) 130 | (paint-background-rect g ui-opts) 131 | (when hovering? 132 | (paint-hovered-background g ui-opts)) 133 | (paint-clojure-text g text r editor metrics ui-opts))) 134 | 135 | (defn ^:private create-renderer [^String text ^Editor editor] 136 | (let [inlay-model (.getInlayModel editor) 137 | offset (.getOffset (.getCaretModel editor)) 138 | renderer (proxy+ EvalInlineInlayHintRenderer [] 139 | EditorCustomElementRenderer 140 | (calcWidthInPixels [_ ^Inlay _inlay] 141 | (let [font-metrics (.getFontMetrics (.getComponent editor) 142 | (.getFont (.getColorsScheme editor) EditorFontType/PLAIN)) 143 | space (.charWidth font-metrics \s) 144 | margin (/ space 2)] 145 | (+ margin space (.stringWidth font-metrics text)))) 146 | (paint [_ ^Inlay inlay ^Graphics g ^Rectangle r ^TextAttributes text-attributes] 147 | (paint text inlay g r text-attributes)))] 148 | (.addAfterLineEndElement inlay-model offset false renderer) 149 | renderer)) 150 | 151 | (defn remove-all [^Editor editor] 152 | (when-let [renderer-class (renderer-class)] 153 | (doseq [^Inlay inlay (.getAfterLineEndElementsInRange 154 | (.getInlayModel editor) 155 | 0 156 | (.. editor getDocument getTextLength) 157 | renderer-class)] 158 | (remove-inlay inlay)))) 159 | 160 | (defn mark-inlay-hover-status [^Inlay inlay hovered?] 161 | (when (get-in @inlays* [(.getRenderer inlay) :expandable?]) 162 | (let [cursor (if hovered? (Cursor/getPredefinedCursor Cursor/HAND_CURSOR) nil)] 163 | (swap! inlays* assoc-in [(.getRenderer inlay) :hovering?] hovered?) 164 | (.setCustomCursor ^EditorImpl (.getEditor inlay) (renderer-class) cursor) 165 | (.update inlay)))) 166 | 167 | (defn toggle-expand-inlay-hint [^Inlay inlay] 168 | (let [{:keys [expandable? text]} (get @inlays* (.getRenderer inlay)) 169 | [font _] (font+metrics inlay) 170 | editor (.getEditor inlay)] 171 | (when expandable? 172 | (let [component (seesaw/scrollable 173 | (ui.components/clojure-text-field 174 | :project (.. inlay getEditor getProject) 175 | :editable? false 176 | :text text 177 | :font font) 178 | :maximum-size [800 :by 600]) 179 | popup (.createPopup 180 | (doto 181 | (.createComponentPopupBuilder (JBPopupFactory/getInstance) component component) 182 | (.setResizable false) 183 | (.setRequestFocus true) 184 | (.setCancelOnClickOutside true) 185 | (.setMovable false)))] 186 | (.showInBestPositionFor popup editor))))) 187 | 188 | (defn show-code [^String text ^Editor editor] 189 | (remove-in-cur-line editor) 190 | (let [display-limit 100 ;; Move to customizable config 191 | expandable? (> (count text) display-limit) 192 | summary-text (if expandable? (str (subs text 0 display-limit) " ...") text) 193 | renderer (create-renderer summary-text editor)] 194 | (swap! inlays* assoc renderer {:hovering? false 195 | :expandable? expandable? 196 | :text (ui.text/pretty-printed-clojure-text text) 197 | :summary-text summary-text}))) 198 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/ui/repl.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.ui.repl 2 | (:require 3 | [clojure.string :as string] 4 | [com.github.clojure-repl.intellij.db :as db] 5 | [com.github.clojure-repl.intellij.keyboard-manager :as key-manager] 6 | [com.github.clojure-repl.intellij.ui.color :as ui.color] 7 | [com.github.clojure-repl.intellij.ui.components :as ui.components] 8 | [com.github.clojure-repl.intellij.ui.font :as ui.font] 9 | [com.github.clojure-repl.intellij.ui.text :as ui.text] 10 | [com.github.ericdallo.clj4intellij.app-manager :as app-manager] 11 | [seesaw.core :as seesaw] 12 | [seesaw.mig :as mig]) 13 | (:import 14 | [com.intellij.openapi.editor ScrollType] 15 | [com.intellij.openapi.editor.ex EditorEx] 16 | [com.intellij.openapi.project Project] 17 | [com.intellij.ui EditorTextField] 18 | [java.awt.event InputEvent KeyEvent] 19 | [java.time.format DateTimeFormatter])) 20 | 21 | (set! *warn-on-reflection* true) 22 | 23 | (defn ^:private extract-input+code-to-eval [cur-ns ^String repl-content-text] 24 | (let [input (str cur-ns "> ")] 25 | (or (when-let [input+eval-text (some->> (re-seq (re-pattern (str cur-ns "> .*")) repl-content-text) 26 | last 27 | (string/last-index-of repl-content-text) 28 | (subs repl-content-text))] 29 | [input (string/trim-newline (string/replace input+eval-text input ""))]) 30 | [input ""]))) 31 | 32 | (defn ^:private refresh-repl-text 33 | [^Project project] 34 | (let [repl-content ^EditorTextField (seesaw/select (db/get-in project [:console :ui]) [:#repl-content]) 35 | input-text (db/get-in project [:console :state :last-input]) 36 | output-text (db/get-in project [:console :state :last-output]) 37 | final-text (if input-text 38 | (str output-text "\n" input-text) 39 | output-text)] 40 | (app-manager/invoke-later! 41 | {:invoke-fn (fn [] (.setText repl-content final-text))}))) 42 | 43 | (defn ^:private set-output 44 | [^Project project output-text] 45 | (db/assoc-in! project [:console :state :last-output] (ui.text/remove-ansi-color output-text)) 46 | (refresh-repl-text project)) 47 | 48 | (defn append-output 49 | [^Project project append-text] 50 | (let [last-output (db/get-in project [:console :state :last-output])] 51 | (set-output project (str last-output append-text)))) 52 | 53 | (defn ^:private set-temp-input 54 | [^Project project temp-input] 55 | (let [repl-content ^EditorTextField (seesaw/select (db/get-in project [:console :ui]) [:#repl-content]) 56 | input-text (db/get-in project [:console :state :last-input]) 57 | output-text (db/get-in project [:console :state :last-output]) 58 | final-text (str output-text "\n" input-text temp-input)] 59 | (app-manager/invoke-later! 60 | {:invoke-fn (fn [] (.setText repl-content final-text))}))) 61 | 62 | (defn clear-input [project] 63 | (let [ns-text (str (db/get-in project [:current-nrepl :ns]) "> ")] 64 | (db/assoc-in! project [:console :state :last-input] ns-text) 65 | (refresh-repl-text project))) 66 | 67 | (defn ^:private move-caret-and-scroll-to-latest [^EditorTextField repl-content] 68 | (app-manager/invoke-later! 69 | {:invoke-fn (fn [] 70 | (when-let [editor ^EditorEx (.getEditor repl-content)] 71 | (.moveToOffset (.getCaretModel editor) (.getTextLength (.getDocument repl-content))) 72 | (.scrollToCaret (.getScrollingModel editor) ScrollType/MAKE_VISIBLE)))})) 73 | 74 | (defn ^:private on-repl-input [project on-eval ^KeyEvent event] 75 | (.consume event) 76 | (let [repl-content ^EditorTextField (seesaw/select (db/get-in project [:console :ui]) [:#repl-content]) 77 | repl-content-text (.getText repl-content) 78 | cur-ns (db/get-in project [:current-nrepl :ns]) 79 | [input code-to-eval] (extract-input+code-to-eval cur-ns repl-content-text) 80 | entries (db/get-in project [:current-nrepl :entry-history])] 81 | (db/assoc-in! project [:current-nrepl :entry-index] -1) 82 | (when-not (or (string/blank? code-to-eval) 83 | (= code-to-eval (first entries))) 84 | (db/update-in! project [:current-nrepl :entry-history] #(conj % code-to-eval))) 85 | (let [{:keys [value ns] :as response} (on-eval code-to-eval) 86 | result-text (str 87 | "\n" input code-to-eval (when value "\n") 88 | (when value (str "=> " (last value))))] 89 | (append-output project result-text) 90 | (move-caret-and-scroll-to-latest repl-content) 91 | (when (and ns (not= ns cur-ns)) 92 | (db/assoc-in! project [:current-nrepl :ns] ns) 93 | (doseq [fn (db/get-in project [:on-ns-changed-fns])] 94 | (fn project response))))) 95 | true) 96 | 97 | (defn history-up 98 | [project] 99 | (let [entries (db/get-in project [:current-nrepl :entry-history]) 100 | current-index (db/get-in project [:current-nrepl :entry-index])] 101 | (when (and (pos? (count entries)) 102 | (< current-index (dec (count entries)))) 103 | (let [entry (nth entries (inc current-index)) 104 | console (db/get-in project [:console :ui]) 105 | repl-content (seesaw/select console [:#repl-content])] 106 | (db/update-in! project [:current-nrepl :entry-index] inc) 107 | (set-temp-input project entry) 108 | (move-caret-and-scroll-to-latest repl-content))))) 109 | 110 | (defn history-down 111 | [project] 112 | (let [entries (db/get-in project [:current-nrepl :entry-history]) 113 | current-index (db/get-in project [:current-nrepl :entry-index])] 114 | (when (and (pos? (count entries)) 115 | (> current-index 0)) 116 | (let [entry (nth entries (dec current-index)) 117 | console (db/get-in project [:console :ui]) 118 | repl-content (seesaw/select console [:#repl-content])] 119 | (db/update-in! project [:current-nrepl :entry-index] dec) 120 | (set-temp-input project entry) 121 | (move-caret-and-scroll-to-latest repl-content))))) 122 | 123 | (defn ^:private on-repl-new-line [project] 124 | (append-output project "\n") 125 | true) 126 | 127 | (defn clear-repl [^Project project] 128 | (set-output project ";; Output cleared")) 129 | 130 | (defn ^:private on-repl-clear [project] 131 | (clear-repl project) 132 | true) 133 | 134 | (defn ^:private on-repl-backspace [project ^KeyEvent event] 135 | (let [ns (db/get-in project [:current-nrepl :ns]) 136 | repl-content ^EditorTextField (seesaw/select (db/get-in project [:console :ui]) [:#repl-content]) 137 | repl-lines (string/split-lines (.getText repl-content)) 138 | last-repl-line (last repl-lines) 139 | repl-input (re-find (re-pattern (str ns "+>\\s")) last-repl-line)] 140 | (when-not repl-input 141 | (clear-input project) 142 | (move-caret-and-scroll-to-latest repl-content) 143 | (.consume event)))) 144 | 145 | (defn build-console [project {:keys [initial-text on-eval]}] 146 | (db/assoc-in! project [:console :state :status] :disabled) 147 | (db/assoc-in! project [:console :state :initial-text] initial-text) 148 | (db/assoc-in! project [:console :state :last-output] "") 149 | (db/assoc-in! project [:console :state :last-input] nil) 150 | (mig/mig-panel 151 | :id :repl-input-layout 152 | :constraints ["fill, insets 0"] 153 | :background (.getBackgroundColor (ui.color/repl-window)) 154 | :items [[(ui.components/clojure-text-field 155 | :id :repl-content 156 | :project project 157 | :text "" 158 | :background-color (.getBackgroundColor (ui.color/repl-window)) 159 | :font (ui.font/code-font (ui.color/repl-window)) 160 | :on-key-pressed (fn [^KeyEvent event] 161 | (if (identical? :enabled (db/get-in project [:console :state :status])) 162 | (let [ctrl? (not= 0 (bit-and (.getModifiers event) InputEvent/CTRL_MASK)) 163 | shift? (not= 0 (bit-and (.getModifiers event) InputEvent/SHIFT_MASK)) 164 | enter? (= KeyEvent/VK_ENTER (.getKeyCode event)) 165 | backspace? (= KeyEvent/VK_BACK_SPACE (.getKeyCode event)) 166 | l? (= KeyEvent/VK_L (.getKeyCode event))] 167 | (cond 168 | 169 | (and shift? enter?) 170 | (on-repl-new-line project) 171 | 172 | (and enter? (not shift?)) 173 | (on-repl-input project on-eval event) 174 | 175 | (and ctrl? l?) 176 | (on-repl-clear project) 177 | 178 | backspace? 179 | (on-repl-backspace project event))) 180 | false))) 181 | "grow"]])) 182 | 183 | (defn set-repl-started-initial-text [project console text] 184 | (let [output (str text "\n") 185 | repl-content (seesaw/select console [:#repl-content])] 186 | (db/assoc-in! project [:console :state :status] :enabled) 187 | (db/assoc-in! project [:console :state :initial-text] text) 188 | (append-output project output) 189 | (clear-input project) 190 | (refresh-repl-text project) 191 | (move-caret-and-scroll-to-latest repl-content))) 192 | 193 | (def ^:private ^DateTimeFormatter time-formatter (DateTimeFormatter/ofPattern "dd/MM/yyyy HH:mm:ss")) 194 | 195 | (defn close-console [project console] 196 | (let [repl-content ^EditorTextField (seesaw/select console [:#repl-content])] 197 | (db/assoc-in! project [:console :state :status] :disabled) 198 | (db/assoc-in! project [:console :state :last-input] nil) 199 | (.setViewer repl-content false) 200 | (append-output project 201 | (format "\n*** Closed on %s ***" (.format time-formatter (java.time.LocalDateTime/now)))) 202 | (key-manager/unregister-listener-for-editor! (.getEditor repl-content)))) 203 | -------------------------------------------------------------------------------- /src/main/clojure/com/github/clojure_repl/intellij/ui/text.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.ui.text 2 | (:require 3 | [clojure.edn :as edn] 4 | [clojure.pprint :as pprint] 5 | [clojure.string :as string] 6 | [com.github.ericdallo.clj4intellij.logger :as logger])) 7 | 8 | (defn pretty-printed-clojure-text [text] 9 | (try 10 | (with-out-str (pprint/pprint (edn/read-string {:default (fn [tag value] 11 | (if (coll? value) 12 | (pretty-printed-clojure-text value) 13 | (symbol (str "#" tag value))))} text))) 14 | (catch Exception e 15 | (logger/warn "Can't parse clojure code for eval block" e) 16 | text))) 17 | 18 | (defn remove-ansi-color ^String [text] 19 | ;; TODO support ANSI colors for libs like matcher-combinators pretty prints. 20 | (string/replace text #"\u001B\[[;\d]*m" "")) 21 | -------------------------------------------------------------------------------- /src/main/dev-resources/META-INF/clj4intellij.edn: -------------------------------------------------------------------------------- 1 | {:nrepl {:port 7770}} 2 | -------------------------------------------------------------------------------- /src/main/dev-resources/META-INF/clojure-repl-intellij.edn: -------------------------------------------------------------------------------- 1 | {:nrepl-debug true} 2 | -------------------------------------------------------------------------------- /src/main/kotlin/icons.kt: -------------------------------------------------------------------------------- 1 | package com.github.clojure_repl.intellij 2 | 3 | import com.intellij.openapi.util.IconLoader 4 | import javax.swing.Icon 5 | 6 | object Icons { 7 | @JvmField val CLOJURE = IconLoader.getIcon("/icons/clojure.svg", Icons::class.java) 8 | @JvmField val CLOJURE_REPL = IconLoader.getIcon("/icons/clojure_repl.svg", Icons::class.java) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/run-configuration-options.kt: -------------------------------------------------------------------------------- 1 | package com.github.clojure_repl.intellij.configuration 2 | 3 | import com.intellij.execution.configurations.RunConfigurationOptions 4 | 5 | class ReplRemoteRunOptions : RunConfigurationOptions() { 6 | 7 | private var nreplHostOption = string("localhost").provideDelegate(this, "nreplHost") 8 | private var nreplPortOption = string("").provideDelegate(this, "nreplPort") 9 | private var projectOption = string("").provideDelegate(this, "project") 10 | private var modeOption = string("manual-config").provideDelegate(this, "mode") 11 | 12 | var nreplHost: String 13 | get() = nreplHostOption.getValue(this) ?: "localhost" 14 | set(value) = nreplHostOption.setValue(this, value) 15 | 16 | var nreplPort: String 17 | get() = nreplPortOption.getValue(this) ?: "" 18 | set(value) = nreplPortOption.setValue(this, value) 19 | 20 | var project: String 21 | get() = projectOption.getValue(this) ?: "" 22 | set(value) = projectOption.setValue(this, value) 23 | 24 | var mode: String 25 | get() = modeOption.getValue(this) ?: "manual-config" 26 | set(value) = modeOption.setValue(this, value) 27 | } 28 | 29 | class ReplLocalRunOptions : RunConfigurationOptions() { 30 | 31 | private var projectOption = string("").provideDelegate(this, "project") 32 | private var projectTypeOption = string("").provideDelegate(this, "projectType") 33 | private var aliasesOption = list().provideDelegate(this, "aliases") 34 | private var envVarsOption = list().provideDelegate(this, "envVars") 35 | private var jvmArgsOption = list().provideDelegate(this, "jvmArgs") 36 | 37 | var project: String 38 | get() = projectOption.getValue(this) ?: "" 39 | set(value) = projectOption.setValue(this, value) 40 | 41 | var projectType: String? 42 | get() = projectTypeOption.getValue(this) 43 | set(value) = projectTypeOption.setValue(this, value) 44 | 45 | var aliases: MutableList 46 | get() = aliasesOption.getValue(this) 47 | set(value) = aliasesOption.setValue(this, value) 48 | 49 | var envVars: MutableList 50 | get() = envVarsOption.getValue(this) 51 | set(value) = envVarsOption.setValue(this, value) 52 | 53 | var jvmArgs: MutableList 54 | get() = jvmArgsOption.getValue(this) 55 | set(value) = jvmArgsOption.setValue(this, value) 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.github.clojure-repl 3 | Clojure REPL 4 | Arthur Fücher 5 | 6 | com.intellij.modules.platform 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | > 54 | _ 66 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/resources/icons/clojure-mono.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 38 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/resources/icons/clojure.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 35 | 37 | 38 | 40 | image/svg+xml 41 | 43 | 44 | 45 | 46 | 48 | 52 | 56 | 60 | 64 | 68 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/resources/icons/clojure_repl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | > 54 | _ 66 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/scripts/scripts.clj: -------------------------------------------------------------------------------- 1 | (ns scripts 2 | (:require 3 | [babashka.fs :as fs] 4 | [babashka.tasks :refer [shell]] 5 | [clojure.string :as string])) 6 | 7 | (def version-regex #"pluginVersion = ([0-9]+.[0-9]+.[0-9]+.*)") 8 | 9 | (defn ^:private replace-in-file [file regex content] 10 | (as-> (slurp file) $ 11 | (string/replace $ regex content) 12 | (spit file $))) 13 | 14 | (defn ^:private add-changelog-entry [tag comment] 15 | (replace-in-file "CHANGELOG.md" 16 | #"## \[Unreleased\]" 17 | (if comment 18 | (format "## [Unreleased]\n\n## %s\n\n- %s" tag comment) 19 | (format "## [Unreleased]\n\n## %s" tag)))) 20 | 21 | (defn ^:private replace-tag [tag] 22 | (replace-in-file "gradle.properties" 23 | version-regex 24 | (format "pluginVersion = %s" tag))) 25 | 26 | #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} 27 | (defn tag [& [tag]] 28 | (shell "git fetch origin") 29 | (shell "git pull origin HEAD") 30 | (replace-tag tag) 31 | (add-changelog-entry tag nil) 32 | (shell "git add gradle.properties CHANGELOG.md") 33 | (shell (format "git commit -m \"Release: %s\"" tag)) 34 | (shell (str "git tag " tag)) 35 | (shell "git push origin HEAD") 36 | (shell "git push origin --tags")) 37 | 38 | #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} 39 | (defn tests [] 40 | (shell "./gradlew test")) 41 | 42 | #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} 43 | (defn build-plugin [] 44 | (shell "./gradlew buildPlugin")) 45 | 46 | #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} 47 | (defn run-ide [] 48 | (shell "./gradlew runIde")) 49 | 50 | #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} 51 | (defn install-plugin [& [intellij-plugins-path]] 52 | (if-not intellij-plugins-path 53 | (println "Specify the Intellij plugins path\ne.g: bb install-plugin /home/youruser/.local/share/JetBrains/IdeaIC2024.3") 54 | (let [version (last (re-find version-regex (slurp "gradle.properties")))] 55 | (build-plugin) 56 | (fs/unzip (format "./build/distributions/clojure-repl-%s.zip" version) 57 | intellij-plugins-path 58 | {:replace-existing true}) 59 | (println "Installed!")))) 60 | 61 | #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} 62 | (defn publish-plugin [] 63 | (shell "./gradlew publishPlugin")) 64 | -------------------------------------------------------------------------------- /src/test/clojure/com/github/clojure_repl/intellij/repl_command_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.repl-command-test 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [com.github.clojure-repl.intellij.repl-command :as repl-command])) 5 | 6 | (deftest project->repl-start-command-test 7 | (testing "clojure" 8 | (is (= ["clojure" 9 | "-Sdeps" 10 | "{:deps {nrepl/nrepl {:mvn/version \"1.3.1\"} cider/cider-nrepl {:mvn/version \"0.55.7\"}} :aliases {:cider/nrepl {:main-opts [\"-m\" \"nrepl.cmdline\" \"--middleware\" \"[cider.nrepl/cider-middleware]\"]}}}" 11 | "-M:cider/nrepl"] 12 | (repl-command/project->repl-start-command :clojure [] []))) 13 | (is (= ["clojure" 14 | "-Sdeps" 15 | "{:deps {nrepl/nrepl {:mvn/version \"1.3.1\"} cider/cider-nrepl {:mvn/version \"0.55.7\"}} :aliases {:cider/nrepl {:main-opts [\"-m\" \"nrepl.cmdline\" \"--middleware\" \"[cider.nrepl/cider-middleware]\"]}}}" 16 | "-M:foo-b:bar:cider/nrepl"] 17 | (repl-command/project->repl-start-command :clojure ["foo-b" "bar"] []))) 18 | (testing "windows powershell" 19 | (with-redefs [repl-command/windows-os? (constantly true) 20 | repl-command/locate-executable (fn [cmd] 21 | (when (= "powershell" cmd) 22 | "/full/path/powershell")) 23 | repl-command/shell (constantly {:exit 0})] 24 | (is (= ["/full/path/powershell" "-NoProfile" "-Command" "clojure" 25 | "-Sdeps" 26 | "{:deps {nrepl/nrepl {:mvn/version \"1.3.1\"} cider/cider-nrepl {:mvn/version \"0.55.7\"}} :aliases {:cider/nrepl {:main-opts [\"-m\" \"nrepl.cmdline\" \"--middleware\" \"[cider.nrepl/cider-middleware]\"]}}}" 27 | "-M:cider/nrepl"] 28 | (repl-command/project->repl-start-command :clojure [] []))))) 29 | (testing "windows pwsh" 30 | (with-redefs [repl-command/windows-os? (constantly true) 31 | repl-command/locate-executable (fn [cmd] 32 | (when (= "pwsh" cmd) 33 | "/full/path/pwsh")) 34 | repl-command/shell (constantly {:exit 0})] 35 | (is (= ["/full/path/pwsh" "-NoProfile" "-Command" "clojure" 36 | "-Sdeps" 37 | "{:deps {nrepl/nrepl {:mvn/version \"1.3.1\"} cider/cider-nrepl {:mvn/version \"0.55.7\"}} :aliases {:cider/nrepl {:main-opts [\"-m\" \"nrepl.cmdline\" \"--middleware\" \"[cider.nrepl/cider-middleware]\"]}}}" 38 | "-M:cider/nrepl"] 39 | (repl-command/project->repl-start-command :clojure [] [])))))) 40 | (testing "lein" 41 | (is (= ["lein" "update-in" ":dependencies" "conj" "[nrepl/nrepl \"1.3.1\"]" 42 | "--" "update-in" ":plugins" "conj" "[cider/cider-nrepl \"0.55.7\"]" 43 | "--" "repl" ":headless" ":host" "localhost"] 44 | (repl-command/project->repl-start-command :lein [] []))) 45 | (is (= ["lein" "update-in" ":dependencies" "conj" "[nrepl/nrepl \"1.3.1\"]" 46 | "--" "update-in" ":plugins" "conj" "[cider/cider-nrepl \"0.55.7\"]" 47 | "--" "with-profile" "+foo-b,+bar" "repl" ":headless" ":host" "localhost"] 48 | (repl-command/project->repl-start-command :lein ["foo-b" "bar"] []))) 49 | (testing "windows powershell" 50 | (with-redefs [repl-command/windows-os? (constantly true) 51 | repl-command/locate-executable (fn [cmd] 52 | (when (= "powershell" cmd) 53 | "/full/path/powershell")) 54 | repl-command/shell (constantly {:exit 0})] 55 | (is (= ["/full/path/powershell" "-NoProfile" "-Command" "lein" 56 | "update-in" ":dependencies" "conj" "[nrepl/nrepl \"1.3.1\"]" 57 | "--" "update-in" ":plugins" "conj" "[cider/cider-nrepl \"0.55.7\"]" 58 | "--" "repl" ":headless" ":host" "localhost"] 59 | (repl-command/project->repl-start-command :lein [] []))))) 60 | (testing "windows pwsh" 61 | (with-redefs [repl-command/windows-os? (constantly true) 62 | repl-command/locate-executable (fn [cmd] 63 | (when (= "pwsh" cmd) 64 | "/full/path/pwsh")) 65 | repl-command/shell (constantly {:exit 0})] 66 | (is (= ["/full/path/pwsh" "-NoProfile" "-Command" "lein" 67 | "update-in" ":dependencies" "conj" "[nrepl/nrepl \"1.3.1\"]" 68 | "--" "update-in" ":plugins" "conj" "[cider/cider-nrepl \"0.55.7\"]" 69 | "--" "repl" ":headless" ":host" "localhost"] 70 | (repl-command/project->repl-start-command :lein [] [])))))) 71 | (testing "babashka" 72 | (is (= ["bb" "nrepl-server" "localhost:0"] 73 | (repl-command/project->repl-start-command :babashka [] [])))) 74 | (testing "shadow-cljs" 75 | (is (= ["npx" "shadow-cljs" "server"] 76 | (repl-command/project->repl-start-command :shadow-cljs [] [])))) 77 | (testing "boot" 78 | (is (= ["boot" "repl" "-s" "-b" "localhost" "wait"] 79 | (repl-command/project->repl-start-command :boot [] [])))) 80 | (testing "nbb" 81 | (is (= ["nbb" "nrepl-server"] 82 | (repl-command/project->repl-start-command :nbb [] [])))) 83 | (testing "gradle" 84 | (is (= ["./gradlew" 85 | "-Pdev.clojurephant.jack-in.nrepl=nrepl:nrepl:1.3.1,cider:cider-nrepl:0.55.7" 86 | "clojureRepl" 87 | "--middleware=cider.nrepl/cider-middleware"] 88 | (repl-command/project->repl-start-command :gradle [] []))))) 89 | -------------------------------------------------------------------------------- /src/test/clojure/com/github/clojure_repl/intellij/repl_eval_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.repl-eval-test 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.test :refer [deftest is testing]] 5 | [com.github.clojure-repl.intellij.configuration.factory.local :as config.factory.local] 6 | [com.github.clojure-repl.intellij.db :as db] 7 | [com.github.ericdallo.clj4intellij.app-manager :as app-manager] 8 | [com.github.ericdallo.clj4intellij.test :as clj4intellij.test] 9 | [seesaw.core :as seesaw]) 10 | (:import 11 | [com.github.clojure_repl.intellij.configuration ReplRunConfigurationType] 12 | [com.intellij.execution ProgramRunnerUtil RunManager] 13 | [com.intellij.execution.executors DefaultRunExecutor] 14 | [com.intellij.openapi.util ThrowableComputable] 15 | [com.intellij.testFramework EditorTestUtil EdtTestUtil] 16 | [java.awt.event KeyEvent])) 17 | 18 | (set! *warn-on-reflection* true) 19 | 20 | ;;Move to a helper inside tests folder 21 | (defn ^:private repl-content [project] 22 | (-> project 23 | (db/get-in [:console :ui]) 24 | (seesaw/select [:#repl-content]))) 25 | 26 | (defn ensure-editor 27 | "Ensure the editor was created in the UI thread" 28 | [project] 29 | (let [repl-content (repl-content project)] 30 | @(app-manager/invoke-later! 31 | {:invoke-fn (fn [] 32 | (.addNotify repl-content) 33 | (.getEditor repl-content true))}))) 34 | 35 | (defn wait-console-ui-creation 36 | "Waits until the console UI is set in the db*, then ensures the editor is created" 37 | [project] 38 | @(clj4intellij.test/dispatch-all-until 39 | {:cond-fn (fn [] (-> project 40 | (db/get-in [:console :ui])))}) 41 | (ensure-editor project)) 42 | 43 | (defn execute-configuration 44 | "API for ProgramRunnerUtil/executeConfiguration 45 | 46 | ref: https://github.com/JetBrains/intellij-community/blob/2766d0bf1cec76c0478244f6ad5309af527c245e/platform/execution-impl/src/com/intellij/execution/ProgramRunnerUtil.java#L46" 47 | [configuration-instance] 48 | (ProgramRunnerUtil/executeConfiguration 49 | configuration-instance 50 | (DefaultRunExecutor/getRunExecutorInstance))) 51 | 52 | (defn eval-code-on-repl [repl-content text] 53 | (let [editor (.getEditor repl-content) 54 | component (.getContentComponent editor) 55 | key-enter-event (KeyEvent. component KeyEvent/KEY_PRESSED (System/currentTimeMillis) 0 KeyEvent/VK_ENTER Character/MIN_VALUE)] 56 | (EdtTestUtil/runInEdtAndGet 57 | (reify ThrowableComputable 58 | (compute [_] 59 | (doseq [char text] 60 | (EditorTestUtil/performTypingAction editor char)) 61 | (.dispatchEvent component key-enter-event) 62 | nil))))) 63 | 64 | (deftest repl-eval-test 65 | (let [project-name "clojure.core" 66 | fixture (clj4intellij.test/setup project-name) 67 | deps-file (.createFile fixture "deps.edn" "{}") 68 | project (.getProject fixture)] 69 | (is (= project-name (.getName project))) 70 | (is deps-file) 71 | 72 | (app-manager/write-command-action 73 | project 74 | (fn [] (.openFileInEditor fixture deps-file))) 75 | 76 | (let [run-manager (RunManager/getInstance project) 77 | configuration (config.factory.local/configuration-factory (ReplRunConfigurationType.)) 78 | configuration-instance (.createConfiguration run-manager "Local REPL" configuration)] 79 | (doto (-> configuration-instance .getConfiguration .getOptions) 80 | (.setProject project-name) 81 | (.setProjectType "clojure")) 82 | (execute-configuration configuration-instance)) 83 | 84 | (wait-console-ui-creation project) 85 | 86 | (testing "user input evaluation" 87 | (let [repl-content (repl-content project)] 88 | 89 | @(clj4intellij.test/dispatch-all-until 90 | {:cond-fn (fn [] (str/ends-with? (.getText repl-content) "user> "))}) 91 | 92 | (eval-code-on-repl repl-content "(+ 1 1)\n") 93 | (clj4intellij.test/dispatch-all) 94 | 95 | (let [content (.getText repl-content)] 96 | (is (str/ends-with? content "user> (+ 1 1)\n=> 2\nuser> "))))))) 97 | 98 | -------------------------------------------------------------------------------- /src/test/clojure/com/github/clojure_repl/intellij/ui/repl_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.github.clojure-repl.intellij.ui.repl-test 2 | (:require 3 | [clojure.string :as string] 4 | [clojure.test :refer [deftest is]] 5 | [com.github.clojure-repl.intellij.ui.repl :as ui.repl])) 6 | 7 | (defn code [& strings] 8 | (string/join "\n" strings)) 9 | 10 | (deftest extract-code-to-eval-test 11 | (is (= ["user> " "(+ 2 3)"] 12 | (#'ui.repl/extract-input+code-to-eval "user" 13 | (code ";; some text here" 14 | "user> (+ 1 2)" 15 | ";; => 1" 16 | "user> (+ 2 3)")))) 17 | (is (= ["user> " "(tap> 1)"] 18 | (#'ui.repl/extract-input+code-to-eval "user" 19 | (code ";; some text here" 20 | "user> 1" 21 | ";; => 1" 22 | "user> (tap> 1)")))) 23 | (is (= ["user-bar.baz> " "(tap> 1)"] 24 | (#'ui.repl/extract-input+code-to-eval "user-bar.baz" 25 | (code ";; some text here" 26 | "user-bar.baz> 1" 27 | ";; => 1" 28 | "user-bar.baz> (tap> 1)")))) 29 | (is (= ["user-bar.baz> " (code "(defn foo []" 30 | " 123)")] 31 | (#'ui.repl/extract-input+code-to-eval "user-bar.baz" 32 | (code ";; some text here" 33 | "user-bar.baz> 1" 34 | ";; => 1" 35 | "user-bar.baz> (defn foo []" 36 | " 123)")))) 37 | (is (= ["user> " (code "(defn foo []" 38 | " (tap> 123)" 39 | " 123)")] 40 | (#'ui.repl/extract-input+code-to-eval "user" 41 | (code ";; some text here" 42 | "user> 1" 43 | ";; => 1" 44 | "user> (defn foo []" 45 | " (tap> 123)" 46 | " 123)")))) 47 | (is (= ["user-bar.baz> " (code "(defn foo []" 48 | " (tap> 123)" 49 | " 123)")] 50 | (#'ui.repl/extract-input+code-to-eval "user-bar.baz" 51 | (code ";; some text here" 52 | "user-bar.baz> 1" 53 | ";; => 1" 54 | "user-bar.baz> (defn foo []" 55 | " (tap> 123)" 56 | " 123)")))) 57 | (is (= ["user-bar.baz> " "(+ 2 3)"] 58 | (#'ui.repl/extract-input+code-to-eval "user-bar.baz" 59 | (code ";; some text here" 60 | "user-bar.baz> " 61 | "user-bar.baz> (+ 2 3)")))) 62 | (is (= ["user> " "1"] 63 | (#'ui.repl/extract-input+code-to-eval "user" 64 | (code ";; some text here" 65 | "user> 1" 66 | "=> 1" 67 | "user> 1"))))) 68 | 69 | (comment 70 | (extract-code-to-eval-test)) 71 | --------------------------------------------------------------------------------