mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 06:15:43 +00:00
Merge branch 'main' into prepare-v1
This commit is contained in:
commit
5b70d7c2b4
24 files changed
+799
-1588
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
-1486
File diff suppressed because it is too large.
Load diff
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user