diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index a6eff435..01dbb0f5 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -65,6 +65,11 @@ jobs: uses: DeLaGuardo/setup-clojure@13.4 with: cli: latest + - name: Setup kotlin + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' - name: Install Terraform uses: hashicorp/setup-terraform@v3 with: diff --git a/pyproject.toml b/pyproject.toml index 30afdc37..badf5252 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -254,6 +254,7 @@ markers = [ "python: language server running for Python", "go: language server running for Go", "java: language server running for Java", + "kotlin: language server running for kotlin", "rust: language server running for Rust", "typescript: language server running for TypeScript", "php: language server running for PHP", @@ -275,4 +276,4 @@ markers = [ skip = '.git*,*.svg,*.lock,*.min.*' check-hidden = true # ignore-regex = '' -# ignore-words-list = '' +# ignore-words-list = '' \ No newline at end of file diff --git a/src/solidlsp/language_servers/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server.py index 5d0d2083..0ea2b4da 100644 --- a/src/solidlsp/language_servers/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server.py @@ -43,7 +43,7 @@ class KotlinLanguageServer(SolidLanguageServer): self.runtime_dependency_paths = runtime_dependency_paths # Create command to execute the Kotlin Language Server script - cmd = f'"{self.runtime_dependency_paths.kotlin_executable_path}"' + cmd = [self.runtime_dependency_paths.kotlin_executable_path, "--stdio"] # Set environment variables including JAVA_HOME proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} @@ -76,7 +76,7 @@ class KotlinLanguageServer(SolidLanguageServer): "runtimeDependency": { "id": "KotlinLsp", "description": "Kotlin Language Server", - "url": "https://github.com/fwcd/kotlin-language-server/releases/download/1.3.13/server.zip", + "url": "https://download-cdn.jetbrains.com/kotlin-lsp/0.253.10629/kotlin-0.253.10629.zip", "archiveType": "zip", }, "java": { @@ -138,13 +138,13 @@ class KotlinLanguageServer(SolidLanguageServer): assert os.path.exists(java_path), f"Java executable not found at {java_path}" # Setup Kotlin Language Server paths - kotlin_ls_dir = os.path.join(static_dir, "server") + kotlin_ls_dir = static_dir # Get platform-specific executable script path if platform_id.value.startswith("win-"): - kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server.bat") + kotlin_script = os.path.join(kotlin_ls_dir, "kotlin-lsp.cmd") else: - kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server") + kotlin_script = os.path.join(kotlin_ls_dir, "kotlin-lsp.sh") # Download and extract Kotlin Language Server if script doesn't exist if not os.path.exists(kotlin_script): diff --git a/test/resources/repos/kotlin/test_repo/build.gradle.kts b/test/resources/repos/kotlin/test_repo/build.gradle.kts new file mode 100644 index 00000000..60c1d631 --- /dev/null +++ b/test/resources/repos/kotlin/test_repo/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + kotlin("jvm") version "1.9.21" + application +} + +group = "test.serena" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/Main.kt b/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/Main.kt new file mode 100644 index 00000000..ce2beccf --- /dev/null +++ b/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/Main.kt @@ -0,0 +1,15 @@ +package test_repo + +object Main { + @JvmStatic + fun main(args: Array) { + Utils.printHello() + val model = Model("Cascade") + println(model.name) + acceptModel(model) + } + + fun acceptModel(m: Model?) { + // Do nothing, just for LSP reference + } +} \ No newline at end of file diff --git a/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/Model.kt b/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/Model.kt new file mode 100644 index 00000000..a32d18cf --- /dev/null +++ b/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/Model.kt @@ -0,0 +1,3 @@ +package test_repo + +data class Model(val name: String) \ No newline at end of file diff --git a/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/ModelUser.kt b/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/ModelUser.kt new file mode 100644 index 00000000..5100436e --- /dev/null +++ b/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/ModelUser.kt @@ -0,0 +1,9 @@ +package test_repo + +object ModelUser { + @JvmStatic + fun main(args: Array) { + val model = Model("Cascade") + println(model.name) + } +} \ No newline at end of file diff --git a/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/Utils.kt b/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/Utils.kt new file mode 100644 index 00000000..cd2ba3ce --- /dev/null +++ b/test/resources/repos/kotlin/test_repo/src/main/kotlin/test_repo/Utils.kt @@ -0,0 +1,7 @@ +package test_repo + +object Utils { + fun printHello() { + println("Hello from Utils!") + } +} \ No newline at end of file diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 060dc8ec..4f067ffb 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -23,6 +23,7 @@ def serena_config(): Language.PYTHON, Language.GO, Language.JAVA, + Language.KOTLIN, Language.RUST, Language.TYPESCRIPT, Language.PHP, @@ -67,6 +68,7 @@ class TestSerenaAgent: pytest.param(Language.PYTHON, "User", "Class", "models.py", marks=pytest.mark.python), pytest.param(Language.GO, "Helper", "Function", "main.go", marks=pytest.mark.go), pytest.param(Language.JAVA, "Model", "Class", "Model.java", marks=pytest.mark.java), + pytest.param(Language.KOTLIN, "Model", "Struct", "Model.kt", marks=pytest.mark.kotlin), pytest.param(Language.RUST, "add", "Function", "lib.rs", marks=pytest.mark.rust), pytest.param(Language.TYPESCRIPT, "DemoClass", "Class", "index.ts", marks=pytest.mark.typescript), pytest.param(Language.PHP, "helperFunction", "Function", "helper.php", marks=pytest.mark.php), @@ -110,6 +112,13 @@ class TestSerenaAgent: os.path.join("src", "main", "java", "test_repo", "Main.java"), marks=pytest.mark.java, ), + pytest.param( + Language.KOTLIN, + "Model", + os.path.join("src", "main", "kotlin", "test_repo", "Model.kt"), + os.path.join("src", "main", "kotlin", "test_repo", "Main.kt"), + marks=pytest.mark.kotlin, + ), pytest.param(Language.RUST, "add", os.path.join("src", "lib.rs"), os.path.join("src", "main.rs"), marks=pytest.mark.rust), pytest.param(Language.TYPESCRIPT, "helperFunction", "index.ts", "use_helper.ts", marks=pytest.mark.typescript), pytest.param(Language.PHP, "helperFunction", "helper.php", "index.php", marks=pytest.mark.php), diff --git a/test/solidlsp/kotlin/test_kotlin_basic.py b/test/solidlsp/kotlin/test_kotlin_basic.py new file mode 100644 index 00000000..6c421ab4 --- /dev/null +++ b/test/solidlsp/kotlin/test_kotlin_basic.py @@ -0,0 +1,52 @@ +import os + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils + + +@pytest.mark.kotlin +class TestKotlinLanguageServer: + @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True) + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree() + assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model class not found in symbol tree" + + @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True) + def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: + # Use correct Kotlin file paths + file_path = os.path.join("src", "main", "kotlin", "test_repo", "Utils.kt") + refs = language_server.request_references(file_path, 3, 12) + assert any("Main.kt" in ref.get("relativePath", "") for ref in refs), "Main should reference Utils.printHello" + + # Dynamically determine the correct line/column for the 'Model' class name + file_path = os.path.join("src", "main", "kotlin", "test_repo", "Model.kt") + symbols = language_server.request_document_symbols(file_path) + model_symbol = None + for sym in symbols[0]: + print(sym) + print("\n") + if sym.get("name") == "Model" and sym.get("kind") == 23: # 23 = Class + model_symbol = sym + break + assert model_symbol is not None, "Could not find 'Model' class symbol in Model.kt" + # Use selectionRange if present, otherwise fall back to range + if "selectionRange" in model_symbol: + sel_start = model_symbol["selectionRange"]["start"] + else: + sel_start = model_symbol["range"]["start"] + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "Main.kt" in ref.get("relativePath", "") for ref in refs + ), "Main should reference Model (tried all positions in selectionRange)" + + @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True) + def test_overview_methods(self, language_server: SolidLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree() + assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview"