From 47bc4e0b3241ebc827683e2ec1e25cd77a48d1be Mon Sep 17 00:00:00 2001 From: eez-eh Date: Tue, 16 Dec 2025 19:11:46 +0500 Subject: [PATCH] Added partial Groovy language support (#833) --- CHANGELOG.md | 1 + README.md | 2 +- docs/01-about/020_programming-languages.md | 2 + .../groovy_setup_guide_for_serena.md | 88 ++++++ pyproject.toml | 1 + .../groovy_language_server.py | 274 ++++++++++++++++++ src/solidlsp/ls_config.py | 8 + .../repos/groovy/test_repo/.gitignore | 1 + .../repos/groovy/test_repo/build.gradle | 7 + .../src/main/groovy/com/example/Main.groovy | 9 + .../src/main/groovy/com/example/Model.groovy | 9 + .../main/groovy/com/example/ModelUser.groovy | 8 + .../src/main/groovy/com/example/Utils.groovy | 7 + test/solidlsp/groovy/test_groovy_basic.py | 100 +++++++ 14 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 docs/03-special-guides/groovy_setup_guide_for_serena.md create mode 100644 src/solidlsp/language_servers/groovy_language_server.py create mode 100644 test/resources/repos/groovy/test_repo/.gitignore create mode 100644 test/resources/repos/groovy/test_repo/build.gradle create mode 100644 test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Main.groovy create mode 100644 test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Model.groovy create mode 100644 test/resources/repos/groovy/test_repo/src/main/groovy/com/example/ModelUser.groovy create mode 100644 test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Utils.groovy create mode 100644 test/solidlsp/groovy/test_groovy_basic.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 281dd84b..b4497495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Language support: * **Add support for Fortran** via fortls language server (requires `pip install fortls`) + * **Add partial support for Groovy** requires user-provided Groovy language server JAR (see [setup guide](docs/03-special-guides/groovy_setup_guide_for_serena.md)) * **Add support for Julia** via LanguageServer.jl * **Add support for Haskell** via Haskell Language Server (HLS) with automatic discovery via ghcup, stack, or system PATH; supports both Stack and Cabal projects * **Add support for Scala** via Metals language server (requires some [manual setup](docs/03-special-guides/scala_setup_guide_for_serena.md)) diff --git a/README.md b/README.md index e5905808..49c715be 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ than existing solutions that charge a premium. Language servers provide support for a wide range of programming languages. With Serena's LSP library, we provide **support for over 30 programming languages**, including -AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, Go, Haskell, Java, Javascript, Julia, Kotlin, Lua, Markdown, Nix, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, YAML, and Zig. +AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, Go, Groovy (partial support), Haskell, Java, Javascript, Julia, Kotlin, Lua, Markdown, Nix, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, YAML, and Zig. > [!IMPORTANT] > Some languages require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 3637ce04..c456e724 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -31,6 +31,8 @@ With Serena, we provide direct, out-of-the-box support for: (requires installation of fortls: `pip install fortls`) * **Go** (requires installation of `gopls`) +* **Groovy** + (requires local groovy-language-server.jar setup via GROOVY_LS_JAR_PATH or configuration) * **Haskell** (automatically locates HLS via ghcup, stack, or system PATH; supports Stack and Cabal projects) * **Java** diff --git a/docs/03-special-guides/groovy_setup_guide_for_serena.md b/docs/03-special-guides/groovy_setup_guide_for_serena.md new file mode 100644 index 00000000..50e5be23 --- /dev/null +++ b/docs/03-special-guides/groovy_setup_guide_for_serena.md @@ -0,0 +1,88 @@ +# Groovy Setup Guide for Serena + +The Groovy support in Serena is incomplete and requires the user to provide a functioning, +JVM-based Groovy language server as a jar. This intermediate state allows the contributors +of Groovy support to use Serena internally and hopefully to accelerate their open-source +release of a Groovy language server in the future. + +If you happen to have a Groovy language server JAR file, you can configure Serena to use it +by following the instructions below. + +--- +## Prerequisites + +- Groovy Language Server JAR file + - Can be any open-source Groovy language server or your custom implementation + - The JAR must be compatible with standard LSP protocol + +--- +## Configuration + +Configure Groovy Language Server by adding settings to your `~/.serena/serena_config.yml`: + +### Basic Configuration + +```yaml +ls_specific_settings: + groovy: + ls_jar_path: '/path/to/groovy-language-server.jar' + ls_jar_options: '-Xmx2G -Xms512m' +``` + +### Custom Java Paths + +If you have specific Java installations: + +```yaml +ls_specific_settings: + groovy: + ls_jar_path: '/path/to/groovy-language-server.jar' + ls_java_home_path: '/usr/lib/jvm/java-21-openjdk' # Custom JAVA_HOME directory + ls_jar_options: '-Xmx2G -Xms512m' # Optional JVM options +``` + +### Configuration Options + +- `ls_jar_path`: Absolute path to your Groovy Language Server JAR file (required) +- `ls_java_home_path`: Custom JAVA_HOME directory for Java installation (optional) + - When specified, Serena will use this Java installation instead of downloading bundled Java + - Java executable path is automatically determined based on platform: + - Windows: `{ls_java_home_path}/bin/java.exe` + - Linux/macOS: `{ls_java_home_path}/bin/java` + - Validates that Java executable exists at the expected location +- `ls_jar_options`: JVM options for language server (optional) + - Common options: + - `-Xmx`: Maximum heap size (e.g., `-Xmx2G` for 2GB) + - `-Xms`: Initial heap size (e.g., `-Xms512m` for 512MB) + +--- +## Project Structure Requirements + +For optimal Groovy Language Server performance, ensure your project follows standard Groovy/Gradle structure: + +``` +project-root/ +├── src/ +│ ├── main/ +│ │ ├── groovy/ +│ │ └── resources/ +│ └── test/ +│ ├── groovy/ +│ └── resources/ +├── build.gradle or build.gradle.kts +├── settings.gradle or settings.gradle.kts +└── gradle/ + └── wrapper/ +``` + +--- +## Using Serena with Groovy + +- Serena automatically detects Groovy files (`*.groovy`, `*.gvy`) and will start a Groovy Language Server JAR process per project when needed. +- Optimal results require that your project compiles successfully via Gradle or Maven. If compilation fails, fix build errors in your build tool first. + +## Reference + +- **Groovy Documentation**: [https://groovy-lang.org/documentation.html](https://groovy-lang.org/documentation.html) +- **Gradle Documentation**: [https://docs.gradle.org](https://docs.gradle.org) +- **Serena Configuration**: [../02-usage/050_configuration.md](../02-usage/050_configuration.md) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b68f07ae..71f2516a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -273,6 +273,7 @@ markers = [ "go: language server running for Go", "java: language server running for Java", "kotlin: language server running for kotlin", + "groovy: language server running for Groovy", "rust: language server running for Rust", "typescript: language server running for TypeScript", "vue: language server running for Vue (uses TypeScript LSP)", diff --git a/src/solidlsp/language_servers/groovy_language_server.py b/src/solidlsp/language_servers/groovy_language_server.py new file mode 100644 index 00000000..1c302de7 --- /dev/null +++ b/src/solidlsp/language_servers/groovy_language_server.py @@ -0,0 +1,274 @@ +""" +Provides Groovy specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Groovy. +""" + +import dataclasses +import logging +import os +import pathlib +import shlex + +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings + +log = logging.getLogger(__name__) + + +@dataclasses.dataclass +class GroovyRuntimeDependencyPaths: + """ + Stores the paths to the runtime dependencies of Groovy Language Server + """ + + java_path: str + java_home_path: str + ls_jar_path: str + groovy_home_path: str | None = None + + +class GroovyLanguageServer(SolidLanguageServer): + """ + Provides Groovy specific instantiation of the LanguageServer class. + Contains various configurations and settings specific to Groovy. + """ + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + """ + Creates a Groovy Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + runtime_dependency_paths = self._setup_runtime_dependencies(solidlsp_settings) + self.runtime_dependency_paths = runtime_dependency_paths + + # Get jar options from configuration + ls_jar_options = [] + + if solidlsp_settings.ls_specific_settings: + groovy_settings = solidlsp_settings.get_ls_specific_settings(Language.GROOVY) + jar_options_str = groovy_settings.get("ls_jar_options", "") + if jar_options_str: + ls_jar_options = shlex.split(jar_options_str) + log.info(f"Using Groovy LS JAR options from configuration: {jar_options_str}") + + # Create command to execute the Groovy Language Server + cmd = [self.runtime_dependency_paths.java_path, "-jar", self.runtime_dependency_paths.ls_jar_path] + cmd.extend(ls_jar_options) + + # Set environment variables including JAVA_HOME + proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} + + super().__init__( + config, + repository_root_path, + ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), + "groovy", + solidlsp_settings, + ) + + log.info(f"Starting Groovy Language Server with jar options: {ls_jar_options}") + + @classmethod + def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> GroovyRuntimeDependencyPaths: + """ + Setup runtime dependencies for Groovy Language Server and return paths. + """ + platform_id = PlatformUtils.get_platform_id() + + # Verify platform support + assert ( + platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-") + ), "Only Windows, Linux and macOS platforms are supported for Groovy in multilspy at the moment" + + # Check if user specified custom Java home path + java_home_path = None + java_path = None + + if solidlsp_settings and solidlsp_settings.ls_specific_settings: + groovy_settings = solidlsp_settings.get_ls_specific_settings(Language.GROOVY) + custom_java_home = groovy_settings.get("ls_java_home_path") + if custom_java_home: + log.info(f"Using custom Java home path from configuration: {custom_java_home}") + java_home_path = custom_java_home + + # Determine java executable path based on platform + if platform_id.value.startswith("win-"): + java_path = os.path.join(java_home_path, "bin", "java.exe") + else: + java_path = os.path.join(java_home_path, "bin", "java") + + # If no custom Java home path, download and use bundled Java + if java_home_path is None: + # Runtime dependency information + runtime_dependencies = { + "java": { + "win-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-win32-x86_64", + "java_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", + }, + "linux-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-linux-x86_64", + "java_path": "extension/jre/21.0.7-linux-x86_64/bin/java", + }, + "linux-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-linux-aarch64", + "java_path": "extension/jre/21.0.7-linux-aarch64/bin/java", + }, + "osx-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-macosx-x86_64", + "java_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", + }, + "osx-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-macosx-aarch64", + "java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", + }, + }, + } + + java_dependency = runtime_dependencies["java"][platform_id.value] + + static_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "groovy_language_server") + os.makedirs(static_dir, exist_ok=True) + + java_dir = os.path.join(static_dir, "java") + os.makedirs(java_dir, exist_ok=True) + + java_home_path = os.path.join(java_dir, java_dependency["java_home_path"]) + java_path = os.path.join(java_dir, java_dependency["java_path"]) + + if not os.path.exists(java_path): + log.info(f"Downloading Java for {platform_id.value}...") + FileUtils.download_and_extract_archive(java_dependency["url"], java_dir, java_dependency["archiveType"]) + + if not platform_id.value.startswith("win-"): + os.chmod(java_path, 0o755) + + assert java_path and os.path.exists(java_path), f"Java executable not found at {java_path}" + + ls_jar_path = cls._find_groovy_ls_jar(solidlsp_settings) + + return GroovyRuntimeDependencyPaths(java_path=java_path, java_home_path=java_home_path, ls_jar_path=ls_jar_path) + + @classmethod + def _find_groovy_ls_jar(cls, solidlsp_settings: SolidLSPSettings) -> str: + """ + Find Groovy Language Server JAR file + """ + if solidlsp_settings and solidlsp_settings.ls_specific_settings: + groovy_settings = solidlsp_settings.get_ls_specific_settings(Language.GROOVY) + config_jar_path = groovy_settings.get("ls_jar_path") + if config_jar_path and os.path.exists(config_jar_path): + log.info(f"Using Groovy LS JAR from configuration: {config_jar_path}") + return config_jar_path + + # if JAR not found + raise RuntimeError( + "Groovy Language Server JAR not found. To use Groovy language support:\n" + "Set 'ls_jar_path' in groovy settings in serena_config.yml:\n" + " ls_specific_settings:\n" + " groovy:\n" + " ls_jar_path: '/path/to/groovy-language-server.jar'\n" + " Ensure the JAR file is available at the configured path\n" + ) + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Groovy Language Server. + """ + if not os.path.isabs(repository_absolute_path): + repository_absolute_path = os.path.abspath(repository_absolute_path) + + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "clientInfo": {"name": "Serena Groovy Client", "version": "1.0.0"}, + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "capabilities": { + "textDocument": { + "synchronization": {"dynamicRegistration": True, "didSave": True}, + "completion": {"dynamicRegistration": True}, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": {"dynamicRegistration": True}, + "workspaceSymbol": {"dynamicRegistration": True}, + "signatureHelp": {"dynamicRegistration": True}, + "rename": {"dynamicRegistration": True}, + }, + "workspace": { + "workspaceFolders": True, + }, + }, + "initializationOptions": { + "settings": { + "groovy": { + "classpath": [], + "diagnostics": {"enabled": True}, + "completion": {"enabled": True}, + } + }, + }, + "processId": os.getpid(), + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return initialize_params # type: ignore + + def _start_server(self) -> None: + """ + Starts the Groovy Language Server + """ + + def execute_client_command_handler(params: dict) -> list: + return [] + + def do_nothing(params: dict) -> None: + return + + def window_log_message(msg: dict) -> None: + log.info(f"LSP: window/logMessage: {msg}") + + self.server.on_request("client/registerCapability", do_nothing) + self.server.on_notification("language/status", do_nothing) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + + log.info("Starting Groovy server process") + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + log.info("Sending initialize request from LSP client to LSP server and awaiting response") + init_response = self.server.send.initialize(initialize_params) + + capabilities = init_response["capabilities"] + assert "textDocumentSync" in capabilities, "Server must support textDocumentSync" + assert "hoverProvider" in capabilities, "Server must support hover" + assert "completionProvider" in capabilities, "Server must support code completion" + assert "signatureHelpProvider" in capabilities, "Server must support signature help" + assert "definitionProvider" in capabilities, "Server must support go to definition" + assert "referencesProvider" in capabilities, "Server must support find references" + assert "documentSymbolProvider" in capabilities, "Server must support document symbols" + assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols" + + self.server.notify.initialized({}) + self.completions_available.set() diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 3d491842..57edbb02 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -61,6 +61,7 @@ class Language(str, Enum): JULIA = "julia" FORTRAN = "fortran" HASKELL = "haskell" + GROOVY = "groovy" VUE = "vue" POWERSHELL = "powershell" # Experimental or deprecated Language Servers @@ -112,6 +113,7 @@ class Language(str, Enum): self.MARKDOWN, self.YAML, self.TOML, + self.GROOVY, } def __str__(self) -> str: @@ -221,6 +223,8 @@ class Language(str, Enum): return FilenameMatcher(*path_patterns) case self.POWERSHELL: return FilenameMatcher("*.ps1", "*.psm1", "*.psd1") + case self.GROOVY: + return FilenameMatcher("*.groovy", "*.gvy") case _: raise ValueError(f"Unhandled language: {self}") @@ -382,6 +386,10 @@ class Language(str, Enum): from solidlsp.language_servers.powershell_language_server import PowerShellLanguageServer return PowerShellLanguageServer + case self.GROOVY: + from solidlsp.language_servers.groovy_language_server import GroovyLanguageServer + + return GroovyLanguageServer case _: raise ValueError(f"Unhandled language: {self}") diff --git a/test/resources/repos/groovy/test_repo/.gitignore b/test/resources/repos/groovy/test_repo/.gitignore new file mode 100644 index 00000000..3bb19ffd --- /dev/null +++ b/test/resources/repos/groovy/test_repo/.gitignore @@ -0,0 +1 @@ +.gradle/ diff --git a/test/resources/repos/groovy/test_repo/build.gradle b/test/resources/repos/groovy/test_repo/build.gradle new file mode 100644 index 00000000..61a1de3a --- /dev/null +++ b/test/resources/repos/groovy/test_repo/build.gradle @@ -0,0 +1,7 @@ +plugins { + id 'groovy' +} + +repositories { + mavenCentral() +} diff --git a/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Main.groovy b/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Main.groovy new file mode 100644 index 00000000..0e42fe71 --- /dev/null +++ b/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Main.groovy @@ -0,0 +1,9 @@ +package com.example + +class Main { + static void main(String[] args) { + Utils.printHello() + Model model = new Model("Cascade") + println(model.name) + } +} diff --git a/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Model.groovy b/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Model.groovy new file mode 100644 index 00000000..f6451ca4 --- /dev/null +++ b/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Model.groovy @@ -0,0 +1,9 @@ +package com.example + +class Model { + String name + + Model(String name) { + this.name = name + } +} diff --git a/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/ModelUser.groovy b/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/ModelUser.groovy new file mode 100644 index 00000000..bde77812 --- /dev/null +++ b/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/ModelUser.groovy @@ -0,0 +1,8 @@ +package com.example + +class ModelUser { + static void main(String[] args) { + Model model = new Model("Cascade") + println(model.name) + } +} diff --git a/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Utils.groovy b/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Utils.groovy new file mode 100644 index 00000000..b2cf63a1 --- /dev/null +++ b/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/Utils.groovy @@ -0,0 +1,7 @@ +package com.example + +class Utils { + static void printHello() { + println("Hello from Utils!") + } +} diff --git a/test/solidlsp/groovy/test_groovy_basic.py b/test/solidlsp/groovy/test_groovy_basic.py new file mode 100644 index 00000000..0ebae30c --- /dev/null +++ b/test/solidlsp/groovy/test_groovy_basic.py @@ -0,0 +1,100 @@ +import os +from pathlib import Path + +import pytest + +from serena.constants import SERENA_MANAGED_DIR_NAME +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_utils import SymbolUtils +from solidlsp.settings import SolidLSPSettings + + +@pytest.mark.groovy +class TestGroovyLanguageServer: + @classmethod + def setup_class(cls): + """ + Set up test class with Groovy test repository. + """ + cls.test_repo_path = Path(__file__).parent.parent.parent / "resources" / "repos" / "groovy" / "test_repo" + + if not cls.test_repo_path.exists(): + pytest.skip("Groovy test repository not found") + + # Use JAR path from environment variable + ls_jar_path = os.environ.get("GROOVY_LS_JAR_PATH") + if not ls_jar_path or not os.path.exists(ls_jar_path): + pytest.skip( + "Groovy Language Server JAR not found. Set GROOVY_LS_JAR_PATH environment variable to run tests.", + allow_module_level=True, + ) + + # Get JAR options from environment variable + ls_jar_options = os.environ.get("GROOVY_LS_JAR_OPTIONS", "") + ls_java_home_path = os.environ.get("GROOVY_LS_JAVA_HOME_PATH") + + groovy_settings = {"ls_jar_path": ls_jar_path, "ls_jar_options": ls_jar_options} + if ls_java_home_path: + groovy_settings["ls_java_home_path"] = ls_java_home_path + + # Create language server directly with Groovy-specific settings + repo_path = str(cls.test_repo_path) + config = LanguageServerConfig(code_language=Language.GROOVY, ignored_paths=[], trace_lsp_communication=False) + + solidlsp_settings = SolidLSPSettings( + solidlsp_dir=str(Path.home() / ".serena"), + project_data_relative_path=SERENA_MANAGED_DIR_NAME, + ls_specific_settings={Language.GROOVY: groovy_settings}, + ) + + cls.language_server = SolidLanguageServer.create(config, repo_path, solidlsp_settings=solidlsp_settings) + cls.language_server.start() + + @classmethod + def teardown_class(cls): + """ + Clean up language server. + """ + if hasattr(cls, "language_server"): + cls.language_server.stop() + + def test_find_symbol(self) -> None: + symbols = self.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" + assert SymbolUtils.symbol_tree_contains_name(symbols, "ModelUser"), "ModelUser class not found in symbol tree" + + def test_find_referencing_class_symbols(self) -> None: + file_path = os.path.join("src", "main", "groovy", "com", "example", "Utils.groovy") + refs = self.language_server.request_references(file_path, 3, 6) + assert any("Main.groovy" in ref.get("relativePath", "") for ref in refs), "Utils should be referenced from Main.groovy" + + file_path = os.path.join("src", "main", "groovy", "com", "example", "Model.groovy") + symbols = self.language_server.request_document_symbols(file_path).get_all_symbols_and_roots() + model_symbol = None + for sym in symbols[0]: + if sym.get("name") == "com.example.Model" and sym.get("kind") == 5: + model_symbol = sym + break + assert model_symbol is not None, "Could not find 'Model' class symbol in Model.groovy" + + if "selectionRange" in model_symbol: + sel_start = model_symbol["selectionRange"]["start"] + else: + sel_start = model_symbol["range"]["start"] + refs = self.language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + + main_refs = [ref for ref in refs if "Main.groovy" in ref.get("relativePath", "")] + assert len(main_refs) >= 2, f"Model should be referenced from Main.groovy at least 2 times, found {len(main_refs)}" + + model_user_refs = [ref for ref in refs if "ModelUser.groovy" in ref.get("relativePath", "")] + assert len(model_user_refs) >= 1, f"Model should be referenced from ModelUser.groovy at least 1 time, found {len(model_user_refs)}" + + def test_overview_methods(self) -> None: + symbols = self.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" + assert SymbolUtils.symbol_tree_contains_name(symbols, "ModelUser"), "ModelUser missing from overview"