diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..4b8de7d --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,40 @@ +name: Tests +on: + pull_request: + +permissions: + contents: read + checks: write + pull-requests: write + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + cache: 'gradle' + + - name: Install Clojure + uses: DeLaGuardo/setup-clojure@master + with: + bb: '1.12.196' + cli: 1.12.0.1530 + + - name: Run tests + run: ./gradlew test + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() # always run even if the previous step fails + with: + report_paths: '**/build/test-results/test/TEST-*.xml' + simplified_summary: true + comment: false diff --git a/bb.edn b/bb.edn index eb8be09..0026ff5 100644 --- a/bb.edn +++ b/bb.edn @@ -1,5 +1,6 @@ {:paths ["src/scripts"] :tasks {tag scripts/tag build-plugin scripts/build-plugin + test scripts/tests install-plugin scripts/install-plugin publish-plugin scripts/publish-plugin}} diff --git a/build.gradle.kts b/build.gradle.kts index 131a52b..156888f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,18 +35,24 @@ repositories { dependencies { implementation ("org.clojure:clojure:1.12.0") - implementation ("com.github.ericdallo:clj4intellij:0.7.1") + implementation ("com.github.ericdallo:clj4intellij:0.8.0") implementation ("seesaw:seesaw:1.5.0") implementation ("camel-snake-kebab:camel-snake-kebab:0.4.3") implementation ("com.rpl:proxy-plus:0.0.9") implementation ("dev.weavejester:cljfmt:0.13.0") implementation ("com.github.clojure-lsp:clojure-lsp:2025.01.22-23.28.23") + implementation ("nrepl:nrepl:1.3.1") + + testImplementation("junit:junit:latest.release") + testImplementation("org.junit.platform:junit-platform-launcher:latest.release") + testRuntimeOnly ("dev.clojurephant:jovial:0.4.2") } sourceSets { main { java.srcDirs("src/main", "src/gen") if (project.gradle.startParameter.taskNames.contains("buildPlugin") || + project.gradle.startParameter.taskNames.contains("clojureRepl") || project.gradle.startParameter.taskNames.contains("runIde")) { resources.srcDirs("src/main/dev-resources") } @@ -146,6 +152,10 @@ tasks { systemProperty("jb.consents.confirmation.enabled", "false") } + test { + systemProperty("idea.mimic.jar.url.connection", "true") + } + signPlugin { certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) privateKey.set(System.getenv("PRIVATE_KEY")) @@ -165,6 +175,26 @@ tasks { enabled = false } + clojureRepl { + dependsOn("compileClojure") + classpath.from(sourceSets.main.get().runtimeClasspath + + file("build/classes/kotlin/main") + + file("build/clojure/main") + ) + // doFirst { + // println(classpath.asPath) + // } + forkOptions.jvmArgs = listOf("--add-opens=java.desktop/java.awt=ALL-UNNAMED", + "--add-opens=java.desktop/java.awt.event=ALL-UNNAMED", + "--add-opens=java.desktop/sun.awt=ALL-UNNAMED", + "--add-opens=java.desktop/sun.font=ALL-UNNAMED", + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "-Djava.system.class.loader=com.intellij.util.lang.PathClassLoader", + "-Didea.mimic.jar.url.connection=true", + "-Didea.force.use.core.classloader=true" + ) + } + generateParser { source.set("src/main/gramar/clojure.bnf") targetRoot.set("src/gen") @@ -180,6 +210,10 @@ tasks { } } +tasks.withType().configureEach { + useJUnitPlatform() +} + grammarKit { jflexRelease.set("1.7.0-1") grammarKitRelease.set("2021.1.2") @@ -187,7 +221,7 @@ grammarKit { } clojure.builds.named("main") { - classpath.from(sourceSets.main.get().runtimeClasspath.asPath) + classpath.from(sourceSets.main.get().runtimeClasspath.asPath + "build/classes/kotlin/main") checkAll() aotAll() reflection.set("fail") @@ -213,6 +247,6 @@ fun fetchLatestLsp4ijNightlyVersion(): String { println("Failed to fetch LSP4IJ nightly build version: ${e.message}") } - val minVersion = "0.0.1-20231213-012910" + val minVersion = "0.12.1-20250404-161025" return if (minVersion < onlineVersion) onlineVersion else minVersion } diff --git a/src/main/clojure/com/github/clojure_lsp/intellij/editor.clj b/src/main/clojure/com/github/clojure_lsp/intellij/editor.clj index 1ae12d6..e128142 100644 --- a/src/main/clojure/com/github/clojure_lsp/intellij/editor.clj +++ b/src/main/clojure/com/github/clojure_lsp/intellij/editor.clj @@ -1,9 +1,10 @@ (ns com.github.clojure-lsp.intellij.editor (:require [com.github.clojure-lsp.intellij.db :as db] - [com.github.clojure-lsp.intellij.editor :as editor]) + [com.github.clojure-lsp.intellij.editor :as editor] + [com.github.ericdallo.clj4intellij.app-manager :as app-manager]) (:import - [com.intellij.openapi.editor Editor] + [com.intellij.openapi.editor CaretModel Editor LogicalPosition] [com.intellij.openapi.fileEditor FileDocumentManager] [com.intellij.openapi.project ProjectLocator] [com.intellij.openapi.util.text StringUtil] @@ -23,3 +24,11 @@ (defn guess-project-for [^VirtualFile file] (or (.guessProjectForFile (ProjectLocator/getInstance) file) (first (db/all-projects)))) + +(defn move-caret-to-position + "Moves the caret to the specified logical position in the editor." + [^Editor editor line column] + (let [caret ^CaretModel (.getCaretModel editor) + new-position (LogicalPosition. line column)] + @(app-manager/invoke-later! + {:invoke-fn (fn [] (.moveToLogicalPosition caret new-position))}))) diff --git a/src/scripts/scripts.clj b/src/scripts/scripts.clj index be98fe9..b4ca30d 100644 --- a/src/scripts/scripts.clj +++ b/src/scripts/scripts.clj @@ -35,6 +35,9 @@ (shell "git push origin HEAD") (shell "git push origin --tags")) +(defn tests [] + (shell "./gradlew test")) + #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn build-plugin [] (shell "./gradlew buildPlugin")) diff --git a/src/test/clojure/com/github/clojure_lsp/intellij/slurp_action_test.clj b/src/test/clojure/com/github/clojure_lsp/intellij/slurp_action_test.clj new file mode 100644 index 0000000..c96846c --- /dev/null +++ b/src/test/clojure/com/github/clojure_lsp/intellij/slurp_action_test.clj @@ -0,0 +1,39 @@ +(ns com.github.clojure-lsp.intellij.slurp-action-test + (:require + [clojure.test :refer [deftest is]] + [com.github.clojure-lsp.intellij.editor :as editor] + [com.github.clojure-lsp.intellij.test-utils :as test-utils] + [com.github.ericdallo.clj4intellij.test :as clj4intellij.test]) + (:import + [com.intellij.openapi.project Project] + [com.intellij.testFramework.fixtures CodeInsightTestFixture])) + +(set! *warn-on-reflection* true) + +(deftest slurp-action-test + "Tests the Forward Slurp editor action functionality in Clojure LSP. + This test: + 1. Sets up a test project with a Clojure file + 2. Opens the file in the editor + 3. Sets up the LSP server + 4. Moves the caret to a specific position + 5. Executes the Forward Slurp action + 6. Verifies the resulting text matches the expected output + + The test ensures that the Forward Slurp action correctly modifies the code structure + by moving the closing parenthesis forward." + (let [project-name "clojure.sample-project" + {:keys [fixtures project deps-file]} (test-utils/setup-test-project project-name) + clj-file (.copyFileToProject ^CodeInsightTestFixture fixtures "foo.clj")] + (is (= project-name (.getName ^Project project))) + (is deps-file) + + (let [editor (test-utils/open-file-in-editor fixtures clj-file)] + (test-utils/setup-lsp-server project) + (editor/move-caret-to-position editor 2 8) + (test-utils/run-editor-action "ClojureLSP.ForwardSlurp" project) + (clj4intellij.test/dispatch-all) + + (.checkResultByFile ^CodeInsightTestFixture fixtures "foo_expected.clj") + + (test-utils/teardown-test-project project)))) diff --git a/src/test/clojure/com/github/clojure_lsp/intellij/test_utils.clj b/src/test/clojure/com/github/clojure_lsp/intellij/test_utils.clj new file mode 100644 index 0000000..ef1d1a2 --- /dev/null +++ b/src/test/clojure/com/github/clojure_lsp/intellij/test_utils.clj @@ -0,0 +1,92 @@ +(ns com.github.clojure-lsp.intellij.test-utils + (:require + [com.github.clojure-lsp.intellij.client :as lsp-client] + [com.github.clojure-lsp.intellij.server :as server] + [com.github.ericdallo.clj4intellij.app-manager :as app-manager] + [com.github.ericdallo.clj4intellij.test :as clj4intellij.test]) + (:import + [com.github.clojure_lsp.intellij.extension SettingsState] + [com.intellij.ide DataManager] + [com.intellij.openapi.actionSystem ActionManager] + [com.intellij.openapi.components ServiceManager] + [com.intellij.testFramework.fixtures CodeInsightTestFixture])) + +(set! *warn-on-reflection* true) + +(defn get-editor-text + "Returns the text content of the editor's document." + [^CodeInsightTestFixture fixture] + (-> fixture .getEditor .getDocument .getText)) + +(defn open-file-in-editor + "Opens a file in the editor and returns the editor instance." + [^CodeInsightTestFixture fixture file] + (let [project (.getProject fixture)] + (app-manager/write-command-action + project + (fn [] (.openFileInEditor fixture file))) + (.getEditor fixture))) + +(defn run-editor-action + "Runs an editor action with the given ID for the specified project." + [action-id project] + (let [action (.getAction (ActionManager/getInstance) action-id) + context (.getDataContext (DataManager/getInstance))] + (app-manager/write-command-action + project + (fn [] + (.actionPerformed + action + (com.intellij.openapi.actionSystem.AnActionEvent/createFromDataContext action-id nil context)))))) + +(defn wait-lsp-start + "Dispatches all events until the LSP server is started or the timeout is reached." + [{:keys [project millis timeout] + :or {millis 1000 + timeout 10000}}] + (let [start-time (System/currentTimeMillis)] + (loop [] + (let [current-time (System/currentTimeMillis) + elapsed-time (- current-time start-time) + status (lsp-client/server-status project)] + (cond + (>= elapsed-time timeout) + (throw (ex-info "LSP server failed to start within timeout" + {:elapsed-time elapsed-time + :final-status status})) + + (= status :started) + true + + :else + (do + (clj4intellij.test/dispatch-all) + (Thread/sleep millis) + (recur))))))) + +(defn teardown-test-project + "Shuts down all resources for the given project." + [project] + (server/shutdown! project)) + +(defn setup-test-project + "Sets up a test project with the given name and optional deps.edn content. + Returns a map with :fixture, :project, and :deps-file." + ([project-name] + (setup-test-project project-name "{}")) + ([project-name deps-content] + (let [fixtures (clj4intellij.test/setup project-name) + deps-file (.createFile fixtures "deps.edn" deps-content) + _ (.setTestDataPath fixtures "testdata") + project (.getProject fixtures)] + {:fixtures fixtures + :project project + :deps-file deps-file}))) + +(defn setup-lsp-server + "Sets up and waits for the LSP server to be ready." + [project] + (let [my-settings ^SettingsState (ServiceManager/getService SettingsState)] + (.loadState my-settings my-settings) + (clj4intellij.test/dispatch-all) + (wait-lsp-start {:project project}))) diff --git a/testdata/foo.clj b/testdata/foo.clj new file mode 100644 index 0000000..bc2f98f --- /dev/null +++ b/testdata/foo.clj @@ -0,0 +1,3 @@ +(ns foo) + +(println) "Oiii" diff --git a/testdata/foo_expected.clj b/testdata/foo_expected.clj new file mode 100644 index 0000000..6d8c33b --- /dev/null +++ b/testdata/foo_expected.clj @@ -0,0 +1,3 @@ +(ns foo) + +(println "Oiii")