Merge branch 'main' into prepare-v1

This commit is contained in:
Michael Panchenko committed 2025-12-16 22:03:53 +07:00
commit 5b70d7c2b4
24 files changed
+799 -1588

No files matched your search

+1
View File
@@ -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))
+1 -1
View File
@@ -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.
@@ -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**
+215 -85
View File
@@ -1,24 +1,82 @@
# Connecting Your MCP Client
In the following, we provide default configurations for popular MCP-enabled clients.
In the following, we provide general instructions on how to connect Serena to your MCP-enabled client,
as well as specific instructions for popular clients.
Depending on your needs, you might want to customize Serena's behaviour by
* [adding command-line arguments](mcp-args)
* [adjusting configuration](050_configuration).
(clients-general-instructions)=
## General Instructions
## Claude Code
In general, Serena can be used with any MCP-enabled client.
To connect Serena to your favourite client, simply
1. determine how to add a custom MCP server to your client (refer to the client's documentation).
2. add a new MCP server entry by specifying either
* a [run command](start-mcp-server) that allows the client to start the MCP server in stdio mode as a subprocess, or
* the URL of the HTTP/SSE endpoint, having started the [Serena MCP server in HTTP/SSE mode](streamable-http) beforehand.
Depending on your needs, you might want to further customize Serena's behaviour by
* [adding command-line arguments](mcp-args)
* [adjusting configuration](050_configuration).
**Mode of Operation**.
Note that some clients have a per-workspace MCP configuration (e.g, VSCode and Claude Code),
while others have a global MCP configuration (e.g. Codex and Claude Desktop).
- In the per-workspace case, you typically want to start Serena with your workspace directory as the project directory
and never switch to a different project. This is achieved by specifying the
`--project <path>` argument with a single-project [context](#contexts) (e.g. `ide` or `claude-code`).
- In the global configuration case, you must first activate the project you want to work on, which you can do by asking
the LLM to do so (e.g., "Activate the current dir as project using serena"). In such settings, the `activate_project`
tool is required.
**Tool Selection**.
While you may be able to turn off tools through your client's interface (e.g., in VSCode or Claude Desktop),
we recommend selecting your base tool set through Serena's configuration, as Serena's prompts automatically
adjust based on which tools are enabled/disabled.
A key mechanism for this is to use the appropriate [context](#contexts) when starting Serena.
(clients-common-pitfalls)=
### Common Pitfalls
**Escaping Paths Correctly**.
Note that if your client configuration uses JSON, special characters (like backslashes) need to be escaped properly.
In particular, if you are specifying paths containing backslashes on Windows
(note that you can also just use forward slashes), be sure to escape them correctly (`\\`).
**Discoverability of `uvx`**.
Your client may not find the `uvx` command, even if it is on your system PATH.
In this case, a workaround is to provide the full path to the `uvx` executable.
**Environment Variables**.
Some language servers may require additional environment variables to be set (e.g. F# on macOS with Homebrew),
which you may need to explicitly add to the MCP server configuration.
Note that for some clients (e.g. Claude Desktop), the spawned MCP server process may not inherit environment variables that
are only configured in your shell profile (e.g. `.bashrc`, `.zshrc`, etc.); they would need to be set system-wide instead.
An easy fix is to add them explicitly to the MCP server entry.
For example, in Claude Desktop and other clients, you can simply add an `env` key to the `serena`
object, e.g.
```
"env": {
"DOTNET_ROOT": "/opt/homebrew/Cellar/dotnet/9.0.8/libexec"
}
```
## Specific Clients
:::{note}
In this section, we present example configurations that run the latest version of Serena
using the `stdio` protocol with `uvx`.
Adapt the commands to your preferred way of [running Serena](020_running), adding any additional
command-line arguments as needed.
:::
### Claude Code
Serena is a great way to make Claude Code both cheaper and more powerful!
From your project directory, add serena with a command like this,
```shell
claude mcp add serena -- <serena> start-mcp-server --context claude-code --project "$(pwd)"
```
where `<serena>` is [your way of running Serena](020_running).
For example, when using `uvx`, the above command becomes
```shell
claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)"
```
@@ -29,10 +87,37 @@ Note:
* We specify the current directory as the project directory with `--project "$(pwd)"`, such
that Serena is configured to work on the current project from the get-go, following
Claude Code's mode of operation.
Be sure to use at least `v1.0.52` of Claude Code (as earlier versions do not read MCP server system prompts upon startup).
## Codex
### VSCode
While serena can be directly installed from the GitHub MCP server registry, we recommend to set it up manually
(at least for now, until the configuration there has been improved). Just paste the following into
`<your_project>/.vscode/mcp.json`, or edit the entry after using the option `install into workspace`:
```json
{
"servers": {
"oraios/serena": {
"type": "stdio",
"command": "uvx",
"args": [
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server",
"--context",
"ide",
"--project",
"${workspaceFolder}"
]
}
},
"inputs": []
}
```
### Codex
Serena works with OpenAI's Codex CLI out of the box, but you have to use the `codex` context for it to work properly. (The technical reason is that Codex doesn't fully support the MCP specifications, so some massaging of tools is required.).
@@ -45,6 +130,7 @@ For example, when using `uvx`, add the following section:
command = "uvx"
args = ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--context", "codex"]
```
After codex has started, you need to activate the project, which you can do by saying:
"Activate the current dir as project using serena"
@@ -59,95 +145,136 @@ that was already taken).
> Codex will often show the tools as `failed` even though they are successfully executed. This is not a problem, seems to be a bug in Codex. Despite the error message, everything works as expected.
## Claude Desktop
### Claude Desktop
On Windows and macOS there are official [Claude Desktop applications by Anthropic](https://claude.ai/download), for Linux there is an [open-source
On Windows and macOS, there are official [Claude Desktop applications by Anthropic](https://claude.ai/download); for Linux, there is an [open-source
community version](https://github.com/aaddrick/claude-desktop-debian).
To configure MCP server settings, go to File / Settings / Developer / MCP Servers / Edit Config,
which will let you open the JSON file `claude_desktop_config.json`.
Add the `serena` MCP server configuration, using a [run command](020_running.md) depending on your setup.
Add the `serena` MCP server configuration
* local installation:
```json
{
"mcpServers": {
"serena": {
"command": "/abs/path/to/uv",
"args": ["run", "--directory", "/abs/path/to/serena", "serena", "start-mcp-server"]
}
}
}
```
* uvx:
```json
{
"mcpServers": {
"serena": {
"command": "/abs/path/to/uvx",
"args": ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"]
}
}
```json
{
"mcpServers": {
"serena": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server"
]
}
}
```
* docker:
```json
{
"mcpServers": {
"serena": {
"command": "docker",
"args": ["run", "--rm", "-i", "--network", "host", "-v", "/path/to/your/projects:/workspaces/projects", "ghcr.io/oraios/serena:latest", "serena", "start-mcp-server", "--transport", "stdio"]
}
}
}
```
If you are using paths containing backslashes for paths on Windows
(note that you can also just use forward slashes), be sure to escape them correctly (`\\`).
:::{note}
Some language servers may require additional environment variables to be set (e.g. F# on macOS with Homebrew),
which you may need to explicitly add to the MCP server configuration.
Note that the spawned processes will not inherit environment variables that are only configured in your shell
profile (e.g. `.bashrc`, `.zshrc`, etc.); they would need to be set system-wide instead.
An easy fix is to add them explicitly to the MCP server entry: Simply add an `env` key to the `serena` object, e.g.
```
"env": {
"DOTNET_ROOT": "/opt/homebrew/Cellar/dotnet/9.0.8/libexec"
}
```
:::
Once you have created the new MCP server entry, save the config and then restart Claude Desktop.
If your language server requires specific environment variables to be set (e.g. F# on macOS with Homebrew),
you can add them via an `env` key (see [above](#clients-common-pitfalls)).
Once you have created the new MCP server entry, save the config and then restart Claude Desktop.
:::{attention}
Be sure to fully quit the Claude Desktop application via File / Exit, as regularly closing the application will just minimize it.
Be sure to fully quit the Claude Desktop application via File / Exit, as regularly closing the application will just
minimize it.
:::
After restarting, you should see Serena's tools in your chat interface (notice the small hammer icon).
For more information on MCP servers with Claude Desktop, see [the official quick start guide](https://modelcontextprotocol.io/quickstart/user).
For more information on MCP servers with Claude Desktop,
see [the official quick start guide](https://modelcontextprotocol.io/quickstart/user).
## Other Clients
### JetBrains Junie
In general, Serena can be used with any MCP-enabled client.
To connect Serena to your favourite client, simply
Open Junie, go to the three dots in the top right corner, then Settings / MCP Settings and add Serena to Junie's global
MCP server configuration:
1. determine how to add a custom MCP server to your client (refer to the client's documentation).
2. add a new MCP server entry by specifying either
* a [run command](start-mcp-server) that allows the client to start the MCP server in stdio mode as a subprocess, or
* the URL of the HTTP/SSE endpoint, having started the [Serena MCP server in HTTP/SSE mode](streamable-http) beforehand.
```json
{
"mcpServers": {
"serena": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server",
"--context",
"ide"
]
}
}
}
```
Below, we list some popular categories of clients that Serena can be used with.
You will have to prompt Junie to "Activate the current project using serena's activation tool" at the
start of each session.
### Terminal-Based Clients
### JetBrains AI Assistant
Here you can set up the more convenient per-project MCP server configuration, as the AI assistant supports specifying
the launch working directory.
Go to Settings / Tools / AI Assistant / MCP and add a new **local** configuration via the `as JSON` option:
```json
{
"mcpServers": {
"serena": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server",
"--context",
"ide",
"--project",
"$(pwd)"
]
}
}
}
```
Then make sure to configure the working directory to be the project root.
### Antigravity
:::{warning}
At the time of writing (12/2025), Antigravity does not seem to work with Serena due to schema validation issues
which are beyond our control.
The client starts Serena but then crashes with `[internal] marshal message: string field contains invalid UTF-8`.
Nevertheless, we provide a configuration that should work once the issue is resolved.
:::
Add this configuration:
```json
{
"mcpServers": {
"serena": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server",
"--context",
"ide"
]
}
}
}
```
### Other Clients
For other clients, follow the [general instructions](#clients-general-instructions) above to set up Serena as an MCP server.
#### Terminal-Based Clients
There are many terminal-based coding assistants that support MCP servers, such as
@@ -160,15 +287,18 @@ There are many terminal-based coding assistants that support MCP servers, such a
They generally benefit from the symbolic tools provided by Serena. You might want to customize some aspects of Serena
by writing your own context, modes or prompts to adjust it to the client's respective internal capabilities (and your general workflow).
### MCP-Enabled IDEs and Coding Clients (Cline, Roo-Code, Cursor, Windsurf, etc.)
In most cases, the `ide` context is likely to be appropriate for such clients, i.e. add the arguments `--context ide`
in order to reduce tool duplication.
#### MCP-Enabled IDEs and Coding Clients (Cline, Roo-Code, Cursor, Windsurf, etc.)
Most of the popular existing coding assistants (e.g. IDE extensions) and AI-enabled IDEs themselves support connections
to MCP Servers. Serena generally boosts performance by providing efficient tools for symbolic operations.
We generally **recommend to use the `ide` context** for these integrations by adding the arguments `--context ide`
We generally recommend to use the `ide` context for these integrations by adding the arguments `--context ide`
in order to reduce tool duplication.
### Local GUIs and Agent Frameworks
#### Local GUIs and Agent Frameworks
Over the last months, several technologies have emerged that allow you to run a local GUI client
and connect it to an MCP server. The respective applications will typically work with Serena out of the box.
@@ -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<size>`: Maximum heap size (e.g., `-Xmx2G` for 2GB)
- `-Xms<size>`: 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)
+1
View File
@@ -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)",
+3
View File
@@ -195,6 +195,7 @@ class SerenaAgent:
# open GUI log window if enabled
self._gui_log_viewer: Optional["GuiLogViewer"] = None
if self.serena_config.gui_log_window_enabled:
log.info("Opening GUI window")
if platform.system() == "Darwin":
log.warning("GUI log window is not supported on macOS")
else:
@@ -204,6 +205,8 @@ class SerenaAgent:
self._gui_log_viewer = GuiLogViewer("dashboard", title="Serena Logs", memory_log_handler=get_memory_log_handler())
self._gui_log_viewer.start()
else:
log.debug("GUI window is disabled")
# set the agent context
if context is None:
+19 -7
View File
@@ -38,6 +38,9 @@ class CodeEditor(Generic[TSymbol], ABC):
self.encoding = encoding
class EditedFile(ABC):
def __init__(self, relative_path: str) -> None:
self.relative_path = relative_path
@abstractmethod
def get_contents(self) -> str:
"""
@@ -67,9 +70,12 @@ class CodeEditor(Generic[TSymbol], ABC):
with self._open_file_context(relative_path) as edited_file:
yield edited_file
# save the file
abs_path = os.path.join(self.project_root, relative_path)
with open(abs_path, "w", encoding=self.encoding) as f:
f.write(edited_file.get_contents())
self._save_edited_file(edited_file)
def _save_edited_file(self, edited_file: "CodeEditor.EditedFile") -> None:
abs_path = os.path.join(self.project_root, edited_file.relative_path)
with open(abs_path, "w", encoding=self.encoding) as f:
f.write(edited_file.get_contents())
@abstractmethod
def _find_unique_symbol(self, name_path: str, relative_file_path: str) -> TSymbol:
@@ -239,21 +245,21 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]):
class EditedFile(CodeEditor.EditedFile):
def __init__(self, lang_server: SolidLanguageServer, relative_path: str, file_buffer: LSPFileBuffer):
super().__init__(relative_path)
self._lang_server = lang_server
self._relative_path = relative_path
self._file_buffer = file_buffer
def get_contents(self) -> str:
return self._file_buffer.contents
def delete_text_between_positions(self, start_pos: PositionInFile, end_pos: PositionInFile) -> None:
self._lang_server.delete_text_between_positions(self._relative_path, start_pos.to_lsp_position(), end_pos.to_lsp_position())
self._lang_server.delete_text_between_positions(self.relative_path, start_pos.to_lsp_position(), end_pos.to_lsp_position())
def insert_text_at_position(self, pos: PositionInFile, text: str) -> None:
self._lang_server.insert_text_at_position(self._relative_path, pos.line, pos.col, text)
self._lang_server.insert_text_at_position(self.relative_path, pos.line, pos.col, text)
def apply_text_edits(self, text_edits: list[ls_types.TextEdit]) -> None:
return self._lang_server.apply_text_edits_to_file(self._relative_path, text_edits)
return self._lang_server.apply_text_edits_to_file(self.relative_path, text_edits)
@contextmanager
def _open_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]:
@@ -319,6 +325,7 @@ class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]):
class EditedFile(CodeEditor.EditedFile):
def __init__(self, relative_path: str, project: Project):
super().__init__(relative_path)
path = os.path.join(project.project_root, relative_path)
log.info("Editing file: %s", path)
with open(path, encoding=project.project_config.encoding) as f:
@@ -339,6 +346,11 @@ class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]):
def _open_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]:
yield self.EditedFile(relative_path, self._project)
def _save_edited_file(self, edited_file: "CodeEditor.EditedFile") -> None:
super()._save_edited_file(edited_file)
with JetBrainsPluginClient.from_project(self._project) as client:
client.refresh_file(edited_file.relative_path)
def _find_unique_symbol(self, name_path: str, relative_file_path: str) -> JetBrainsSymbol:
with JetBrainsPluginClient.from_project(self._project) as client:
result = client.find_symbol(name_path, relative_path=relative_file_path, include_body=False, depth=0, include_location=True)
+11 -3
View File
@@ -70,7 +70,7 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
own_yaml_path = os.path.join(SERENAS_OWN_MODE_YAMLS_DIR, fname)
if not os.path.exists(own_yaml_path):
raise FileNotFoundError(
f"Mode {name} not found in {SerenaPaths().user_modes_dir} or in {SERENAS_OWN_MODE_YAMLS_DIR}."
f"Mode {name} not found in {SerenaPaths().user_modes_dir} or in {SERENAS_OWN_MODE_YAMLS_DIR}. "
f"Available modes:\n{cls.list_registered_mode_names()}"
)
return own_yaml_path
@@ -78,8 +78,16 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
@classmethod
def from_name(cls, name: str) -> Self:
"""Load a registered Serena mode."""
mode_path = cls.get_path(name)
return cls.from_yaml(mode_path)
try:
mode_path = cls.get_path(name)
return cls.from_yaml(mode_path)
except:
try:
# Allow loading an internal mode if it is explicitly requested
return cls.from_name_internal(name)
except:
pass
raise
@classmethod
def from_name_internal(cls, name: str) -> Self:
+6 -4
View File
@@ -464,11 +464,13 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
)
instance.projects.append(project)
# set other configuration parameters
def get_value_or_default(field_name: str) -> Any:
return loaded_commented_yaml.get(field_name, get_dataclass_default(SerenaConfig, field_name))
def get_value_or_default(key: str, field_name: str | None = None) -> Any:
if field_name is None:
field_name = key
return loaded_commented_yaml.get(key, get_dataclass_default(SerenaConfig, field_name))
instance.gui_log_window_enabled = get_value_or_default("gui_log_window_enabled")
# set other configuration parameters
instance.gui_log_window_enabled = get_value_or_default("gui_log_window", "gui_log_window_enabled")
instance.web_dashboard_listen_address = get_value_or_default("web_dashboard_listen_address")
instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO))
instance.web_dashboard = get_value_or_default("web_dashboard")
@@ -202,6 +202,17 @@ class JetBrainsPluginClient(ToStringMixin):
}
self._make_request("POST", "/renameSymbol", request_data)
def refresh_file(self, relative_path: str) -> None:
"""
Triggers a refresh of the given file in the IDE.
:param relative_path: the relative path
"""
request_data = {
"relativePath": relative_path,
}
self._make_request("POST", "/refreshFile", request_data)
def is_service_available(self) -> bool:
try:
self.project_root()
@@ -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()
@@ -57,9 +57,11 @@ class ScalaLanguageServer(SolidLanguageServer):
"""
assert shutil.which("java") is not None, "JDK is not installed or not in PATH."
metals_version = "1.6.4"
metals_home = os.path.join(cls.ls_resources_dir(solidlsp_settings), "metals-lsp")
os.makedirs(metals_home, exist_ok=True)
metals_executable = os.path.join(metals_home, "metals")
metals_executable = os.path.join(metals_home, metals_version, "metals")
coursier_command_path = shutil.which("coursier")
cs_command_path = shutil.which("cs")
assert cs_command_path is not None or coursier_command_path is not None, "coursier is not installed or not in PATH."
@@ -82,7 +84,8 @@ class ScalaLanguageServer(SolidLanguageServer):
log.info("'cs' command installed successfully.")
log.info(f"metals executable not found at {metals_executable}, bootstrapping...")
artifact = "org.scalameta:metals_2.13:1.6.2"
subprocess.run(["mkdir", "-p", os.path.join(metals_home, metals_version)], check=True)
artifact = f"org.scalameta:metals_2.13:{metals_version}"
cmd = [
cs_command_path,
"bootstrap",
+8
View File
@@ -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}")
@@ -0,0 +1 @@
.gradle/
@@ -0,0 +1,7 @@
plugins {
id 'groovy'
}
repositories {
mavenCentral()
}
@@ -0,0 +1,9 @@
package com.example
class Main {
static void main(String[] args) {
Utils.printHello()
Model model = new Model("Cascade")
println(model.name)
}
}
@@ -0,0 +1,9 @@
package com.example
class Model {
String name
Model(String name) {
this.name = name
}
}
@@ -0,0 +1,8 @@
package com.example
class ModelUser {
static void main(String[] args) {
Model model = new Model("Cascade")
println(model.name)
}
}
@@ -0,0 +1,7 @@
package com.example
class Utils {
static void printHello() {
println("Hello from Utils!")
}
}
@@ -17,4 +17,13 @@ object Main {
def add(a: Int, b: Int): Int = {
a + b
}
// https://github.com/oraios/serena/issues/688
def someMethod(config: Config): Unit = {
val str = config.field1
println(str)
}
case class Config(field1:String)
}
File diff suppressed because it is too large. Load diff
+100
View File
@@ -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"
@@ -34,6 +34,10 @@ def test_scala_document_symbols(scala_ls):
assert symbol_names[3] == "result"
assert symbol_names[4] == "sum"
assert symbol_names[5] == "add"
assert symbol_names[6] == "someMethod"
assert symbol_names[7] == "str"
assert symbol_names[8] == "Config"
assert symbol_names[9] == "field1" # confirm https://github.com/oraios/serena/issues/688
def test_scala_references_within_same_file(scala_ls):