Merge branch 'main' into mux

This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2025-12-27 17:14:27 +01:00
commit 6dcaa3894a
186 files changed
+11604 -2301

No files matched your search

+1 -14
View File
@@ -44,26 +44,13 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push production image
- name: Build and push image from main
uses: docker/build-push-action@v5
with:
context: .
target: production
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push development image
uses: docker/build-push-action@v5
with:
context: .
target: development
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+23 -2
View File
@@ -21,6 +21,18 @@ jobs:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v3
- name: Free disk space
if: runner.os == 'Linux'
run: |
df -h
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache
sudo apt-get clean
sudo apt-get autoremove -y
docker system prune -af || true
df -h
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
@@ -78,8 +90,8 @@ jobs:
if: runner.os != 'Windows'
uses: erlef/setup-beam@v1
with:
elixir-version: "1.18.4"
otp-version: "26.1"
elixir-version: "1.19.3"
otp-version: "28"
# Erlang currently not tested in CI, random hangings on macos, always hangs on ubuntu
# In local tests, erlang seems to work though
# - name: Install Erlang Language Server
@@ -103,6 +115,10 @@ jobs:
with:
distribution: 'temurin'
java-version: '17'
- name: Setup .NET SDK (for F# and C# languages)
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Install Terraform
uses: hashicorp/setup-terraform@v3
with:
@@ -359,6 +375,11 @@ jobs:
key: language-servers-${{ runner.os }}-v1
restore-keys: |
language-servers-${{ runner.os }}-
- name: Report free disk space
if: runner.os == 'Linux'
run: |
echo "Free disk space before tests:"
df -h
- name: Test with pytest
shell: bash
run: uv run poe test
+4 -9
View File
@@ -202,20 +202,12 @@ pylint.html
/agent-ui
/test/**/.serena
# dynamic LS installations
/src/multilspy/language_servers/*/static
/src/solidlsp/language_servers/*/static
/src/solidlsp/language_servers/static
# clojure-lsp temporary files
.calva/
.clj-kondo/
.cpcache/
.lsp/
# Cache is in .multilspy
**/.multilspy/**
# temporary and backup files
*.bak
*.tmp
@@ -259,4 +251,7 @@ test/resources/repos/scala/.bloop/
stack.yaml.lock
dist-newstyle/
cabal.project.local*
.ghc.environment.*
.ghc.environment.*
zz-misc/
vue-implementation/
@@ -21,39 +21,35 @@ Have a look at `intelephense.py` for a reference implementation of a language se
dependencies, and on `pyright_server.py` that does not need any additional dependencies
because the language server can be installed directly as python package.
```python
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
class NewLanguageServer(SolidLanguageServer):
"""
Language server implementation for NewLanguage.
"""
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
def __init__(self, config: LanguageServerConfig, repository_root_path: str):
# Determine language server command
cmd = self._get_language_server_command()
super().__init__(
config,
logger,
repository_root_path,
super().__init__(config,
ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
"new_language", # Language ID for LSP
)
"new_language",,
def _get_language_server_command(self) -> list[str]:
"""Get the command to start the language server."""
# Example: return ["new-language-server", "--stdio"]
pass
@override
def is_ignored_dirname(self, dirname: str) -> bool:
"""Define language-specific directories to ignore."""
return super().is_ignored_dirname(dirname) or dirname in ["build", "dist", "target"]
return super().is_ignored_dirname(dirname) or dirname in ["build",
"dist", "target"]
```
### 1.2 Language Server Discovery and Installation
@@ -62,7 +58,7 @@ For languages requiring automatic installation, implement download logic similar
```python
@classmethod
def _ensure_server_installed(cls, logger: LanguageServerLogger) -> str:
def _ensure_server_installed(cls) -> str:
"""Ensure language server is installed and return path."""
# Check system installation first
system_server = shutil.which("new-language-server")
@@ -70,10 +66,10 @@ def _ensure_server_installed(cls, logger: LanguageServerLogger) -> str:
return system_server
# Download and install if needed
server_path = cls._download_and_install_server(logger)
server_path = cls._download_and_install_server()
return server_path
def _download_and_install_server(cls, logger: LanguageServerLogger) -> str:
def _download_and_install_server(cls) -> str:
"""Download and install the language server."""
# Implementation specific to your language server
pass
@@ -129,12 +125,12 @@ In `src/solidlsp/ls.py`, add your language to the `create` method:
```python
@classmethod
def create(cls, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str) -> "SolidLanguageServer":
def create(cls, config: LanguageServerConfig, repository_root_path: str) -> "SolidLanguageServer":
match config.code_language:
# Existing cases...
case Language.NEW_LANGUAGE:
from solidlsp.language_servers.new_language_server import NewLanguageServer
return NewLanguageServer(config, logger, repository_root_path)
return NewLanguageServer(config, repository_root_path)
```
## Step 3: Test Repository
+6 -1
View File
@@ -6,7 +6,8 @@ Status of the `main` branch. Changes prior to the next official version change w
* Add monorepo/multi-language support
* Project configuration files (`project.yml`) can now define multiple languages.
Auto-detection adds only the most prominent language by default.
* Additional languages can be conveniently added via the Dashboard while a project is already activated.
* Additional languages can be conveniently added via the Dashboard while a project is already activated.
* The current project can be activated automatically even when the MCP configuration is global (through the --project-from-cwd flag)
* Support overloaded symbols in `FindSymbolTool` and related tools
* Name paths of overloaded symbols now include an index (e.g., `myOverloadedFunction[2]`)
* Responses of the Java language server, which handled this in its own way, are now adapted accordingly,
@@ -35,9 +36,11 @@ 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))
* **Add support for F#** via FsAutoComplete/Ionide LSP server.
* **Add support for Elm** via @elm-tooling/elm-language-server (automatically downloads if not installed; requires Elm compiler)
* **Add support for Perl** via Perl::LanguageServer with LSP integration for .pl, .pm, and .t files
* **Add support for AL (Application Language)** for Microsoft Dynamics 365 Business Central development. Requires VS Code AL extension (ms-dynamics-smb.al).
@@ -46,11 +49,13 @@ Status of the `main` branch. Changes prior to the next official version change w
* **Add support for Lua** via lua-language-server
* **Add support for Nix** requires nixd installation (Windows not supported)
* **Add experimental support for YAML** via yaml-language-server with LSP integration for .yaml and .yml files
* **Add support for TOML** via Taplo language server with automatic binary download, validation, formatting, and schema support for .toml files
* **Dart now officially supported**: Dart was always working, but now tests were added, and it is promoted to "officially supported"
* **Rust now uses already installed rustup**: The rust-analyzer is no longer bundled with Serena. Instead, it uses the rust-analyzer from your Rust toolchain managed by rustup. This ensures compatibility with your Rust version and eliminates outdated bundled binaries.
* **Kotlin now officially supported**: We now use the official Kotlin LS, tests run through and performance is good, even though the LS is in an early development stage.
* **Add support for Erlang** experimental, may hang or be slow, uses the recently archived [erlang_ls](https://github.com/erlang-ls/erlang_ls)
* **Ruby dual language server support**: Added ruby-lsp as the modern primary Ruby language server. Solargraph remains available as an experimental legacy option. ruby-lsp supports both .rb and .erb files, while Solargraph supports .rb files only.
* **Add support for PowerShell** via PowerShell Editor Services (PSES). Requires `pwsh` (PowerShell Core) to be installed and available in PATH. Supports symbol navigation, go-to-definition, and within-file references for .ps1 files.
# 0.1.4
+3 -2
View File
@@ -9,11 +9,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `uv run poe type-check` - Run mypy type checking - ONLY allowed type checking command
- `uv run poe test` - Run tests with default markers (excludes java/rust by default)
- `uv run poe test -m "python or go"` - Run specific language tests
- `uv run poe test -m vue` - Run Vue tests
- `uv run poe lint` - Check code style without fixing
**Test Markers:**
Available pytest markers for selective testing:
- `python`, `go`, `java`, `rust`, `typescript`, `php`, `perl`, `csharp`, `elixir`, `terraform`, `clojure`, `swift`, `bash`, `ruby`, `ruby_solargraph`
- `python`, `go`, `java`, `rust`, `typescript`, `vue`, `php`, `perl`, `powershell`, `csharp`, `elixir`, `terraform`, `clojure`, `swift`, `bash`, `ruby`, `ruby_solargraph`
- `snapshot` - for symbolic editing operation tests
**Project Management:**
@@ -100,7 +101,7 @@ Configuration is loaded from (in order of precedence):
- **Symbol-based editing** - Uses LSP for precise code manipulation
- **Caching strategy** - Reduces language server overhead
- **Error recovery** - Automatic language server restart on crashes
- **Multi-language support** - 16+ languages with LSP integration
- **Multi-language support** - 19 languages with LSP integration (including Vue)
- **MCP protocol** - Exposes tools to AI agents via Model Context Protocol
- **Async operation** - Non-blocking language server interactions
+31 -37
View File
@@ -1,6 +1,6 @@
# Docker Setup for Serena (Experimental)
⚠️ **EXPERIMENTAL FEATURE**: The Docker setup for Serena is currently experimental and has several limitations. Please read this entire document before using Docker with Serena.
⚠️ **EXPERIMENTAL FEATURE**: The Docker setup for Serena is still experimental and has some limitations. Please read this entire document before using Docker with Serena.
## Overview
@@ -12,27 +12,42 @@ Docker support allows you to run Serena in an isolated container environment, wh
- **Consistent dependencies**: No need to manage language servers and dependencies on your host system
- **Cross-platform support**: Works consistently across Windows, macOS, and Linux
## Important Limitations and Caveats
## Important Usage Pointers
### 1. Configuration File Conflicts
### Configuration
⚠️ **Critical**: Docker uses a separate configuration file (`serena_config.docker.yml`) to avoid path conflicts. When running in Docker:
- Container paths will be stored in the configuration (e.g., `/workspaces/serena/...`)
- These paths are incompatible with non-Docker usage
- After using Docker, you cannot directly switch back to non-Docker usage without manual configuration adjustment
Serena's configuration and log files are stored in the container in `/workspaces/serena/config/`.
Any local configuration you may have for Serena will not apply; the container uses its own separate configuration.
### 2. Project Activation Limitations
You can mount a local configuration/data directory to persist settings across container restarts
(which will also contain session log files).
Simply mount your local directory to `/workspaces/serena/config` in the container.
Initially, be sure to add a `serena_config.yml` file to the mounted directory which applies the following
special settings for Docker usage:
```
# Disable the GUI log window since it's not supported in Docker
gui_log_window: False
# Listen on all interfaces for the web dashboard to be accessible from outside the container
web_dashboard_listen_address: 0.0.0.0
# Disable opening the web dashboard on launch (not possible within the container)
web_dashboard_open_on_launch: False
```
Set other configuration options as needed.
### Project Activation Limitations
- **Only mounted directories work**: Projects must be mounted as volumes to be accessible
- Projects outside the mounted directories cannot be activated or accessed
- Default setup only mounts the current directory
- Since projects are not remembered across container restarts (unless you mount a local configuration as described above),
activate them using the full path (e.g. `/workspaces/projects/my-project`) when using dynamic project activation
### 3. GUI Window Disabled
### Language Support Limitations
- The GUI log window option is automatically disabled in Docker environments
- Use the web dashboard instead (see below)
The default Docker image does not include dependencies for languages that
require explicit system-level installations.
Only languages that install their requirements on the fly will work out of the box.
### 4. Dashboard Port Configuration
### Dashboard Port Configuration
The web dashboard runs on port 24282 (0x5EDA) by default. You can configure this using environment variables:
@@ -46,7 +61,7 @@ SERENA_DASHBOARD_PORT=8080 docker-compose up serena
⚠️ **Note**: If the local port is occupied, you'll need to specify a different port using the environment variable.
### 5. Line Ending Issues on Windows
### Line Ending Issues on Windows
⚠️ **Windows Users**: Be aware of potential line ending inconsistencies:
- Files edited within the Docker container may use Unix line endings (LF)
@@ -70,7 +85,7 @@ SERENA_DASHBOARD_PORT=8080 docker-compose up serena
Note: Edit the `compose.yaml` file to customize volume mounts for your projects.
### Using Docker directly
### Building the Docker Image Manually
```bash
# Build the image
@@ -98,7 +113,7 @@ services:
- /path/to/another/project:/workspace/another-project
# Add the context for the IDE assistant option:
command:
- "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0 --context ide-assistant"
- "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0 --context claude-code"
```
See the [Docker Merge Compose files documentation](https://docs.docker.com/compose/how-tos/multiple-compose-files/merge/) for more details on using merge files.
@@ -157,24 +172,3 @@ Ensure projects are properly mounted:
- Check volume mounts in `docker-compose.yaml`
- Use absolute paths for external projects
- Verify permissions on mounted directories
## Migration Path
To switch between Docker and non-Docker usage:
1. **Docker to Non-Docker**:
- Manually edit project paths in `serena_config.yml`
- Change container paths to host paths
- Or use separate config files for each environment
2. **Non-Docker to Docker**:
- Projects will be re-registered with container paths
- Original config remains unchanged
## Future Improvements
We're working on:
- Automatic config migration between environments
- Better project path handling
- Dynamic port allocation
- Windows line-ending handling.
+10 -18
View File
@@ -53,28 +53,21 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
# Set the working directory
WORKDIR /workspaces/serena
# Development target
FROM base AS development
# Copy all files for development
COPY . /workspaces/serena/
# Create virtual environment and install dependencies with dev extras
RUN uv venv
RUN . .venv/bin/activate
RUN uv pip install --all-extras -r pyproject.toml -e .
ENV PATH="/workspaces/serena/.venv/bin:${PATH}"
# Install sed
RUN apt-get update && apt-get install -y sed
# Entrypoint to ensure environment is activated
ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"]
# Create Serena configuration
ENV SERENA_HOME=/workspaces/serena/config
RUN mkdir -p $SERENA_HOME
RUN cp src/serena/resources/serena_config.template.yml $SERENA_HOME/serena_config.yml
RUN sed -i 's/^gui_log_window: .*/gui_log_window: False/' $SERENA_HOME/serena_config.yml
RUN sed -i 's/^web_dashboard_listen_address: .*/web_dashboard_listen_address: 0.0.0.0/' $SERENA_HOME/serena_config.yml
RUN sed -i 's/^web_dashboard_open_on_launch: .*/web_dashboard_open_on_launch: False/' $SERENA_HOME/serena_config.yml
# Production target
FROM base AS production
# Copy only necessary files for production
COPY pyproject.toml /workspaces/serena/
COPY README.md /workspaces/serena/
COPY src/ /workspaces/serena/src/
# Create virtual environment and install dependencies (production only)
# Create virtual environment and install dependencies
RUN uv venv
RUN . .venv/bin/activate
RUN uv pip install -r pyproject.toml -e .
@@ -82,4 +75,3 @@ ENV PATH="/workspaces/serena/.venv/bin:${PATH}"
# Entrypoint to ensure environment is activated
ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"]
+36 -9
View File
@@ -22,6 +22,9 @@ Instead, it can use code-centric tools like `find_symbol`, `find_referencing_sym
<a href="lessons_learned.md"><img src="https://img.shields.io/badge/Lessons-Learned-7c4700?style=flat&logo=readthedocs&logoColor=white&labelColor=7c4700" alt="Lessons Learned" /></a>
</p>
> [!TIP]
> The [**Serena JetBrains plugin**](#the-serena-jetbrains-plugin) has been released!
## LLM Integration
Serena provides the necessary [tools](https://oraios.github.io/serena/01-about/035_tools.html) for coding workflows, but an LLM is required to do the actual work,
@@ -55,23 +58,47 @@ Note how Serena's tools enable Claude to find and edit the right symbols.
https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753
### Programming Language Support & Semantic Analysis Capabilities
## Programming Language Support & Semantic Analysis Capabilities
Serena's semantic code analysis capabilities build on **language servers** using the widely implemented
language server protocol (LSP). The LSP provides a set of versatile code querying
and editing functionalities based on symbolic understanding of the code.
Serena provides a set of versatile code querying and editing functionalities
based on symbolic understanding of the code.
Equipped with these capabilities, Serena discovers and edits code just like a seasoned developer
making use of an IDE's capabilities would.
Serena can efficiently find the right context and do the right thing even in very large and
complex projects! So not only is it free and open-source, it frequently achieves better results
than existing solutions that charge a premium.
complex projects!
There are two alternative technologies powering these capabilities:
* **Language servers** implementing the language server Protocol (LSP) — the free/open-source alternative.
* **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing
capabilities of your JetBrains IDE.
You can choose either of these backends depending on your preferences and requirements.
### Language Servers
Serena incorporates a powerful abstraction layer for the integration of language servers
that implement the language server protocol (LSP).
The underlying language servers are typically open-source projects (like Serena) or at least freely available for use.
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, Python, R, Ruby, Rust, Scala, Swift, 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.
> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details.
### The Serena JetBrains Plugin
As an alternative to language servers, the [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/)
leverages the powerful code analysis capabilities of your JetBrains IDE.
The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs,
including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, GoLand, CLion, and others.
Only Rider is not supported.
<a href="https://plugins.jetbrains.com/plugin/28946-serena/"><img src="docs/_static/images/jetbrains-marketplace-button.png"></a>
The plugin offers the most robust and most powerful Serena experience.
See our [documentation page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html) for further details and instructions.
## Quick Start
+3 -20
View File
@@ -1,7 +1,7 @@
services:
serena:
image: serena:latest
# To work with projects, you must mount them as volumes:
# To work with projects, you must mount them into /workspace/ in the container:
# volumes:
# - ./my-project:/workspace/my-project
# - /path/to/another/project:/workspace/another-project
@@ -16,23 +16,6 @@ services:
- SERENA_DOCKER=1
command:
- "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0"
# Add the context for the IDE assistant
# - "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0 --context ide-assistant"
# Alternatively add further arguments, e.g. a context
# - "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0 --context ide"
serena-dev:
image: serena:dev
build:
context: ./
dockerfile: Dockerfile
target: development
tty: true
stdin_open: true
environment:
- SERENA_DOCKER=1
volumes:
- .:/workspaces/serena
ports:
- "${SERENA_PORT:-9121}:9121" # MCP server port
- "${SERENA_DASHBOARD_PORT:-24282}:24282" # Dashboard port
command:
- "uv run --directory . serena-mcp-server"
+62 -9
View File
@@ -1,16 +1,31 @@
# Language Support
Serena's semantic code analysis capabilities build on **language servers** using the widely implemented
language server protocol (LSP). The LSP provides a set of versatile code querying
and editing functionalities based on symbolic understanding of the code.
Serena provides a set of versatile code querying and editing functionalities
based on symbolic understanding of the code.
Equipped with these capabilities, Serena discovers and edits code just like a seasoned developer
making use of an IDE's capabilities would.
Serena can efficiently find the right context and do the right thing even in very large and
complex projects! So not only is it free and open-source, it frequently achieves better results
than existing solutions that charge a premium.
complex projects!
Language servers provide support for a wide range of programming languages.
With Serena, we provide direct, out-of-the-box support for:
There are two alternative technologies powering these capabilities:
* **Language servers** implementing the language server Protocol (LSP) — the free/open-source alternative.
* **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing
capabilities of your JetBrains IDE.
You can choose either of these backends depending on your preferences and requirements.
## Language Servers
Serena incorporates a powerful abstraction layer for the integration of language servers
that implement the language server protocol (LSP).
It even supports multiple language servers in parallel to support polyglot projects.
The language servers themselves are typically open-source projects (like Serena)
or at least freely available for use.
We currently provide direct, out-of-the-box support for the programming languages listed below.
Some languages require additional installations or setup steps, as noted.
* **AL**
* **Bash**
@@ -20,15 +35,19 @@ With Serena, we provide direct, out-of-the-box support for:
* **Clojure**
* **Dart**
* **Elixir**
(requires installation of NextLS and Elixir; Windows not supported)
(requires Elixir installation; Expert language server is downloaded automatically)
* **Elm**
(requires Elm compiler)
* **Erlang**
(requires installation of beam and [erlang_ls](https://github.com/erlang-ls/erlang_ls); experimental, might be slow or hang)
* **Fortran**
* **F#**
(requires .NET SDK 8.0+; uses FsAutoComplete/Ionide, which is auto-installed; for Homebrew .NET on macOS, set DOTNET_ROOT in your environment)
* **Fortran**
(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**
@@ -56,8 +75,42 @@ With Serena, we provide direct, out-of-the-box support for:
(requires some [manual setup](../03-special-guides/scala_setup_guide_for_serena); uses Metals LSP)
* **Swift**
* **TypeScript**
* **Vue**
(3.x with TypeScript; requires Node.js v18+ and npm; supports .vue Single File Components with monorepo detection)
* **YAML**
* **Zig**
(requires installation of ZLS - Zig Language Server)
Support for further languages can easily be added by providing a shallow adapter for a new language server implementation,
see Serena's [memory on that](https://github.com/oraios/serena/blob/main/.serena/memories/adding_new_language_support_guide.md).
## The Serena JetBrains Plugin
As an alternative to language servers, the [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/)
leverages the powerful code analysis capabilities of JetBrains IDEs.
The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs,
including IntelliJ IDEA, PyCharm, Android Studio, AppCode, WebStorm, PhpStorm, RubyMine, GoLand, AppCode, CLion, Rider, and others.
When using the plugin, Serena connects to an instance of your JetBrains IDE via the plugin. For users who already
work in a JetBrains IDE, this means Serena seamlessly integrates with the IDE instance you typically have open anyway,
requiring no additional setup or configuration beyond the plugin itself. This approach offers several key advantages:
* **External library indexing**: Dependencies and libraries are fully indexed and accessible to Serena
* **No additional setup**: No need to download or configure separate language servers
* **Enhanced performance**: Faster tool execution thanks to optimized IDE integration
* **Multi-language excellence**: First-class support for polyglot projects with multiple languages and frameworks
Even if you prefer to work in a different code editor, you can still benefit from the JetBrains plugin by running
a JetBrains IDE instance (most have free community editions) alongside your preferred editor with your project
opened and indexed. Serena will connect to the IDE for code analysis while you continue working in your editor
of choice.
```{raw} html
<p>
<a href="https://plugins.jetbrains.com/plugin/28946-serena/">
<img style="background-color:transparent;" src="../_static/images/jetbrains-marketplace-button.png">
</a>
</p>
```
See the [JetBrains Plugin documentation](../02-usage/025_jetbrains_plugin) for usage details.
+34 -15
View File
@@ -2,14 +2,20 @@
Serena is a command-line tool with a variety of sub-commands.
This section describes
* varies ways of running Serena
* various ways of running Serena
* how to run and configure the most important command, i.e. starting the MCP server
* other useful commands.
## Ways of Running Serena
In the following, we will refer to the command used to run Serena as `<serena>`,
which you should replace with the appropriate command based on your chosen method.
which you should replace with the appropriate command based on your chosen method,
as detailed below.
In general, to get help, append `--help` to the command, i.e.
<serena> --help
<serena> <command> --help
### Using uvx
@@ -47,28 +53,30 @@ As a consequence, you will need to specify paths when using CLI commands that wo
:::
(docker)=
### Using Docker (Experimental)
### Using Docker
:::{warning}
Docker support is currently experimental with several limitations.
:::
The Docker approach offers several advantages:
You can run the Serena MCP server directly via docker as follows,
* better security isolation for shell command execution
* no need to install language servers and dependencies locally
* consistent environment across different systems
You can run the Serena MCP server directly via Docker as follows,
assuming that the projects you want to work on are all located in `/path/to/your/projects`:
```shell
docker run --rm -i --network host -v /path/to/your/projects:/workspaces/projects ghcr.io/oraios/serena:latest serena
```
Replace `/path/to/your/projects` with the absolute path to your projects directory. The Docker approach provides:
This command mounts your projects into the container under `/workspaces/projects`, so when working with projects,
you need to refer to them using the respective path (e.g. `/workspaces/projects/my-project`).
* Better security isolation for shell command execution
* No need to install language servers and dependencies locally
* Consistent environment across different systems
Alternatively, you may use Docker compose with the `compose.yml` file provided in the repository.
See our [advanced Docker usage](https://github.com/oraios/serena/blob/main/DOCKER.md) documentation for more detailed instructions, configuration options, and limitations.
Alternatively, use docker compose with the `compose.yml` file provided in the repository.
See our [Docker Setup](https://github.com/oraios/serena/blob/main/DOCKER.md) documentation for more detailed setup instructions, configuration options, and known limitations.
:::{note}
Docker usage is subject to limitations; see the [advanced Docker usage](https://github.com/oraios/serena/blob/main/DOCKER.md) documentation for details.
:::
### Using Nix
@@ -90,6 +98,12 @@ Given your preferred method of running Serena, you can start the MCP server usin
Note that no matter how you run the MCP server, Serena will, by default, start a web-based dashboard on localhost that will allow you to inspect
the server's operations, logs, and configuration.
:::{tip}
By default, Serena will use language servers for code understanding and analysis.
With the [Serena JetBrains Plugin](025_jetbrains_plugin), we recently introduced a powerful alternative,
which has several advantages over the language server-based approach.
:::
### Standard I/O Mode
The typical usage involves the client (e.g. Claude Code, Codex or Cursor) running
@@ -148,6 +162,11 @@ to get a list of all available options.
Some useful options include:
* `--project <path|name>`: specify the project to work on by name or path.
* `--project-from-cwd`: auto-detect the project from current working directory
(looking for a directory containing `.serena/project.yml` or `.git` in parent directories, activating the current directory if none is found);
This option is intended for CLI-based agents like Claude Code, Gemini and Codex, which are typically started from within the project directory
and which do not change directories during their operation.
* `--language-backend JetBrains`: use the Serena JetBrains Plugin as the language backend (overriding the default backend configured in the central configuration)
* `--context <context>`: specify the operation [context](contexts) in which Serena shall operate
* `--mode <mode>`: specify one or more [modes](modes) to enable (can be passed several times)
* `--enable-web-dashboard <true|false>`: enable or disable the web dashboard (enabled by default)
@@ -225,4 +244,4 @@ Here are some examples of commands you might find useful:
<serena> prompts edit-override prompt-name
```
Explore the full set of commands and options using the CLI itself!
Explore the full set of commands and options using the CLI itself!
+56
View File
@@ -0,0 +1,56 @@
# The Serena JetBrains Plugin
The [JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) allows Serena to
leverage the powerful code analysis and editing capabilities of your JetBrains IDE.
```{raw} html
<p>
<a href="https://plugins.jetbrains.com/plugin/28946-serena/">
<img style="background-color:transparent;" src="../_static/images/jetbrains-marketplace-button.png">
</a>
</p>
```
We recommend the JetBrains plugin as the preferred way of using Serena,
especially for users of JetBrains IDEs.
**Purchasing the JetBrains Plugin supports the Serena project.**
The proceeds from plugin sales allow us to dedicate more resources to further developing and improving Serena.
## Configuring Serena
After installing the plugin, you need to configure Serena to use it.
**Central Configuration**.
Edit the global Serena configuration file located at `~/.serena/serena_config.yml`
(`%USERPROFILE%\.serena\config.yaml` on Windows).
Change the `language_backend` setting as follows:
```yaml
language_backend: JetBrains
```
**Per-Instance Configuration**.
The configuration setting in the global config file can be overridden on a
per-instance basis by providing the arguments `--language-backend JetBrains` when
launching the Serena MCP server.
## Advantages of the JetBrains Plugin
There are multiple features that are only available when using the JetBrains plugin:
* **External library indexing**: Dependencies and libraries are fully indexed and accessible to Serena
* **No additional setup**: No need to download or configure separate language servers
* **Enhanced performance**: Faster tool execution thanks to optimized IDE integration
* **Multi-language excellence**: First-class support for polyglot projects with multiple languages and frameworks
We are also working on additional features like a `move_symbol` tool and debugging-related capabilities that
will be available exclusively through the JetBrains plugin.
## Usage with Other Editors
We realize that not everyone uses a JetBrains IDE as their main code editor.
You can still take advantage of the JetBrains plugin by running a JetBrains IDE instance alongside your
preferred editor. Most JetBrains IDEs have a free community edition that you can use for this purpose.
You just need to make sure that the project you are working on is open and indexed in the JetBrains IDE,
so that Serena can connect to it.
+224 -68
View File
@@ -1,10 +1,75 @@
# 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).
:::{note}
The configurations we provide for particular clients below will 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.
:::
(clients-general-instructions)=
## General Instructions
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.
Find concrete examples for popular clients below.
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"
}
```
## Claude Code
@@ -13,25 +78,57 @@ 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 ide-assistant --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 ide-assistant --project "$(pwd)"
claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)"
```
Note:
* We use the `ide-assistant` context to disable unnecessary tools (avoiding duplication
* We use the `claude-code` context to disable unnecessary tools (avoiding duplication
with Claude Code's built-in capabilities).
* 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.
Alternatively, use `--project-from-cwd` for user-level configuration that works across all projects:
```shell
claude mcp add --scope user serena -- <serena> start-mcp-server --context=claude-code --project-from-cwd
```
This searches up from the current directory for `.serena/project.yml` or `.git` markers,
falling back to the current directory if neither is found. This makes it suitable for a
single global MCP configuration. The `--project-from-cwd` option is intended for CLI-based agents
(like Claude Code, Gemina or Codex) that invoke Serena from within project directories.
Be sure to use at least `v1.0.52` of Claude Code (as earlier versions do not read MCP server system prompts upon startup).
## 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 +142,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"
@@ -61,77 +159,132 @@ that was already taken).
## 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:
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)).
```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 (`\\`).
That's it! Save the config and then restart Claude Desktop. You are ready for activating your first project.
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).
## JetBrains Junie
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:
```json
{
"mcpServers": {
"serena": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server",
"--context",
"ide"
]
}
}
}
```
You will have to prompt Junie to "Activate the current project using serena's activation tool" at the
start of each session.
## 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
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.
Below, we list some popular categories of clients that Serena can be used with.
For other clients, follow the [general instructions](#clients-general-instructions) above to set up Serena as an MCP server.
### Terminal-Based Clients
@@ -146,12 +299,15 @@ 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).
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-assistant` context** for these integrations by adding the arguments `--context ide-assistant`
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
+4 -4
View File
@@ -55,18 +55,18 @@ Indexing has to be called only once. During regular usage, Serena will automatic
Project activation makes Serena aware of the project you want to work with.
You can either choose to do this
* while in a conversation, by telling the model to activate a project, e.g.,
* while in a conversation, by telling the LLM to activate a project, e.g.,
* "Activate the project /path/to/my_project" (for first-time activation with auto-creation)
* "Activate the project my_project"
Note that this option requires the `activate_project` tool to be active,
which it isn't in the (default version) of context `ide-assistant` if a project is provided at startup.
(The tool is deactivated because we assume that in the ide-assistant context the user will only work on the open project and have
which it isn't in single-project [contexts](contexts) like `ide` or `claude-code` *if* a project is provided at startup.
(The tool is deactivated, because we assume that in these contexts, user will only work on the single, open project and have
no need to switch it.)
* when the MCP server starts, by passing the project path or name as a command-line argument
(e.g. when working on a fixed project in `ide-assistant` mode): `--project <path|name>`
(e.g. when using a single-project mode like `ide` or `claude-code`): `--project <path|name>`
## Onboarding & Memories
+67 -10
View File
@@ -8,14 +8,15 @@ Serena is configured in four places:
1. The `serena_config.yml` for general settings that apply to all clients and projects.
It is located in your user directory under `.serena/serena_config.yml`.
If you do not explicitly create the file, it will be auto-generated when you first run Serena.
The file will be auto-generated when you first run Serena.
You can edit it directly or use
```shell
uvx --from git+https://github.com/oraios/serena serena config edit
<serena> config edit
```
(or use the `--directory` command version).
where `<serena>` is [your way of running Serena](020_running).
The configuration file can also be accessed through [Serena's dashboard](060_dashboard).
2. In the arguments passed to the `start-mcp-server` in your client's config (see below),
which will apply to all sessions started by the respective client. In particular, the [context](contexts) parameter
should be set appropriately for Serena to be best adjusted to existing tools and capabilities of your client.
@@ -44,9 +45,22 @@ A context is set at startup when launching Serena (e.g., via CLI options for an
Serena comes with pre-defined contexts:
* `desktop-app`: Tailored for use with desktop applications like Claude Desktop. This is the default.
The full set of Serena's tools is provided, as the application is assumed to have no prior coding-specific capabilities.
* `claude-code`: Optimized for use with Claude Code, it disables tools that would duplicate Claude Code's built-in capabilities.
* `codex`: Optimized for use with OpenAI Codex.
* `ide`: Generic context for IDE assistants/coding agents, e.g. VSCode, Cursor, or Cline, focusing on augmenting existing capabilities.
Basic file operations and shell execution are assumed to be handled by the assistant's own capabilities.
* `agent`: Designed for scenarios where Serena acts as a more autonomous agent, for example, when used with Agno.
* `ide-assistant`: Optimized for integration into IDEs like VSCode, Cursor, or Cline, focusing on in-editor coding assistance.
Choose the context that best matches the type of integration you are using.
Choose the context that best matches the type of integration you are using.
Find the concrete definitions of the above contexts [here](https://github.com/oraios/serena/tree/main/src/serena/resources/config/contexts).
Note that the contexts `ide` and `claude-code` are **single-project contexts** (defining `single_project: true`).
For such contexts, if a project is provided at startup, the set of tools is limited to those required by the project's
concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal.
Tools explicitly disabled by the project will not be available at all. Since changing the active project
ceases to be a relevant operation in this case, the project activation tool is disabled.
When launching Serena, specify the context using `--context <context-name>`.
Note that for cases where parameter lists are specified (e.g. Claude Desktop), you must add two parameters to the list.
@@ -74,18 +88,34 @@ Examples of built-in modes include:
* `editing`: Optimizes Serena for direct code modification tasks.
* `interactive`: Suitable for a conversational, back-and-forth interaction style.
* `one-shot`: Configures Serena for tasks that should be completed in a single response, often used with `planning` for generating reports or initial plans.
* `no-onboarding`: Skips the initial onboarding process if it's not needed for a particular session.
* `onboarding`: (Usually triggered automatically) Focuses on the project onboarding process.
* `no-onboarding`: Skips the initial onboarding process if it's not needed for a particular session but retains the memory tools (assuming initial memories were created externally).
* `onboarding`: Focuses on the project onboarding process.
* `no-memories`: Disables all memory tools (and tools building on memories such as onboarding tools)
Find the concrete definitions of these modes [here](https://github.com/oraios/serena/tree/main/src/serena/resources/config/modes).
:::{important}
By default, Serena activates the two modes `interactive` and `editing`.
As soon as you start to specify modes, only the modes you explicitly specify will be active, however.
Therefore, if you want to keep the default modes, you must specify them as well.
For example, to add mode `no-memories` to the default behaviour, specify
```shell
--mode interactive --mode editing --mode no-memories
```
:::
Modes can be set at startup (similar to contexts) but can also be _switched dynamically_ during a session.
You can instruct the LLM to use the `switch_modes` tool to activate a different set of modes (e.g., "Switch to planning and one-shot modes").
When launching Serena, specify modes using `--mode <mode-name>`; multiple modes can be specified, e.g. `--mode planning --mode no-onboarding`.
:warning: **Mode Compatibility**: While you can combine modes, some may be semantically incompatible (e.g., `interactive` and `one-shot`).
:::{note}
**Mode Compatibility**: While you can combine modes, some may be semantically incompatible (e.g., `interactive` and `one-shot`).
Serena currently does not prevent incompatible combinations; it is up to the user to choose sensible mode configurations.
:::
You can manage contexts using the `mode` command,
You can manage modes using the `mode` command,
<serena> mode --help
<serena> mode list
@@ -93,4 +123,31 @@ You can manage contexts using the `mode` command,
<serena> mode edit <mode-name>
<serena> mode delete <mode-name>
where `<serena>` is [your way of running Serena](020_running).
where `<serena>` is [your way of running Serena](020_running).
## Advanced Configuration
For advanced users, Serena's configuration can be further customized.
### Serena Data Directory
The Serena user data directory (where configuration, language server files, logs, etc. are stored) defaults to `~/.serena`.
You can change this location by setting the `SERENA_HOME` environment variable to your desired path.
### Custom Prompts
All of Serena's prompts can be fully customized.
We define prompt as jinja templates in yaml files, and you can inspect our default prompts [here](https://github.com/oraios/serena/tree/main/src/serena/resources/config/prompt_templates).
To override a prompt, simply add a .yml file to the `prompt_templates` folder in your Serena data directory
which defines the prompt with the same name as the default prompt you want to override.
For example, to override the `system_prompt`, you could create a file `~/.serena/prompt_templates/system_prompt.yml` (assuming default Serena data folder location)
with content like:
```yaml
prompts:
system_prompt: |
Whatever you want ...
```
It is advisable to use the default prompt as a starting point and modify it to suit your needs.
@@ -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 -1
View File
@@ -53,7 +53,7 @@ parse:
#######################################################################################
# HTML-specific settings
html:
#favicon : # A path to a favicon image
favicon : "../src/serena/resources/dashboard/serena-icon-32.png"
use_edit_page_button : false # Whether to add an "edit this page" button to pages. If `true`, repository information in repository: must be filled in
use_repository_button : false # Whether to add a link to your repository button
use_issues_button : false # Whether to add an "open an issue" button
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File
Whitespace-only changes.
+10 -3
View File
@@ -19,7 +19,7 @@ dependencies = [
"fortls>=3.2.2",
"overrides>=7.7.0,<8",
"python-dotenv>=1.0.0, <2",
"mcp==1.12.3",
"mcp==1.23.0",
"flask>=3.0.0",
"sensai-utils>=1.5.0",
"pydantic>=2.10.6",
@@ -72,6 +72,7 @@ dev = [
"jupyter-book>=1,<2",
"nbsphinx",
"pyinstrument",
"pytest-timeout>=2.4.0",
]
agno = ["agno>=2.2.1", "sqlalchemy>=2.0.40"]
google = ["google-genai>=1.8.0"]
@@ -121,7 +122,7 @@ PYDEVD_DISABLE_FILE_VALIDATION = "1"
# Uses PYTEST_MARKERS env var for default markers
# For custom markers, one can either adjust the env var or just use -m option in the command line,
# as the second -m option will override the first one.
test = "pytest test -vv -m \"${PYTEST_MARKERS:-not rust and not erlang}\""
test = "pytest test -vv"
_black_check = "black --check src scripts test"
_ruff_check = "ruff check src scripts test"
_black_format = "black src scripts test"
@@ -273,8 +274,10 @@ 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)",
"php: language server running for PHP",
"perl: language server running for Perl",
"csharp: language server running for C#",
@@ -293,12 +296,16 @@ markers = [
"erlang: language server running for Erlang",
"scala: language server running for Scala",
"al: language server running for AL (Microsoft Dynamics 365 Business Central)",
"fsharp: language server running for F#",
"rego: language server running for Rego",
"markdown: language server running for Markdown",
"julia: Julia language server tests",
"fortran: language server running for Fortran",
"haskell: Haskell language server tests",
"yaml: language server running for YAML",
"powershell: language server running for PowerShell",
"slow: tests that require additional Expert instances and have long startup times (~60-90s each)",
"toml: language server running for TOML",
]
[tool.codespell]
@@ -306,4 +313,4 @@ markers = [
skip = '.git*,*.svg,*.lock,*.min.*'
check-hidden = true
# ignore-regex = ''
# ignore-words-list = ''
ignore-words-list = 'paket'
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 66 KiB

-1
View File
@@ -25,7 +25,6 @@ if __name__ == "__main__":
serena_config = SerenaConfig.from_config_file()
serena_config.log_level = logging.INFO
serena_config.jetbrains = False
serena_config.gui_log_window_enabled = False
serena_config.web_dashboard = False
+30 -13
View File
@@ -18,13 +18,14 @@ from interprompt.jinja_template import JinjaTemplate
from serena import serena_version
from serena.analytics import RegisteredTokenCountEstimator, ToolUsageStats
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
from serena.config.serena_config import SerenaConfig, ToolInclusionDefinition
from serena.config.serena_config import LanguageBackend, SerenaConfig, ToolInclusionDefinition
from serena.dashboard import SerenaDashboardAPI
from serena.ls_manager import LanguageServerManager
from serena.project import Project
from serena.prompt_factory import SerenaPromptFactory
from serena.task_executor import TaskExecutor
from serena.tools import ActivateProjectTool, GetCurrentConfigTool, ReplaceContentTool, Tool, ToolMarker, ToolRegistry
from serena.util.gui import system_has_usable_display
from serena.util.inspection import iter_subclasses
from serena.util.logging import MemoryLogHandler
from solidlsp.ls_config import Language
@@ -204,6 +205,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:
@@ -213,6 +215,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:
@@ -232,7 +236,10 @@ class SerenaAgent:
self._tool_usage_stats = ToolUsageStats(token_count_estimator)
# log fundamental information
log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})")
log.info(
f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()}; "
f"language backend={self.serena_config.language_backend.name})"
)
log.info("Configuration file: %s", self.serena_config.config_file_path)
log.info("Available projects: {}".format(", ".join(self.serena_config.project_names)))
log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name_from_cls() for tool in self._all_tools.values()])}")
@@ -244,7 +251,7 @@ class SerenaAgent:
tool_inclusion_definitions: list[ToolInclusionDefinition] = [self.serena_config, self._context]
if self._context.single_project:
tool_inclusion_definitions.extend(self._single_project_context_tool_inclusion_definitions(project))
if self.serena_config.jetbrains:
if self.serena_config.language_backend == LanguageBackend.JETBRAINS:
tool_inclusion_definitions.append(SerenaAgentMode.from_name_internal("jetbrains"))
self._base_tool_set = ToolSet.default().apply(*tool_inclusion_definitions)
@@ -280,15 +287,21 @@ class SerenaAgent:
if self.serena_config.web_dashboard:
self._dashboard_thread, port = SerenaDashboardAPI(
get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats
).run_in_thread()
dashboard_url = f"http://127.0.0.1:{port}/dashboard/index.html"
).run_in_thread(host=self.serena_config.web_dashboard_listen_address)
dashboard_host = self.serena_config.web_dashboard_listen_address
if dashboard_host == "0.0.0.0":
dashboard_host = "localhost"
dashboard_url = f"http://{dashboard_host}:{port}/dashboard/index.html"
log.info("Serena web dashboard started at %s", dashboard_url)
if self.serena_config.web_dashboard_open_on_launch:
# open the dashboard URL in the default web browser (using a separate process to control
# output redirection)
process = multiprocessing.Process(target=self._open_dashboard, args=(dashboard_url,))
process.start()
process.join(timeout=1)
if not system_has_usable_display():
log.warning("Not opening the Serena web dashboard automatically because no usable display was detected.")
else:
# open the dashboard URL in the default web browser (using a separate process to control
# output redirection)
process = multiprocessing.Process(target=self._open_dashboard, args=(dashboard_url,))
process.start()
process.join(timeout=1)
# inform the GUI window (if any)
if self._gui_log_viewer is not None:
self._gui_log_viewer.set_dashboard_url(dashboard_url)
@@ -517,7 +530,7 @@ class SerenaAgent:
"""
:return: whether this agent uses language server-based code analysis
"""
return not self.serena_config.jetbrains
return self.serena_config.language_backend == LanguageBackend.LSP
def _activate_project(self, project: Project) -> None:
log.info(f"Activating {project.project_name} at {project.project_root}")
@@ -684,18 +697,22 @@ class SerenaAgent:
ToolRegistry().print_tool_overview(self._active_tools.values())
def __del__(self) -> None:
self.shutdown()
def shutdown(self, timeout: float = 2.0) -> None:
"""
Destructor to clean up the language server instance and GUI logger
Shuts down the agent, freeing resources and stopping background tasks.
"""
if not hasattr(self, "_is_initialized"):
return
log.info("SerenaAgent is shutting down ...")
if self._active_project is not None:
self._active_project.shutdown()
self._active_project.shutdown(timeout=timeout)
self._active_project = None
if self._gui_log_viewer:
log.info("Stopping the GUI log window ...")
self._gui_log_viewer.stop()
self._gui_log_viewer = None
def get_tool_by_name(self, tool_name: str) -> Tool:
tool_class = ToolRegistry().get_tool_class_by_name(tool_name)
+86 -21
View File
@@ -5,6 +5,7 @@ import os
import shutil
import subprocess
import sys
from collections.abc import Iterator
from logging import Logger
from pathlib import Path
from typing import Any, Literal
@@ -17,18 +18,14 @@ from tqdm import tqdm
from serena.agent import SerenaAgent
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
from serena.config.serena_config import ProjectConfig, SerenaConfig, SerenaPaths
from serena.config.serena_config import LanguageBackend, ProjectConfig, SerenaConfig, SerenaPaths
from serena.constants import (
DEFAULT_CONTEXT,
DEFAULT_MODES,
PROMPT_TEMPLATES_DIR_IN_USER_HOME,
PROMPT_TEMPLATES_DIR_INTERNAL,
SERENA_LOG_FORMAT,
SERENA_MANAGED_DIR_IN_HOME,
SERENAS_OWN_CONTEXT_YAMLS_DIR,
SERENAS_OWN_MODE_YAMLS_DIR,
USER_CONTEXT_YAMLS_DIR,
USER_MODE_YAMLS_DIR,
)
from serena.mcp import SerenaMCPFactory, SerenaMCPFactorySingleProcess
from serena.project import Project
@@ -39,6 +36,42 @@ from solidlsp.util.subprocess_util import subprocess_kwargs
log = logging.getLogger(__name__)
def find_project_root(root: str | Path | None = None) -> str:
"""Find project root by walking up from CWD.
Checks for .serena/project.yml first (explicit Serena project), then .git (git root).
Falls back to CWD if no marker is found.
:param root: If provided, constrains the search to this directory and below
(acts as a virtual filesystem root). Search stops at this boundary.
:return: absolute path to project root (falls back to CWD if no marker found)
"""
current = Path.cwd().resolve()
boundary = Path(root).resolve() if root is not None else None
def ancestors() -> Iterator[Path]:
"""Yield current directory and ancestors up to boundary."""
yield current
for parent in current.parents:
yield parent
if boundary is not None and parent == boundary:
return
# First pass: look for .serena
for directory in ancestors():
if (directory / ".serena" / "project.yml").is_file():
return str(directory)
# Second pass: look for .git
for directory in ancestors():
if (directory / ".git").exists(): # .git can be file (worktree) or dir
return str(directory)
# Fall back to CWD
return str(current)
# --------------------- Utilities -------------------------------------
@@ -121,6 +154,12 @@ class TopLevelCommands(AutoRegisteringGroup):
show_default=True,
help="Built-in mode names or paths to custom mode YAMLs.",
)
@click.option(
"--language-backend",
type=click.Choice([lb.value for lb in LanguageBackend]),
default=None,
help="Override the configured language backend.",
)
@click.option(
"--transport",
type=click.Choice(["stdio", "sse", "streamable-http"]),
@@ -128,8 +167,16 @@ class TopLevelCommands(AutoRegisteringGroup):
show_default=True,
help="Transport protocol.",
)
@click.option("--host", type=str, default="0.0.0.0", show_default=True)
@click.option("--port", type=int, default=8000, show_default=True)
@click.option(
"--host",
type=str,
default="0.0.0.0",
show_default=True,
help="Listen address for the MCP server (when using corresponding transport).",
)
@click.option(
"--port", type=int, default=8000, show_default=True, help="Listen port for the MCP server (when using corresponding transport)."
)
@click.option("--enable-web-dashboard", type=bool, is_flag=False, default=None, help="Override dashboard setting in config.")
@click.option("--enable-gui-log-window", type=bool, is_flag=False, default=None, help="Override GUI log window setting in config.")
@click.option(
@@ -140,11 +187,19 @@ class TopLevelCommands(AutoRegisteringGroup):
)
@click.option("--trace-lsp-communication", type=bool, is_flag=False, default=None, help="Whether to trace LSP communication.")
@click.option("--tool-timeout", type=float, default=None, help="Override tool execution timeout in config.")
@click.option(
"--project-from-cwd",
is_flag=True,
default=False,
help="Auto-detect project from current working directory (searches for .serena/project.yml or .git, falls back to CWD). Intended for CLI-based agents like Claude Code, Gemini and Codex.",
)
def start_mcp_server(
project: str | None,
project_file_arg: str | None,
project_from_cwd: bool | None,
context: str,
modes: tuple[str, ...],
language_backend: str | None,
transport: Literal["stdio", "sse", "streamable-http"],
host: str,
port: int,
@@ -173,12 +228,21 @@ class TopLevelCommands(AutoRegisteringGroup):
log.info("Initializing Serena MCP server")
log.info("Storing logs in %s", log_path)
# Handle --project-from-cwd flag
if project_from_cwd:
if project is not None or project_file_arg is not None:
raise click.UsageError("--project-from-cwd cannot be used with --project or positional project argument")
project = find_project_root()
log.info("Auto-detected project root: %s", project)
project_file = project_file_arg or project
factory = SerenaMCPFactorySingleProcess(context=context, project=project_file, memory_log_handler=memory_log_handler)
server = factory.create_mcp_server(
host=host,
port=port,
modes=modes,
language_backend=LanguageBackend.from_str(language_backend) if language_backend else None,
enable_web_dashboard=enable_web_dashboard,
enable_gui_log_window=enable_gui_log_window,
log_level=log_level,
@@ -270,7 +334,7 @@ class ModeCommands(AutoRegisteringGroup):
if not (name or from_internal):
raise click.UsageError("Provide at least one of --name or --from-internal.")
mode_name = name or from_internal
dest = os.path.join(USER_MODE_YAMLS_DIR, f"{mode_name}.yml")
dest = os.path.join(SerenaPaths().user_modes_dir, f"{mode_name}.yml")
src = (
os.path.join(SERENAS_OWN_MODE_YAMLS_DIR, f"{from_internal}.yml")
if from_internal
@@ -289,7 +353,7 @@ class ModeCommands(AutoRegisteringGroup):
@click.command("edit", help="Edit a custom mode YAML file.")
@click.argument("mode_name")
def edit(mode_name: str) -> None:
path = os.path.join(USER_MODE_YAMLS_DIR, f"{mode_name}.yml")
path = os.path.join(SerenaPaths().user_modes_dir, f"{mode_name}.yml")
if not os.path.exists(path):
if mode_name in SerenaAgentMode.list_registered_mode_names(include_user_modes=False):
click.echo(
@@ -305,7 +369,7 @@ class ModeCommands(AutoRegisteringGroup):
@click.command("delete", help="Delete a custom mode file.")
@click.argument("mode_name")
def delete(mode_name: str) -> None:
path = os.path.join(USER_MODE_YAMLS_DIR, f"{mode_name}.yml")
path = os.path.join(SerenaPaths().user_modes_dir, f"{mode_name}.yml")
if not os.path.exists(path):
click.echo(f"Custom mode '{mode_name}' not found.")
return
@@ -347,7 +411,7 @@ class ContextCommands(AutoRegisteringGroup):
if not (name or from_internal):
raise click.UsageError("Provide at least one of --name or --from-internal.")
ctx_name = name or from_internal
dest = os.path.join(USER_CONTEXT_YAMLS_DIR, f"{ctx_name}.yml")
dest = os.path.join(SerenaPaths().user_contexts_dir, f"{ctx_name}.yml")
src = (
os.path.join(SERENAS_OWN_CONTEXT_YAMLS_DIR, f"{from_internal}.yml")
if from_internal
@@ -366,7 +430,7 @@ class ContextCommands(AutoRegisteringGroup):
@click.command("edit", help="Edit a custom context YAML file.")
@click.argument("context_name")
def edit(context_name: str) -> None:
path = os.path.join(USER_CONTEXT_YAMLS_DIR, f"{context_name}.yml")
path = os.path.join(SerenaPaths().user_contexts_dir, f"{context_name}.yml")
if not os.path.exists(path):
if context_name in SerenaAgentContext.list_registered_context_names(include_user_contexts=False):
click.echo(
@@ -382,7 +446,7 @@ class ContextCommands(AutoRegisteringGroup):
@click.command("delete", help="Delete a custom context file.")
@click.argument("context_name")
def delete(context_name: str) -> None:
path = os.path.join(USER_CONTEXT_YAMLS_DIR, f"{context_name}.yml")
path = os.path.join(SerenaPaths().user_contexts_dir, f"{context_name}.yml")
if not os.path.exists(path):
click.echo(f"Custom context '{context_name}' not found.")
return
@@ -401,10 +465,9 @@ class SerenaConfigCommands(AutoRegisteringGroup):
"edit", help="Edit serena_config.yml in your default editor. Will create a config file from the template if no config is found."
)
def edit() -> None:
config_path = os.path.join(SERENA_MANAGED_DIR_IN_HOME, "serena_config.yml")
if not os.path.exists(config_path):
SerenaConfig.generate_config_file(config_path)
_open_in_editor(config_path)
serena_config = SerenaConfig.from_config_file()
assert serena_config.config_file_path is not None
_open_in_editor(serena_config.config_file_path)
class ProjectCommands(AutoRegisteringGroup):
@@ -811,8 +874,9 @@ class PromptCommands(AutoRegisteringGroup):
@staticmethod
def _get_user_prompt_yaml_path(prompt_yaml_name: str) -> str:
os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
return os.path.join(PROMPT_TEMPLATES_DIR_IN_USER_HOME, prompt_yaml_name)
templates_dir = SerenaPaths().user_prompt_templates_dir
os.makedirs(templates_dir, exist_ok=True)
return os.path.join(templates_dir, prompt_yaml_name)
@staticmethod
@click.command("list", help="Lists yamls that are used for defining prompts.")
@@ -863,9 +927,10 @@ class PromptCommands(AutoRegisteringGroup):
@staticmethod
@click.command("list-overrides", help="List existing prompt override files")
def list_overrides() -> None:
os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
user_templates_dir = SerenaPaths().user_prompt_templates_dir
os.makedirs(user_templates_dir, exist_ok=True)
serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")]
override_files = glob.glob(os.path.join(PROMPT_TEMPLATES_DIR_IN_USER_HOME, "*.yml"))
override_files = glob.glob(os.path.join(user_templates_dir, "*.yml"))
for file_path in override_files:
if os.path.basename(file_path) in serena_prompt_yaml_names:
click.echo(file_path)
+108 -42
View File
@@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Generic, Optional, TypeVar, cast
from serena.symbol import JetBrainsSymbol, LanguageServerSymbol, LanguageServerSymbolRetriever, PositionInFile, Symbol
from solidlsp import SolidLanguageServer, ls_types
from solidlsp.ls import LSPFileBuffer
from solidlsp.ls_types import extract_text_edits
from solidlsp.ls_utils import PathUtils, TextUtils
from .constants import DEFAULT_SOURCE_FILE_ENCODING
@@ -38,12 +37,23 @@ 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:
"""
:return: the contents of the file.
"""
@abstractmethod
def set_contents(self, contents: str) -> None:
"""
Fully resets the contents of the file.
:param contents: the new contents
"""
@abstractmethod
def delete_text_between_positions(self, start_pos: PositionInFile, end_pos: PositionInFile) -> None:
pass
@@ -60,16 +70,19 @@ class CodeEditor(Generic[TSymbol], ABC):
raise NotImplementedError("This method must be overridden for each subclass")
@contextmanager
def _edited_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]:
def edited_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]:
"""
Context manager for editing a file.
"""
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:
@@ -94,7 +107,7 @@ class CodeEditor(Generic[TSymbol], ABC):
start_pos = symbol.get_body_start_position_or_raise()
end_pos = symbol.get_body_end_position_or_raise()
with self._edited_file_context(relative_file_path) as edited_file:
with self.edited_file_context(relative_file_path) as edited_file:
# make sure the replacement adds no additional newlines (before or after) - all newlines
# and whitespace before/after should remain the same, so we strip it entirely
body = body.strip()
@@ -149,7 +162,7 @@ class CodeEditor(Generic[TSymbol], ABC):
# `line += 1`, is replaced
body = body.rstrip("\r\n") + "\n"
with self._edited_file_context(relative_file_path) as edited_file:
with self.edited_file_context(relative_file_path) as edited_file:
edited_file.insert_text_at_position(PositionInFile(line, col), body)
def insert_before_symbol(self, name_path: str, relative_file_path: str, body: str) -> None:
@@ -177,7 +190,7 @@ class CodeEditor(Generic[TSymbol], ABC):
body += "\n" * num_trailing_newlines
# apply edit
with self._edited_file_context(relative_file_path) as edited_file:
with self.edited_file_context(relative_file_path) as edited_file:
edited_file.insert_text_at_position(PositionInFile(line=line, col=col), body)
def insert_at_line(self, relative_path: str, line: int, content: str) -> None:
@@ -188,7 +201,7 @@ class CodeEditor(Generic[TSymbol], ABC):
:param line: the 0-based index of the line to insert content at
:param content: the content to insert
"""
with self._edited_file_context(relative_path) as edited_file:
with self.edited_file_context(relative_path) as edited_file:
edited_file.insert_text_at_position(PositionInFile(line, 0), content)
def delete_lines(self, relative_path: str, start_line: int, end_line: int) -> None:
@@ -202,7 +215,7 @@ class CodeEditor(Generic[TSymbol], ABC):
start_col = 0
end_line_for_delete = end_line + 1
end_col = 0
with self._edited_file_context(relative_path) as edited_file:
with self.edited_file_context(relative_path) as edited_file:
start_pos = PositionInFile(line=start_line, col=start_col)
end_pos = PositionInFile(line=end_line_for_delete, col=end_col)
edited_file.delete_text_between_positions(start_pos, end_pos)
@@ -214,7 +227,7 @@ class CodeEditor(Generic[TSymbol], ABC):
symbol = self._find_unique_symbol(name_path, relative_file_path)
start_pos = symbol.get_body_start_position_or_raise()
end_pos = symbol.get_body_end_position_or_raise()
with self._edited_file_context(relative_file_path) as edited_file:
with self.edited_file_context(relative_file_path) as edited_file:
edited_file.delete_text_between_positions(start_pos, end_pos)
@abstractmethod
@@ -239,21 +252,24 @@ 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 set_contents(self, contents: str) -> None:
self._file_buffer.contents = 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"]:
@@ -267,35 +283,70 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]):
return lang_server.language_server.retrieve_full_file_content(relative_path)
def _find_unique_symbol(self, name_path: str, relative_file_path: str) -> LanguageServerSymbol:
symbol_candidates = self._symbol_retriever.find_by_name(name_path, within_relative_path=relative_file_path)
if len(symbol_candidates) == 0:
raise ValueError(f"No symbol with name {name_path} found in file {relative_file_path}")
if len(symbol_candidates) > 1:
raise ValueError(
f"Found multiple {len(symbol_candidates)} symbols with name {name_path} in file {relative_file_path}. "
"Their locations are: \n " + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2)
)
return symbol_candidates[0]
return self._symbol_retriever.find_unique(name_path, within_relative_path=relative_file_path)
def _apply_workspace_edit(self, workspace_edit: ls_types.WorkspaceEdit) -> list[str]:
"""
Apply a WorkspaceEdit by making the changes to files.
def _relative_path_from_uri(self, uri: str) -> str:
return os.path.relpath(PathUtils.uri_to_path(uri), self.project_root)
:param workspace_edit: The WorkspaceEdit containing the changes to apply
:return: List of relative file paths that were modified
"""
uri_to_edits = extract_text_edits(workspace_edit)
modified_relative_paths = []
class EditOperation(ABC):
@abstractmethod
def apply(self) -> None:
pass
# Handle the 'changes' format (URI -> list of TextEdits)
for uri, edits in uri_to_edits.items():
file_path = PathUtils.uri_to_path(uri)
relative_path = os.path.relpath(file_path, self._symbol_retriever.get_root_path())
modified_relative_paths.append(relative_path)
with self._edited_file_context(relative_path) as edited_file:
class EditOperationFileTextEdits(EditOperation):
def __init__(self, code_editor: "LanguageServerCodeEditor", file_uri: str, text_edits: list[ls_types.TextEdit]):
self._code_editor = code_editor
self._relative_path = code_editor._relative_path_from_uri(file_uri)
self._text_edits = text_edits
def apply(self) -> None:
with self._code_editor.edited_file_context(self._relative_path) as edited_file:
edited_file = cast(LanguageServerCodeEditor.EditedFile, edited_file)
edited_file.apply_text_edits(edits)
return modified_relative_paths
edited_file.apply_text_edits(self._text_edits)
class EditOperationRenameFile(EditOperation):
def __init__(self, code_editor: "LanguageServerCodeEditor", old_uri: str, new_uri: str):
self._code_editor = code_editor
self._old_relative_path = code_editor._relative_path_from_uri(old_uri)
self._new_relative_path = code_editor._relative_path_from_uri(new_uri)
def apply(self) -> None:
old_abs_path = os.path.join(self._code_editor.project_root, self._old_relative_path)
new_abs_path = os.path.join(self._code_editor.project_root, self._new_relative_path)
os.rename(old_abs_path, new_abs_path)
def _workspace_edit_to_edit_operations(self, workspace_edit: ls_types.WorkspaceEdit) -> list["LanguageServerCodeEditor.EditOperation"]:
operations: list[LanguageServerCodeEditor.EditOperation] = []
if "changes" in workspace_edit:
for uri, edits in workspace_edit["changes"].items():
operations.append(self.EditOperationFileTextEdits(self, uri, edits))
if "documentChanges" in workspace_edit:
for change in workspace_edit["documentChanges"]:
if "textDocument" in change and "edits" in change:
operations.append(self.EditOperationFileTextEdits(self, change["textDocument"]["uri"], change["edits"]))
elif "kind" in change:
if change["kind"] == "rename":
operations.append(self.EditOperationRenameFile(self, change["oldUri"], change["newUri"]))
else:
raise ValueError(f"Unhandled document change kind: {change}; Please report to Serena developers.")
else:
raise ValueError(f"Unhandled document change format: {change}; Please report to Serena developers.")
return operations
def _apply_workspace_edit(self, workspace_edit: ls_types.WorkspaceEdit) -> int:
"""
Applies a WorkspaceEdit
:param workspace_edit: the edit to apply
:return: number of edit operations applied
"""
operations = self._workspace_edit_to_edit_operations(workspace_edit)
for operation in operations:
operation.apply()
return len(operations)
def rename_symbol(self, name_path: str, relative_file_path: str, new_name: str) -> str:
symbol = self._find_unique_symbol(name_path, relative_file_path)
@@ -315,8 +366,14 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]):
f"Language server for {lang_server.language_id} returned no rename edits for symbol '{name_path}'. "
f"The symbol might not support renaming."
)
modified_files = self._apply_workspace_edit(rename_result)
msg = f"Successfully renamed '{name_path}' to '{new_name}' in {len(modified_files)} file(s)"
num_changes = self._apply_workspace_edit(rename_result)
if num_changes == 0:
raise ValueError(
f"Renaming symbol '{name_path}' to '{new_name}' resulted in no changes being applied; renaming may not be supported."
)
msg = f"Successfully renamed '{name_path}' to '{new_name}' ({num_changes} changes applied)"
return msg
@@ -327,6 +384,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:
@@ -335,6 +393,9 @@ class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]):
def get_contents(self) -> str:
return self._content
def set_contents(self, contents: str) -> None:
self._content = contents
def delete_text_between_positions(self, start_pos: PositionInFile, end_pos: PositionInFile) -> None:
self._content, _ = TextUtils.delete_text_between_positions(
self._content, start_pos.line, start_pos.col, end_pos.line, end_pos.col
@@ -347,6 +408,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)
+17 -42
View File
@@ -4,7 +4,6 @@ Context and Mode configuration loader
import os
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Self
@@ -12,7 +11,7 @@ import yaml
from sensai.util import logging
from sensai.util.string import ToStringMixin
from serena.config.serena_config import ToolInclusionDefinition
from serena.config.serena_config import SerenaPaths, ToolInclusionDefinition
from serena.constants import (
DEFAULT_CONTEXT,
DEFAULT_MODES,
@@ -20,8 +19,6 @@ from serena.constants import (
SERENA_FILE_ENCODING,
SERENAS_OWN_CONTEXT_YAMLS_DIR,
SERENAS_OWN_MODE_YAMLS_DIR,
USER_CONTEXT_YAMLS_DIR,
USER_MODE_YAMLS_DIR,
)
if TYPE_CHECKING:
@@ -66,14 +63,14 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
def get_path(cls, name: str) -> str:
"""Get the path to the YAML file for a mode."""
fname = f"{name}.yml"
custom_mode_path = os.path.join(USER_MODE_YAMLS_DIR, fname)
custom_mode_path = os.path.join(SerenaPaths().user_modes_dir, fname)
if os.path.exists(custom_mode_path):
return custom_mode_path
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 {USER_MODE_YAMLS_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
@@ -103,7 +100,7 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
@classmethod
def list_custom_mode_names(cls) -> list[str]:
"""Names of all custom modes defined by the user."""
return [f.stem for f in Path(USER_MODE_YAMLS_DIR).glob("*.yml")]
return [f.stem for f in Path(SerenaPaths().user_modes_dir).glob("*.yml")]
@classmethod
def load_default_modes(cls) -> list[Self]:
@@ -167,14 +164,14 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin):
def get_path(cls, name: str) -> str:
"""Get the path to the YAML file for a context."""
fname = f"{name}.yml"
custom_context_path = os.path.join(USER_CONTEXT_YAMLS_DIR, fname)
custom_context_path = os.path.join(SerenaPaths().user_contexts_dir, fname)
if os.path.exists(custom_context_path):
return custom_context_path
own_yaml_path = os.path.join(SERENAS_OWN_CONTEXT_YAMLS_DIR, fname)
if not os.path.exists(own_yaml_path):
raise FileNotFoundError(
f"Context {name} not found in {USER_CONTEXT_YAMLS_DIR} or in {SERENAS_OWN_CONTEXT_YAMLS_DIR}."
f"Context {name} not found in {SerenaPaths().user_contexts_dir} or in {SERENAS_OWN_CONTEXT_YAMLS_DIR}."
f"Available contexts:\n{cls.list_registered_context_names()}"
)
return own_yaml_path
@@ -182,6 +179,16 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin):
@classmethod
def from_name(cls, name: str) -> Self:
"""Load a registered Serena context."""
legacy_name_mapping = {
"ide-assistant": "claude-code",
}
if name in legacy_name_mapping:
log.warning(
f"Context name '{name}' is deprecated and has been renamed to '{legacy_name_mapping[name]}'. "
f"Please update your configuration; refer to the configuration guide for more details: "
"https://oraios.github.io/serena/02-usage/050_configuration.html#contexts"
)
name = legacy_name_mapping[name]
context_path = cls.get_path(name)
return cls.from_yaml(context_path)
@@ -202,7 +209,7 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin):
@classmethod
def list_custom_context_names(cls) -> list[str]:
"""Names of all custom contexts defined by the user."""
return [f.stem for f in Path(USER_CONTEXT_YAMLS_DIR).glob("*.yml")]
return [f.stem for f in Path(SerenaPaths().user_contexts_dir).glob("*.yml")]
@classmethod
def load_default(cls) -> Self:
@@ -214,35 +221,3 @@ class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin):
print(f"{self.name}:\n {self.description}")
if self.excluded_tools:
print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools)))
class RegisteredContext(Enum):
"""A registered context."""
IDE_ASSISTANT = "ide-assistant"
"""For Serena running within an assistant that already has basic tools, like Claude Code, Cline, Cursor, etc."""
DESKTOP_APP = "desktop-app"
"""For Serena running within Claude Desktop or a similar app which does not have built-in tools for code editing."""
AGENT = "agent"
"""For Serena running as a standalone agent, e.g. through agno."""
def load(self) -> SerenaAgentContext:
"""Load the context."""
return SerenaAgentContext.from_name(self.value)
class RegisteredMode(Enum):
"""A registered mode."""
INTERACTIVE = "interactive"
"""Interactive mode, for multi-turn interactions."""
EDITING = "editing"
"""Editing tools are activated."""
PLANNING = "planning"
"""Editing tools are deactivated."""
ONE_SHOT = "one-shot"
"""Non-interactive mode, where the goal is to finish a task autonomously."""
def load(self) -> SerenaAgentMode:
"""Load the mode."""
return SerenaAgentMode.from_name(self.value)
+118 -63
View File
@@ -9,6 +9,7 @@ from collections.abc import Sequence
from copy import deepcopy
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional, Self, TypeVar
@@ -25,10 +26,9 @@ from serena.constants import (
REPO_ROOT,
SERENA_CONFIG_TEMPLATE_FILE,
SERENA_FILE_ENCODING,
SERENA_MANAGED_DIR_IN_HOME,
SERENA_MANAGED_DIR_NAME,
)
from serena.util.general import load_yaml, save_yaml
from serena.util.general import get_dataclass_default, load_yaml, save_yaml
from serena.util.inspection import determine_programming_language_composition
from solidlsp.ls_config import Language
@@ -53,9 +53,32 @@ class SerenaPaths:
"""
def __init__(self) -> None:
self.user_config_dir: str = SERENA_MANAGED_DIR_IN_HOME
home_dir = os.getenv("SERENA_HOME")
if home_dir is None or home_dir.strip() == "":
home_dir = str(Path.home() / SERENA_MANAGED_DIR_NAME)
else:
home_dir = home_dir.strip()
self.serena_user_home_dir: str = home_dir
"""
the path to the user's Serena configuration directory, which is typically ~/.serena
the path to the Serena home directory, where the user's configuration/data is stored.
This is ~/.serena by default, but it can be overridden via the SERENA_HOME environment variable.
"""
self.user_prompt_templates_dir: str = os.path.join(self.serena_user_home_dir, "prompt_templates")
"""
directory containing prompt templates defined by the user.
Prompts defined by the user take precedence over Serena's built-in prompt templates.
"""
self.user_contexts_dir: str = os.path.join(self.serena_user_home_dir, "contexts")
"""
directory containing contexts defined by the user.
If a name of a context matches a name of a context in SERENAS_OWN_CONTEXT_YAMLS_DIR,
the user context will override the default context definition.
"""
self.user_modes_dir: str = os.path.join(self.serena_user_home_dir, "modes")
"""
directory containing modes defined by the user.
If a name of a mode matches a name of a mode in SERENAS_OWN_MODES_YAML_DIR,
the user mode will override the default mode definition.
"""
def get_next_log_file_path(self, prefix: str) -> str:
@@ -63,7 +86,7 @@ class SerenaPaths:
:param prefix: the filename prefix indicating the type of the log file
:return: the full path to the log file to use
"""
log_dir = os.path.join(self.user_config_dir, "logs", datetime.now().strftime("%Y-%m-%d"))
log_dir = os.path.join(self.serena_user_home_dir, "logs", datetime.now().strftime("%Y-%m-%d"))
os.makedirs(log_dir, exist_ok=True)
return os.path.join(log_dir, prefix + "_" + datetime_tag() + ".txt")
@@ -108,17 +131,24 @@ def get_serena_managed_in_project_dir(project_root: str | Path) -> str:
return os.path.join(project_root, SERENA_MANAGED_DIR_NAME)
def is_running_in_docker() -> bool:
"""Check if we're running inside a Docker container."""
# Check for Docker-specific files
if os.path.exists("/.dockerenv"):
return True
# Check cgroup for docker references
try:
with open("/proc/self/cgroup") as f:
return "docker" in f.read()
except FileNotFoundError:
return False
class LanguageBackend(Enum):
LSP = "LSP"
"""
Use the language server protocol (LSP), spawning freely available language servers
via the SolidLSP library that is part of Serena
"""
JETBRAINS = "JetBrains"
"""
Use the Serena plugin in your JetBrains IDE.
(requires the plugin to be installed and the project being worked on to be open in your IDE)
"""
@staticmethod
def from_str(backend_str: str) -> "LanguageBackend":
for backend in LanguageBackend:
if backend.value.lower() == backend_str.lower():
return backend
raise ValueError(f"Unknown language backend '{backend_str}': valid values are {[b.value for b in LanguageBackend]}")
@dataclass(kw_only=True)
@@ -160,10 +190,13 @@ class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
if not project_root.exists():
raise FileNotFoundError(f"Project root not found: {project_root}")
with LogTime("Project configuration auto-generation", logger=log):
log.info("Project root: %s", project_root)
project_name = project_name or project_root.name
if languages is None:
# determine languages automatically
log.info("Determining programming languages used in the project")
language_composition = determine_programming_language_composition(str(project_root))
log.info("Language composition: %s", language_composition)
if len(language_composition) == 0:
language_values = ", ".join([lang.value for lang in Language])
raise ValueError(
@@ -176,15 +209,18 @@ class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
f"Read the documentation for more information."
)
# sort languages by number of files found
languages_and_percentages = sorted(language_composition.items(), key=lambda item: item[1], reverse=True)
languages_and_percentages = sorted(
language_composition.items(), key=lambda item: (item[1], item[0].get_priority()), reverse=True
)
# find the language with the highest percentage and enable it
top_language_pair = languages_and_percentages[0]
other_language_pairs = languages_and_percentages[1:]
languages_to_use = [top_language_pair[0]]
languages_to_use: list[str] = [top_language_pair[0].value]
# if in interactive mode, ask the user which other languages to enable
if len(other_language_pairs) > 0 and interactive:
print(
"Detected and enabled main language '%s' (%.2f%% of source files)." % (top_language_pair[0], top_language_pair[1])
"Detected and enabled main language '%s' (%.2f%% of source files)."
% (top_language_pair[0].value, top_language_pair[1])
)
print(f"Additionally detected {len(other_language_pairs)} other language(s).\n")
print("Note: Enable only languages you need symbolic retrieval/editing capabilities for.")
@@ -192,17 +228,20 @@ class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
print(" system-level installations/configuration (see Serena documentation).")
print("\nWhich additional languages do you want to enable?")
for lang, perc in other_language_pairs:
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang, perc), default=False)
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang.value, perc), default=False)
if enable:
languages_to_use.append(lang)
languages_to_use.append(lang.value)
print()
log.info("Using languages: %s", languages_to_use)
else:
languages_to_use = [lang.value for lang in languages]
config_with_comments = cls.load_commented_map(PROJECT_TEMPLATE_FILE)
config_with_comments["project_name"] = project_name
config_with_comments["languages"] = languages_to_use
if save_to_disk:
save_yaml(cls.path_to_project_yml(project_root), config_with_comments, preserve_comments=True)
project_yml_path = cls.path_to_project_yml(project_root)
log.info("Saving project configuration to %s", project_yml_path)
save_yaml(project_yml_path, config_with_comments, preserve_comments=True)
return cls._from_dict(config_with_comments)
@classmethod
@@ -366,6 +405,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
trace_lsp_communication: bool = False
web_dashboard: bool = True
web_dashboard_open_on_launch: bool = True
web_dashboard_listen_address: str = "127.0.0.1"
tool_timeout: float = DEFAULT_TOOL_TIMEOUT
loaded_commented_yaml: CommentedMap | None = None
config_file_path: str | None = None
@@ -373,9 +413,10 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
the path to the configuration file to which updates of the configuration shall be saved;
if None, the configuration is not saved to disk
"""
jetbrains: bool = False
language_backend: LanguageBackend = LanguageBackend.LSP
"""
whether to apply JetBrains mode
the language backend to use for code understanding features
"""
token_count_estimator: str = RegisteredTokenCountEstimator.CHAR_COUNT.name
@@ -395,13 +436,12 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
"""Advanced configuration option allowing to configure language server implementation specific options, see SolidLSPSettings for more info."""
CONFIG_FILE = "serena_config.yml"
CONFIG_FILE_DOCKER = "serena_config.docker.yml" # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization
def _tostring_includes(self) -> list[str]:
return ["config_file_path"]
@classmethod
def generate_config_file(cls, config_file_path: str) -> None:
def _generate_config_file(cls, config_file_path: str) -> None:
"""
Generates a Serena configuration file at the specified path from the template file.
@@ -416,20 +456,17 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
"""
:return: the location where the Serena configuration file is stored/should be stored
"""
if is_running_in_docker():
return os.path.join(REPO_ROOT, cls.CONFIG_FILE_DOCKER)
else:
config_path = os.path.join(SERENA_MANAGED_DIR_IN_HOME, cls.CONFIG_FILE)
config_path = os.path.join(SerenaPaths().serena_user_home_dir, cls.CONFIG_FILE)
# if the config file does not exist, check if we can migrate it from the old location
if not os.path.exists(config_path):
old_config_path = os.path.join(REPO_ROOT, cls.CONFIG_FILE)
if os.path.exists(old_config_path):
log.info(f"Moving Serena configuration file from {old_config_path} to {config_path}")
os.makedirs(os.path.dirname(config_path), exist_ok=True)
shutil.move(old_config_path, config_path)
# if the config file does not exist, check if we can migrate it from the old location
if not os.path.exists(config_path):
old_config_path = os.path.join(REPO_ROOT, cls.CONFIG_FILE)
if os.path.exists(old_config_path):
log.info(f"Moving Serena configuration file from {old_config_path} to {config_path}")
os.makedirs(os.path.dirname(config_path), exist_ok=True)
shutil.move(old_config_path, config_path)
return config_path
return config_path
@classmethod
def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig":
@@ -443,7 +480,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
if not generate_if_missing:
raise FileNotFoundError(f"Serena configuration file not found: {config_file_path}")
log.info(f"Serena configuration file not found at {config_file_path}, autogenerating...")
cls.generate_config_file(config_file_path)
cls._generate_config_file(config_file_path)
# load the configuration
log.info(f"Loading Serena configuration from {config_file_path}")
@@ -461,7 +498,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
# load list of known projects
instance.projects = []
num_project_migrations = 0
num_migrations = 0
for path in loaded_commented_yaml["projects"]:
path = Path(path).resolve()
if not path.exists() or (path.is_dir() and not (path / ProjectConfig.rel_path_to_project_yml()).exists()):
@@ -471,7 +508,7 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
path = cls._migrate_out_of_project_config_file(path)
if path is None:
continue
num_project_migrations += 1
num_migrations += 1
project_config = ProjectConfig.load(path)
project = RegisteredProject(
project_root=str(path),
@@ -479,30 +516,42 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
)
instance.projects.append(project)
# set other configuration parameters
if is_running_in_docker():
instance.gui_log_window_enabled = False # not supported in Docker
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))
# determine language backend
language_backend = get_dataclass_default(SerenaConfig, "language_backend")
if "language_backend" in loaded_commented_yaml:
backend_str = loaded_commented_yaml["language_backend"]
language_backend = LanguageBackend.from_str(backend_str)
else:
instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False)
# backward compatibility (migrate Boolean field "jetbrains")
if "jetbrains" in loaded_commented_yaml:
num_migrations += 1
if loaded_commented_yaml["jetbrains"]:
language_backend = LanguageBackend.JETBRAINS
del loaded_commented_yaml["jetbrains"]
instance.language_backend = language_backend
# set other configuration parameters (primitive types)
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 = loaded_commented_yaml.get("web_dashboard", True)
instance.web_dashboard_open_on_launch = loaded_commented_yaml.get("web_dashboard_open_on_launch", True)
instance.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT)
instance.trace_lsp_communication = loaded_commented_yaml.get("trace_lsp_communication", False)
instance.excluded_tools = loaded_commented_yaml.get("excluded_tools", [])
instance.included_optional_tools = loaded_commented_yaml.get("included_optional_tools", [])
instance.jetbrains = loaded_commented_yaml.get("jetbrains", False)
instance.token_count_estimator = loaded_commented_yaml.get(
"token_count_estimator", RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name
)
instance.default_max_tool_answer_chars = loaded_commented_yaml.get("default_max_tool_answer_chars", 150_000)
instance.ls_specific_settings = loaded_commented_yaml.get("ls_specific_settings", {})
instance.web_dashboard = get_value_or_default("web_dashboard")
instance.web_dashboard_open_on_launch = get_value_or_default("web_dashboard_open_on_launch")
instance.tool_timeout = get_value_or_default("tool_timeout")
instance.trace_lsp_communication = get_value_or_default("trace_lsp_communication")
instance.excluded_tools = get_value_or_default("excluded_tools")
instance.included_optional_tools = get_value_or_default("included_optional_tools")
instance.token_count_estimator = get_value_or_default("token_count_estimator")
instance.default_max_tool_answer_chars = get_value_or_default("default_max_tool_answer_chars")
instance.ls_specific_settings = get_value_or_default("ls_specific_settings")
# re-save the configuration file if any migrations were performed
if num_project_migrations > 0:
log.info(
f"Migrated {num_project_migrations} project configurations from legacy format to in-project configuration; re-saving configuration"
)
if num_migrations > 0:
log.info("Legacy configuration was migrated; re-saving configuration file")
instance.save()
return instance
@@ -605,9 +654,15 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
"""
if self.config_file_path is None:
return
assert self.loaded_commented_yaml is not None, "Cannot save configuration without loaded YAML"
loaded_original_yaml = deepcopy(self.loaded_commented_yaml)
# projects are unique absolute paths
# we also canonicalize them before saving
# convert project objects into list of paths
loaded_original_yaml["projects"] = sorted({str(project.project_root) for project in self.projects})
# convert language backend to string
loaded_original_yaml["language_backend"] = self.language_backend.value
save_yaml(self.config_file_path, loaded_original_yaml, preserve_comments=True)
-8
View File
@@ -4,22 +4,14 @@ _repo_root_path = Path(__file__).parent.parent.parent.resolve()
_serena_pkg_path = Path(__file__).parent.resolve()
SERENA_MANAGED_DIR_NAME = ".serena"
_serena_in_home_managed_dir = Path.home() / ".serena"
SERENA_MANAGED_DIR_IN_HOME = str(_serena_in_home_managed_dir)
# TODO: Path-related constants should be moved to SerenaPaths; don't add further constants here.
REPO_ROOT = str(_repo_root_path)
PROMPT_TEMPLATES_DIR_INTERNAL = str(_serena_pkg_path / "resources" / "config" / "prompt_templates")
PROMPT_TEMPLATES_DIR_IN_USER_HOME = str(_serena_in_home_managed_dir / "prompt_templates")
SERENAS_OWN_CONTEXT_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "contexts")
"""The contexts that are shipped with the Serena package, i.e. the default contexts."""
USER_CONTEXT_YAMLS_DIR = str(_serena_in_home_managed_dir / "contexts")
"""Contexts defined by the user. If a name of a context matches a name of a context in SERENAS_OWN_CONTEXT_YAMLS_DIR, the user context will override the default one."""
SERENAS_OWN_MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "modes")
"""The modes that are shipped with the Serena package, i.e. the default modes."""
USER_MODE_YAMLS_DIR = str(_serena_in_home_managed_dir / "modes")
"""Modes defined by the user. If a name of a mode matches a name of a mode in SERENAS_OWN_MODE_YAMLS_DIR, the user mode will override the default one."""
INTERNAL_MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "internal_modes")
"""Internal modes, never overridden by user modes."""
SERENA_DASHBOARD_DIR = str(_serena_pkg_path / "resources" / "dashboard")
+6 -4
View File
@@ -9,6 +9,7 @@ from pydantic import BaseModel
from sensai.util import logging
from serena.analytics import ToolUsageStats
from serena.config.serena_config import LanguageBackend
from serena.constants import SERENA_DASHBOARD_DIR
from serena.task_executor import TaskExecutor
from serena.util.logging import MemoryLogHandler
@@ -443,7 +444,7 @@ class SerenaDashboardAPI:
available_modes=available_modes,
available_contexts=available_contexts,
available_memories=available_memories,
jetbrains_mode=self._agent.serena_config.jetbrains,
jetbrains_mode=self._agent.serena_config.language_backend == LanguageBackend.JETBRAINS,
languages=languages,
encoding=encoding,
)
@@ -560,7 +561,7 @@ class SerenaDashboardAPI:
raise RuntimeError(f"No free ports found starting from {start_port}")
def run(self, host: str = "0.0.0.0", port: int = 0x5EDA) -> int:
def run(self, host: str, port: int) -> int:
"""
Runs the dashboard on the given host and port and returns the port number.
"""
@@ -572,8 +573,9 @@ class SerenaDashboardAPI:
self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True)
return port
def run_in_thread(self) -> tuple[threading.Thread, int]:
def run_in_thread(self, host: str) -> tuple[threading.Thread, int]:
port = self._find_first_free_port(0x5EDA)
thread = threading.Thread(target=lambda: self.run(port=port), daemon=True)
log.info("Starting dashboard (listen_address=%s, port=%d)", host, port)
thread = threading.Thread(target=lambda: self.run(host=host, port=port), daemon=True)
thread.start()
return thread, port
+8 -11
View File
@@ -4,10 +4,10 @@ from collections.abc import Iterator
from sensai.util.logging import LogTime
from serena.constants import SERENA_MANAGED_DIR_IN_HOME, SERENA_MANAGED_DIR_NAME
from serena.config.serena_config import SerenaPaths
from serena.constants import SERENA_MANAGED_DIR_NAME
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -21,7 +21,6 @@ class LanguageServerFactory:
ignored_patterns: list[str],
ls_timeout: float | None = None,
ls_specific_settings: dict | None = None,
log_level: int = logging.INFO,
trace_lsp_communication: bool = False,
):
self.project_root = project_root
@@ -29,7 +28,6 @@ class LanguageServerFactory:
self.ignored_patterns = ignored_patterns
self.ls_timeout = ls_timeout
self.ls_specific_settings = ls_specific_settings
self.log_level = log_level
self.trace_lsp_communication = trace_lsp_communication
def create_language_server(self, language: Language) -> SolidLanguageServer:
@@ -39,16 +37,14 @@ class LanguageServerFactory:
trace_lsp_communication=self.trace_lsp_communication,
encoding=self.encoding,
)
ls_logger = LanguageServerLogger(log_level=self.log_level)
log.info(f"Creating language server instance for {self.project_root}, language={language}.")
return SolidLanguageServer.create(
ls_config,
ls_logger,
self.project_root,
timeout=self.ls_timeout,
solidlsp_settings=SolidLSPSettings(
solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME,
solidlsp_dir=SerenaPaths().serena_user_home_dir,
project_data_relative_path=SERENA_MANAGED_DIR_NAME,
ls_specific_settings=self.ls_specific_settings or {},
),
@@ -187,25 +183,26 @@ class LanguageServerManager:
self._stop_language_server(ls, save_cache=save_cache)
@staticmethod
def _stop_language_server(ls: SolidLanguageServer, save_cache: bool = False) -> None:
def _stop_language_server(ls: SolidLanguageServer, save_cache: bool = False, timeout: float = 2.0) -> None:
if ls.is_running():
if save_cache:
ls.save_cache()
log.info(f"Stopping language server for language {ls.language} ...")
ls.stop()
ls.stop(shutdown_timeout=timeout)
def iter_language_servers(self) -> Iterator[SolidLanguageServer]:
for ls in self._language_servers.values():
yield self._ensure_functional_ls(ls)
def stop_all(self, save_cache: bool = False) -> None:
def stop_all(self, save_cache: bool = False, timeout: float = 2.0) -> None:
"""
Stops all managed language servers.
:param save_cache: whether to save the cache before stopping
:param timeout: timeout for shutdown of each language server
"""
for ls in self.iter_language_servers():
self._stop_language_server(ls, save_cache=save_cache)
self._stop_language_server(ls, save_cache=save_cache, timeout=timeout)
def save_all_caches(self) -> None:
"""
+9 -1
View File
@@ -13,6 +13,7 @@ import docstring_parser
from mcp.server.fastmcp import server
from mcp.server.fastmcp.server import FastMCP, Settings
from mcp.server.fastmcp.tools.base import Tool as MCPTool
from mcp.types import ToolAnnotations
from pydantic_settings import SettingsConfigDict
from sensai.util import logging
@@ -21,6 +22,7 @@ from serena.agent import (
SerenaConfig,
)
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
from serena.config.serena_config import LanguageBackend
from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, SERENA_LOG_FORMAT
from serena.tools import Tool
from serena.util.exception import show_fatal_exception_safe
@@ -219,6 +221,8 @@ class SerenaMCPFactory:
def execute_fn(**kwargs) -> str: # type: ignore
return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs)
annotations = ToolAnnotations(readOnlyHint=not tool.can_edit())
return MCPTool(
fn=execute_fn,
name=func_name,
@@ -227,7 +231,7 @@ class SerenaMCPFactory:
fn_metadata=func_arg_metadata,
is_async=is_async,
context_kwarg=None,
annotations=None,
annotations=annotations,
title=None,
)
@@ -258,6 +262,7 @@ class SerenaMCPFactory:
host: str = "0.0.0.0",
port: int = 8000,
modes: Sequence[str] = DEFAULT_MODES,
language_backend: LanguageBackend | None = None,
enable_web_dashboard: bool | None = None,
enable_gui_log_window: bool | None = None,
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
@@ -270,6 +275,7 @@ class SerenaMCPFactory:
:param host: The host to bind to
:param port: The port to bind to
:param modes: List of mode names or paths to mode files
:param language_backend: the language backend to use, overriding the configuration setting.
:param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration.
:param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then.
If not specified, will take the value from the serena configuration.
@@ -293,6 +299,8 @@ class SerenaMCPFactory:
config.trace_lsp_communication = trace_lsp_communication
if tool_timeout is not None:
config.tool_timeout = tool_timeout
if language_backend is not None:
config.language_backend = language_backend
modes_instances = [SerenaAgentMode.load(mode) for mode in modes]
self.agent = self._create_serena_agent(config, modes_instances)
+2 -3
View File
@@ -388,7 +388,6 @@ class Project(ToStringMixin):
ignored_patterns=self._ignored_patterns,
ls_timeout=ls_timeout,
ls_specific_settings=ls_specific_settings,
log_level=log_level,
trace_lsp_communication=trace_lsp_communication,
)
self.language_server_manager = LanguageServerManager.from_languages(self.project_config.languages, factory)
@@ -439,7 +438,7 @@ class Project(ToStringMixin):
log.info("Removing and stopping the language server for language %s ...", language.value)
self.language_server_manager.remove_language_server(language)
def shutdown(self) -> None:
def shutdown(self, timeout: float = 2.0) -> None:
if self.language_server_manager is not None:
self.language_server_manager.stop_all(save_cache=True)
self.language_server_manager.stop_all(save_cache=True, timeout=timeout)
self.language_server_manager = None
+5 -3
View File
@@ -1,6 +1,7 @@
import os
from serena.constants import PROMPT_TEMPLATES_DIR_IN_USER_HOME, PROMPT_TEMPLATES_DIR_INTERNAL
from serena.config.serena_config import SerenaPaths
from serena.constants import PROMPT_TEMPLATES_DIR_INTERNAL
from serena.generated.generated_prompt_factory import PromptFactory
@@ -10,5 +11,6 @@ class SerenaPromptFactory(PromptFactory):
"""
def __init__(self) -> None:
os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
super().__init__(prompts_dir=[PROMPT_TEMPLATES_DIR_IN_USER_HOME, PROMPT_TEMPLATES_DIR_INTERNAL])
user_templates_dir = SerenaPaths().user_prompt_templates_dir
os.makedirs(user_templates_dir, exist_ok=True)
super().__init__(prompts_dir=[user_templates_dir, PROMPT_TEMPLATES_DIR_INTERNAL])
@@ -1,7 +1,6 @@
description: All tools except InitialInstructionsTool for agent context
description: Agent context where the system prompt (initial instructions) are provided at startup
prompt: |
You are running in agent context where the system prompt is provided externally. You should use symbolic
tools when possible for code understanding and modification.
You are running in an agent context.
excluded_tools:
- initial_instructions
@@ -1,4 +1,4 @@
description: A configuration specific for chatgpt, which has a limit of 30 tools and requires short descriptions.
description: A configuration specific for ChatGPT, which has a limit of 30 tools and requires short descriptions.
prompt: |
You are running in desktop app context where the tools give you access to the code base as well as some
access to the file system, if configured. You interact with the user through a chat interface that is separated
@@ -0,0 +1,26 @@
description: Claude Code (CLI agent where file operations, basic edits, etc. are already covered; single project mode)
prompt: |
You are running in a CLI coding agent context where file operations, basic (line-based) edits and reads
as well as shell commands are handled by your own, internal tools.
If Serena's tools can be used to achieve your task, you should prioritize them.
In particular, it is important that you avoid reading entire source code files unless it is strictly necessary!
Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools.
For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool.
excluded_tools:
- create_text_file
- read_file
- execute_shell_command
- prepare_for_new_conversation
- replace_content
tool_description_overrides: {}
# whether to assume that Serena shall only work on a single project in this context (provided that a project is given
# when Serena is started).
# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's
# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal.
# Tools explicitly disabled by the project will not be available at all.
# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed.
single_project: true
+8 -15
View File
@@ -1,20 +1,13 @@
description: Non-symbolic editing tools and general shell tool are excluded
description: Codex Non-symbolic editing tools and general shell tool are excluded
prompt: |
You are running in IDE assistant context where file operations, basic (line-based) edits and reads,
and shell commands are handled by your own, internal tools.
The initial instructions and the current config inform you on which tools are available to you,
and how to use them.
Don't attempt to use any excluded tools, instead rely on your own internal tools
for achieving the basic file or shell operations.
You are running in the Codex IDE assistant mode, where file operations, basic (line-based) edits and reads
as well as shell commands are handled by your own, internal tools.
Don't attempt to use any excluded tools; instead, rely on your own internal tools for basic file or shell operations.
If serena's tools can be used for achieving your task,
you should prioritize them. In particular, it is important that you avoid reading entire source code files,
unless it is strictly necessary! Instead, for exploring and reading code in a token-efficient manner,
you should use serena's overview and symbolic search tools. The call of the read_file tool on an entire source code
file should only happen in exceptional cases, usually you should first explore the file (by itself or as part of exploring
the directory containing it) using the symbol_overview tool, and then make targeted reads using find_symbol and other symbolic tools.
For non-code files or for reads where you don't know the symbol's name path you can use the patterns searching tool,
using the read_file as a last resort.
If Serena's tools can be used to achieve your task, you should prioritize them.
In particular, it is important that you avoid reading entire source code files unless it is strictly necessary!
Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools.
For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool.
excluded_tools:
- create_text_file
@@ -1,15 +1,13 @@
description: All tools included for desktop app context
description: Desktop application context (chat application detached from code) where Serena's full toolset is provided
prompt: |
You are running in desktop app context where the tools give you access to the code base as well as some
access to the file system, if configured. You interact with the user through a chat interface that is separated
from the code base. As a consequence, if you are in interactive mode, your communication with the user should
You are running in a desktop application context.
Serena's tools give you access to the code base as well as some access to the file system (if enabled).
You interact with the user through a chat interface that is separated from the code base.
As a consequence, if you are in interactive mode, your communication with the user should
involve high-level thinking and planning as well as some summarization of any code edits that you make.
For viewing the code edits the user will view them in a separate code editor window, and the back-and-forth
between the chat and the code editor should be minimized as well as facilitated by you.
If complex changes have been made, advise the user on how to review them in the code editor.
If complex relationships that the user asked for should be visualized or explained, consider creating
a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering
options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions.
To view the code edits you make, the user will have switch to a separate application.
To illustrate complex relationships, consider creating diagrams in addition to your text-based communication
(depending on the options for text, html, mermaid diagrams, etc. that you are provided with in your initial instructions).
excluded_tools: []
included_optional_tools:
- switch_modes
@@ -1,34 +0,0 @@
description: Non-symbolic editing tools and general shell tool are excluded
prompt: |
You are running in IDE assistant context where file operations, basic (line-based) edits and reads,
and shell commands are handled by your own, internal tools.
The initial instructions and the current config inform you on which tools are available to you,
and how to use them.
Don't attempt to use any excluded tools, instead rely on your own internal tools
for achieving the basic file or shell operations.
If serena's tools can be used for achieving your task,
you should prioritize them. In particular, it is important that you avoid reading entire source code files,
unless it is strictly necessary! Instead, for exploring and reading code in a token-efficient manner,
you should use serena's overview and symbolic search tools.
Before reading a full file, it is usually best to first explore the file using the symbol_overview tool,
and then make targeted reads using find_symbol and other symbolic tools.
For non-code files or for reads where you don't know the symbol's name path, you can use the pattern searching tool,
and read full files only as a last resort.
excluded_tools:
- create_text_file
- read_file
- execute_shell_command
- prepare_for_new_conversation
- replace_content
tool_description_overrides: {}
# whether to assume that Serena shall only work on a single project in this context (provided that a project is given
# when Serena is started).
# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's
# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal.
# Tools explicitly disabled by the project will not be available at all.
# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed.
single_project: true
@@ -0,0 +1,25 @@
description: Generic IDE coding agent context (basic file operations and shell operations assumed to be covered; single project mode)
prompt: |
You are running in an IDE assistant context where file operations, basic (line-based) edits and reads,
and shell commands are handled by your own, internal tools.
If Serena's tools can be used to achieve your task, you should prioritize them.
In particular, it is important that you avoid reading entire source code files unless it is strictly necessary!
Instead, for exploring and reading code in a token-efficient manner, use Serena's overview and symbolic search tools.
For non-code files or for reads where you don't know the symbol's name path, you can use the pattern search tool.
excluded_tools:
- create_text_file
- read_file
- execute_shell_command
- prepare_for_new_conversation
tool_description_overrides: {}
# whether to assume that Serena shall only work on a single project in this context (provided that a project is given
# when Serena is started).
# If set to true and a project is provided at startup, the set of tools is limited to those required by the project's
# concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal.
# Tools explicitly disabled by the project will not be available at all.
# The `activate_project` tool is always disabled in this case, as project switching cannot be allowed.
single_project: true
+18 -26
View File
@@ -1,39 +1,31 @@
description: All tools, with detailed instructions for code editing
prompt: |
You are operating in editing mode. You can edit files with the provided tools
to implement the requested changes to the code base while adhering to the project's code style and patterns.
You are operating in editing mode. You can edit files with the provided tools.
You adhere to the project's code style and patterns.
Use symbolic editing tools whenever possible for precise code modifications.
If no editing task has yet been provided, wait for the user to provide one.
If no explicit editing task has yet been provided, wait for the user to provide one. Do not be overly eager.
When writing new code, think about where it belongs best. Don't generate new files if you don't plan on actually
integrating them into the codebase, instead use the editing tools to insert the code directly into the existing files in that case.
properly integrating them into the codebase.
You have two main approaches for editing code - editing at the symbol level and more file-based editing.
You have two main approaches for editing code: (a) editing at the symbol level and (b) file-based editing.
The symbol-based approach is appropriate if you need to adjust an entire symbol, e.g. a method, a class, a function, etc.
But it is not appropriate if you need to adjust just a few lines of code within a symbol.
It is not appropriate if you need to adjust just a few lines of code within a larger symbol.
Let us first discuss the symbol-based approach.
Symbols are identified by their name path and relative file path (description of the `find_symbol` tool).
You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file,
or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible
while still solving your task, meaning you only read the bodies when you need to, and after you have found the symbol you want to edit.
Before calling symbolic reading tools, you should have a basic understanding of the repository structure that you can get from memories
or by using the `list_dir` and `find_file` tools (or similar).
For example, if you are working with python code and already know that you need to read the body of the constructor of the class Foo, you can directly
use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit,
you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding
to read the desired methods with `include_body=True`.
In particular, keep in mind the description of the `replace_symbol_body` tool. If you want to add some new code at the end of the file, you should
use the `insert_after_symbol` tool with the last top-level symbol in the file. If you want to add an import, often a good strategy is to use
`insert_before_symbol` with the first top-level symbol in the file.
You can understand relationships between symbols by using the `find_referencing_symbols` tool. If not explicitly requested otherwise by a user,
you make sure that when you edit a symbol, it is either done in a backward-compatible way, or you find and adjust the references as needed.
The `find_referencing_symbols` tool will give you code snippets around the references, as well as symbolic information.
You will generally be able to use the info from the snippets and the regex-based approach to adjust the references as well.
You can assume that all symbol editing tools are reliable, so you don't need to verify the results if the tool returns without error.
**Symbolic editing**
Use symbolic retrieval tools to identify the symbols you need to edit.
If you need to replace the definition of a symbol, use the `replace_symbol_body` tool.
If you want to add some new code at the end of the file, use the `insert_after_symbol` tool with the last top-level symbol in the file.
Similarly, you can use `insert_before_symbol` with the first top-level symbol in the file to insert code at the beginning of a file.
You can understand relationships between symbols by using the `find_referencing_symbols` tool. If not explicitly requested otherwise by the user,
you make sure that when you edit a symbol, the change is either backward-compatible or you find and update all references as needed.
The `find_referencing_symbols` tool will give you code snippets around the references as well as symbolic information.
You can assume that all symbol editing tools are reliable, so you never need to verify the results if the tools return without error.
{% if 'replace_content' in available_tools %}
The `replace_content` tool allows you to perform regex-based replacements within files.
**File-based editing**
The `replace_content` tool allows you to perform regex-based replacements within files (as well as simple string replacements).
This is your primary tool for editing code whenever replacing or deleting a whole symbol would be a more expensive operation,
e.g. if you need to adjust just a few lines of code within a method.
You are extremely good at regex, so you never need to check whether the replacement produced the correct result.
@@ -6,6 +6,6 @@ prompt: |
Break down complex tasks into smaller steps and explain your thinking at each stage. When you're uncertain about
a decision, present options to the user and ask for guidance rather than making assumptions.
Focus on providing informative results for intermediate steps so the user can follow along with your progress and
Focus on providing informative results for intermediate steps, such that the user can follow along with your progress and
provide feedback as needed.
excluded_tools: []
@@ -1,7 +1,8 @@
# See Serena's documentation for more details on concept of modes.
description: Description of the mode, not used in the code.
prompt: Prompt that will form part of the message sent to the model when activating this mode.
description: Description of the mode (meta-information only)
prompt: |
Provide a prompt that will form part of the instructions sent to the model when this mode is activated.
# tools that are to be excluded by this mode
excluded_tools: []
# several tools are excluded by default and have to be explicitly included by the user
included_optional_tools: []
@@ -0,0 +1,11 @@
description: Excludes Serena's memory tools (and onboarding tools, which rely on memory)
prompt: |
Serena's memory tools are not available and the onboarding workflow is not being applied.
excluded_tools:
- write_memory
- read_memory
- delete_memory
- edit_memory
- list_memories
- onboarding
- check_onboarding_performed
@@ -1,8 +1,6 @@
description: Onboarding was already performed, exclude all onboarding tools
description: The onboarding process is not used (memories may have been created externally)
prompt: |
You have already performed onboarding, meaning that memories have already been created.
Read a list of available memories using the `list_memories` tool before proceeding with the task.
You don't need to read the actual memories, just remember that they exist and that you can read them later if they are relevant for your task.
The onboarding process is not applied.
excluded_tools:
- onboarding
- check_onboarding_performed
@@ -1,8 +1,7 @@
description: Only read-only tools, focused on analysis and planning
prompt: |
You are operating in planning mode. Your task is to analyze code but not write any code.
The user may ask you to assist in creating a comprehensive plan, or to learn something about the codebase -
either a small aspect of it or about the whole project.
The user may ask you to assist in creating a comprehensive plan, or to learn something about the codebase.
excluded_tools:
- create_text_file
- replace_symbol_body
@@ -3,66 +3,52 @@
# is encouraged (via the tool description) to call at the beginning of the conversation.
prompts:
system_prompt: |
You are a professional coding agent concerned with one particular codebase. You have
access to semantic coding tools on which you rely heavily for all your work, as well as collection of memory
files containing general information about the codebase. You operate in a resource-efficient and intelligent manner, always
keeping in mind to not read or generate content that is not needed for the task at hand.
You are a professional coding agent.
You have access to semantic coding tools upon which you rely heavily for all your work.
You operate in a resource-efficient and intelligent manner, always keeping in mind to not read or generate
content that is not needed for the task at hand.
When reading code in order to answer a user question or task, you should try reading only the necessary code.
Some tasks may require you to understand the architecture of large parts of the codebase, while for others,
it may be enough to read a small set of symbols or a single file.
Generally, you should avoid reading entire files unless it is absolutely necessary, instead relying on
intelligent step-by-step acquisition of information. {% if 'ToolMarkerSymbolicRead' in available_markers %}However, if you already read a file, it does not make
sense to further analyse it with the symbolic tools (except for the `find_referencing_symbols` tool),
as you already have the information.{% endif %}
You avoid reading entire files unless it is absolutely necessary, instead relying on intelligent step-by-step
acquisition of information. {% if 'ToolMarkerSymbolicRead' in available_markers %}Once you have read a full file, it does not make
sense to analyse it with the symbolic read tools; you already have the information.{% endif %}
I WILL BE SERIOUSLY UPSET IF YOU READ ENTIRE FILES WITHOUT NEED!
{% if 'ToolMarkerSymbolicRead' in available_markers %}
CONSIDER INSTEAD USING THE OVERVIEW TOOL AND SYMBOLIC TOOLS TO READ ONLY THE NECESSARY CODE FIRST!
I WILL BE EVEN MORE UPSET IF AFTER HAVING READ AN ENTIRE FILE YOU KEEP READING THE SAME CONTENT WITH THE SYMBOLIC TOOLS!
THE PURPOSE OF THE SYMBOLIC TOOLS IS TO HAVE TO READ LESS CODE, NOT READ THE SAME CONTENT MULTIPLE TIMES!
{% endif %}
You can achieve the intelligent reading of code by using the symbolic tools for getting an overview of symbols and
the relations between them, and then only reading the bodies of symbols that are necessary to answer the question
or complete the task.
You can achieve intelligent reading of code by using the symbolic tools for getting an overview of symbols and
the relations between them, and then only reading the bodies of symbols that are necessary to complete the task at hand.
You can use the standard tools like list_dir, find_file and search_for_pattern if you need to.
When tools allow it, you pass the `relative_path` parameter to restrict the search to a specific file or directory.
For some tools, `relative_path` can only be a file path, so make sure to properly read the tool descriptions.
Where appropriate, you pass the `relative_path` parameter to restrict the search to a specific file or directory.
{% if 'search_for_pattern' in available_tools %}
If you are unsure about a symbol's name or location{% if 'find_symbol' in available_tools %} (to the extent that substring_matching for the symbol name is not enough){% endif %}, you can use the `search_for_pattern` tool, which allows fast
and flexible search for patterns in the codebase.{% if 'ToolMarkerSymbolicRead' in available_markers %}This way you can first find candidates for symbols or files,
and flexible search for patterns in the codebase.{% if 'ToolMarkerSymbolicRead' in available_markers %} In this way, you can first find candidates for symbols or files,
and then proceed with the symbolic tools.{% endif %}
{% endif %}
{% if 'ToolMarkerSymbolicRead' in available_markers %}
Symbols are identified by their `name_path and `relative_path`, see the description of the `find_symbol` tool for more details
on how the `name_path` matches symbols.
You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file,
or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible
while still solving your task, meaning you only read the bodies when you need to, and after you have found the symbol you want to edit.
For example, if you are working with python code and already know that you need to read the body of the constructor of the class Foo, you can directly
use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit,
you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding
to read the desired methods with `include_body=True`
Symbols are identified by their `name_path` and `relative_path` (see the description of the `find_symbol` tool).
You can get information about the symbols in a file by using the `get_symbols_overview` tool or use the `find_symbol` to search.
You only read the bodies of symbols when you need to (e.g. if you want to fully understand or edit it).
For example, if you are working with Python code and already know that you need to read the body of the constructor of the class Foo, you can directly
use `find_symbol` with name path pattern `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit,
you can use `find_symbol` with name path pattern `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding
to read the desired methods with `include_body=True`.
You can understand relationships between symbols by using the `find_referencing_symbols` tool.
{% endif %}
{% if 'read_memory' in available_tools %}
You generally have access to memories and it may be useful for you to read them, but also only if they help you
to answer the question or complete the task. You can infer which memories are relevant to the current task by reading
the memory names and descriptions.
You generally have access to memories and it may be useful for you to read them.
You infer whether memories are relevant based on their names.
{% endif %}
The context and modes of operation are described below. From them you can infer how to interact with your user
and which tasks and kinds of interactions are expected of you.
The context and modes of operation are described below. These determine how to interact with your user
and which kinds of interactions are expected of you.
Context description:
{{ context_system_prompt }}
Modes descriptions:
{% for prompt in mode_system_prompts %}
- {{ prompt }}
{{ prompt }}
{% endfor %}
You have hereby read the 'Serena Instructions Manual' and do not need to read it again.
+18 -15
View File
@@ -97,11 +97,18 @@ body {
}
.logo-container {
height: var(--header-height);
order: 1;
flex-shrink: 0;
}
.header-sponsor {
.logo-container img {
height: calc(var(--header-height) - 20px);
margin-top: 10px;
display: block;
}
.header-banner {
position: relative;
top: 0;
left: 0;
@@ -109,11 +116,6 @@ body {
max-height: var(--header-height);
}
.logo-container img {
height: var(--header-height);
display: block;
}
.header-nav {
position: relative;
display: flex;
@@ -178,7 +180,7 @@ body {
margin: 5px 0;
}
.platinum-sponsor-slide {
.platinum-banner-slide {
position: absolute;
top: 0;
left: 0;
@@ -188,26 +190,26 @@ body {
transition: opacity 0.5s ease-in-out;
}
.platinum-sponsor-slide.active {
.platinum-banner-slide.active {
opacity: 1;
pointer-events: auto;
}
.sponsor-image {
.banner-image {
object-fit: contain;
border-radius: var(--border-radius);
}
.sponsor-border {
.banner-border {
border: 1px solid var(--border-color);
}
.platinum-sponsor-slide .sponsor-image {
.platinum-banner-slide .banner-image {
max-height: 100%;
object-fit: contain;
}
.gold-sponsors-section {
.gold-banners-section {
margin: 0 auto;
width: 100%;
position: relative;
@@ -223,18 +225,19 @@ body {
overflow: hidden;
}
.gold-sponsor-slide {
.gold-banner-slide {
position: absolute;
pointer-events: none;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
.gold-sponsor-slide.active {
.gold-banner-slide.active {
opacity: 1;
pointer-events: auto;
}
.gold-sponsor-slide .sponsor-image {
.gold-banner-slide .banner-image {
max-width: 100%;
object-fit: contain;
}
+50 -37
View File
@@ -40,56 +40,74 @@ class LogMessage {
};
}
class SponsorRotation {
function updateThemeAwareImage($img, theme=null) {
if (!theme) {
const isDarkMode = $('html').data("theme") == 'dark';
theme = isDarkMode ? 'dark' : 'light';
}
console.log("updating theme-aware image to theme:", theme);
const newSrc = $img.data('src-' + theme);
if (newSrc) {
$img.attr('src', newSrc);
}
}
class BannerRotation {
constructor() {
this.platinumIndex = 0;
this.goldIndex = 0;
this.platinumTimer = null;
this.goldTimer = null;
this.platinumInterval = 5000;
this.goldInterval = 5000;
this.platinumInterval = 15000;
this.goldInterval = 15000;
this.init();
}
init() {
let self = this;
this.loadSponsors(function() {
this.loadBanners(function() {
self.startPlatinumRotation();
self.startGoldRotation();
});
}
loadSponsors(onSuccess) {
loadBanners(onSuccess) {
$.ajax({
url: 'https://oraios-software.de/serena-sponsors/manifest.php',
url: 'https://oraios-software.de/serena-banners/manifest.php',
type: 'GET',
success: function (response) {
console.log('Sponsors loaded:', response);
console.log('Banners loaded:', response);
function fillSponsors($container, sponsors, className) {
$.each(sponsors, function (index, sponsor) {
let $img = $('<img src="' + sponsor.image + '" alt="' + sponsor.alt + '" class="sponsor-image">');
let $anchor = $('<a href="' + sponsor.link + '" target="_blank"></a>');
function fillBanners($container, banners, className) {
$.each(banners, function (index, banner) {
let $img = $('<img src="' + banner.image + '" alt="' + banner.alt + '" class="banner-image">');
if (banner.image_dark) {
$img.addClass('theme-aware-img');
$img.attr('data-src-dark', banner.image_dark);
$img.attr('data-src-light', banner.image);
updateThemeAwareImage($img);
}
let $anchor = $('<a href="' + banner.link + '" target="_blank"></a>');
$anchor.append($img);
let $sponsor = $('<div class="' + className + '-slide" data-sponsor="' + (index + 1) + '"></div>');
$sponsor.append($anchor);
let $banner = $('<div class="' + className + '-slide" data-banner="' + (index + 1) + '"></div>');
$banner.append($anchor);
if (index === 0) {
$sponsor.addClass('active');
$banner.addClass('active');
}
if (sponsor.border) {
$img.addClass('sponsor-border');
if (banner.border) {
$img.addClass('banner-border');
}
$container.append($sponsor);
$container.append($banner);
});
}
fillSponsors($('#gold-sponsors'), response.gold, 'gold-sponsor');
fillSponsors($('#platinum-sponsors'), response.platinum, 'platinum-sponsor');
fillBanners($('#gold-banners'), response.gold, 'gold-banner');
fillBanners($('#platinum-banners'), response.platinum, 'platinum-banner');
onSuccess();
},
error: function (xhr, status, error) {
console.error('Error loading sponsors:', error);
console.error('Error loading banners:', error);
}
});
}
@@ -109,7 +127,7 @@ class SponsorRotation {
}
rotatePlatinum(direction) {
const $slides = $('.platinum-sponsor-slide');
const $slides = $('.platinum-banner-slide');
const total = $slides.length;
if (total === 0) return;
@@ -133,7 +151,7 @@ class SponsorRotation {
}
rotateGold(direction) {
const $groups = $('.gold-sponsor-slide');
const $groups = $('.gold-banner-slide');
const total = $groups.length;
if (total === 0) return;
@@ -360,8 +378,8 @@ class Dashboard {
// Initialize theme
this.initializeTheme();
// Initialize sponsor rotation
//this.sponsorRotation = new SponsorRotation();
// Initialize banner rotation
this.bannerRotation = new BannerRotation();
// Add ESC key handler for closing modals
$(document).keydown(function (e) {
@@ -1463,6 +1481,9 @@ class Dashboard {
this.setTheme(newTheme);
}
/**
* @param theme {'light' | 'dark'}
*/
setTheme(theme) {
// Set the theme on the document element
document.documentElement.setAttribute('data-theme', theme);
@@ -1476,8 +1497,11 @@ class Dashboard {
this.$themeText.text('Dark');
}
// Update the logo based on theme
this.updateLogo(theme);
// Update theme-aware images
$(".theme-aware-img").each(function() {
const $img = $(this);
updateThemeAwareImage($img, theme);
});
// Save to localStorage
localStorage.setItem('serena-theme', theme);
@@ -1486,17 +1510,6 @@ class Dashboard {
this.updateChartsTheme();
}
updateLogo(theme) {
const logoElement = document.getElementById('serena-logo');
if (logoElement) {
if (theme === 'dark') {
logoElement.src = 'serena-logo-dark-mode.svg';
} else {
logoElement.src = 'serena-logo.svg';
}
}
}
updateChartsTheme() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const textColor = isDark ? '#ffffff' : '#000000';
+5 -5
View File
@@ -21,11 +21,11 @@
<div class="header-left">
<div class="logo-container">
<img id="serena-logo" src="serena-logs.png" alt="Serena">
<img id="serena-logo" class="theme-aware-img" src="serena-logo.svg" data-src-light="serena-logo.svg" data-src-dark="serena-logo-dark-mode.svg" alt="Serena">
</div>
<!-- Platinum Sponsors in Header -->
<div id="platinum-sponsors" class="header-sponsor">
<!-- Platinum Banners in Header -->
<div id="platinum-banners" class="header-banner">
</div>
</div>
@@ -134,8 +134,8 @@
</div>
</section>
<!-- Gold Sponsors -->
<section id="gold-sponsors" class="gold-sponsors-section">
<!-- Gold Banners -->
<section id="gold-banners" class="gold-banners-section">
</section>
</div>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

+13 -17
View File
@@ -1,3 +1,12 @@
language_backend: LSP
# the language backend to use for code understanding and manipulation.
# Possible values are:
# * LSP: Use the language server protocol (LSP), spawning freely available language servers
# via the SolidLSP library that is part of Serena.
# * JetBrains: Use the Serena plugin in your JetBrains IDE.
# (requires the plugin to be installed and the project being worked on to be open
# in your IDE).
gui_log_window: False
# whether to open a graphical window with Serena's logs.
# This is mainly supported on Windows and (partly) on Linux; not available on macOS.
@@ -20,6 +29,9 @@ web_dashboard: True
# shows Serena's current session logs - as an alternative to the GUI log window which
# is supported on all platforms.
web_dashboard_listen_address: 127.0.0.1
# The address where the web dashboard will listen on
web_dashboard_open_on_launch: True
# whether to open a browser window with the web dashboard when Serena starts (provided that web_dashboard
# is enabled). If set to False, you can still open the dashboard manually by navigating to
@@ -39,7 +51,6 @@ ls_specific_settings: {}
# (same entry as in project.yml) to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
#
tool_timeout: 240
# timeout, in seconds, after which tool executions are terminated
@@ -50,12 +61,6 @@ excluded_tools: []
included_optional_tools: []
# list of optional tools (which are disabled by default) to be included
jetbrains: False
# whether to enable JetBrains mode and use tools based on the Serena JetBrains IDE plugin
# instead of language server-based tools
# NOTE: The plugin is yet unreleased. This is for Serena developers only.
default_max_tool_answer_chars: 150000
# Used as default for tools where the apply method has a default maximal answer length.
# Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default
@@ -70,14 +75,5 @@ token_count_estimator: CHAR_COUNT
# data files on first run), or ANTHROPIC_CLAUDE_SONNET_4 which will use the (free of cost) Anthropic API to
# estimate the token count using the Claude Sonnet 4 tokenizer.
# MANAGED BY SERENA, KEEP AT THE BOTTOM OF THE YAML AND DON'T EDIT WITHOUT NEED
# The list of registered projects.
# To add a project, within a chat, simply ask Serena to "activate the project /path/to/project" or,
# if the project was previously added, "activate the project <project name>".
# By default, the project's name will be the name of the directory containing the project, but you may change it
# by editing the (auto-generated) project configuration file `/path/project/project/.serena/project.yml` file.
# If you want to maintain full control of the project configuration, create the project.yml file manually and then
# instruct Serena to activate the project by its path for first-time activation.
# NOTE: Make sure there are no name collisions in the names of registered projects.
# The list of registered project paths (updated automatically).
projects: []
+114 -67
View File
@@ -2,7 +2,7 @@ import json
import logging
import os
from abc import ABC, abstractmethod
from collections.abc import Iterator, Sequence
from collections.abc import Callable, Iterator, Sequence
from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING, Any, NotRequired, Self, TypedDict, Union
@@ -115,16 +115,31 @@ class Symbol(ToStringMixin, ABC):
class NamePathMatcher(ToStringMixin):
def __init__(self, name_path_expr: str, substring_matching: bool) -> None:
"""
Matches name paths of symbols against search patterns.
A name path is a path in the symbol tree *within a source file*.
For example, the method `my_method` defined in class `MyClass` would have the name path `MyClass/my_method`.
If a symbol is overloaded (e.g., in Java), a 0-based index is appended (e.g. "MyClass/my_method[0]") to
uniquely identify it.
A matching pattern can be:
* a simple name (e.g. "method"), which will match any symbol with that name
* a relative path like "class/method", which will match any symbol with that name path suffix
* an absolute name path "/class/method" (absolute name path), which requires an exact match of the full name path within the source file.
Append an index `[i]` to match a specific overload only, e.g. "MyClass/my_method[1]".
"""
def __init__(self, name_path_pattern: str, substring_matching: bool) -> None:
"""
:param name_path_expr: the name path expression to match against
:param name_path_pattern: the name path expression to match against
:param substring_matching: whether to use substring matching for the last segment
"""
assert name_path_expr, "name_path must not be empty"
self._expr = name_path_expr
assert name_path_pattern, "name_path must not be empty"
self._expr = name_path_pattern
self._substring_matching = substring_matching
self._is_absolute_pattern = name_path_expr.startswith(NAME_PATH_SEP)
self._pattern_parts = name_path_expr.lstrip(NAME_PATH_SEP).rstrip(NAME_PATH_SEP).split(NAME_PATH_SEP)
self._is_absolute_pattern = name_path_pattern.startswith(NAME_PATH_SEP)
self._pattern_parts = name_path_pattern.lstrip(NAME_PATH_SEP).rstrip(NAME_PATH_SEP).split(NAME_PATH_SEP)
# extract overload index "[idx]" if present at end of last part
self._overload_idx: int | None = None
@@ -194,6 +209,13 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
def symbol_kind(self) -> SymbolKind:
return self.symbol_root["kind"]
def is_low_level(self) -> bool:
"""
:return: whether the symbol is a low-level symbol (variable, constant, etc.), which typically represents data
rather than structure and therefore is not relevant in a high-level overview of the code.
"""
return self.symbol_kind >= SymbolKind.Variable.value
@property
def overload_idx(self) -> int | None:
return self.symbol_root.get("overload_idx")
@@ -323,34 +345,15 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
def find(
self,
name_path: str,
name_path_pattern: str,
substring_matching: bool = False,
include_kinds: Sequence[SymbolKind] | None = None,
exclude_kinds: Sequence[SymbolKind] | None = None,
) -> list[Self]:
"""
Find all symbols within the symbol's subtree that match the given `name_path`.
The matching behavior is determined by the structure of `name_path`, which can
either be a simple name (e.g. "method") or a name path like "class/method" (relative name path)
or "/class/method" (absolute name path).
Find all symbols within the symbol's subtree that match the given name path pattern.
Key aspects of the name path matching behavior:
- Trailing slashes in `name_path` play no role and are ignored.
- The name of the retrieved symbols will match (either exactly or as a substring)
the last segment of `name_path`, while other segments will restrict the search to symbols that
have a desired sequence of ancestors.
- If there is no starting or intermediate slash in `name_path`, there is no
restriction on the ancestor symbols. For example, passing `method` will match
against symbols with name paths like `method`, `class/method`, `class/nested_class/method`, etc.
- If `name_path` contains a `/` but doesn't start with a `/`, the matching is restricted to symbols
with the same ancestors as the last segment of `name_path`. For example, passing `class/method` will match against
`class/method` as well as `nested_class/class/method` but not `method`.
- If `name_path` starts with a `/`, it will be treated as an absolute name path pattern, meaning
that the first segment of it must match the first segment of the symbol's name path.
For example, passing `/class` will match only against top-level symbols like `class` but not against `nested_class/class`.
Passing `/class/method` will match against `class/method` but not `nested_class/class/method` or `method`.
:param name_path: the name path expression to match against
:param name_path_pattern: the name path pattern to match against (see class :class:`NamePathMatcher` for details)
:param substring_matching: whether to use substring matching (as opposed to exact matching)
of the last segment of `name_path` against the symbol name.
:param include_kinds: an optional sequence of ints representing the LSP symbol kind.
@@ -358,7 +361,7 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
:param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result.
"""
result = []
name_path_matcher = NamePathMatcher(name_path, substring_matching)
name_path_matcher = NamePathMatcher(name_path_pattern, substring_matching)
def should_include(s: "LanguageServerSymbol") -> bool:
if include_kinds is not None and s.symbol_kind not in include_kinds:
@@ -384,13 +387,14 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
include_body: bool = False,
include_children_body: bool = False,
include_relative_path: bool = True,
child_inclusion_predicate: Callable[[Self], bool] | None = None,
) -> dict[str, Any]:
"""
Converts the symbol to a dictionary.
:param kind: whether to include the kind of the symbol
:param location: whether to include the location of the symbol
:param depth: the depth of the symbol
:param depth: the depth up to which to include child symbols (0 = do not include children)
:param include_body: whether to include the body of the top-level symbol.
:param include_children_body: whether to also include the body of the children.
Note that the body of the children is part of the body of the parent symbol,
@@ -398,6 +402,8 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
and pass the children without passing the parent body to the LM.
:param include_relative_path: whether to include the relative path of the symbol in the location
entry. Relative paths of the symbol's children are always excluded.
:param child_inclusion_predicate: an optional predicate that decides whether a child symbol
should be included.
:return: a dictionary representation of the symbol
"""
result: dict[str, Any] = {"name": self.name, "name_path": self.get_name_path()}
@@ -415,14 +421,20 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
log.warning("Requested body for symbol, but it is not present. The symbol might have been loaded with include_body=False.")
result["body"] = self.body
def add_children(s: Self) -> list[dict[str, Any]]:
if child_inclusion_predicate is None:
child_inclusion_predicate = lambda s: True
def included_children(s: Self) -> list[dict[str, Any]]:
children = []
for c in s.iter_children():
if not child_inclusion_predicate(c):
continue
children.append(
c.to_dict(
kind=kind,
location=location,
depth=depth - 1,
child_inclusion_predicate=child_inclusion_predicate,
include_body=include_children_body,
include_children_body=include_children_body,
# all children have the same relative path as the parent
@@ -432,7 +444,9 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
return children
if depth > 0:
result["children"] = add_children(self)
children = included_children(self)
if len(children) > 0:
result["children"] = included_children(self)
return result
@@ -488,18 +502,17 @@ class LanguageServerSymbolRetriever:
def get_language_server(self, relative_path: str) -> SolidLanguageServer:
return self._ls_manager.get_language_server(relative_path)
def find_by_name(
def find(
self,
name_path: str,
name_path_pattern: str,
include_kinds: Sequence[SymbolKind] | None = None,
exclude_kinds: Sequence[SymbolKind] | None = None,
substring_matching: bool = False,
within_relative_path: str | None = None,
) -> list[LanguageServerSymbol]:
"""
Find all symbols that match the given name. See docstring of `Symbol.find` for more details.
The only parameter not mentioned there is `within_relative_path`, which can be used to restrict the search
to symbols within a specific file or directory.
Finds all symbols that match the given name path pattern (see class :class:`NamePathMatcher` for details),
optionally limited to a specific file and filtered by kind.
"""
symbols: list[LanguageServerSymbol] = []
for lang_server in self._ls_manager.iter_language_servers():
@@ -507,11 +520,44 @@ class LanguageServerSymbolRetriever:
for root in symbol_roots:
symbols.extend(
LanguageServerSymbol(root).find(
name_path, include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching
name_path_pattern, include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching
)
)
return symbols
def find_unique(
self,
name_path_pattern: str,
include_kinds: Sequence[SymbolKind] | None = None,
exclude_kinds: Sequence[SymbolKind] | None = None,
substring_matching: bool = False,
within_relative_path: str | None = None,
) -> LanguageServerSymbol:
symbol_candidates = self.find(
name_path_pattern,
include_kinds=include_kinds,
exclude_kinds=exclude_kinds,
substring_matching=substring_matching,
within_relative_path=within_relative_path,
)
if len(symbol_candidates) == 1:
return symbol_candidates[0]
elif len(symbol_candidates) == 0:
raise ValueError(f"No symbol matching '{name_path_pattern}' found")
else:
# There are multiple candidates.
# If only one of the candidates has the given pattern as its exact name path, return that one
exact_matches = [s for s in symbol_candidates if s.get_name_path() == name_path_pattern]
if len(exact_matches) == 1:
return exact_matches[0]
# otherwise, raise an error
include_rel_path = within_relative_path is not None
raise ValueError(
f"Found multiple {len(symbol_candidates)} symbols matching '{name_path_pattern}'. "
"They are: \n"
+ json.dumps([s.to_dict(kind=True, include_relative_path=include_rel_path) for s in symbol_candidates], indent=2)
)
def find_by_location(self, location: LanguageServerSymbolLocation) -> LanguageServerSymbol | None:
if location.relative_path is None:
return None
@@ -532,28 +578,17 @@ class LanguageServerSymbolRetriever:
exclude_kinds: Sequence[SymbolKind] | None = None,
) -> list[ReferenceInLanguageServerSymbol]:
"""
Find all symbols that reference the symbol with the given name.
If multiple symbols fit the name (e.g. for variables that are overwritten), will use the first one.
Find all symbols that reference the specified symbol, which is assumed to be unique.
:param name_path: the name path of the symbol to find
:param name_path: the name path of the symbol to find. (While this can be a matching pattern, it should
usually be the full path to ensure uniqueness.)
:param relative_file_path: the relative path of the file in which the referenced symbol is defined.
:param include_body: whether to include the body of all symbols in the result.
Not recommended, as the referencing symbols will often be files, and thus the bodies will be very long.
:param include_kinds: which kinds of symbols to include in the result.
:param exclude_kinds: which kinds of symbols to exclude from the result.
"""
symbol_candidates = self.find_by_name(name_path, substring_matching=False, within_relative_path=relative_file_path)
if len(symbol_candidates) == 0:
log.warning(f"No symbol with name {name_path} found in file {relative_file_path}")
return []
if len(symbol_candidates) > 1:
log.error(
f"Found {len(symbol_candidates)} symbols with name {name_path} in file {relative_file_path}."
f"May be an overwritten variable, in which case you can ignore this error. Proceeding with the first one. "
f"Found symbols for {name_path=} in {relative_file_path=}: \n"
f"{json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2)}"
)
symbol = symbol_candidates[0]
symbol = self.find_unique(name_path, substring_matching=False, within_relative_path=relative_file_path)
return self.find_referencing_symbols_by_location(
symbol.location, include_body=include_body, include_kinds=include_kinds, exclude_kinds=exclude_kinds
)
@@ -604,23 +639,35 @@ class LanguageServerSymbolRetriever:
return [ReferenceInLanguageServerSymbol.from_lsp_reference(r) for r in references]
@dataclass
class SymbolOverviewElement:
name_path: str
kind: int
@classmethod
def from_symbol(cls, symbol: LanguageServerSymbol) -> Self:
return cls(name_path=symbol.get_name_path(), kind=int(symbol.symbol_kind))
def get_symbol_overview(self, relative_path: str) -> dict[str, list[SymbolOverviewElement]]:
def get_symbol_overview(self, relative_path: str, depth: int = 0) -> dict[str, list[dict]]:
"""
:param relative_path: the path of the file or directory for which to get the symbol overview
:param depth: the depth up to which to include child symbols (0 = only top-level symbols)
:return: a mapping from file paths to lists of symbol dictionaries.
For the case where a file is passed, the mapping will contain a single entry.
"""
lang_server = self.get_language_server(relative_path)
path_to_unified_symbols = lang_server.request_overview(relative_path)
def child_inclusion_predicate(s: LanguageServerSymbol) -> bool:
return not s.is_low_level()
result = {}
for file_path, unified_symbols in path_to_unified_symbols.items():
# TODO: maybe include not just top-level symbols? We could filter by kind to exclude variables
# The language server methods would need to be adjusted for this.
result[file_path] = [self.SymbolOverviewElement.from_symbol(LanguageServerSymbol(s)) for s in unified_symbols]
symbols_in_file = []
for us in unified_symbols:
symbol = LanguageServerSymbol(us)
symbols_in_file.append(
symbol.to_dict(
depth=depth,
kind=True,
include_relative_path=False,
location=False,
child_inclusion_predicate=child_inclusion_predicate,
)
)
result[file_path] = symbols_in_file
return result
+3 -1
View File
@@ -14,7 +14,9 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
:param project: the name of a registered project to activate or a path to a project directory
"""
active_project = self.agent.activate_project_from_path_or_name(project)
return active_project.get_activation_message()
result = active_project.get_activation_message()
result += "\nIMPORTANT: If you have not yet read the 'Serena Instructions Manual', do it now before continuing!"
return result
class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional):
+54 -15
View File
@@ -6,10 +6,10 @@ File and file system-related tools, specifically for
* editing at the file level
"""
import json
import os
import re
from collections import defaultdict
from collections.abc import Callable
from fnmatch import fnmatch
from pathlib import Path
from typing import Literal
@@ -79,7 +79,7 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit):
answer = f"File created: {relative_path}."
if will_overwrite_existing:
answer += " Overwrote existing file."
return json.dumps(answer)
return answer
class ListDirTool(Tool):
@@ -106,7 +106,7 @@ class ListDirTool(Tool):
"project_root": self.get_project_root(),
"hint": "Check if the path is correct relative to the project root",
}
return json.dumps(error_info)
return self._to_json(error_info)
self.project.validate_relative_path(relative_path, require_not_ignored=skip_ignored_files)
@@ -118,7 +118,7 @@ class ListDirTool(Tool):
is_ignored_file=self.project.is_ignored_path if skip_ignored_files else None,
)
result = json.dumps({"dirs": dirs, "files": files})
result = self._to_json({"dirs": dirs, "files": files})
return self._limit_length(result, max_answer_chars)
@@ -154,7 +154,7 @@ class FindFileTool(Tool):
relative_to=self.get_project_root(),
)
result = json.dumps({"files": files})
result = self._to_json({"files": files})
return result
@@ -203,6 +203,45 @@ class ReplaceContentTool(Tool, ToolMarkerCanEdit):
relative_path, needle, repl, mode=mode, allow_multiple_occurrences=allow_multiple_occurrences, require_not_ignored=True
)
@staticmethod
def _create_replacement_function(regex_pattern: str, repl_template: str, regex_flags: int) -> Callable[[re.Match], str]:
"""
Creates a replacement function that validates for ambiguity and handles backreferences.
:param regex_pattern: The regex pattern being used for matching
:param repl_template: The replacement template with $!1, $!2, etc. for backreferences
:param regex_flags: The flags to use when searching (e.g., re.DOTALL | re.MULTILINE)
:return: A function suitable for use with re.sub() or re.subn()
"""
def validate_and_replace(match: re.Match) -> str:
matched_text = match.group(0)
# For multi-line match, check if the same pattern matches again within the already-matched text,
# rendering the match ambiguous. Typical pattern in the code:
# <start><other-stuff><start><stuff><end>
# When matching
# <start>.*?<end>
# this will match the entire span above, while only the suffix may have been intended.
# (See test case for a practical example.)
# To detect this, we check if the same pattern matches again within the matched text,
if "\n" in matched_text and re.search(regex_pattern, matched_text[1:], flags=regex_flags):
raise ValueError(
"Match is ambiguous: the search pattern matches multiple overlapping occurrences. "
"Please revise the search pattern to be more specific to avoid ambiguity."
)
# Handle backreferences: replace $!1, $!2, etc. with actual matched groups
def expand_backreference(m: re.Match) -> str:
group_num = int(m.group(1))
group_value = match.group(group_num)
return group_value if group_value is not None else m.group(0)
result = re.sub(r"\$!(\d+)", expand_backreference, repl_template)
return result
return validate_and_replace
def replace_content(
self,
relative_path: str,
@@ -217,7 +256,7 @@ class ReplaceContentTool(Tool, ToolMarkerCanEdit):
This function can be used internally by other tools.
"""
self.project.validate_relative_path(relative_path, require_not_ignored=require_not_ignored)
with EditedFileContext(relative_path, self.agent) as context:
with EditedFileContext(relative_path, self.create_code_editor()) as context:
original_content = context.get_original_content()
if mode == "literal":
@@ -227,19 +266,19 @@ class ReplaceContentTool(Tool, ToolMarkerCanEdit):
else:
raise ValueError(f"Invalid mode: '{mode}', expected 'literal' or 'regex'.")
# escape backslashes in repl
repl = repl.replace("\\", "\\\\")
# convert $!1, $!2, etc. to \1, \2, etc. for backreferences
repl = re.sub(r"\$!(\d+)", "\\\\1", repl)
regex_flags = re.DOTALL | re.MULTILINE
# create replacement function with validation and backreference handling
repl_fn = self._create_replacement_function(regex, repl, regex_flags=regex_flags)
# perform replacement
updated_content, n = re.subn(regex, repl, original_content, flags=re.DOTALL | re.MULTILINE)
updated_content, n = re.subn(regex, repl_fn, original_content, flags=regex_flags)
if n == 0:
return f"Error: No matches of search expression found in file '{relative_path}'."
raise ValueError(f"Error: No matches of search expression found in file '{relative_path}'.")
if not allow_multiple_occurrences and n > 1:
return (
f"Error: Expression matches {n} occurrences in file '{relative_path}'. "
raise ValueError(
f"Expression matches {n} occurrences in file '{relative_path}'. "
"Please revise the expression to be more specific or enable allow_multiple_occurrences if this is expected."
)
context.set_updated_content(updated_content)
@@ -440,5 +479,5 @@ class SearchForPatternTool(Tool):
for match in matches:
assert match.source_file_path is not None
file_to_matches[match.source_file_path].append(match.to_display_string())
result = json.dumps(file_to_matches)
result = self._to_json(file_to_matches)
return self._limit_length(result, max_answer_chars)
+26 -2
View File
@@ -41,9 +41,13 @@ class JetBrainsPluginClient(ToStringMixin):
"""
BASE_PORT = 0x5EA2
PLUGIN_REQUEST_TIMEOUT = 300
"""
the timeout used for request handling within the plugin (a constant in the plugin)
"""
last_port: int | None = None
def __init__(self, port: int, timeout: int = 30):
def __init__(self, port: int, timeout: int = PLUGIN_REQUEST_TIMEOUT):
self.base_url = f"http://127.0.0.1:{port}"
self.timeout = timeout
self.session = requests.Session()
@@ -133,7 +137,13 @@ class JetBrainsPluginClient(ToStringMixin):
return response["project_root"]
def find_symbol(
self, name_path: str, relative_path: str | None = None, include_body: bool = False, depth: int = 0, include_location: bool = False
self,
name_path: str,
relative_path: str | None = None,
include_body: bool = False,
depth: int = 0,
include_location: bool = False,
search_deps: bool = False,
) -> dict[str, Any]:
"""
Finds symbols by name.
@@ -142,6 +152,8 @@ class JetBrainsPluginClient(ToStringMixin):
:param relative_path: the relative path to which to restrict the search
:param include_body: whether to include symbol body content
:param depth: depth of children to include (0 = no children)
:param include_location: whether to include symbol location information
:param search_deps: whether to also search in dependencies
:return: Dictionary containing 'symbols' list with matching symbols
"""
@@ -151,6 +163,7 @@ class JetBrainsPluginClient(ToStringMixin):
"includeBody": include_body,
"depth": depth,
"includeLocation": include_location,
"searchDeps": search_deps,
}
return self._make_request("POST", "/findSymbol", request_data)
@@ -193,6 +206,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()
+8 -5
View File
@@ -1,5 +1,3 @@
import json
from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicRead
from serena.tools.jetbrains_plugin_client import JetBrainsPluginClient
@@ -15,6 +13,7 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
depth: int = 0,
relative_path: str | None = None,
include_body: bool = False,
search_deps: bool = False,
max_answer_chars: int = -1,
) -> str:
"""
@@ -44,18 +43,22 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
If you have some knowledge about the codebase, you should use this parameter, as it will significantly
speed up the search as well as reduce the number of results.
:param include_body: If True, include the symbol's source code. Use judiciously.
:param search_deps: If True, also search in project dependencies (e.g., libraries).
:param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned.
-1 means the default value from the config will be used.
:return: JSON string: a list of symbols (with locations) matching the name.
"""
if relative_path == ".":
relative_path = None
with JetBrainsPluginClient.from_project(self.project) as client:
response_dict = client.find_symbol(
name_path=name_path_pattern,
relative_path=relative_path,
depth=depth,
include_body=include_body,
search_deps=search_deps,
)
result = json.dumps(response_dict)
result = self._to_json(response_dict)
return self._limit_length(result, max_answer_chars)
@@ -86,7 +89,7 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark
name_path=name_path,
relative_path=relative_path,
)
result = json.dumps(response_dict)
result = self._to_json(response_dict)
return self._limit_length(result, max_answer_chars)
@@ -116,5 +119,5 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp
response_dict = client.get_symbols_overview(
relative_path=relative_path,
)
result = json.dumps(response_dict)
result = self._to_json(response_dict)
return self._limit_length(result, max_answer_chars)
+1 -2
View File
@@ -1,4 +1,3 @@
import json
from typing import Literal
from serena.tools import ReplaceContentTool, Tool
@@ -50,7 +49,7 @@ class ListMemoriesTool(Tool):
"""
List available memories. Any memory can be read using the `read_memory` tool.
"""
return json.dumps(self.memories_manager.list_memories())
return self._to_json(self.memories_manager.list_memories())
class DeleteMemoryTool(Tool):
+8 -8
View File
@@ -2,8 +2,6 @@
Language server-related tools
"""
import dataclasses
import json
import os
from collections.abc import Sequence
from copy import copy
@@ -51,13 +49,15 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead):
Gets an overview of the top-level symbols defined in a given file.
"""
def apply(self, relative_path: str, max_answer_chars: int = -1) -> str:
def apply(self, relative_path: str, depth: int = 0, max_answer_chars: int = -1) -> str:
"""
Use this tool to get a high-level understanding of the code symbols in a file.
This should be the first tool to call when you want to understand a new file, unless you already know
what you are looking for.
:param relative_path: the relative path to the file to get the overview of
:param depth: depth up to which descendants of top-level symbols shall be retrieved
(e.g. 1 retrieves immediate children). Default 0.
:param max_answer_chars: if the overview is longer than this number of characters,
no content will be returned. -1 means the default value from the config will be used.
Don't adjust unless there is really no other way to get the content required for the task.
@@ -72,8 +72,8 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead):
raise FileNotFoundError(f"File or directory {relative_path} does not exist in the project.")
if os.path.isdir(file_path):
raise ValueError(f"Expected a file path, but got a directory path: {relative_path}. ")
result = symbol_retriever.get_symbol_overview(relative_path)[relative_path]
result_json_str = json.dumps([dataclasses.asdict(i) for i in result])
result = symbol_retriever.get_symbol_overview(relative_path, depth=depth)[relative_path]
result_json_str = self._to_json(result)
return self._limit_length(result_json_str, max_answer_chars)
@@ -137,7 +137,7 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None
parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None
symbol_retriever = self.create_language_server_symbol_retriever()
symbols = symbol_retriever.find_by_name(
symbols = symbol_retriever.find(
name_path_pattern,
include_kinds=parsed_include_kinds,
exclude_kinds=parsed_exclude_kinds,
@@ -145,7 +145,7 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
within_relative_path=relative_path,
)
symbol_dicts = [_sanitize_symbol_dict(s.to_dict(kind=True, location=True, depth=depth, include_body=include_body)) for s in symbols]
result = json.dumps(symbol_dicts)
result = self._to_json(symbol_dicts)
return self._limit_length(result, max_answer_chars)
@@ -198,7 +198,7 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
)
ref_dict["content_around_reference"] = content_around_ref.to_display_string()
reference_dicts.append(ref_dict)
result = json.dumps(reference_dicts)
result = self._to_json(reference_dicts)
return self._limit_length(result, max_answer_chars)
+18 -19
View File
@@ -1,5 +1,5 @@
import inspect
import os
import json
from abc import ABC
from collections.abc import Iterable
from dataclasses import dataclass
@@ -295,6 +295,10 @@ class Tool(Component):
log.error(msg)
return msg
@staticmethod
def _to_json(x: Any) -> str:
return json.dumps(x, ensure_ascii=False)
class EditedFileContext:
"""
@@ -305,24 +309,23 @@ class EditedFileContext:
When exiting the context without an exception, the updated content will be written back to the file.
"""
def __init__(self, relative_path: str, agent: "SerenaAgent"):
self._project = agent.get_active_project()
assert self._project is not None
self._abs_path = os.path.join(self._project.project_root, relative_path)
if not os.path.isfile(self._abs_path):
raise FileNotFoundError(f"File {self._abs_path} does not exist.")
with open(self._abs_path, encoding=self._project.project_config.encoding) as f:
self._original_content = f.read()
self._updated_content: str | None = None
def __init__(self, relative_path: str, code_editor: "CodeEditor"):
self._relative_path = relative_path
self._code_editor = code_editor
self._edited_file: CodeEditor.EditedFile | None = None
self._edited_file_context: Any = None
def __enter__(self) -> Self:
self._edited_file_context = self._code_editor.edited_file_context(self._relative_path)
self._edited_file = self._edited_file_context.__enter__()
return self
def get_original_content(self) -> str:
"""
:return: the original content of the file before any modifications.
"""
return self._original_content
assert self._edited_file is not None
return self._edited_file.get_contents()
def set_updated_content(self, content: str) -> None:
"""
@@ -331,16 +334,12 @@ class EditedFileContext:
:param content: the updated content of the file
"""
self._updated_content = content
assert self._edited_file is not None
self._edited_file.set_contents(content)
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
if self._updated_content is not None and exc_type is None:
assert self._project is not None
with open(self._abs_path, "w", encoding=self._project.project_config.encoding) as f:
f.write(self._updated_content)
log.info(f"Updated content written to {self._abs_path}")
# Language servers should automatically detect the change and update its state accordingly.
# If they do not, we may have to add a call to notify it.
assert self._edited_file_context is not None
self._edited_file_context.__exit__(exc_type, exc_value, traceback)
@dataclass(kw_only=True)
+3 -3
View File
@@ -16,8 +16,7 @@ class CheckOnboardingPerformedTool(Tool):
def apply(self) -> str:
"""
Checks whether project onboarding was already performed.
You should always call this tool before beginning to actually work on the project/after activating a project,
but after calling the initial instructions tool.
You should always call this tool before beginning to actually work on the project/after activating a project.
"""
from .memory_tools import ListMemoriesTool
@@ -133,6 +132,7 @@ class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject):
def apply(self) -> str:
"""
Provides the 'Serena Instructions Manual', which contains essential information on how to use the Serena toolbox.
Call this tool if you have not yet read this very important manual!
IMPORTANT: If you have not yet read the manual, call this tool immediately after you are given your task by the user,
as it will critically inform you!
"""
return self.agent.create_system_prompt()
+21 -1
View File
@@ -1,5 +1,6 @@
import os
from typing import Literal, overload
from dataclasses import MISSING, Field
from typing import Any, Literal, cast, overload
from ruamel.yaml import YAML
from ruamel.yaml.comments import CommentedMap
@@ -32,3 +33,22 @@ def save_yaml(path: str, data: dict | CommentedMap, preserve_comments: bool = Fa
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding=SERENA_FILE_ENCODING) as f:
yaml.dump(data, f)
def get_dataclass_default(cls: type, field_name: str) -> Any:
"""
Gets the default value of a dataclass field.
:param cls: The dataclass type.
:param field_name: The name of the field.
:return: The default value of the field (either from default or default_factory).
"""
field = cast(Field, cls.__dataclass_fields__[field_name]) # type: ignore[attr-defined]
if field.default is not MISSING:
return field.default
if field.default_factory is not MISSING: # default_factory is a function
return field.default_factory()
raise AttributeError(f"{field_name} has no default")
+22
View File
@@ -0,0 +1,22 @@
import os
import platform
def system_has_usable_display() -> bool:
system = platform.system()
# macOS and native Windows: assume display is available for desktop usage
if system == "Darwin" or system == "Windows":
return True
# Other systems, assumed to be Unix-like (Linux, FreeBSD, Cygwin/MSYS, etc.):
# detect display availability since users may operate in CLI contexts
else:
# Check X11 or Wayland - if environment variables are set to non-empty values, assume display is usable
display = os.environ.get("DISPLAY", "")
wayland_display = os.environ.get("WAYLAND_DISPLAY", "")
if display or wayland_display:
return True
return False
+7 -7
View File
@@ -19,13 +19,13 @@ def iter_subclasses(cls: type[T], recursive: bool = True) -> Generator[type[T],
yield from iter_subclasses(subclass, recursive)
def determine_programming_language_composition(repo_path: str) -> dict[str, float]:
def determine_programming_language_composition(repo_path: str) -> dict[Language, float]:
"""
Determine the programming language composition of a repository.
:param repo_path: Path to the repository to analyze
:return: Dictionary mapping language names to percentages of files matching each language
:return: Dictionary mapping languages to percentages of files matching each language
"""
all_files = find_all_non_ignored_files(repo_path)
@@ -33,7 +33,7 @@ def determine_programming_language_composition(repo_path: str) -> dict[str, floa
return {}
# Count files for each language
language_counts: dict[str, int] = {}
language_counts: dict[Language, int] = {}
total_files = len(all_files)
for language in Language.iter_all(include_experimental=False):
@@ -47,12 +47,12 @@ def determine_programming_language_composition(repo_path: str) -> dict[str, floa
count += 1
if count > 0:
language_counts[str(language)] = count
language_counts[language] = count
# Convert counts to percentages
language_percentages: dict[str, float] = {}
for language_name, count in language_counts.items():
language_percentages: dict[Language, float] = {}
for language, count in language_counts.items():
percentage = (count / total_files) * 100
language_percentages[language_name] = round(percentage, 2)
language_percentages[language] = round(percentage, 2)
return language_percentages
+42
View File
@@ -1,11 +1,14 @@
import queue
import threading
from collections.abc import Callable
from typing import Optional
from sensai.util import logging
from serena.constants import SERENA_LOG_FORMAT
lg = logging
class MemoryLogHandler(logging.Handler):
def __init__(self, level: int = logging.NOTSET) -> None:
@@ -65,3 +68,42 @@ class LogBuffer:
def get_log_messages(self) -> list[str]:
with self._lock:
return self._log_messages.copy()
class SuspendedLoggersContext:
"""A context manager that provides an isolated logging environment.
Temporarily removes all root log handlers upon entry, providing a clean slate
for defining new log handlers within the context. Upon exit, restores the original
logging configuration. This is useful when you need to temporarily configure
an isolated logging setup with well-defined log handlers.
The context manager:
- Removes all existing (root) log handlers on entry
- Allows defining new temporary handlers within the context
- Restores the original configuration (handlers and root log level) on exit
Example:
>>> with SuspendedLoggersContext():
... # No handlers are active here (configure your own and set desired log level)
... pass
>>> # Original log handlers are restored here
"""
def __init__(self) -> None:
self.saved_root_handlers: list = []
self.saved_root_level: Optional[int] = None
def __enter__(self) -> "SuspendedLoggersContext":
root_logger = lg.getLogger()
self.saved_root_handlers = root_logger.handlers.copy()
self.saved_root_level = root_logger.level
root_logger.handlers.clear()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore
root_logger = lg.getLogger()
root_logger.handlers = self.saved_root_handlers
if self.saved_root_level is not None:
root_logger.setLevel(self.saved_root_level)
@@ -15,12 +15,13 @@ from overrides import override
from solidlsp.language_servers.common import quote_windows_path
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
class ALLanguageServer(SolidLanguageServer):
"""
@@ -37,9 +38,7 @@ class ALLanguageServer(SolidLanguageServer):
- File opening requirement before symbol retrieval
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Initialize the AL Language Server.
@@ -58,7 +57,7 @@ class ALLanguageServer(SolidLanguageServer):
"""
# Setup runtime dependencies and get the language server command
# This will download the AL extension if needed
cmd = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
cmd = self._setup_runtime_dependencies(config, solidlsp_settings)
self._project_load_check_supported: bool = True
"""Whether the AL server supports the project load status check request.
@@ -68,17 +67,10 @@ class ALLanguageServer(SolidLanguageServer):
request fails, preventing repeated unsuccessful attempts.
"""
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
"al", # Language ID for LSP
solidlsp_settings,
)
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "al", solidlsp_settings)
@classmethod
def _download_al_extension(cls, logger: LanguageServerLogger, url: str, target_dir: str) -> bool:
def _download_al_extension(cls, url: str, target_dir: str) -> bool:
"""
Download and extract the AL extension from VS Code marketplace.
@@ -99,7 +91,7 @@ class ALLanguageServer(SolidLanguageServer):
"""
try:
logger.log(f"Downloading AL extension from {url}", logging.INFO)
log.info(f"Downloading AL extension from {url}")
# Create target directory for the extension
os.makedirs(target_dir, exist_ok=True)
@@ -118,7 +110,7 @@ class ALLanguageServer(SolidLanguageServer):
temp_file = os.path.join(target_dir, "al_extension_temp.vsix")
total_size = int(response.headers.get("content-length", 0))
logger.log(f"Downloading {total_size / 1024 / 1024:.1f} MB...", logging.INFO)
log.info(f"Downloading {total_size / 1024 / 1024:.1f} MB...")
with open(temp_file, "wb") as f:
downloaded = 0
@@ -128,9 +120,9 @@ class ALLanguageServer(SolidLanguageServer):
downloaded += len(chunk)
if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0: # Log progress every 10MB
progress = (downloaded / total_size) * 100
logger.log(f"Download progress: {progress:.1f}%", logging.INFO)
log.info(f"Download progress: {progress:.1f}%")
logger.log("Download complete, extracting...", logging.INFO)
log.info("Download complete, extracting...")
# Extract VSIX file (VSIX files are just ZIP archives with a different extension)
# This will extract the extension folder containing the language server binaries
@@ -140,17 +132,15 @@ class ALLanguageServer(SolidLanguageServer):
# Clean up temp file
os.remove(temp_file)
logger.log("AL extension extracted successfully", logging.INFO)
log.info("AL extension extracted successfully")
return True
except Exception as e:
logger.log(f"Error downloading/extracting AL extension: {e}", logging.ERROR)
log.error(f"Error downloading/extracting AL extension: {e}")
return False
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
"""
Setup runtime dependencies for AL Language Server and return the command to start the server.
@@ -168,10 +158,10 @@ class ALLanguageServer(SolidLanguageServer):
system = platform.system()
# Find existing extension or download if needed
extension_path = cls._find_al_extension(logger, solidlsp_settings)
extension_path = cls._find_al_extension(solidlsp_settings)
if extension_path is None:
logger.log("AL extension not found on disk, attempting to download...", logging.INFO)
extension_path = cls._download_and_install_al_extension(logger, solidlsp_settings)
log.info("AL extension not found on disk, attempting to download...")
extension_path = cls._download_and_install_al_extension(solidlsp_settings)
if extension_path is None:
raise RuntimeError(
@@ -188,10 +178,10 @@ class ALLanguageServer(SolidLanguageServer):
raise RuntimeError(f"AL Language Server executable not found at: {executable_path}")
# Prepare and return the executable command
return cls._prepare_executable(executable_path, system, logger)
return cls._prepare_executable(executable_path, system)
@classmethod
def _find_al_extension(cls, logger: LanguageServerLogger, solidlsp_settings: SolidLSPSettings) -> str | None:
def _find_al_extension(cls, solidlsp_settings: SolidLSPSettings) -> str | None:
"""
Find AL extension in various locations.
@@ -207,28 +197,28 @@ class ALLanguageServer(SolidLanguageServer):
# Check environment variable
env_path = os.environ.get("AL_EXTENSION_PATH")
if env_path and os.path.exists(env_path):
logger.log(f"Found AL extension via AL_EXTENSION_PATH: {env_path}", logging.DEBUG)
log.debug(f"Found AL extension via AL_EXTENSION_PATH: {env_path}")
return env_path
elif env_path:
logger.log(f"AL_EXTENSION_PATH set but directory not found: {env_path}", logging.WARNING)
log.warning(f"AL_EXTENSION_PATH set but directory not found: {env_path}")
# Check default download location
default_path = os.path.join(cls.ls_resources_dir(solidlsp_settings), "al-extension", "extension")
if os.path.exists(default_path):
logger.log(f"Found AL extension in default location: {default_path}", logging.DEBUG)
log.debug(f"Found AL extension in default location: {default_path}")
return default_path
# Search VS Code extensions
vscode_path = cls._find_al_extension_in_vscode(logger)
vscode_path = cls._find_al_extension_in_vscode()
if vscode_path:
logger.log(f"Found AL extension in VS Code: {vscode_path}", logging.DEBUG)
log.debug(f"Found AL extension in VS Code: {vscode_path}")
return vscode_path
logger.log("AL extension not found in any known location", logging.DEBUG)
log.debug("AL extension not found in any known location")
return None
@classmethod
def _download_and_install_al_extension(cls, logger: LanguageServerLogger, solidlsp_settings: SolidLSPSettings) -> str | None:
def _download_and_install_al_extension(cls, solidlsp_settings: SolidLSPSettings) -> str | None:
"""
Download and install AL extension from VS Code marketplace.
@@ -242,17 +232,17 @@ class ALLanguageServer(SolidLanguageServer):
AL_VERSION = "latest"
url = f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/vsextensions/al/{AL_VERSION}/vspackage"
logger.log(f"Downloading AL extension from: {url}", logging.INFO)
log.info(f"Downloading AL extension from: {url}")
if cls._download_al_extension(logger, url, al_extension_dir):
if cls._download_al_extension(url, al_extension_dir):
extension_path = os.path.join(al_extension_dir, "extension")
if os.path.exists(extension_path):
logger.log("AL extension downloaded and installed successfully", logging.INFO)
log.info("AL extension downloaded and installed successfully")
return extension_path
else:
logger.log(f"Download completed but extension not found at: {extension_path}", logging.ERROR)
log.error(f"Download completed but extension not found at: {extension_path}")
else:
logger.log("Failed to download AL extension from marketplace", logging.ERROR)
log.error("Failed to download AL extension from marketplace")
return None
@@ -279,7 +269,7 @@ class ALLanguageServer(SolidLanguageServer):
raise RuntimeError(f"Unsupported platform: {system}")
@classmethod
def _prepare_executable(cls, executable_path: str, system: str, logger: LanguageServerLogger) -> str:
def _prepare_executable(cls, executable_path: str, system: str) -> str:
"""
Prepare the executable by setting permissions and handling path quoting.
@@ -296,16 +286,16 @@ class ALLanguageServer(SolidLanguageServer):
if system in ["Linux", "Darwin"]:
st = os.stat(executable_path)
os.chmod(executable_path, st.st_mode | stat.S_IEXEC)
logger.log(f"Set execute permission on: {executable_path}", logging.DEBUG)
log.debug(f"Set execute permission on: {executable_path}")
logger.log(f"Using AL Language Server executable: {executable_path}", logging.INFO)
log.info(f"Using AL Language Server executable: {executable_path}")
# The AL Language Server uses stdio for LSP communication by default
# Use the utility function to handle Windows path quoting
return quote_windows_path(executable_path)
@classmethod
def _get_language_server_command_fallback(cls, logger: LanguageServerLogger) -> str:
def _get_language_server_command_fallback(cls) -> str:
"""
Get the command to start the AL language server.
@@ -333,10 +323,10 @@ class ALLanguageServer(SolidLanguageServer):
if potential_extension:
al_extension_path = str(potential_extension)
logger.log(f"Found AL extension in current directory: {al_extension_path}", logging.DEBUG)
log.debug(f"Found AL extension in current directory: {al_extension_path}")
else:
# Try to find in common VS Code extension locations
al_extension_path = cls._find_al_extension_in_vscode(logger)
al_extension_path = cls._find_al_extension_in_vscode()
if not al_extension_path:
raise RuntimeError(
@@ -368,14 +358,14 @@ class ALLanguageServer(SolidLanguageServer):
st = os.stat(executable)
os.chmod(executable, st.st_mode | stat.S_IEXEC)
logger.log(f"Using AL Language Server executable: {executable}", logging.INFO)
log.info(f"Using AL Language Server executable: {executable}")
# The AL Language Server uses stdio for LSP communication (no --stdio flag needed)
# Use the utility function to handle Windows path quoting
return quote_windows_path(executable)
@classmethod
def _find_al_extension_in_vscode(cls, logger: LanguageServerLogger) -> str | None:
def _find_al_extension_in_vscode(cls) -> str | None:
"""
Try to find AL extension in common VS Code extension locations.
@@ -407,11 +397,11 @@ class ALLanguageServer(SolidLanguageServer):
for base_path in possible_paths:
if base_path.exists():
logger.log(f"Searching for AL extension in: {base_path}", logging.DEBUG)
log.debug(f"Searching for AL extension in: {base_path}")
# Look for AL extension directories
for item in base_path.iterdir():
if item.is_dir() and item.name.startswith("ms-dynamics-smb.al-"):
logger.log(f"Found AL extension at: {item}", logging.DEBUG)
log.debug(f"Found AL extension at: {item}")
return str(item)
return None
@@ -499,18 +489,18 @@ class ALLanguageServer(SolidLanguageServer):
def do_nothing(params: str) -> None:
return
def window_log_message(msg: str) -> None:
self.logger.log(f"AL LSP: window/logMessage: {msg}", logging.INFO)
def window_log_message(msg: dict) -> None:
log.info(f"AL LSP: window/logMessage: {msg}")
def publish_diagnostics(params: dict) -> None:
# AL server publishes diagnostics during initialization
uri = params.get("uri", "")
diagnostics = params.get("diagnostics", [])
self.logger.log(f"AL LSP: Diagnostics for {uri}: {len(diagnostics)} issues", logging.DEBUG)
log.debug(f"AL LSP: Diagnostics for {uri}: {len(diagnostics)} issues")
def handle_al_notifications(params: dict) -> None:
# AL server sends custom notifications during project loading
self.logger.log("AL LSP: Notification received", logging.DEBUG)
log.debug("AL LSP: Notification received")
# Register handlers for AL-specific notifications
# These notifications are sent by the AL server during initialization and operation
@@ -520,27 +510,24 @@ class ALLanguageServer(SolidLanguageServer):
self.server.on_notification("al/refreshExplorerObjects", handle_al_notifications) # AL-specific object updates
# Start the server process
self.logger.log("Starting AL Language Server process", logging.INFO)
log.info("Starting AL Language Server process")
self.server.start()
# Send initialize request
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to AL LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to AL LSP server and awaiting response")
# Send initialize and wait for response
resp = self.server.send_request("initialize", initialize_params)
if resp is None:
raise RuntimeError("AL Language Server initialization failed - no response")
self.logger.log("AL Language Server initialized successfully", logging.INFO)
log.info("AL Language Server initialized successfully")
# Send initialized notification
self.server.send_notification("initialized", {})
self.logger.log("Sent initialized notification", logging.INFO)
log.info("Sent initialized notification")
@override
def start(self) -> "ALLanguageServer":
@@ -598,9 +585,9 @@ class ALLanguageServer(SolidLanguageServer):
}
},
)
self.logger.log("Sent workspace configuration", logging.DEBUG)
log.debug("Sent workspace configuration")
except Exception as e:
self.logger.log(f"Failed to send workspace config: {e}", logging.WARNING)
log.warning(f"Failed to send workspace config: {e}")
# Check if app.json exists and open it
# app.json is the AL project manifest file (similar to package.json for Node.js)
@@ -620,9 +607,9 @@ class ALLanguageServer(SolidLanguageServer):
{"textDocument": {"uri": app_json_uri, "languageId": "json", "version": 1, "text": app_json_content}},
)
self.logger.log(f"Opened app.json: {app_json_uri}", logging.DEBUG)
log.debug(f"Opened app.json: {app_json_uri}")
except Exception as e:
self.logger.log(f"Failed to open app.json: {e}", logging.WARNING)
log.warning(f"Failed to open app.json: {e}")
# Try to set active workspace (AL-specific custom LSP request)
# This is optional and may not be supported by all AL server versions
@@ -639,10 +626,10 @@ class ALLanguageServer(SolidLanguageServer):
"timeout": 2, # Quick timeout since this is optional
},
)
self.logger.log(f"Set active workspace result: {result}", logging.DEBUG)
log.debug(f"Set active workspace result: {result}")
except Exception as e:
# This is a custom AL request, not critical if it fails
self.logger.log(f"Failed to set active workspace (non-critical): {e}", logging.DEBUG)
log.debug(f"Failed to set active workspace (non-critical): {e}")
# Check if project supports load status check (optional)
# Many AL server versions don't support this, so we use a short timeout
@@ -704,7 +691,7 @@ class ALLanguageServer(SolidLanguageServer):
Full symbol tree with all AL symbols from opened files organized by directory
"""
self.logger.log("AL: Starting request_full_symbol_tree with file opening", logging.DEBUG)
log.debug("AL: Starting request_full_symbol_tree with file opening")
# Determine the root path for scanning
if within_relative_path is not None:
@@ -743,10 +730,10 @@ class ALLanguageServer(SolidLanguageServer):
# File is outside repository root, skip it
continue
self.logger.log(f"AL: Found {len(al_files)} AL files", logging.DEBUG)
log.debug(f"AL: Found {len(al_files)} AL files")
if not al_files:
self.logger.log("AL: No AL files found in repository", logging.WARNING)
log.warning("AL: No AL files found in repository")
return []
# Collect all symbols from all files
@@ -756,7 +743,7 @@ class ALLanguageServer(SolidLanguageServer):
for file_path, relative_path in al_files:
try:
# Use our overridden request_document_symbols which handles opening
self.logger.log(f"AL: Getting symbols for {relative_path}", logging.DEBUG)
log.debug(f"AL: Getting symbols for {relative_path}")
all_syms, root_syms = self.request_document_symbols(relative_path).get_all_symbols_and_roots()
if root_syms:
@@ -773,7 +760,7 @@ class ALLanguageServer(SolidLanguageServer):
},
}
all_file_symbols.append(file_symbol)
self.logger.log(f"AL: Added {len(root_syms)} symbols from {relative_path}", logging.DEBUG)
log.debug(f"AL: Added {len(root_syms)} symbols from {relative_path}")
elif all_syms:
# If we only got all_syms but not root, use all_syms
file_symbol = {
@@ -788,13 +775,13 @@ class ALLanguageServer(SolidLanguageServer):
},
}
all_file_symbols.append(file_symbol)
self.logger.log(f"AL: Added {len(all_syms)} symbols from {relative_path}", logging.DEBUG)
log.debug(f"AL: Added {len(all_syms)} symbols from {relative_path}")
except Exception as e:
self.logger.log(f"AL: Failed to get symbols for {relative_path}: {e}", logging.WARNING)
log.warning(f"AL: Failed to get symbols for {relative_path}: {e}")
if all_file_symbols:
self.logger.log(f"AL: Returning symbols from {len(all_file_symbols)} files", logging.DEBUG)
log.debug(f"AL: Returning symbols from {len(all_file_symbols)} files")
# Group files by directory
directory_structure: dict[str, list] = {}
@@ -839,7 +826,7 @@ class ALLanguageServer(SolidLanguageServer):
return result
else:
self.logger.log("AL: No symbols found in any files", logging.WARNING)
log.warning("AL: No symbols found in any files")
return []
# ===== Phase 1: Custom AL Command Implementations =====
@@ -862,10 +849,10 @@ class ALLanguageServer(SolidLanguageServer):
try:
# Use custom AL command instead of standard LSP
response = self.server.send_request("al/gotodefinition", al_params)
self.logger.log(f"AL gotodefinition response: {response}", logging.DEBUG)
log.debug(f"AL gotodefinition response: {response}")
return response # type: ignore[return-value]
except Exception as e:
self.logger.log(f"Failed to use al/gotodefinition, falling back to standard: {e}", logging.WARNING)
log.warning(f"Failed to use al/gotodefinition, falling back to standard: {e}")
# Fallback to standard LSP method if custom command fails
return super()._send_definition_request(definition_params)
@@ -883,7 +870,7 @@ class ALLanguageServer(SolidLanguageServer):
"""
if not hasattr(self, "server") or not self.server_started:
self.logger.log("Cannot check project load - server not started", logging.DEBUG)
log.debug("Cannot check project load - server not started")
return False
# Check if we've already determined this request isn't supported
@@ -900,15 +887,15 @@ class ALLanguageServer(SolidLanguageServer):
return response.get("loaded", False)
elif response is None:
# None typically means the project is still loading
self.logger.log("Project load check returned None", logging.DEBUG)
log.debug("Project load check returned None")
return False
else:
self.logger.log(f"Unexpected response type for project load check: {type(response)}", logging.DEBUG)
log.debug(f"Unexpected response type for project load check: {type(response)}")
return False
except Exception as e:
# Mark as unsupported to avoid repeated failed attempts
self._project_load_check_supported = False
self.logger.log(f"Project load check not supported by this AL server version: {e}", logging.DEBUG)
log.debug(f"Project load check not supported by this AL server version: {e}")
# Assume loaded if we can't check
return True
@@ -928,16 +915,16 @@ class ALLanguageServer(SolidLanguageServer):
"""
start_time = time.time()
self.logger.log(f"Checking AL project load status (timeout: {timeout}s)...", logging.DEBUG)
log.debug(f"Checking AL project load status (timeout: {timeout}s)...")
while time.time() - start_time < timeout:
if self.check_project_loaded():
elapsed = time.time() - start_time
self.logger.log(f"AL project fully loaded after {elapsed:.1f}s", logging.INFO)
log.info(f"AL project fully loaded after {elapsed:.1f}s")
return True
time.sleep(0.5)
self.logger.log(f"Project load check timed out after {timeout}s (non-critical)", logging.DEBUG)
log.debug(f"Project load check timed out after {timeout}s (non-critical)")
return False
def set_active_workspace(self, workspace_uri: str | None = None) -> None:
@@ -956,7 +943,7 @@ class ALLanguageServer(SolidLanguageServer):
"""
if not hasattr(self, "server") or not self.server_started:
self.logger.log("Cannot set active workspace - server not started", logging.DEBUG)
log.debug("Cannot set active workspace - server not started")
return
if workspace_uri is None:
@@ -966,7 +953,7 @@ class ALLanguageServer(SolidLanguageServer):
try:
self.server.send_request("al/setActiveWorkspace", params)
self.logger.log(f"Set active workspace to: {workspace_uri}", logging.INFO)
log.info(f"Set active workspace to: {workspace_uri}")
except Exception as e:
self.logger.log(f"Failed to set active workspace: {e}", logging.WARNING)
log.warning(f"Failed to set active workspace: {e}")
# Non-critical error, continue operation
@@ -12,11 +12,12 @@ import threading
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class BashLanguageServer(SolidLanguageServer):
"""
@@ -24,17 +25,14 @@ class BashLanguageServer(SolidLanguageServer):
Contains various configurations and settings specific to Bash scripting.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a BashLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
bash_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
bash_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=bash_lsp_executable_path, cwd=repository_root_path),
"bash",
@@ -44,9 +42,7 @@ class BashLanguageServer(SolidLanguageServer):
self.initialize_searcher_command_available = threading.Event()
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
"""
Setup runtime dependencies for Bash Language Server and return the command to start the server.
"""
@@ -76,9 +72,9 @@ class BashLanguageServer(SolidLanguageServer):
bash_executable_path += ".cmd"
if not os.path.exists(bash_executable_path):
logger.log(f"Bash Language Server executable not found at {bash_executable_path}. Installing...", logging.INFO)
deps.install(logger, bash_ls_dir)
logger.log("Bash language server dependencies installed successfully", logging.INFO)
log.info(f"Bash Language Server executable not found at {bash_executable_path}. Installing...")
deps.install(bash_ls_dir)
log.info("Bash language server dependencies installed successfully")
if not os.path.exists(bash_executable_path):
raise FileNotFoundError(
@@ -146,11 +142,11 @@ class BashLanguageServer(SolidLanguageServer):
return
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
# Check for bash-language-server ready signals
message_text = msg.get("message", "")
if "Analyzing" in message_text or "analysis complete" in message_text.lower():
self.logger.log("Bash language server analysis signals detected", logging.INFO)
log.info("Bash language server analysis signals detected")
self.server_ready.set()
self.completions_available.set()
@@ -160,16 +156,13 @@ class BashLanguageServer(SolidLanguageServer):
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting Bash server process", logging.INFO)
log.info("Starting Bash server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
self.logger.log(f"Received initialize response from bash server: {init_response}", logging.DEBUG)
log.debug(f"Received initialize response from bash server: {init_response}")
# Enhanced capability checks for bash-language-server 5.6.0
assert init_response["capabilities"]["textDocumentSync"] in [1, 2] # Full or Incremental
@@ -177,22 +170,22 @@ class BashLanguageServer(SolidLanguageServer):
# Verify document symbol support is available
if "documentSymbolProvider" in init_response["capabilities"]:
self.logger.log("Bash server supports document symbols", logging.INFO)
log.info("Bash server supports document symbols")
else:
self.logger.log("Warning: Bash server does not report document symbol support", logging.WARNING)
log.warning("Warning: Bash server does not report document symbol support")
self.server.notify.initialized({})
# Wait for server readiness with timeout
self.logger.log("Waiting for Bash language server to be ready...", logging.INFO)
log.info("Waiting for Bash language server to be ready...")
if not self.server_ready.wait(timeout=3.0):
# Fallback: assume server is ready after timeout
# This is common. bash-language-server doesn't always send explicit ready signals. Log as info
self.logger.log("Timeout waiting for bash server ready signal, proceeding anyway", logging.INFO)
log.info("Timeout waiting for bash server ready signal, proceeding anyway")
self.server_ready.set()
self.completions_available.set()
else:
self.logger.log("Bash server initialization complete", logging.INFO)
log.info("Bash server initialization complete")
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
# Uses the standard LSP documentSymbol request which provides reliable function detection
@@ -202,16 +195,13 @@ class BashLanguageServer(SolidLanguageServer):
# - Functions with various indentation levels
# - Functions with comments before/after/inside
self.logger.log(f"Requesting document symbols via LSP for {relative_file_path}", logging.DEBUG)
log.debug(f"Requesting document symbols via LSP for {relative_file_path}")
# Use the standard LSP approach - bash-language-server handles all function syntaxes correctly
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
# Log detection results for debugging
functions = [s for s in document_symbols.iter_symbols() if s.get("kind") == 12]
self.logger.log(
f"LSP function detection for {relative_file_path}: Found {len(functions)} functions",
logging.INFO,
)
log.info(f"LSP function detection for {relative_file_path}: Found {len(functions)} functions")
return document_symbols
@@ -10,13 +10,14 @@ from typing import Any, cast
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
class ClangdLanguageServer(SolidLanguageServer):
"""
@@ -25,20 +26,13 @@ class ClangdLanguageServer(SolidLanguageServer):
Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
clangd_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
clangd_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path),
"cpp",
solidlsp_settings,
config, repository_root_path, ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), "cpp", solidlsp_settings
)
self.server_ready = threading.Event()
self.service_ready_event = threading.Event()
@@ -46,9 +40,7 @@ class ClangdLanguageServer(SolidLanguageServer):
self.resolve_main_method_available = threading.Event()
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
"""
Setup runtime dependencies for ClangdLanguageServer and return the command to start the server.
"""
@@ -110,16 +102,13 @@ class ClangdLanguageServer(SolidLanguageServer):
+ " Arch Linux: sudo pacman -S clang\n"
+ "See https://clangd.llvm.org/installation for more details."
)
logger.log(f"Using system-installed clangd at {clangd_executable_path}", logging.INFO)
log.info(f"Using system-installed clangd at {clangd_executable_path}")
else:
# Standard download and install for platforms with prebuilt binaries
clangd_executable_path = deps.binary_path(clangd_ls_dir)
if not os.path.exists(clangd_executable_path):
logger.log(
f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}",
logging.INFO,
)
_ = deps.install(logger, clangd_ls_dir)
log.info(f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}")
_ = deps.install(clangd_ls_dir)
if not os.path.exists(clangd_executable_path):
raise FileNotFoundError(
f"Clangd executable not found at {clangd_executable_path}.\n"
@@ -197,7 +186,7 @@ class ClangdLanguageServer(SolidLanguageServer):
self.server_ready.set()
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("language/status", lang_status_handler)
@@ -208,14 +197,11 @@ class ClangdLanguageServer(SolidLanguageServer):
self.server.on_notification("language/actionableNotification", do_nothing)
self.server.on_notification("experimental/serverStatus", check_experimental_status)
self.logger.log("Starting Clangd server process", logging.INFO)
log.info("Starting Clangd server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore
assert "completionProvider" in init_response["capabilities"]
+10 -18
View File
@@ -12,13 +12,14 @@ from typing import cast
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
def run_command(cmd: list, capture_output: bool = True) -> subprocess.CompletedProcess:
return subprocess.run(
@@ -86,16 +87,13 @@ class ClojureLSP(SolidLanguageServer):
]
)
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
clojure_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
clojure_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path),
"clojure",
@@ -107,9 +105,7 @@ class ClojureLSP(SolidLanguageServer):
self.service_ready_event = threading.Event()
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
"""Setup runtime dependencies for clojure-lsp and return the command to start the server."""
verify_clojure_cli()
deps = ClojureLSP.runtime_dependencies
@@ -118,11 +114,10 @@ class ClojureLSP(SolidLanguageServer):
clojurelsp_ls_dir = cls.ls_resources_dir(solidlsp_settings)
clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir)
if not os.path.exists(clojurelsp_executable_path):
logger.log(
log.info(
f"Downloading and extracting clojure-lsp from {dependency.url} to {clojurelsp_ls_dir}",
logging.INFO,
)
deps.install(logger, clojurelsp_ls_dir)
deps.install(clojurelsp_ls_dir)
if not os.path.exists(clojurelsp_executable_path):
raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}")
os.chmod(clojurelsp_executable_path, 0o755)
@@ -189,7 +184,7 @@ class ClojureLSP(SolidLanguageServer):
self.server_ready.set()
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("language/status", lang_status_handler)
@@ -200,15 +195,12 @@ class ClojureLSP(SolidLanguageServer):
self.server.on_notification("language/actionableNotification", do_nothing)
self.server.on_notification("experimental/serverStatus", check_experimental_status)
self.logger.log("Starting clojure-lsp server process", logging.INFO)
log.info("Starting clojure-lsp server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore
assert "completionProvider" in init_response["capabilities"]
+5 -6
View File
@@ -8,7 +8,6 @@ from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass, replace
from typing import Any, cast
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.util.subprocess_util import subprocess_kwargs
@@ -79,7 +78,7 @@ class RuntimeDependencyCollection:
return target_dir
return os.path.join(target_dir, dep.binary_name)
def install(self, logger: LanguageServerLogger, target_dir: str) -> dict[str, str]:
def install(self, target_dir: str) -> dict[str, str]:
"""Install all dependencies for the current platform into *target_dir*.
Returns a mapping from dependency id to the resolved binary path.
@@ -88,7 +87,7 @@ class RuntimeDependencyCollection:
results: dict[str, str] = {}
for dep in self.get_dependencies_for_current_platform():
if dep.url:
self._install_from_url(dep, logger, target_dir)
self._install_from_url(dep, target_dir)
if dep.command:
self._run_command(dep.command, target_dir)
if dep.binary_name:
@@ -131,15 +130,15 @@ class RuntimeDependencyCollection:
)
@staticmethod
def _install_from_url(dep: RuntimeDependency, logger: LanguageServerLogger, target_dir: str) -> None:
def _install_from_url(dep: RuntimeDependency, target_dir: str) -> None:
if not dep.url:
raise ValueError(f"Dependency {dep.id} has no URL")
if dep.archive_type in ("gz", "binary") and dep.binary_name:
dest = os.path.join(target_dir, dep.binary_name)
FileUtils.download_and_extract_archive(logger, dep.url, dest, dep.archive_type)
FileUtils.download_and_extract_archive(dep.url, dest, dep.archive_type)
else:
FileUtils.download_and_extract_archive(logger, dep.url, target_dir, dep.archive_type or "zip")
FileUtils.download_and_extract_archive(dep.url, target_dir, dep.archive_type or "zip")
def quote_windows_path(path: str) -> str:
@@ -21,7 +21,6 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import PathUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -30,6 +29,8 @@ from solidlsp.util.zip import SafeZipExtractor
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
_RUNTIME_DEPENDENCIES = [
RuntimeDependency(
id="CSharpLanguageServer",
@@ -198,14 +199,12 @@ class CSharpLanguageServer(SolidLanguageServer):
See the `_RUNTIME_DEPENDENCIES` variable above for the available dependency ids and platform_ids.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
dotnet_path, language_server_path = self._ensure_server_installed(logger, config, solidlsp_settings)
dotnet_path, language_server_path = self._ensure_server_installed(config, solidlsp_settings)
# Find solution or project file
solution_or_project = find_solution_or_project_file(repository_root_path)
@@ -219,20 +218,13 @@ class CSharpLanguageServer(SolidLanguageServer):
# The language server will discover the solution/project from the workspace root
if solution_or_project:
logger.log(f"Found solution/project file: {solution_or_project}", logging.INFO)
log.info(f"Found solution/project file: {solution_or_project}")
else:
logger.log("No .sln or .csproj file found, language server will attempt auto-discovery", logging.WARNING)
log.warning("No .sln or .csproj file found, language server will attempt auto-discovery")
logger.log(f"Language server command: {' '.join(cmd)}", logging.DEBUG)
log.debug(f"Language server command: {' '.join(cmd)}")
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
"csharp",
solidlsp_settings,
)
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", solidlsp_settings)
self.initialization_complete = threading.Event()
@@ -241,9 +233,7 @@ class CSharpLanguageServer(SolidLanguageServer):
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]
@classmethod
def _ensure_server_installed(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> tuple[str, str]:
def _ensure_server_installed(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[str, str]:
"""
Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available.
Returns a tuple of (dotnet_path, language_server_dll_path).
@@ -251,30 +241,27 @@ class CSharpLanguageServer(SolidLanguageServer):
language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance())
runtime_dependency_overrides = cast(list[dict[str, Any]], language_specific_config.get("runtime_dependencies", []))
logger.log("Resolving runtime dependencies", logging.DEBUG)
log.debug("Resolving runtime dependencies")
runtime_dependencies = RuntimeDependencyCollection(
_RUNTIME_DEPENDENCIES,
overrides=runtime_dependency_overrides,
)
logger.log(
log.debug(
f"Available runtime dependencies: {runtime_dependencies.get_dependencies_for_current_platform}",
logging.DEBUG,
)
# Find the dependencies for our platform
lang_server_dep = runtime_dependencies.get_single_dep_for_current_platform("CSharpLanguageServer")
dotnet_runtime_dep = runtime_dependencies.get_single_dep_for_current_platform("DotNetRuntime")
dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(logger, dotnet_runtime_dep, solidlsp_settings)
server_dll_path = CSharpLanguageServer._ensure_language_server(logger, lang_server_dep, solidlsp_settings)
dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(dotnet_runtime_dep, solidlsp_settings)
server_dll_path = CSharpLanguageServer._ensure_language_server(lang_server_dep, solidlsp_settings)
return dotnet_path, server_dll_path
@classmethod
def _ensure_dotnet_runtime(
cls, logger: LanguageServerLogger, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
) -> str:
def _ensure_dotnet_runtime(cls, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str:
"""Ensure .NET runtime is available and return the dotnet executable path."""
# TODO: use RuntimeDependency util methods instead of custom validation/download logic
@@ -285,18 +272,16 @@ class CSharpLanguageServer(SolidLanguageServer):
try:
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
if "Microsoft.NETCore.App 9." in result.stdout:
logger.log("Found system .NET 9 runtime", logging.INFO)
log.info("Found system .NET 9 runtime")
return system_dotnet
except subprocess.CalledProcessError:
pass
# Download .NET 9 runtime using config
return cls._ensure_dotnet_runtime_from_config(logger, dotnet_runtime_dep, solidlsp_settings)
return cls._ensure_dotnet_runtime_from_config(dotnet_runtime_dep, solidlsp_settings)
@classmethod
def _ensure_language_server(
cls, logger: LanguageServerLogger, lang_server_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
) -> str:
def _ensure_language_server(cls, lang_server_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str:
"""Ensure language server is available and return the DLL path."""
package_name = lang_server_dep.package_name
package_version = lang_server_dep.package_version
@@ -306,14 +291,14 @@ class CSharpLanguageServer(SolidLanguageServer):
server_dll = server_dir / lang_server_dep.binary_name
if server_dll.exists():
logger.log(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}", logging.INFO)
log.info(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}")
return str(server_dll)
# Download and install the language server
logger.log(f"Downloading {package_name} version {package_version}...", logging.INFO)
log.info(f"Downloading {package_name} version {package_version}...")
assert package_version is not None
assert package_name is not None
package_path = cls._download_nuget_package_direct(logger, package_name, package_version, solidlsp_settings)
package_path = cls._download_nuget_package_direct(package_name, package_version, solidlsp_settings)
# Extract and install
cls._extract_language_server(lang_server_dep, package_path, server_dir)
@@ -325,7 +310,7 @@ class CSharpLanguageServer(SolidLanguageServer):
if platform.system().lower() != "windows":
server_dll.chmod(0o755)
logger.log(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}", logging.INFO)
log.info(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}")
return str(server_dll)
@staticmethod
@@ -352,9 +337,7 @@ class CSharpLanguageServer(SolidLanguageServer):
shutil.copytree(source_dir, server_dir, dirs_exist_ok=True)
@classmethod
def _download_nuget_package_direct(
cls, logger: LanguageServerLogger, package_name: str, package_version: str, solidlsp_settings: SolidLSPSettings
) -> Path:
def _download_nuget_package_direct(cls, package_name: str, package_version: str, solidlsp_settings: SolidLSPSettings) -> Path:
"""
Download a NuGet package directly from the Azure NuGet feed.
Returns the path to the extracted package directory.
@@ -367,7 +350,7 @@ class CSharpLanguageServer(SolidLanguageServer):
try:
# First, get the service index from the Azure feed
logger.log("Fetching NuGet service index from Azure feed...", logging.DEBUG)
log.debug("Fetching NuGet service index from Azure feed...")
with urllib.request.urlopen(azure_feed_url) as response:
service_index = json.loads(response.read().decode())
@@ -386,7 +369,7 @@ class CSharpLanguageServer(SolidLanguageServer):
package_version_lower = package_version.lower()
package_url = f"{package_base_address.rstrip('/')}/{package_id_lower}/{package_version_lower}/{package_id_lower}.{package_version_lower}.nupkg"
logger.log(f"Downloading package from: {package_url}", logging.DEBUG)
log.debug(f"Downloading package from: {package_url}")
# Download the .nupkg file
nupkg_file = temp_dir / f"{package_name}.{package_version}.nupkg"
@@ -403,7 +386,7 @@ class CSharpLanguageServer(SolidLanguageServer):
# Clean up the nupkg file
nupkg_file.unlink()
logger.log(f"Successfully downloaded and extracted {package_name} version {package_version}", logging.INFO)
log.info(f"Successfully downloaded and extracted {package_name} version {package_version}")
return package_extract_dir
except Exception as e:
@@ -412,9 +395,7 @@ class CSharpLanguageServer(SolidLanguageServer):
) from e
@classmethod
def _ensure_dotnet_runtime_from_config(
cls, logger: LanguageServerLogger, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings
) -> str:
def _ensure_dotnet_runtime_from_config(cls, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str:
"""
Ensure .NET 9 runtime is available using runtime dependency configuration.
Returns the path to the dotnet executable.
@@ -428,7 +409,7 @@ class CSharpLanguageServer(SolidLanguageServer):
try:
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
if "Microsoft.NETCore.App 9." in result.stdout:
logger.log("Found system .NET 9 runtime", logging.INFO)
log.info("Found system .NET 9 runtime")
return system_dotnet
except subprocess.CalledProcessError:
pass
@@ -439,17 +420,17 @@ class CSharpLanguageServer(SolidLanguageServer):
dotnet_exe = dotnet_dir / dotnet_runtime_dep.binary_name
if dotnet_exe.exists():
logger.log(f"Using cached .NET runtime from {dotnet_exe}", logging.INFO)
log.info(f"Using cached .NET runtime from {dotnet_exe}")
return str(dotnet_exe)
# Download .NET runtime
logger.log("Downloading .NET 9 runtime...", logging.INFO)
log.info("Downloading .NET 9 runtime...")
dotnet_dir.mkdir(parents=True, exist_ok=True)
custom_settings = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance())
custom_dotnet_runtime_url = custom_settings.get("dotnet_runtime_url")
if custom_dotnet_runtime_url is not None:
logger.log(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}", logging.INFO)
log.info(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}")
url = custom_dotnet_runtime_url
else:
url = dotnet_runtime_dep.url
@@ -459,7 +440,7 @@ class CSharpLanguageServer(SolidLanguageServer):
# Download the runtime
download_path = dotnet_dir / f"dotnet-runtime.{archive_type}"
try:
logger.log(f"Downloading from {url}", logging.DEBUG)
log.debug(f"Downloading from {url}")
urllib.request.urlretrieve(url, download_path)
# Extract the archive
@@ -478,7 +459,7 @@ class CSharpLanguageServer(SolidLanguageServer):
if platform.system().lower() != "windows":
dotnet_exe.chmod(0o755)
logger.log(f"Successfully installed .NET 9 runtime to {dotnet_exe}", logging.INFO)
log.info(f"Successfully installed .NET 9 runtime to {dotnet_exe}")
return str(dotnet_exe)
except Exception as e:
@@ -551,7 +532,7 @@ class CSharpLanguageServer(SolidLanguageServer):
# Map LSP message types to Python logging levels
level_map = {1: logging.ERROR, 2: logging.WARNING, 3: logging.INFO, 4: logging.DEBUG} # Error # Warning # Info # Log
self.logger.log(f"LSP: {message_text}", level_map.get(level, logging.DEBUG))
log.log(level_map.get(level, logging.DEBUG), f"LSP: {message_text}")
def handle_progress(params: dict) -> None:
"""Handle progress notifications from the language server."""
@@ -559,7 +540,7 @@ class CSharpLanguageServer(SolidLanguageServer):
value = params.get("value", {})
# Log raw progress for debugging
self.logger.log(f"Progress notification received: {params}", logging.DEBUG)
log.debug(f"Progress notification received: {params}")
# Handle different progress notification types
kind = value.get("kind")
@@ -570,22 +551,22 @@ class CSharpLanguageServer(SolidLanguageServer):
percentage = value.get("percentage")
if percentage is not None:
self.logger.log(f"Progress [{token}]: {title} - {message} ({percentage}%)", logging.INFO)
log.debug(f"Progress [{token}]: {title} - {message} ({percentage}%)")
else:
self.logger.log(f"Progress [{token}]: {title} - {message}", logging.INFO)
log.info(f"Progress [{token}]: {title} - {message}")
elif kind == "report":
message = value.get("message", "")
percentage = value.get("percentage")
if percentage is not None:
self.logger.log(f"Progress [{token}]: {message} ({percentage}%)", logging.INFO)
log.info(f"Progress [{token}]: {message} ({percentage}%)")
elif message:
self.logger.log(f"Progress [{token}]: {message}", logging.INFO)
log.info(f"Progress [{token}]: {message}")
elif kind == "end":
message = value.get("message", "Operation completed")
self.logger.log(f"Progress [{token}]: {message}", logging.INFO)
log.info(f"Progress [{token}]: {message}")
def handle_workspace_configuration(params: dict) -> list:
"""Handle workspace/configuration requests from the server."""
@@ -646,30 +627,34 @@ class CSharpLanguageServer(SolidLanguageServer):
def handle_project_needs_restore(params: dict) -> None:
return
def handle_workspace_indexing_complete(params: dict) -> None:
self.completions_available.set()
# Set up notification handlers
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", handle_progress)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.server.on_notification("workspace/projectInitializationComplete", handle_workspace_indexing_complete)
self.server.on_request("workspace/configuration", handle_workspace_configuration)
self.server.on_request("window/workDoneProgress/create", handle_work_done_progress_create)
self.server.on_request("client/registerCapability", handle_register_capability)
self.server.on_request("workspace/_roslyn_projectNeedsRestore", handle_project_needs_restore)
self.logger.log("Starting Microsoft.CodeAnalysis.LanguageServer process", logging.INFO)
log.info("Starting Microsoft.CodeAnalysis.LanguageServer process")
try:
self.server.start()
except Exception as e:
self.logger.log(f"Failed to start language server process: {e}", logging.ERROR)
log.info(f"Failed to start language server process: {e}", logging.ERROR)
raise SolidLSPException(f"Failed to start C# language server: {e}")
# Send initialization
initialize_params = self._get_initialize_params()
self.logger.log("Sending initialize request to language server", logging.INFO)
log.info("Sending initialize request to language server")
try:
init_response = self.server.send.initialize(initialize_params)
self.logger.log(f"Received initialize response: {init_response}", logging.DEBUG)
log.info(f"Received initialize response: {init_response}")
except Exception as e:
raise SolidLSPException(f"Failed to initialize C# language server for {self.repository_root_path}: {e}") from e
@@ -698,15 +683,19 @@ class CSharpLanguageServer(SolidLanguageServer):
self._open_solution_and_projects()
self.initialization_complete.set()
self.completions_available.set()
self.logger.log(
log.info(
"Microsoft.CodeAnalysis.LanguageServer initialized and ready\n"
"Waiting for language server to index project files...\n"
"This may take a while for large projects",
logging.INFO,
"This may take a while for large projects"
)
if self.completions_available.wait(30): # Wait up to 30 seconds for indexing
log.info("Indexing complete")
else:
log.warning("Timeout waiting for indexing to complete, proceeding anyway")
self.completions_available.set()
def _force_pull_diagnostics(self, init_response: dict | InitializeResult) -> None:
"""
Apply the diagnostic capabilities hack.
@@ -724,7 +713,7 @@ class CSharpLanguageServer(SolidLanguageServer):
"workspaceDiagnostics": True,
}
)
self.logger.log("Applied diagnostic capabilities hack for better C# diagnostics", logging.DEBUG)
log.debug("Applied diagnostic capabilities hack for better C# diagnostics")
def _open_solution_and_projects(self) -> None:
"""
@@ -741,7 +730,7 @@ class CSharpLanguageServer(SolidLanguageServer):
if solution_file:
solution_uri = PathUtils.path_to_uri(solution_file)
self.server.notify.send_notification("solution/open", {"solution": solution_uri})
self.logger.log(f"Opened solution file: {solution_file}", logging.INFO)
log.debug(f"Opened solution file: {solution_file}")
# Find and open project files
project_files = []
@@ -753,7 +742,7 @@ class CSharpLanguageServer(SolidLanguageServer):
if project_files:
project_uris = [PathUtils.path_to_uri(project_file) for project_file in project_files]
self.server.notify.send_notification("project/open", {"projects": project_uris})
self.logger.log(f"Opened project files: {project_files}", logging.DEBUG)
log.debug(f"Opened project files: {project_files}")
@override
def _get_wait_time_for_cross_file_referencing(self) -> float:
@@ -4,7 +4,6 @@ import pathlib
from typing import cast
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -12,30 +11,25 @@ from ..ls_config import LanguageServerConfig
from ..lsp_protocol_handler.lsp_types import InitializeParams
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
class DartLanguageServer(SolidLanguageServer):
"""
Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
) -> None:
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
"""
Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
executable_path = self._setup_runtime_dependencies(logger, solidlsp_settings)
executable_path = self._setup_runtime_dependencies(solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path),
"dart",
solidlsp_settings,
config, repository_root_path, ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), "dart", solidlsp_settings
)
@classmethod
def _setup_runtime_dependencies(cls, logger: "LanguageServerLogger", solidlsp_settings: SolidLSPSettings) -> str:
def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str:
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
@@ -85,7 +79,7 @@ class DartLanguageServer(SolidLanguageServer):
dart_executable_path = deps.binary_path(dart_ls_dir)
if not os.path.exists(dart_executable_path):
deps.install(logger, dart_ls_dir)
deps.install(dart_ls_dir)
assert os.path.exists(dart_executable_path)
os.chmod(dart_executable_path, 0o755)
@@ -136,7 +130,7 @@ class DartLanguageServer(SolidLanguageServer):
pass
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
self.server.on_request("client/registerCapability", do_nothing)
self.server.on_notification("language/status", do_nothing)
@@ -147,17 +141,11 @@ class DartLanguageServer(SolidLanguageServer):
self.server.on_notification("language/actionableNotification", do_nothing)
self.server.on_notification("experimental/serverStatus", check_experimental_status)
self.logger.log("Starting dart-language-server server process", logging.INFO)
log.info("Starting dart-language-server server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request to dart-language-server",
logging.DEBUG,
)
log.debug("Sending initialize request to dart-language-server")
init_response = self.server.send_request("initialize", initialize_params) # type: ignore
self.logger.log(
f"Received initialize response from dart-language-server: {init_response}",
logging.INFO,
)
log.info(f"Received initialize response from dart-language-server: {init_response}")
self.server.notify.initialized({})
+19 -30
View File
@@ -16,13 +16,14 @@ from overrides import override
from solidlsp.ls import LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_types import UnifiedSymbolInformation
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@dataclasses.dataclass
class RuntimeDependencyPaths:
@@ -61,14 +62,12 @@ class EclipseJDTLS(SolidLanguageServer):
```
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a new EclipseJDTLS instance initializing the language server settings appropriately.
This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
runtime_dependency_paths = self._setupRuntimeDependencies(logger, config, solidlsp_settings)
runtime_dependency_paths = self._setupRuntimeDependencies(config, solidlsp_settings)
self.runtime_dependency_paths = runtime_dependency_paths
# ws_dir is the workspace directory for the EclipseJDTLS server
@@ -150,7 +149,7 @@ class EclipseJDTLS(SolidLanguageServer):
self.initialize_searcher_command_available = threading.Event()
super().__init__(
config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java", solidlsp_settings=solidlsp_settings
config, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java", solidlsp_settings=solidlsp_settings
)
@override
@@ -172,9 +171,7 @@ class EclipseJDTLS(SolidLanguageServer):
]
@classmethod
def _setupRuntimeDependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> RuntimeDependencyPaths:
def _setupRuntimeDependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> RuntimeDependencyPaths:
"""
Setup runtime dependencies for EclipseJDTLS and return the paths.
"""
@@ -266,7 +263,6 @@ class EclipseJDTLS(SolidLanguageServer):
if not os.path.exists(gradle_path):
FileUtils.download_and_extract_archive(
logger,
runtime_dependencies["gradle"]["platform-agnostic"]["url"],
str(PurePath(gradle_path).parent),
runtime_dependencies["gradle"]["platform-agnostic"]["archiveType"],
@@ -292,7 +288,7 @@ class EclipseJDTLS(SolidLanguageServer):
os.path.exists(jdtls_readonly_config_path),
]
):
FileUtils.download_and_extract_archive(logger, dependency["url"], vscode_java_path, dependency["archiveType"])
FileUtils.download_and_extract_archive(dependency["url"], vscode_java_path, dependency["archiveType"])
os.chmod(jre_path, 0o755)
@@ -315,7 +311,7 @@ class EclipseJDTLS(SolidLanguageServer):
os.path.exists(intellisense_members_path),
]
):
FileUtils.download_and_extract_archive(logger, dependency["url"], intellicode_directory_path, dependency["archiveType"])
FileUtils.download_and_extract_archive(dependency["url"], intellicode_directory_path, dependency["archiveType"])
assert os.path.exists(intellicode_directory_path)
assert os.path.exists(intellicode_jar_path)
@@ -355,18 +351,16 @@ class EclipseJDTLS(SolidLanguageServer):
f"Fix: create the file, update path in ~/.serena/serena_config.yml (ls_specific_settings -> java -> maven_user_settings), "
f"or remove the setting to use default ({default_maven_settings_path})"
)
self.logger.log(error_msg, logging.ERROR)
log.error(error_msg)
raise FileNotFoundError(error_msg)
maven_settings_path = custom_maven_settings_path
self.logger.log(f"Using Maven settings from custom location: {maven_settings_path}", logging.INFO)
log.info(f"Using Maven settings from custom location: {maven_settings_path}")
elif os.path.exists(default_maven_settings_path):
maven_settings_path = default_maven_settings_path
self.logger.log(f"Using Maven settings from default location: {maven_settings_path}", logging.INFO)
log.info(f"Using Maven settings from default location: {maven_settings_path}")
else:
maven_settings_path = None
self.logger.log(
f"Maven settings not found at default location ({default_maven_settings_path}), will use JDTLS defaults", logging.INFO
)
log.info(f"Maven settings not found at default location ({default_maven_settings_path}), will use JDTLS defaults")
# Gradle user home: default to ~/.gradle
default_gradle_home = os.path.join(os.path.expanduser("~"), ".gradle")
@@ -379,18 +373,16 @@ class EclipseJDTLS(SolidLanguageServer):
f"Fix: create the directory, update path in ~/.serena/serena_config.yml (ls_specific_settings -> java -> gradle_user_home), "
f"or remove the setting to use default (~/.gradle)"
)
self.logger.log(error_msg, logging.ERROR)
log.error(error_msg)
raise FileNotFoundError(error_msg)
gradle_user_home = custom_gradle_home
self.logger.log(f"Using Gradle user home from custom location: {gradle_user_home}", logging.INFO)
log.info(f"Using Gradle user home from custom location: {gradle_user_home}")
elif os.path.exists(default_gradle_home):
gradle_user_home = default_gradle_home
self.logger.log(f"Using Gradle user home from default location: {gradle_user_home}", logging.INFO)
log.info(f"Using Gradle user home from default location: {gradle_user_home}")
else:
gradle_user_home = None
self.logger.log(
f"Gradle user home not found at default location ({default_gradle_home}), will use JDTLS defaults", logging.INFO
)
log.info(f"Gradle user home not found at default location ({default_gradle_home}), will use JDTLS defaults")
initialize_params = {
"locale": "en",
@@ -745,7 +737,7 @@ class EclipseJDTLS(SolidLanguageServer):
return []
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(params: dict) -> None:
return
@@ -758,14 +750,11 @@ class EclipseJDTLS(SolidLanguageServer):
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.server.on_notification("language/actionableNotification", do_nothing)
self.logger.log("Starting EclipseJDTLS server process", logging.INFO)
log.info("Starting EclipseJDTLS server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore
assert "completionProvider" not in init_response["capabilities"]
@@ -1,18 +1,6 @@
# Elixir Language Server Integration
This directory contains the integration for Elixir language support using [Next LS](https://github.com/elixir-tools/next-ls) from the elixir-tools project.
> **⚠️ Windows Not Supported**: Next LS does not provide Windows binaries, so Elixir language server integration is only available on Linux and macOS.
## Known Issues
### Next LS v0.23.3 Timeout Enumeration Bug
There is a known intermittent bug in Next LS v0.23.3 where `textDocument/definition` requests can fail with:
```
Protocol.UndefinedError: protocol Enumerable not implemented for :timeout of type Atom
```
This bug is tracked in [Next LS Issue #543](https://github.com/elixir-tools/next-ls/issues/543) and primarily occurs in CI environments. The affected test (`test_request_defining_symbol_none`) is marked as expected to fail until this upstream bug is resolved.
This directory contains the integration for Elixir language support using [Expert](https://github.com/elixir-lang/expert), the official Elixir language server.
## Prerequisites
@@ -22,9 +10,10 @@ Before using the Elixir language server integration, you need to have:
- Install from: https://elixir-lang.org/install.html
- Verify with: `elixir --version`
2. **Next LS** installed and available in your PATH
- Install from: https://github.com/elixir-tools/next-ls#installation
- Verify with: `nextls --version`
2. **Expert** (optional - will be downloaded automatically if not found)
- Expert binaries are automatically downloaded from GitHub releases
- Manual installation: https://github.com/elixir-lang/expert#installation
- If installed manually, ensure `expert` is in your PATH
## Features
@@ -42,13 +31,20 @@ The Elixir integration provides:
## Configuration
The integration uses the default Next LS configuration with:
The integration uses the default Expert configuration with:
- **MIX_ENV**: `dev`
- **MIX_TARGET**: `host`
- **Experimental completions**: Disabled by default
- **Credo extension**: Enabled by default
### Version Management (asdf)
Expert automatically respects project-specific Elixir versions when using asdf:
- If a `.tool-versions` file exists in the project root, Expert will use the specified Elixir version
- Expert is launched from the project directory, allowing it to pick up project configuration
- No additional configuration needed - just ensure asdf is installed and the project has a `.tool-versions` file
## Usage
The Elixir language server is automatically selected when working with Elixir projects. It will be used for:
@@ -63,14 +59,14 @@ The Elixir language server is automatically selected when working with Elixir pr
### Important: Project Compilation
Next LS requires your Elixir project to be **compiled** for optimal performance, especially for:
Expert requires your Elixir project to be **compiled** for optimal performance, especially for:
- Cross-file reference resolution
- Complete symbol information
- Accurate go-to-definition
**For production use**: Ensure your project is compiled with `mix compile` before using the language server.
**For testing**: The test suite automatically compiles the test repositories before running tests to ensure optimal Next LS performance.
**For testing**: The test suite automatically compiles the test repositories before running tests to ensure optimal Expert performance.
## Testing
@@ -83,8 +79,9 @@ pytest test/solidlsp/elixir/ -m elixir
## Implementation Details
- **Main class**: `ElixirTools` in `elixir_tools.py`
- **Initialization parameters**: Defined in `initialize_params.json`
- **Language identifier**: `"elixir"`
- **Command**: `nextls --stdio`
- **Command**: `expert --stdio`
- **Supported platforms**: Linux (x64, arm64), macOS (x64, arm64), Windows (x64, arm64)
- **Binary distribution**: Downloaded from [GitHub releases](https://github.com/elixir-lang/expert/releases)
The implementation follows the same patterns as other language servers in this project, inheriting from `SolidLanguageServer` and providing Elixir-specific configuration and behavior.
The implementation follows the same patterns as other language servers in this project, inheriting from `SolidLanguageServer` and providing Elixir-specific configuration and behavior.
@@ -10,24 +10,24 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler import lsp_types
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from ..common import RuntimeDependency
log = logging.getLogger(__name__)
class ElixirTools(SolidLanguageServer):
"""
Provides Elixir specific instantiation of the LanguageServer class using Next LS from elixir-tools.
Provides Elixir specific instantiation of the LanguageServer class using Expert, the official Elixir language server.
"""
@override
def _get_wait_time_for_cross_file_referencing(self) -> float:
return 10.0 # Elixir projects need a lot of time to compile and index before cross-file references work
return 10.0 # Elixir projects need time to compile and index before cross-file references work
@override
def is_ignored_dirname(self, dirname: str) -> bool:
@@ -37,7 +37,8 @@ class ElixirTools(SolidLanguageServer):
# - node_modules: if the project has JavaScript components
# - .elixir_ls: ElixirLS artifacts (in case both are present)
# - cover: coverage reports
return super().is_ignored_dirname(dirname) or dirname in ["_build", "deps", "node_modules", ".elixir_ls", "cover"]
# - .expert: Expert artifacts
return super().is_ignored_dirname(dirname) or dirname in ["_build", "deps", "node_modules", ".elixir_ls", ".expert", "cover"]
@override
def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool:
@@ -48,42 +49,6 @@ class ElixirTools(SolidLanguageServer):
return super().is_ignored_path(relative_path, ignore_unsupported_files)
@staticmethod
def _is_next_ls_internal_file(abs_path: str) -> bool:
"""Check if an absolute path is a Next LS internal file that should be ignored."""
return any(
pattern in abs_path
for pattern in [
".burrito", # Next LS runtime directory
"next_ls_erts-", # Next LS Erlang runtime
"_next_ls_private_", # Next LS private files
"/priv/monkey/", # Next LS monkey patching directory
]
)
@override
def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
"""Override to filter out Next LS internal files from references."""
from solidlsp.ls_utils import PathUtils
# Get the raw response from the parent implementation
raw_response = super()._send_references_request(relative_file_path, line, column)
if raw_response is None:
return None
# Filter out Next LS internal files
filtered_response = []
for item in raw_response:
if isinstance(item, dict) and "uri" in item:
abs_path = PathUtils.uri_to_path(item["uri"])
if self._is_next_ls_internal_file(abs_path):
self.logger.log(f"Filtering out Next LS internal file: {abs_path}", logging.DEBUG)
continue
filtered_response.append(item)
return filtered_response
@classmethod
def _get_elixir_version(cls) -> str | None:
"""Get the installed Elixir version or None if not found."""
@@ -96,12 +61,10 @@ class ElixirTools(SolidLanguageServer):
return None
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
"""
Setup runtime dependencies for Next LS.
Downloads the Next LS binary for the current platform and returns the path to the executable.
Setup runtime dependencies for Expert.
Downloads the Expert binary for the current platform and returns the path to the executable.
"""
# Check if Elixir is available first
elixir_version = cls._get_elixir_version()
@@ -110,118 +73,139 @@ class ElixirTools(SolidLanguageServer):
"Elixir is not installed. Please install Elixir from https://elixir-lang.org/install.html and make sure it is added to your PATH."
)
logger.log(f"Found Elixir: {elixir_version}", logging.INFO)
log.info(f"Found Elixir: {elixir_version}")
# First, check if expert is already in PATH (user may have installed it manually)
import shutil
expert_in_path = shutil.which("expert")
if expert_in_path:
log.info(f"Found Expert in PATH: {expert_in_path}")
return expert_in_path
platform_id = PlatformUtils.get_platform_id()
# Check for Windows and provide a helpful error message
if platform_id.value.startswith("win"):
raise RuntimeError(
"Windows is not supported by Next LS. The Next LS project does not provide Windows binaries. "
"Consider using Windows Subsystem for Linux (WSL) or a virtual machine with Linux/macOS."
)
valid_platforms = [
PlatformId.LINUX_x64,
PlatformId.LINUX_arm64,
PlatformId.OSX_x64,
PlatformId.OSX_arm64,
PlatformId.WIN_x64,
PlatformId.WIN_arm64,
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Next LS at the moment"
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Expert at the moment"
next_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "next-ls")
expert_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "expert")
NEXTLS_VERSION = "v0.23.4"
EXPERT_VERSION = "nightly"
# Define runtime dependencies inline
runtime_deps = {
PlatformId.LINUX_x64: RuntimeDependency(
id="next_ls_linux_amd64",
id="expert_linux_amd64",
platform_id="linux-x64",
url=f"https://github.com/elixir-tools/next-ls/releases/download/{NEXTLS_VERSION}/next_ls_linux_amd64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_linux_amd64",
archive_type="binary",
binary_name="next_ls_linux_amd64",
extract_path="next_ls",
binary_name="expert_linux_amd64",
extract_path="expert",
),
PlatformId.LINUX_arm64: RuntimeDependency(
id="next_ls_linux_arm64",
id="expert_linux_arm64",
platform_id="linux-arm64",
url=f"https://github.com/elixir-tools/next-ls/releases/download/{NEXTLS_VERSION}/next_ls_linux_arm64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_linux_arm64",
archive_type="binary",
binary_name="next_ls_linux_arm64",
extract_path="next_ls",
binary_name="expert_linux_arm64",
extract_path="expert",
),
PlatformId.OSX_x64: RuntimeDependency(
id="next_ls_darwin_amd64",
id="expert_darwin_amd64",
platform_id="osx-x64",
url=f"https://github.com/elixir-tools/next-ls/releases/download/{NEXTLS_VERSION}/next_ls_darwin_amd64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_darwin_amd64",
archive_type="binary",
binary_name="next_ls_darwin_amd64",
extract_path="next_ls",
binary_name="expert_darwin_amd64",
extract_path="expert",
),
PlatformId.OSX_arm64: RuntimeDependency(
id="next_ls_darwin_arm64",
id="expert_darwin_arm64",
platform_id="osx-arm64",
url=f"https://github.com/elixir-tools/next-ls/releases/download/{NEXTLS_VERSION}/next_ls_darwin_arm64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_darwin_arm64",
archive_type="binary",
binary_name="next_ls_darwin_arm64",
extract_path="next_ls",
binary_name="expert_darwin_arm64",
extract_path="expert",
),
PlatformId.WIN_x64: RuntimeDependency(
id="expert_windows_amd64",
platform_id="win-x64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_windows_amd64.exe",
archive_type="binary",
binary_name="expert_windows_amd64.exe",
extract_path="expert.exe",
),
PlatformId.WIN_arm64: RuntimeDependency(
id="expert_windows_arm64",
platform_id="win-arm64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_windows_arm64.exe",
archive_type="binary",
binary_name="expert_windows_arm64.exe",
extract_path="expert.exe",
),
}
dependency = runtime_deps[platform_id]
executable_path = os.path.join(next_ls_dir, "nextls")
# On Windows, use .exe extension
executable_name = "expert.exe" if platform_id.value.startswith("win") else "expert"
executable_path = os.path.join(expert_dir, executable_name)
assert dependency.binary_name is not None
binary_path = os.path.join(next_ls_dir, dependency.binary_name)
binary_path = os.path.join(expert_dir, dependency.binary_name)
if not os.path.exists(executable_path):
logger.log(f"Downloading Next LS binary from {dependency.url}", logging.INFO)
log.info(f"Downloading Expert binary from {dependency.url}")
assert dependency.url is not None
FileUtils.download_file(logger, dependency.url, binary_path)
FileUtils.download_file(dependency.url, binary_path)
# Make the binary executable on Unix-like systems
os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
if not platform_id.value.startswith("win"):
os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
# Create a symlink with the expected name
if binary_path != executable_path:
# Create a symlink with the expected name on Unix-like systems
if binary_path != executable_path and not platform_id.value.startswith("win"):
if os.path.exists(executable_path):
os.remove(executable_path)
os.symlink(os.path.basename(binary_path), executable_path)
assert os.path.exists(executable_path), f"Next LS executable not found at {executable_path}"
assert os.path.exists(executable_path), f"Expert executable not found at {executable_path}"
logger.log(f"Next LS binary ready at: {executable_path}", logging.INFO)
log.info(f"Expert binary ready at: {executable_path}")
return executable_path
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
nextls_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
expert_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=f'"{nextls_executable_path}" --stdio', cwd=repository_root_path),
ProcessLaunchInfo(cmd=f"{expert_executable_path} --stdio", cwd=repository_root_path),
"elixir",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.request_id = 0
# Set generous timeout for Next LS which can be slow to initialize and respond
self.set_request_timeout(180.0) # 60 seconds for all environments
# Set generous timeout for Expert which can be slow to initialize and respond
self.set_request_timeout(180.0)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Next LS Language Server.
Returns the initialize params for the Expert Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
# Ensure the path is absolute
abs_path = os.path.abspath(repository_absolute_path)
root_uri = pathlib.Path(abs_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"locale": "en",
"rootPath": repository_absolute_path,
"rootPath": abs_path,
"rootUri": root_uri,
"initializationOptions": {
"mix_env": "dev",
@@ -279,92 +263,80 @@ class ElixirTools(SolidLanguageServer):
return cast(InitializeParams, initialize_params)
def _start_server(self) -> None:
"""Start Next LS server process"""
"""Start Expert server process"""
def register_capability_handler(params: Any) -> None:
log.debug(f"LSP: client/registerCapability: {params}")
return
def window_log_message(msg: Any) -> None:
"""Handle window/logMessage notifications from Next LS"""
"""Handle window/logMessage notifications from Expert"""
message_type = msg.get("type", 4) # 1=Error, 2=Warning, 3=Info, 4=Log
message_text = msg.get("message", "")
self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO)
# Check for the specific Next LS readiness signal
# Based on Next LS source: "Runtime for folder #{name} is ready..."
if "Runtime for folder" in message_text and "is ready..." in message_text:
self.logger.log("Next LS runtime is ready based on official log message", logging.INFO)
self.server_ready.set()
def do_nothing(params: Any) -> None:
return
# Log at appropriate level based on message type
if message_type == 1:
log.error(f"Expert: {message_text}")
elif message_type == 2:
log.warning(f"Expert: {message_text}")
else:
log.debug(f"Expert: {message_text}")
def check_server_ready(params: Any) -> None:
"""
Handle $/progress notifications from Next LS.
Keep as fallback for error detection, but primary readiness detection
is now done via window/logMessage handler.
Handle $/progress notifications from Expert.
Expert sends progress updates during compilation and indexing.
The server is considered ready when project build completes.
"""
value = params.get("value", {})
kind = value.get("kind", "")
title = value.get("title", "")
# Check for initialization completion progress (fallback signal)
if value.get("kind") == "end":
message = value.get("message", "")
if "has initialized!" in message:
self.logger.log("Next LS initialization progress completed", logging.INFO)
# Note: We don't set server_ready here - we wait for the log message
if kind == "begin":
# Track when building the project starts (not "Building engine")
if title.startswith("Building ") and not title.startswith("Building engine"):
self._building_project = True
elif kind == "end":
# Project build completion is the main readiness signal
if getattr(self, "_building_project", False):
log.debug("Expert project build completed - server is ready")
self._building_project = False
self.server_ready.set()
def work_done_progress(params: Any) -> None:
"""
Handle $/workDoneProgress notifications from Next LS.
Keep for completeness but primary readiness detection is via window/logMessage.
"""
value = params.get("value", {})
if value.get("kind") == "end":
self.logger.log("Next LS work done progress completed", logging.INFO)
# Note: We don't set server_ready here - we wait for the log message
def work_done_progress_create(params: Any) -> None:
"""Handle window/workDoneProgress/create requests from Expert."""
return
def publish_diagnostics(params: Any) -> None:
"""Handle textDocument/publishDiagnostics notifications."""
return
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", check_server_ready)
self.server.on_request("window/workDoneProgress/create", do_nothing)
self.server.on_notification("$/workDoneProgress", work_done_progress)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.server.on_request("window/workDoneProgress/create", work_done_progress_create)
self.server.on_notification("textDocument/publishDiagnostics", publish_diagnostics)
self.logger.log("Starting Next LS server process", logging.INFO)
log.debug("Starting Expert server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.debug("Sending initialize request to Expert")
init_response = self.server.send.initialize(initialize_params)
# Verify server capabilities - be more lenient with Next LS
self.logger.log(f"Next LS capabilities: {list(init_response['capabilities'].keys())}", logging.INFO)
# Next LS may not provide all capabilities immediately, so we check for basic ones
# Verify basic server capabilities
assert "textDocumentSync" in init_response["capabilities"], f"Missing textDocumentSync in {init_response['capabilities']}"
# Some capabilities might be optional or provided later. This is expected, so we log as info
if "completionProvider" not in init_response["capabilities"]:
self.logger.log("completionProvider not available in initial capabilities", logging.INFO)
if "definitionProvider" not in init_response["capabilities"]:
self.logger.log("definitionProvider not available in initial capabilities", logging.INFO)
self.server.notify.initialized({})
self.completions_available.set()
# Wait for Next LS to send the specific "Runtime for folder X is ready..." log message
# This is the authoritative signal that Next LS is truly ready for requests
ready_timeout = 180.0
self.logger.log(f"Waiting up to {ready_timeout} seconds for Next LS runtime readiness...", logging.INFO)
# Expert needs time to compile the project and build indexes on first run.
# This can take 2-3+ minutes for mid-sized codebases.
# After the first run, subsequent startups are much faster.
ready_timeout = 300.0 # 5 minutes
log.debug(f"Waiting up to {ready_timeout}s for Expert to compile and index...")
if self.server_ready.wait(timeout=ready_timeout):
self.logger.log("Next LS is ready and available for requests", logging.INFO)
log.debug("Expert is ready for requests")
else:
error_msg = f"Next LS failed to initialize within {ready_timeout} seconds. This may indicate a problem with the Elixir installation, project compilation, or Next LS itself."
self.logger.log(error_msg, logging.ERROR)
raise RuntimeError(error_msg)
log.warning(f"Expert did not signal readiness within {ready_timeout}s. Proceeding with requests anyway.")
self.server_ready.set() # Mark as ready anyway to allow requests
@@ -13,33 +13,27 @@ from sensai.util.logging import LogTime
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
class ElmLanguageServer(SolidLanguageServer):
"""
Provides Elm specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Elm.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates an ElmLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
elm_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
elm_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=elm_lsp_executable_path, cwd=repository_root_path),
"elm",
solidlsp_settings,
config, repository_root_path, ProcessLaunchInfo(cmd=elm_lsp_executable_path, cwd=repository_root_path), "elm", solidlsp_settings
)
self.server_ready = threading.Event()
@@ -53,16 +47,14 @@ class ElmLanguageServer(SolidLanguageServer):
]
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> list[str]:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]:
"""
Setup runtime dependencies for Elm Language Server and return the command to start the server.
"""
# Check if elm-language-server is already installed globally
system_elm_ls = shutil.which("elm-language-server")
if system_elm_ls:
logger.log(f"Found system-installed elm-language-server at {system_elm_ls}", logging.INFO)
log.info(f"Found system-installed elm-language-server at {system_elm_ls}")
return [system_elm_ls, "--stdio"]
# Verify node and npm are installed
@@ -86,9 +78,9 @@ class ElmLanguageServer(SolidLanguageServer):
elm_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "elm-lsp")
elm_ls_executable_path = os.path.join(elm_ls_dir, "node_modules", ".bin", "elm-language-server")
if not os.path.exists(elm_ls_executable_path):
logger.log(f"Elm Language Server executable not found at {elm_ls_executable_path}. Installing...", logging.INFO)
with LogTime("Installation of Elm language server dependencies", logger=logger.logger):
deps.install(logger, elm_ls_dir)
log.info(f"Elm Language Server executable not found at {elm_ls_executable_path}. Installing...")
with LogTime("Installation of Elm language server dependencies", logger=log):
deps.install(elm_ls_dir)
if not os.path.exists(elm_ls_executable_path):
raise FileNotFoundError(
@@ -154,20 +146,17 @@ class ElmLanguageServer(SolidLanguageServer):
return
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting Elm server process", logging.INFO)
log.info("Starting Elm server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
# Elm-specific capability checks
@@ -178,11 +167,11 @@ class ElmLanguageServer(SolidLanguageServer):
assert "documentSymbolProvider" in init_response["capabilities"]
self.server.notify.initialized({})
self.logger.log("Elm server initialized successfully, waiting for workspace scan...", logging.INFO)
log.info("Elm server initialized successfully, waiting for workspace scan...")
self.server_ready.set()
self.completions_available.set()
self.logger.log("Elm server ready", logging.INFO)
log.info("Elm server ready")
@override
def _get_wait_time_for_cross_file_referencing(self) -> float:
@@ -11,21 +11,16 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
class ErlangLanguageServer(SolidLanguageServer):
"""Language server for Erlang using Erlang LS."""
def __init__(
self,
config: LanguageServerConfig,
logger: LanguageServerLogger,
repository_root_path: str,
solidlsp_settings: SolidLSPSettings,
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates an ErlangLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
@@ -39,7 +34,6 @@ class ErlangLanguageServer(SolidLanguageServer):
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=[self.erlang_ls_path, "--transport", "stdio"], cwd=repository_root_path),
"erlang",
@@ -89,7 +83,7 @@ class ErlangLanguageServer(SolidLanguageServer):
def window_log_message(msg: dict) -> None:
"""Handle window/logMessage notifications from Erlang LS"""
message_text = msg.get("message", "")
self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO)
log.info(f"LSP: window/logMessage: {message_text}")
# Look for Erlang LS readiness signals
# Common patterns: "Started Erlang LS", "initialized", "ready"
@@ -105,7 +99,7 @@ class ErlangLanguageServer(SolidLanguageServer):
message_lower = message_text.lower()
for signal in readiness_signals:
if signal.lower() in message_lower:
self.logger.log(f"Erlang LS readiness signal detected: {message_text}", logging.INFO)
log.info(f"Erlang LS readiness signal detected: {message_text}")
self.server_ready.set()
break
@@ -120,7 +114,7 @@ class ErlangLanguageServer(SolidLanguageServer):
if value.get("kind") == "end":
message = value.get("message", "")
if any(word in message.lower() for word in ["initialized", "ready", "complete"]):
self.logger.log("Erlang LS initialization progress completed", logging.INFO)
log.info("Erlang LS initialization progress completed")
# Set as fallback if no window/logMessage was received
if not self.server_ready.is_set():
self.server_ready.set()
@@ -133,7 +127,7 @@ class ErlangLanguageServer(SolidLanguageServer):
self.server.on_notification("$/workDoneProgress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting Erlang LS server process", logging.INFO)
log.info("Starting Erlang LS server process")
self.server.start()
# Send initialize request
@@ -153,12 +147,12 @@ class ErlangLanguageServer(SolidLanguageServer):
},
}
self.logger.log("Sending initialize request to Erlang LS", logging.INFO)
log.info("Sending initialize request to Erlang LS")
init_response = self.server.send.initialize(initialize_params) # type: ignore[arg-type]
# Verify server capabilities
if "capabilities" in init_response:
self.logger.log(f"Erlang LS capabilities: {list(init_response['capabilities'].keys())}", logging.INFO)
log.info(f"Erlang LS capabilities: {list(init_response['capabilities'].keys())}")
self.server.notify.initialized({})
self.completions_available.set()
@@ -178,28 +172,26 @@ class ErlangLanguageServer(SolidLanguageServer):
ready_timeout = 60.0 # 1 minute for local
env_desc = "local"
self.logger.log(f"Waiting up to {ready_timeout} seconds for Erlang LS readiness ({env_desc} environment)...", logging.INFO)
log.info(f"Waiting up to {ready_timeout} seconds for Erlang LS readiness ({env_desc} environment)...")
if self.server_ready.wait(timeout=ready_timeout):
self.logger.log("Erlang LS is ready and available for requests", logging.INFO)
log.info("Erlang LS is ready and available for requests")
# Add settling period for indexing - adjust based on environment
settling_time = 15.0 if is_ci else 5.0
self.logger.log(f"Allowing {settling_time} seconds for Erlang LS indexing to complete...", logging.INFO)
log.info(f"Allowing {settling_time} seconds for Erlang LS indexing to complete...")
time.sleep(settling_time)
self.logger.log("Erlang LS settling period complete", logging.INFO)
log.info("Erlang LS settling period complete")
else:
# Set ready anyway and continue - Erlang LS might not send explicit ready messages
self.logger.log(
f"Erlang LS readiness timeout reached after {ready_timeout}s, proceeding anyway (common in CI)", logging.WARNING
)
log.warning(f"Erlang LS readiness timeout reached after {ready_timeout}s, proceeding anyway (common in CI)")
self.server_ready.set()
# Still give some time for basic initialization even without explicit readiness signal
basic_settling_time = 20.0 if is_ci else 10.0
self.logger.log(f"Allowing {basic_settling_time} seconds for basic Erlang LS initialization...", logging.INFO)
log.info(f"Allowing {basic_settling_time} seconds for basic Erlang LS initialization...")
time.sleep(basic_settling_time)
self.logger.log("Basic Erlang LS initialization period complete", logging.INFO)
log.info("Basic Erlang LS initialization period complete")
@override
def is_ignored_dirname(self, dirname: str) -> bool:
@@ -14,11 +14,12 @@ from overrides import override
from solidlsp import ls_types
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class FortranLanguageServer(SolidLanguageServer):
"""Fortran Language Server implementation using fortls."""
@@ -127,10 +128,7 @@ class FortranLanguageServer(SolidLanguageServer):
corrected_symbol = symbol.copy()
corrected_symbol["selectionRange"] = new_sel_range # type: ignore[typeddict-item]
self.logger.log(
f"Fixed fortls selectionRange for {identifier_name}: char {start_char} -> {identifier_start}",
logging.DEBUG,
)
log.debug(f"Fixed fortls selectionRange for {identifier_name}: char {start_char} -> {identifier_start}")
return corrected_symbol
@@ -181,13 +179,7 @@ class FortranLanguageServer(SolidLanguageServer):
raise RuntimeError("fortls is not installed or not in PATH.\nInstall it with: pip install fortls")
return fortls_path
def __init__(
self,
config: LanguageServerConfig,
logger: LanguageServerLogger,
repository_root_path: str,
solidlsp_settings: SolidLSPSettings,
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
# Check fortls installation
fortls_path = self._check_fortls_installation()
@@ -196,12 +188,7 @@ class FortranLanguageServer(SolidLanguageServer):
fortls_cmd = f"{fortls_path}"
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=fortls_cmd, cwd=repository_root_path),
"fortran", # Language ID for LSP
solidlsp_settings,
config, repository_root_path, ProcessLaunchInfo(cmd=fortls_cmd, cwd=repository_root_path), "fortran", solidlsp_settings
)
self.server_ready = threading.Event()
@@ -261,7 +248,7 @@ class FortranLanguageServer(SolidLanguageServer):
"""Start Fortran Language Server process."""
def window_log_message(msg: dict) -> None:
self.logger.log(f"Fortran LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"Fortran LSP: window/logMessage: {msg}")
def do_nothing(params: dict) -> None:
return
@@ -275,14 +262,11 @@ class FortranLanguageServer(SolidLanguageServer):
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting Fortran Language Server (fortls) process", logging.INFO)
log.info("Starting Fortran Language Server (fortls) process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request to Fortran Language Server",
logging.INFO,
)
log.info("Sending initialize request to Fortran Language Server")
init_response = self.server.send.initialize(initialize_params)
@@ -290,17 +274,17 @@ class FortranLanguageServer(SolidLanguageServer):
capabilities = init_response.get("capabilities", {})
assert "textDocumentSync" in capabilities
if "completionProvider" in capabilities:
self.logger.log("Fortran LSP completion provider available", logging.INFO)
log.info("Fortran LSP completion provider available")
if "definitionProvider" in capabilities:
self.logger.log("Fortran LSP definition provider available", logging.INFO)
log.info("Fortran LSP definition provider available")
if "referencesProvider" in capabilities:
self.logger.log("Fortran LSP references provider available", logging.INFO)
log.info("Fortran LSP references provider available")
if "documentSymbolProvider" in capabilities:
self.logger.log("Fortran LSP document symbol provider available", logging.INFO)
log.info("Fortran LSP document symbol provider available")
self.server.notify.initialized({})
self.completions_available.set()
# Fortran Language Server is ready after initialization
self.server_ready.set()
self.logger.log("Fortran Language Server initialization complete", logging.INFO)
log.info("Fortran Language Server initialization complete")
@@ -0,0 +1,382 @@
"""
Provides F# specific instantiation of the LanguageServer class.
"""
import logging
import os
import pathlib
import shutil
import threading
from pathlib import Path
from overrides import override
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
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__)
class FSharpLanguageServer(SolidLanguageServer):
"""
Provides F# specific instantiation of the LanguageServer class using Ionide LSP (FsAutoComplete).
Contains various configurations and settings specific to F# development.
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates an FSharpLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
fsharp_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config,
repository_root_path,
ProcessLaunchInfo(cmd=fsharp_lsp_executable_path, cwd=repository_root_path),
"fsharp",
solidlsp_settings,
)
self.server_ready = threading.Event()
self.initialize_searcher_command_available = threading.Event()
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in [
"bin",
"obj",
"packages",
".paket",
"paket-files",
".fake",
".ionide",
]
@classmethod
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
"""
Setup runtime dependencies for F# Language Server and return the command to start the server.
"""
# First check if .NET SDK is installed
dotnet_exe = shutil.which("dotnet")
if not dotnet_exe:
raise RuntimeError(
".NET SDK is not installed or not in PATH. Please install .NET SDK 8.0 or later and ensure 'dotnet' is in your PATH."
)
# Verify dotnet version
import subprocess
try:
result = subprocess.run([dotnet_exe, "--version"], capture_output=True, text=True, check=True)
log.info(f"Found .NET SDK version: {result.stdout.strip()}")
except subprocess.CalledProcessError:
raise RuntimeError("Failed to get .NET SDK version. Please ensure .NET SDK is properly installed.")
RuntimeDependencyCollection(
[
RuntimeDependency(
id="fsautocomplete",
description="FsAutoComplete (Ionide F# Language Server)",
command="dotnet tool install --tool-path ./ fsautocomplete",
platform_id="any",
),
]
)
# Install FsAutoComplete if not already installed
fsharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "fsharp-lsp")
fsautocomplete_path = os.path.join(fsharp_ls_dir, "fsautocomplete")
# Handle Windows executable extension
if os.name == "nt":
fsautocomplete_path += ".exe"
if not os.path.exists(fsautocomplete_path):
log.info(f"FsAutoComplete executable not found at {fsautocomplete_path}. Installing...")
# Ensure the directory exists
os.makedirs(fsharp_ls_dir, exist_ok=True)
# Install FsAutoComplete using dotnet tool install
try:
import subprocess
result = subprocess.run(
[dotnet_exe, "tool", "install", "--tool-path", fsharp_ls_dir, "fsautocomplete"],
cwd=fsharp_ls_dir,
capture_output=True,
text=True,
check=True,
)
log.info("FsAutoComplete installed successfully")
log.debug(f"Installation output: {result.stdout}")
except subprocess.CalledProcessError as e:
log.error(f"Failed to install FsAutoComplete: {e.stderr}")
raise RuntimeError(f"Failed to install FsAutoComplete: {e.stderr}")
if not os.path.exists(fsautocomplete_path):
raise FileNotFoundError(
f"FsAutoComplete executable not found at {fsautocomplete_path}, something went wrong with the installation."
)
# FsAutoComplete uses --lsp flag for LSP mode
return f"{fsautocomplete_path} --adaptive-lsp-server-enabled --project-graph-enabled --use-fcs-transparent-compiler"
def _get_initialize_params(self) -> InitializeParams:
"""
Returns the initialize params for the F# Language Server.
"""
root_uri = pathlib.Path(self.repository_root_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"rootPath": self.repository_root_path,
"rootUri": root_uri,
"workspaceFolders": [{"name": "workspace", "uri": root_uri}],
"capabilities": {
"workspace": {
"applyEdit": True,
"workspaceEdit": {"documentChanges": True},
"didChangeConfiguration": {"dynamicRegistration": True},
"didChangeWatchedFiles": {"dynamicRegistration": True},
"symbol": {"dynamicRegistration": True},
"executeCommand": {"dynamicRegistration": True},
"configuration": True,
"workspaceFolders": True,
},
"textDocument": {
"synchronization": {
"dynamicRegistration": True,
"willSave": True,
"willSaveWaitUntil": True,
"didSave": True,
},
"completion": {
"dynamicRegistration": True,
"contextSupport": True,
"completionItem": {
"snippetSupport": True,
"commitCharactersSupport": True,
"documentationFormat": ["markdown", "plaintext"],
"deprecatedSupport": True,
},
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"],
},
"signatureHelp": {
"dynamicRegistration": True,
"signatureInformation": {"documentationFormat": ["markdown", "plaintext"]},
},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentHighlight": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 26))}, # All SymbolKind values
"hierarchicalDocumentSymbolSupport": True,
},
"codeAction": {
"dynamicRegistration": True,
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"",
"quickfix",
"refactor",
"refactor.extract",
"refactor.inline",
"refactor.rewrite",
"source",
"source.organizeImports",
]
}
},
},
"codeLens": {"dynamicRegistration": True},
"formatting": {"dynamicRegistration": True},
"rangeFormatting": {"dynamicRegistration": True},
"onTypeFormatting": {"dynamicRegistration": True},
"rename": {"dynamicRegistration": True},
"documentLink": {"dynamicRegistration": True},
"publishDiagnostics": {
"relatedInformation": True,
"versionSupport": False,
"tagSupport": {"valueSet": [1, 2]},
},
"implementation": {"dynamicRegistration": True},
"typeDefinition": {"dynamicRegistration": True},
"colorProvider": {"dynamicRegistration": True},
"foldingRange": {
"dynamicRegistration": True,
"rangeLimit": 5000,
"lineFoldingOnly": True,
},
"declaration": {"dynamicRegistration": True},
"selectionRange": {"dynamicRegistration": True},
},
"window": {
"workDoneProgress": True,
},
},
"initializationOptions": {
# F# specific initialization options
"automaticWorkspaceInit": True,
"abstractClassStubGeneration": True,
"abstractClassStubGenerationObjectIdentifier": "this",
"abstractClassStubGenerationMethodBody": 'failwith "Not Implemented"',
"addFsiWatcher": False,
"codeLenses": {"signature": {"enabled": True}, "references": {"enabled": True}},
"disableInMemoryProjectReferences": False,
"dotNetRoot": self._get_dotnet_root(),
"enableMSBuildProjectGraph": False,
"excludeProjectDirectories": ["paket-files"],
"externalAutocomplete": False,
"fsac": {"attachDebugger": False, "silencedLogs": [], "conserveMemory": False, "netCoreDllPath": ""},
"fsiExtraParameters": [],
"generateBinlog": False,
"interfaceStubGeneration": True,
"interfaceStubGenerationObjectIdentifier": "this",
"interfaceStubGenerationMethodBody": 'failwith "Not Implemented"',
"keywordsAutocomplete": True,
"linter": True,
"pipelineHints": {"enabled": True},
"recordStubGeneration": True,
"recordStubGenerationBody": 'failwith "Not Implemented"',
"resolveNamespaces": True,
"saveOnlyOpenFiles": False,
"showProjectExplorerIn": ["ionide", "solution"],
"simplifyNameAnalyzer": True,
"smartIndent": False,
"suggestGitignore": True,
"suggestSdkScripts": True,
"unionCaseStubGeneration": True,
"unionCaseStubGenerationBody": 'failwith "Not Implemented"',
"unusedDeclarationsAnalyzer": True,
"unusedOpensAnalyzer": True,
"verboseLogging": False,
"workspaceModePeekDeepLevel": 2,
"workspacePath": self.repository_root_path,
},
"trace": "off",
}
return initialize_params # type: ignore
def _get_dotnet_root(self) -> str:
"""
Get the .NET root directory.
"""
dotnet_exe = shutil.which("dotnet")
if dotnet_exe:
# Try to get the installation path
try:
import subprocess
result = subprocess.run([dotnet_exe, "--info"], capture_output=True, text=True, check=True)
lines = result.stdout.split("\n")
for line in lines:
if "Base Path:" in line or "Base path:" in line:
base_path = line.split(":", 1)[1].strip()
# Get the parent directory (remove 'sdk/version' part)
return str(Path(base_path).parent.parent)
except (subprocess.CalledProcessError, Exception):
pass
# Fallback: use the directory containing dotnet executable
if dotnet_exe:
return str(Path(dotnet_exe).parent)
return ""
def _start_server(self) -> None:
"""
Start the F# Language Server with custom handlers.
"""
def handle_window_log_message(params: dict) -> None:
"""Handle window/logMessage from the LSP server."""
message = params.get("message", "")
message_type = params.get("type", 1)
# Map LSP log levels to Python logging levels
level_map = {1: logging.ERROR, 2: logging.WARNING, 3: logging.INFO, 4: logging.DEBUG}
level = level_map.get(message_type, logging.INFO)
log.log(level, f"FsAutoComplete: {message}")
def handle_window_show_message(params: dict) -> None:
"""Handle window/showMessage from the LSP server."""
message = params.get("message", "")
message_type = params.get("type", 1)
# Map LSP message types to Python logging levels
level_map = {1: logging.ERROR, 2: logging.WARNING, 3: logging.INFO, 4: logging.DEBUG}
level = level_map.get(message_type, logging.INFO)
log.log(level, f"FsAutoComplete Message: {message}")
def handle_workspace_configuration(params: dict) -> list:
"""Handle workspace/configuration requests from the LSP server."""
# Return empty configuration for now
items = params.get("items", [])
return [None] * len(items)
def handle_client_register_capability(params: dict) -> None:
"""Handle client/registerCapability requests from the LSP server."""
# For now, just acknowledge the registration
return
def handle_client_unregister_capability(params: dict) -> None:
"""Handle client/unregisterCapability requests from the LSP server."""
# For now, just acknowledge the unregistration
return
def handle_work_done_progress_create(params: dict) -> None:
"""Handle window/workDoneProgress/create requests from the LSP server."""
# Just acknowledge the request - we don't need to track progress for now
return
# Register custom handlers
self.server.on_notification("window/logMessage", handle_window_log_message)
self.server.on_notification("window/showMessage", handle_window_show_message)
self.server.on_request("workspace/configuration", handle_workspace_configuration)
self.server.on_request("client/registerCapability", handle_client_register_capability)
self.server.on_request("client/unregisterCapability", handle_client_unregister_capability)
self.server.on_request("window/workDoneProgress/create", handle_work_done_progress_create)
log.info("Starting FsAutoComplete F# language server process")
try:
self.server.start()
except Exception as e:
log.error(f"Failed to start F# language server process: {e}")
raise SolidLSPException(f"Failed to start F# language server: {e}")
# Send initialization
initialize_params = self._get_initialize_params()
log.info("Sending initialize request to F# language server")
try:
self.server.send.initialize(initialize_params)
log.debug("Received initialize response from F# language server")
except Exception as e:
raise SolidLSPException(f"Failed to initialize F# language server for {self.repository_root_path}: {e}") from e
# Complete initialization
self.server.notify.initialized({})
log.info("F# language server initialized successfully")
@override
def _get_wait_time_for_cross_file_referencing(self) -> float:
"""
F# projects can be large and may need more time for cross-file analysis.
"""
return 15.0 # 15 seconds should be sufficient for most F# projects
+7 -18
View File
@@ -9,11 +9,12 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class Gopls(SolidLanguageServer):
"""
@@ -90,19 +91,10 @@ class Gopls(SolidLanguageServer):
return True
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
self._setup_runtime_dependency()
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path),
"go",
solidlsp_settings,
)
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), "go", solidlsp_settings)
self.server_ready = threading.Event()
self.request_id = 0
@@ -145,7 +137,7 @@ class Gopls(SolidLanguageServer):
return
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(params: dict) -> None:
return
@@ -155,14 +147,11 @@ class Gopls(SolidLanguageServer):
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting gopls server process", logging.INFO)
log.info("Starting gopls server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
# Verify server capabilities
@@ -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()
@@ -13,11 +13,12 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class HaskellLanguageServer(SolidLanguageServer):
"""
@@ -71,14 +72,12 @@ class HaskellLanguageServer(SolidLanguageServer):
" - Homebrew (macOS): brew install haskell-language-server"
)
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a HaskellLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
hls_executable_path = self._ensure_hls_installed()
logger.log(f"Using haskell-language-server at: {hls_executable_path}", logging.INFO)
log.info(f"Using haskell-language-server at: {hls_executable_path}")
# Check if there's a haskell subdirectory with Stack/Cabal project
haskell_subdir = os.path.join(repository_root_path, "haskell")
@@ -86,7 +85,7 @@ class HaskellLanguageServer(SolidLanguageServer):
os.path.exists(os.path.join(haskell_subdir, "stack.yaml")) or os.path.exists(os.path.join(haskell_subdir, "cabal.project"))
):
working_dir = haskell_subdir
logger.log(f"Using Haskell project directory: {working_dir}", logging.INFO)
log.info(f"Using Haskell project directory: {working_dir}")
else:
working_dir = repository_root_path
@@ -98,7 +97,6 @@ class HaskellLanguageServer(SolidLanguageServer):
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=[hls_executable_path, "--lsp", "--cwd", working_dir], cwd=working_dir, env=env),
"haskell",
@@ -322,19 +320,19 @@ class HaskellLanguageServer(SolidLanguageServer):
return
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
def register_capability_handler(params: dict) -> None:
"""Handle dynamic capability registration from HLS"""
if "registrations" in params:
for registration in params.get("registrations", []):
method = registration.get("method", "")
self.logger.log(f"HLS registered capability: {method}", logging.INFO)
log.info(f"HLS registered capability: {method}")
return
def workspace_configuration_handler(params: dict) -> Any:
"""Handle workspace/configuration requests from HLS"""
self.logger.log(f"HLS requesting configuration: {params}", logging.INFO)
log.info(f"HLS requesting configuration: {params}")
# Configuration matching VS Code settings and initialization options
haskell_config = {
@@ -352,7 +350,7 @@ class HaskellLanguageServer(SolidLanguageServer):
result.append(haskell_config)
else:
result.append({})
self.logger.log(f"Returning configuration: {result}", logging.INFO)
log.info(f"Returning configuration: {result}")
return result
# Fallback: return single config
@@ -364,26 +362,23 @@ class HaskellLanguageServer(SolidLanguageServer):
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_request("workspace/configuration", workspace_configuration_handler)
self.logger.log("Starting Haskell Language Server process", logging.INFO)
log.info("Starting Haskell Language Server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
# Log capabilities returned by HLS
capabilities = init_response.get("capabilities", {})
self.logger.log(f"HLS capabilities: {list(capabilities.keys())}", logging.INFO)
log.info(f"HLS capabilities: {list(capabilities.keys())}")
self.server.notify.initialized({})
self.completions_available.set()
# Give HLS time to index the project
# HLS can be slow to index, especially on first run
self.logger.log("Waiting for HLS to index project...", logging.INFO)
log.info("Waiting for HLS to index project...")
time.sleep(5)
self.logger.log("Haskell Language Server initialized successfully", logging.INFO)
log.info("Haskell Language Server initialized successfully")
+13 -28
View File
@@ -12,7 +12,6 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, InitializeParams, LocationLink
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -21,6 +20,8 @@ from solidlsp.settings import SolidLSPSettings
from ..lsp_protocol_handler import lsp_types
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
class Intelephense(SolidLanguageServer):
"""
@@ -37,7 +38,7 @@ class Intelephense(SolidLanguageServer):
return super().is_ignored_dirname(dirname) or dirname in self._ignored_dirnames
@classmethod
def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, solidlsp_settings: SolidLSPSettings) -> list[str]:
def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> list[str]:
"""
Setup runtime dependencies for Intelephense and return the command to start the server.
"""
@@ -52,7 +53,7 @@ class Intelephense(SolidLanguageServer):
PlatformId.WIN_x64,
PlatformId.WIN_arm64,
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy PHP at the moment"
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported by {cls.__name__} at the moment"
# Verify both node and npm are installed
is_node_installed = shutil.which("node") is not None
@@ -74,7 +75,7 @@ class Intelephense(SolidLanguageServer):
)
]
)
deps.install(logger, intelephense_ls_dir)
deps.install(intelephense_ls_dir)
assert os.path.exists(
intelephense_executable_path
@@ -82,19 +83,12 @@ class Intelephense(SolidLanguageServer):
return [intelephense_executable_path, "--stdio"]
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
# Setup runtime dependencies before initializing
intelephense_cmd = self._setup_runtime_dependencies(logger, solidlsp_settings)
intelephense_cmd = self._setup_runtime_dependencies(solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path),
"php",
solidlsp_settings,
config, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php", solidlsp_settings
)
self.request_id = 0
@@ -105,10 +99,7 @@ class Intelephense(SolidLanguageServer):
self._ignored_dirnames = {"node_modules", "cache"}
if self._custom_settings.get("ignore_vendor", True):
self._ignored_dirnames.add("vendor")
self.logger.log(
f"Ignoring the following directories for PHP projects: {', '.join(sorted(self._ignored_dirnames))}",
logging.INFO,
)
log.info(f"Ignoring the following directories for PHP projects: {', '.join(sorted(self._ignored_dirnames))}")
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
@@ -157,7 +148,7 @@ class Intelephense(SolidLanguageServer):
return
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(params: dict) -> None:
return
@@ -167,19 +158,13 @@ class Intelephense(SolidLanguageServer):
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting Intelephense server process", logging.INFO)
log.info("Starting Intelephense server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
self.logger.log(
"After sent initialize params",
logging.INFO,
)
log.info("After sent initialize params")
# Verify server capabilities
assert "textDocumentSync" in init_response["capabilities"]
+6 -11
View File
@@ -11,26 +11,24 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class JediServer(SolidLanguageServer):
"""
Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path),
"python",
@@ -169,7 +167,7 @@ class JediServer(SolidLanguageServer):
self.completions_available.set()
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
self.server.on_request("client/registerCapability", do_nothing)
self.server.on_notification("language/status", do_nothing)
@@ -180,14 +178,11 @@ class JediServer(SolidLanguageServer):
self.server.on_notification("language/actionableNotification", do_nothing)
self.server.on_notification("experimental/serverStatus", check_experimental_status)
self.logger.log("Starting jedi-language-server server process", logging.INFO)
log.info("Starting jedi-language-server server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore
assert "completionProvider" in init_response["capabilities"]
+16 -29
View File
@@ -10,25 +10,20 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class JuliaLanguageServer(SolidLanguageServer):
"""
Language server implementation for Julia using LanguageServer.jl.
"""
def __init__(
self,
config: LanguageServerConfig,
logger: LanguageServerLogger,
repository_root_path: str,
solidlsp_settings: SolidLSPSettings,
):
julia_executable = self._setup_runtime_dependency(logger) # PASS LOGGER
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
julia_executable = self._setup_runtime_dependency() # PASS LOGGER
julia_code = "using LanguageServer; runserver()"
julia_ls_cmd: str | list[str]
@@ -47,19 +42,14 @@ class JuliaLanguageServer(SolidLanguageServer):
f"{shlex.quote(repository_root_path)}"
)
logger.log(f"[JULIA DEBUG] Command: {julia_ls_cmd}", logging.INFO)
log.info(f"[JULIA DEBUG] Command: {julia_ls_cmd}")
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=julia_ls_cmd, cwd=repository_root_path),
"julia",
solidlsp_settings,
config, repository_root_path, ProcessLaunchInfo(cmd=julia_ls_cmd, cwd=repository_root_path), "julia", solidlsp_settings
)
@staticmethod
def _setup_runtime_dependency(logger: LanguageServerLogger) -> str:
def _setup_runtime_dependency() -> str:
"""
Check if the Julia runtime is available and return its full path.
Raises RuntimeError with a helpful message if the dependency is missing.
@@ -94,17 +84,17 @@ class JuliaLanguageServer(SolidLanguageServer):
result = subprocess.run(check_cmd, check=False, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
# LanguageServer.jl not found, install it
JuliaLanguageServer._install_language_server(julia_path, logger)
JuliaLanguageServer._install_language_server(julia_path)
except subprocess.TimeoutExpired:
# Assume it needs installation
JuliaLanguageServer._install_language_server(julia_path, logger)
JuliaLanguageServer._install_language_server(julia_path)
return julia_path
@staticmethod
def _install_language_server(julia_path: str, logger: LanguageServerLogger) -> None:
def _install_language_server(julia_path: str) -> None:
"""Install LanguageServer.jl package."""
logger.log("LanguageServer.jl not found. Installing... (this may take a minute)", logging.INFO)
log.info("LanguageServer.jl not found. Installing... (this may take a minute)")
install_cmd = [julia_path, "-e", 'using Pkg; Pkg.add("LanguageServer")']
@@ -112,7 +102,7 @@ class JuliaLanguageServer(SolidLanguageServer):
result = subprocess.run(install_cmd, check=False, capture_output=True, text=True, timeout=300) # 5 minutes for installation
if result.returncode == 0:
logger.log("LanguageServer.jl installed successfully!", logging.INFO)
log.info("LanguageServer.jl installed successfully!")
else:
raise RuntimeError(f"Failed to install LanguageServer.jl: {result.stderr}")
except subprocess.TimeoutExpired:
@@ -158,20 +148,17 @@ class JuliaLanguageServer(SolidLanguageServer):
return
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting LanguageServer.jl server process", logging.INFO)
log.info("Starting LanguageServer.jl server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request to Julia Language Server",
logging.INFO,
)
log.info("Sending initialize request to Julia Language Server")
init_response = self.server.send.initialize(initialize_params)
assert "definitionProvider" in init_response["capabilities"]
@@ -180,4 +167,4 @@ class JuliaLanguageServer(SolidLanguageServer):
self.server.notify.initialized({})
self.completions_available.set()
self.logger.log("Julia Language Server is initialized and ready.", logging.INFO)
log.info("Julia Language Server is initialized and ready.")
@@ -1,5 +1,17 @@
"""
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
You can configure the following options in ls_specific_settings (in serena_config.yml):
ls_specific_settings:
kotlin:
jvm_options: '-Xmx4G' # JVM options for Kotlin Language Server (default: -Xmx4G)
Example configuration for large projects:
ls_specific_settings:
kotlin:
jvm_options: '-Xmx8G -XX:+UseG1GC'
"""
import dataclasses
@@ -10,13 +22,18 @@ import stat
from typing import cast
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
# Default JVM options for Kotlin Language Server
# -Xmx4G: Limit max heap to 4GB to prevent OOM on large projects
DEFAULT_KOTLIN_JVM_OPTIONS = "-Xmx4G"
@dataclasses.dataclass
class KotlinRuntimeDependencyPaths:
@@ -34,34 +51,38 @@ class KotlinLanguageServer(SolidLanguageServer):
Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a Kotlin Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
runtime_dependency_paths = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
runtime_dependency_paths = self._setup_runtime_dependencies(config, solidlsp_settings)
self.runtime_dependency_paths = runtime_dependency_paths
# Create command to execute the Kotlin Language Server script
cmd = [self.runtime_dependency_paths.kotlin_executable_path, "--stdio"]
# Set environment variables including JAVA_HOME
proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path}
# Get JVM options from settings or use default
jvm_options = DEFAULT_KOTLIN_JVM_OPTIONS
if solidlsp_settings.ls_specific_settings:
kotlin_settings = solidlsp_settings.get_ls_specific_settings(Language.KOTLIN)
custom_jvm_options = kotlin_settings.get("jvm_options", "")
if custom_jvm_options:
jvm_options = custom_jvm_options
log.info(f"Using custom JVM options for Kotlin Language Server: {jvm_options}")
# Set environment variables including JAVA_HOME and JVM options
# JAVA_TOOL_OPTIONS is automatically picked up by any Java process
proc_env = {
"JAVA_HOME": self.runtime_dependency_paths.java_home_path,
"JAVA_TOOL_OPTIONS": jvm_options,
}
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path),
"kotlin",
solidlsp_settings,
config, repository_root_path, ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), "kotlin", solidlsp_settings
)
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> KotlinRuntimeDependencyPaths:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> KotlinRuntimeDependencyPaths:
"""
Setup runtime dependencies for Kotlin Language Server and return the paths.
"""
@@ -130,8 +151,8 @@ class KotlinLanguageServer(SolidLanguageServer):
# Download and extract Java if not exists
if not os.path.exists(java_path):
logger.log(f"Downloading Java for {platform_id.value}...", logging.INFO)
FileUtils.download_and_extract_archive(logger, java_dependency["url"], java_dir, java_dependency["archiveType"])
log.info(f"Downloading Java for {platform_id.value}...")
FileUtils.download_and_extract_archive(java_dependency["url"], java_dir, java_dependency["archiveType"])
# Make Java executable
if not platform_id.value.startswith("win-"):
os.chmod(java_path, 0o755)
@@ -149,8 +170,8 @@ class KotlinLanguageServer(SolidLanguageServer):
# Download and extract Kotlin Language Server if script doesn't exist
if not os.path.exists(kotlin_script):
logger.log("Downloading Kotlin Language Server...", logging.INFO)
FileUtils.download_and_extract_archive(logger, kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"]) # type: ignore
log.info("Downloading Kotlin Language Server...")
FileUtils.download_and_extract_archive(kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"]) # type: ignore
# Make script executable on Unix platforms
if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"):
@@ -161,7 +182,7 @@ class KotlinLanguageServer(SolidLanguageServer):
# Use script file
if os.path.exists(kotlin_script):
kotlin_executable_path = kotlin_script
logger.log(f"Using Kotlin Language Server script at {kotlin_script}", logging.INFO)
log.info(f"Using Kotlin Language Server script at {kotlin_script}")
else:
raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}")
@@ -435,7 +456,7 @@ class KotlinLanguageServer(SolidLanguageServer):
return
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
self.server.on_request("client/registerCapability", do_nothing)
self.server.on_notification("language/status", do_nothing)
@@ -445,14 +466,11 @@ class KotlinLanguageServer(SolidLanguageServer):
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.server.on_notification("language/actionableNotification", do_nothing)
self.logger.log("Starting Kotlin server process", logging.INFO)
log.info("Starting Kotlin server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
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"]
+7 -16
View File
@@ -17,11 +17,12 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class LuaLanguageServer(SolidLanguageServer):
"""
@@ -154,18 +155,11 @@ class LuaLanguageServer(SolidLanguageServer):
return lua_ls_path
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
lua_ls_path = self._setup_runtime_dependency()
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=lua_ls_path, cwd=repository_root_path),
"lua",
solidlsp_settings,
config, repository_root_path, ProcessLaunchInfo(cmd=lua_ls_path, cwd=repository_root_path), "lua", solidlsp_settings
)
self.server_ready = threading.Event()
self.request_id = 0
@@ -263,7 +257,7 @@ class LuaLanguageServer(SolidLanguageServer):
return
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(params: dict) -> None:
return
@@ -273,14 +267,11 @@ class LuaLanguageServer(SolidLanguageServer):
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting Lua Language Server process", logging.INFO)
log.info("Starting Lua Language Server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
# Verify server capabilities
+12 -20
View File
@@ -12,13 +12,14 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
class Marksman(SolidLanguageServer):
"""
@@ -67,9 +68,7 @@ class Marksman(SolidLanguageServer):
)
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> str:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
"""Setup runtime dependencies for marksman and return the command to start the server."""
deps = cls.runtime_dependencies
dependency = deps.get_single_dep_for_current_platform()
@@ -77,28 +76,24 @@ class Marksman(SolidLanguageServer):
marksman_ls_dir = cls.ls_resources_dir(solidlsp_settings)
marksman_executable_path = deps.binary_path(marksman_ls_dir)
if not os.path.exists(marksman_executable_path):
logger.log(
log.info(
f"Downloading marksman from {dependency.url} to {marksman_ls_dir}",
logging.INFO,
)
deps.install(logger, marksman_ls_dir)
deps.install(marksman_ls_dir)
if not os.path.exists(marksman_executable_path):
raise FileNotFoundError(f"Download failed? Could not find marksman executable at {marksman_executable_path}")
os.chmod(marksman_executable_path, 0o755)
return marksman_executable_path
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a Marksman instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
marksman_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
marksman_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=f"{marksman_executable_path} server", cwd=repository_root_path),
"markdown",
@@ -159,7 +154,7 @@ class Marksman(SolidLanguageServer):
return
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(_params: dict) -> None:
return
@@ -169,16 +164,13 @@ class Marksman(SolidLanguageServer):
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting marksman server process", logging.INFO)
log.info("Starting marksman server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to marksman server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to marksman server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
self.logger.log(f"Received initialize response from marksman server: {init_response}", logging.DEBUG)
log.debug(f"Received initialize response from marksman server: {init_response}")
# Verify server capabilities
assert "textDocumentSync" in init_response["capabilities"]
@@ -188,6 +180,6 @@ class Marksman(SolidLanguageServer):
self.server.notify.initialized({})
# marksman is typically ready immediately after initialization
self.logger.log("Marksman server initialization complete", logging.INFO)
log.info("Marksman server initialization complete")
self.server_ready.set()
self.completions_available.set()
+7 -18
View File
@@ -19,11 +19,12 @@ from overrides import override
from solidlsp import ls_types
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class NixLanguageServer(SolidLanguageServer):
"""
@@ -244,19 +245,10 @@ class NixLanguageServer(SolidLanguageServer):
return nixd_path
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
nixd_path = self._setup_runtime_dependency()
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=nixd_path, cwd=repository_root_path),
"nix",
solidlsp_settings,
)
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=nixd_path, cwd=repository_root_path), "nix", solidlsp_settings)
self.server_ready = threading.Event()
self.request_id = 0
@@ -358,7 +350,7 @@ class NixLanguageServer(SolidLanguageServer):
return
def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(params):
return
@@ -368,14 +360,11 @@ class NixLanguageServer(SolidLanguageServer):
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting nixd server process", logging.INFO)
log.info("Starting nixd server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
# Verify server capabilities
+11 -19
View File
@@ -14,12 +14,13 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, 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__)
def breadth_first_file_scan(root: str) -> Iterable[str]:
"""
@@ -59,17 +60,15 @@ class OmniSharp(SolidLanguageServer):
Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(config, solidlsp_settings)
slnfilename = find_least_depth_sln_file(repository_root_path)
if slnfilename is None:
logger.log("No *.sln file found in repository", logging.ERROR)
log.error("No *.sln file found in repository")
raise SolidLSPException("No SLN file found in repository")
cmd = " ".join(
@@ -104,9 +103,7 @@ class OmniSharp(SolidLanguageServer):
"formattingOptions:indentationSize=4",
]
)
super().__init__(
config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", solidlsp_settings
)
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", solidlsp_settings)
self.server_ready = threading.Event()
self.definition_available = threading.Event()
@@ -142,9 +139,7 @@ class OmniSharp(SolidLanguageServer):
return d
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> tuple[str, str]:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[str, str]:
"""
Setup runtime dependencies for OmniSharp.
"""
@@ -190,7 +185,7 @@ class OmniSharp(SolidLanguageServer):
omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "OmniSharp")
if not os.path.exists(omnisharp_ls_dir):
os.makedirs(omnisharp_ls_dir)
FileUtils.download_and_extract_archive(logger, runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip")
FileUtils.download_and_extract_archive(runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip")
omnisharp_executable_path = os.path.join(omnisharp_ls_dir, runtime_dependencies["OmniSharp"]["binaryName"])
assert os.path.exists(omnisharp_executable_path)
os.chmod(omnisharp_executable_path, 0o755)
@@ -198,7 +193,7 @@ class OmniSharp(SolidLanguageServer):
razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RazorOmnisharp")
if not os.path.exists(razor_omnisharp_ls_dir):
os.makedirs(razor_omnisharp_ls_dir)
FileUtils.download_and_extract_archive(logger, runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip")
FileUtils.download_and_extract_archive(runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip")
razor_omnisharp_dll_path = os.path.join(razor_omnisharp_ls_dir, runtime_dependencies["RazorOmnisharp"]["dll_path"])
assert os.path.exists(razor_omnisharp_dll_path)
@@ -358,14 +353,11 @@ class OmniSharp(SolidLanguageServer):
self.server.on_notification("experimental/serverStatus", check_experimental_status)
self.server.on_request("workspace/configuration", workspace_configuration_handler)
self.logger.log("Starting OmniSharp server process", logging.INFO)
log.info("Starting OmniSharp server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
self.server.notify.initialized({})
with open(os.path.join(os.path.dirname(__file__), "omnisharp", "workspace_did_change_configuration.json"), encoding="utf-8") as f:
@@ -15,12 +15,13 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import DidChangeConfigurationParams, InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
class PerlLanguageServer(SolidLanguageServer):
"""
@@ -98,19 +99,12 @@ class PerlLanguageServer(SolidLanguageServer):
return "perl -MPerl::LanguageServer -e 'Perl::LanguageServer::run'"
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
# Setup runtime dependencies before initializing
perl_ls_cmd = self._setup_runtime_dependencies()
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=perl_ls_cmd, cwd=repository_root_path),
"perl",
solidlsp_settings,
config, repository_root_path, ProcessLaunchInfo(cmd=perl_ls_cmd, cwd=repository_root_path), "perl", solidlsp_settings
)
self.request_id = 0
@@ -156,15 +150,15 @@ class PerlLanguageServer(SolidLanguageServer):
def register_capability_handler(params: Any) -> None:
return
def window_log_message(msg: str) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(params: Any) -> None:
return
def workspace_configuration_handler(params: Any) -> Any:
"""Handle workspace/configuration request from Perl::LanguageServer."""
self.logger.log(f"Received workspace/configuration request: {params}", logging.INFO)
log.info(f"Received workspace/configuration request: {params}")
perl_config = {
"perlInc": [self.repository_root_path, "."],
@@ -180,18 +174,14 @@ class PerlLanguageServer(SolidLanguageServer):
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting Perl::LanguageServer process", logging.INFO)
log.info("Starting Perl::LanguageServer process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
self.logger.log(
log.info(
"After sent initialize params",
logging.INFO,
)
# Verify server capabilities
@@ -213,7 +203,7 @@ class PerlLanguageServer(SolidLanguageServer):
}
}
}
self.logger.log(f"Sending workspace/didChangeConfiguration notification with config: {perl_config}", logging.INFO)
log.info(f"Sending workspace/didChangeConfiguration notification with config: {perl_config}")
self.server.notify.workspace_did_change_configuration(perl_config)
self.completions_available.set()
@@ -221,6 +211,6 @@ class PerlLanguageServer(SolidLanguageServer):
# Perl::LanguageServer needs time to index files and resolve cross-file references
# Without this delay, requests for definitions/references may return empty results
settling_time = 0.5
self.logger.log(f"Allowing {settling_time} seconds for Perl::LanguageServer to index files...", logging.INFO)
log.info(f"Allowing {settling_time} seconds for Perl::LanguageServer to index files...")
time.sleep(settling_time)
self.logger.log("Perl::LanguageServer settling period complete", logging.INFO)
log.info("Perl::LanguageServer settling period complete")
@@ -0,0 +1,325 @@
"""
Provides PowerShell specific instantiation of the LanguageServer class using PowerShell Editor Services.
Contains various configurations and settings specific to PowerShell scripting.
"""
import logging
import os
import pathlib
import platform
import shutil
import tempfile
import threading
import zipfile
from pathlib import Path
import requests
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
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__)
# PowerShell Editor Services version to download
PSES_VERSION = "4.4.0"
class PowerShellLanguageServer(SolidLanguageServer):
"""
Provides PowerShell specific instantiation of the LanguageServer class using PowerShell Editor Services.
Contains various configurations and settings specific to PowerShell scripting.
"""
@override
def is_ignored_dirname(self, dirname: str) -> bool:
# For PowerShell projects, ignore common build/output directories
return super().is_ignored_dirname(dirname) or dirname in [
"bin",
"obj",
".vscode",
"TestResults",
"Output",
]
@staticmethod
def _get_pwsh_path() -> str | None:
"""Get the path to PowerShell Core (pwsh) executable."""
# Check if pwsh is in PATH
pwsh = shutil.which("pwsh")
if pwsh:
return pwsh
# Check common installation locations
home = Path.home()
system = platform.system()
possible_paths: list[Path] = []
if system == "Windows":
possible_paths = [
Path(os.environ.get("PROGRAMFILES", "C:\\Program Files")) / "PowerShell" / "7" / "pwsh.exe",
Path(os.environ.get("PROGRAMFILES", "C:\\Program Files")) / "PowerShell" / "7-preview" / "pwsh.exe",
home / "AppData" / "Local" / "Microsoft" / "PowerShell" / "pwsh.exe",
]
elif system == "Darwin":
possible_paths = [
Path("/usr/local/bin/pwsh"),
Path("/opt/homebrew/bin/pwsh"),
home / ".dotnet" / "tools" / "pwsh",
]
else: # Linux
possible_paths = [
Path("/usr/bin/pwsh"),
Path("/usr/local/bin/pwsh"),
Path("/opt/microsoft/powershell/7/pwsh"),
home / ".dotnet" / "tools" / "pwsh",
]
for path in possible_paths:
if path.exists():
return str(path)
return None
@classmethod
def _get_pses_path(cls, solidlsp_settings: SolidLSPSettings) -> str | None:
"""Get the path to PowerShell Editor Services installation."""
install_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "powershell"
start_script = install_dir / "PowerShellEditorServices" / "Start-EditorServices.ps1"
if start_script.exists():
return str(start_script)
return None
@classmethod
def _download_pses(cls, solidlsp_settings: SolidLSPSettings) -> str:
"""Download and install PowerShell Editor Services."""
download_url = (
f"https://github.com/PowerShell/PowerShellEditorServices/releases/download/v{PSES_VERSION}/PowerShellEditorServices.zip"
)
# Create installation directory
install_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "powershell"
install_dir.mkdir(parents=True, exist_ok=True)
# Download the file
log.info(f"Downloading PowerShell Editor Services from {download_url}...")
response = requests.get(download_url, stream=True, timeout=120)
response.raise_for_status()
# Save the zip file
zip_path = install_dir / "PowerShellEditorServices.zip"
with open(zip_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
log.info(f"Extracting PowerShell Editor Services to {install_dir}...")
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(install_dir)
# Clean up zip file
zip_path.unlink()
start_script = install_dir / "PowerShellEditorServices" / "Start-EditorServices.ps1"
if not start_script.exists():
raise RuntimeError(f"Failed to find Start-EditorServices.ps1 after extraction at {start_script}")
log.info(f"PowerShell Editor Services installed at: {install_dir}")
return str(start_script)
@classmethod
def _setup_runtime_dependency(cls, solidlsp_settings: SolidLSPSettings) -> tuple[str, str, str]:
"""
Check if required PowerShell runtime dependencies are available.
Downloads PowerShell Editor Services if not present.
Returns:
tuple: (pwsh_path, start_script_path, bundled_modules_path)
"""
# Check for PowerShell Core
pwsh_path = cls._get_pwsh_path()
if not pwsh_path:
raise RuntimeError(
"PowerShell Core (pwsh) is not installed or not in PATH. "
"Please install PowerShell 7+ from https://github.com/PowerShell/PowerShell"
)
# Check for PowerShell Editor Services
pses_path = cls._get_pses_path(solidlsp_settings)
if not pses_path:
log.info("PowerShell Editor Services not found. Downloading...")
pses_path = cls._download_pses(solidlsp_settings)
# The bundled modules path is the directory containing PowerShellEditorServices
bundled_modules_path = str(Path(pses_path).parent)
return pwsh_path, pses_path, bundled_modules_path
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
pwsh_path, pses_path, bundled_modules_path = self._setup_runtime_dependency(solidlsp_settings)
# Create a temp directory for PSES logs and session details
pses_temp_dir = Path(tempfile.gettempdir()) / "solidlsp_pses"
pses_temp_dir.mkdir(parents=True, exist_ok=True)
log_path = pses_temp_dir / "pses.log"
session_details_path = pses_temp_dir / "session.json"
# Build the command to start PowerShell Editor Services in stdio mode
# PSES requires several parameters beyond just -Stdio
# Using list format for robust argument handling - the PowerShell command
# after -Command must be a single string element
pses_command = (
f"& '{pses_path}' "
f"-HostName 'SolidLSP' "
f"-HostProfileId 'solidlsp' "
f"-HostVersion '1.0.0' "
f"-BundledModulesPath '{bundled_modules_path}' "
f"-LogPath '{log_path}' "
f"-LogLevel 'Information' "
f"-SessionDetailsPath '{session_details_path}' "
f"-Stdio"
)
cmd: list[str] = [
pwsh_path,
"-NoLogo",
"-NoProfile",
"-Command",
pses_command,
]
super().__init__(
config,
repository_root_path,
ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
"powershell",
solidlsp_settings,
)
self.server_ready = threading.Event()
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the PowerShell Editor Services.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"completion": {
"dynamicRegistration": True,
"completionItem": {
"snippetSupport": True,
"commitCharactersSupport": True,
"documentationFormat": ["markdown", "plaintext"],
"deprecatedSupport": True,
},
},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"signatureHelp": {
"dynamicRegistration": True,
"signatureInformation": {
"documentationFormat": ["markdown", "plaintext"],
"parameterInformation": {"labelOffsetSupport": True},
},
},
"codeAction": {"dynamicRegistration": True},
"formatting": {"dynamicRegistration": True},
"rangeFormatting": {"dynamicRegistration": True},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"configuration": True,
"symbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore[return-value]
def _start_server(self) -> None:
"""
Starts the PowerShell Editor Services, waits for the server to be ready.
"""
self._dynamic_capabilities: set[str] = set()
def register_capability_handler(params: dict) -> None:
"""Handle dynamic capability registration from PSES."""
registrations = params.get("registrations", [])
for reg in registrations:
method = reg.get("method", "")
log.info(f"PSES registered dynamic capability: {method}")
self._dynamic_capabilities.add(method)
# Mark server ready when we get document symbol registration
if method == "textDocument/documentSymbol":
self.server_ready.set()
return
def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")
# Check for PSES ready signals
message_text = msg.get("message", "")
if "started" in message_text.lower() or "ready" in message_text.lower():
log.info("PowerShell Editor Services ready signal detected")
self.server_ready.set()
self.completions_available.set()
def do_nothing(params: dict) -> None:
return
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.server.on_notification("powerShell/executionStatusChanged", do_nothing)
log.info("Starting PowerShell Editor Services 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)
log.info(f"Received initialize response from PowerShell server: {init_response}")
# Verify server capabilities - PSES uses dynamic capability registration
# so we check for either static or dynamic capabilities
capabilities = init_response.get("capabilities", {})
log.info(f"Server capabilities: {capabilities}")
# Send initialized notification to trigger dynamic capability registration
self.server.notify.initialized({})
# Wait for server readiness with timeout
log.info("Waiting for PowerShell Editor Services to be ready...")
if not self.server_ready.wait(timeout=10.0):
# Fallback: assume server is ready after timeout
log.info("Timeout waiting for PSES ready signal, proceeding anyway")
self.server_ready.set()
self.completions_available.set()
else:
log.info("PowerShell Editor Services initialization complete")
+12 -19
View File
@@ -13,11 +13,12 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class PyrightServer(SolidLanguageServer):
"""
@@ -25,19 +26,14 @@ class PyrightServer(SolidLanguageServer):
Contains various configurations and settings specific to Python.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a PyrightServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
super().__init__(
config,
logger,
repository_root_path,
# Note 1: we can also use `pyright-langserver --stdio` but it requires pyright to be installed with npm
# Note 2: we can also use `bpyright-langserver --stdio` if we ever are unhappy with pyright
ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path),
"python",
solidlsp_settings,
@@ -140,12 +136,12 @@ class PyrightServer(SolidLanguageServer):
Pyright logs "Found X source files" when it finishes scanning the workspace.
"""
message_text = msg.get("message", "")
self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO)
log.info(f"LSP: window/logMessage: {message_text}")
# Look for "Found X source files" which indicates workspace scanning is complete
# Unfortunately, pyright is unreliable and there seems to be no better way
if re.search(r"Found \d+ source files?", message_text):
self.logger.log("Pyright workspace scanning complete", logging.INFO)
log.info("Pyright workspace scanning complete")
self.found_source_files = True
self.analysis_complete.set()
self.completions_available.set()
@@ -155,7 +151,7 @@ class PyrightServer(SolidLanguageServer):
Also listen for experimental/serverStatus as a backup signal
"""
if params.get("quiescent") == True:
self.logger.log("Received experimental/serverStatus with quiescent=true", logging.INFO)
log.info("Received experimental/serverStatus with quiescent=true")
if not self.found_source_files:
self.analysis_complete.set()
self.completions_available.set()
@@ -170,18 +166,15 @@ class PyrightServer(SolidLanguageServer):
self.server.on_notification("language/actionableNotification", do_nothing)
self.server.on_notification("experimental/serverStatus", check_experimental_status)
self.logger.log("Starting pyright-langserver server process", logging.INFO)
log.info("Starting pyright-langserver server process")
self.server.start()
# Send proper initialization parameters
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to pyright server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to pyright server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO)
log.info(f"Received initialize response from pyright server: {init_response}")
# Verify that the server supports our required features
assert "textDocumentSync" in init_response["capabilities"]
@@ -193,11 +186,11 @@ class PyrightServer(SolidLanguageServer):
# Wait for Pyright to complete its initial workspace analysis
# This prevents zombie processes by ensuring background tasks finish
self.logger.log("Waiting for Pyright to complete initial workspace analysis...", logging.INFO)
log.info("Waiting for Pyright to complete initial workspace analysis...")
if self.analysis_complete.wait(timeout=5.0):
self.logger.log("Pyright initial analysis complete, server ready", logging.INFO)
log.info("Pyright initial analysis complete, server ready")
else:
self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING)
log.warning("Timeout waiting for Pyright analysis completion, proceeding anyway")
# Fallback: assume analysis is complete after timeout
self.analysis_complete.set()
self.completions_available.set()
@@ -9,11 +9,12 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class RLanguageServer(SolidLanguageServer):
"""R Language Server implementation using the languageserver R package."""
@@ -57,9 +58,7 @@ class RLanguageServer(SolidLanguageServer):
except FileNotFoundError:
raise RuntimeError("R is not installed. Please install R from https://www.r-project.org/")
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
# Check R installation
self._check_r_installation()
@@ -68,14 +67,7 @@ class RLanguageServer(SolidLanguageServer):
# Set specific options to improve parsing stability
r_cmd = 'R --vanilla --quiet --slave -e "options(languageserver.debug_mode = FALSE); languageserver::run()"'
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=r_cmd, cwd=repository_root_path),
"r",
solidlsp_settings,
)
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=r_cmd, cwd=repository_root_path), "r", solidlsp_settings)
self.server_ready = threading.Event()
@staticmethod
@@ -132,8 +124,8 @@ class RLanguageServer(SolidLanguageServer):
def _start_server(self) -> None:
"""Start R Language Server process."""
def window_log_message(msg: str) -> None:
self.logger.log(f"R LSP: window/logMessage: {msg}", logging.INFO)
def window_log_message(msg: dict) -> None:
log.info(f"R LSP: window/logMessage: {msg}")
def do_nothing(params: Any) -> None:
return
@@ -147,13 +139,12 @@ class RLanguageServer(SolidLanguageServer):
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting R Language Server process", logging.INFO)
log.info("Starting R Language Server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
log.info(
"Sending initialize request to R Language Server",
logging.INFO,
)
init_response = self.server.send.initialize(initialize_params)
@@ -162,9 +153,9 @@ class RLanguageServer(SolidLanguageServer):
capabilities = init_response.get("capabilities", {})
assert "textDocumentSync" in capabilities
if "completionProvider" in capabilities:
self.logger.log("R LSP completion provider available", logging.INFO)
log.info("R LSP completion provider available")
if "definitionProvider" in capabilities:
self.logger.log("R LSP definition provider available", logging.INFO)
log.info("R LSP definition provider available")
self.server.notify.initialized({})
self.completions_available.set()
+6 -10
View File
@@ -9,12 +9,13 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import PathUtils
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__)
class RegalLanguageServer(SolidLanguageServer):
"""
@@ -28,16 +29,13 @@ class RegalLanguageServer(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in [".regal", ".opa"]
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a RegalLanguageServer instance.
This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
:param config: Language server configuration
:param logger: Logger instance
:param repository_root_path: Path to the repository root
:param solidlsp_settings: Settings for solidlsp
"""
@@ -50,7 +48,6 @@ class RegalLanguageServer(SolidLanguageServer):
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=f"{regal_executable_path} language-server", cwd=repository_root_path),
"rego",
@@ -108,7 +105,7 @@ class RegalLanguageServer(SolidLanguageServer):
return
def window_log_message(msg) -> None: # type: ignore[no-untyped-def]
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(params) -> None: # type: ignore[no-untyped-def]
return
@@ -118,13 +115,12 @@ class RegalLanguageServer(SolidLanguageServer):
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting Regal language server process", logging.INFO)
log.info("Starting Regal language server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
log.info(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
init_response = self.server.send.initialize(initialize_params)
+37 -49
View File
@@ -15,11 +15,12 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
class RubyLsp(SolidLanguageServer):
"""
@@ -27,21 +28,14 @@ class RubyLsp(SolidLanguageServer):
Contains various configurations and settings specific to Ruby with modern LSP features.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a RubyLsp instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
ruby_lsp_executable = self._setup_runtime_dependencies(logger, config, repository_root_path)
ruby_lsp_executable = self._setup_runtime_dependencies(config, repository_root_path)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=ruby_lsp_executable, cwd=repository_root_path),
"ruby",
solidlsp_settings,
config, repository_root_path, ProcessLaunchInfo(cmd=ruby_lsp_executable, cwd=repository_root_path), "ruby", solidlsp_settings
)
self.analysis_complete = threading.Event()
self.service_ready_event = threading.Event()
@@ -98,7 +92,7 @@ class RubyLsp(SolidLanguageServer):
return shutil.which(executable_name)
@staticmethod
def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig, repository_root_path: str) -> list[str]:
def _setup_runtime_dependencies(config: LanguageServerConfig, repository_root_path: str) -> list[str]:
"""
Setup runtime dependencies for ruby-lsp and return the command list to start the server.
Installation strategy: Bundler project > global ruby-lsp > gem install ruby-lsp
@@ -125,24 +119,23 @@ class RubyLsp(SolidLanguageServer):
if use_rbenv:
ruby_cmd = ["rbenv", "exec", "ruby"]
bundle_cmd = ["rbenv", "exec", "bundle"]
logger.log(f"Using rbenv-managed Ruby (found {ruby_version_file})", logging.INFO)
log.info(f"Using rbenv-managed Ruby (found {ruby_version_file})")
else:
ruby_cmd = ["ruby"]
bundle_cmd = ["bundle"]
if os.path.exists(ruby_version_file):
logger.log(
log.warning(
f"Found {ruby_version_file} but rbenv is not installed. "
"Using system Ruby. Consider installing rbenv for better version management: https://github.com/rbenv/rbenv",
logging.WARNING,
)
else:
logger.log("No .ruby-version file found, using system Ruby", logging.INFO)
log.info("No .ruby-version file found, using system Ruby")
# Check if Ruby is installed
try:
result = subprocess.run(ruby_cmd + ["--version"], check=True, capture_output=True, cwd=repository_root_path, text=True)
ruby_version = result.stdout.strip()
logger.log(f"Ruby version: {ruby_version}", logging.INFO)
log.info(f"Ruby version: {ruby_version}")
# Extract version number for compatibility checks
import re
@@ -151,7 +144,7 @@ class RubyLsp(SolidLanguageServer):
if version_match:
major, minor, patch = map(int, version_match.groups())
if major < 2 or (major == 2 and minor < 6):
logger.log(f"Warning: Ruby {major}.{minor}.{patch} detected. ruby-lsp works best with Ruby 2.6+", logging.WARNING)
log.warning(f"Warning: Ruby {major}.{minor}.{patch} detected. ruby-lsp works best with Ruby 2.6+")
except subprocess.CalledProcessError as e:
error_msg = e.stderr if isinstance(e.stderr, str) else e.stderr.decode() if e.stderr else "Unknown error"
@@ -173,7 +166,7 @@ class RubyLsp(SolidLanguageServer):
is_bundler_project = os.path.exists(gemfile_path)
if is_bundler_project:
logger.log("Detected Bundler project (Gemfile found)", logging.INFO)
log.info("Detected Bundler project (Gemfile found)")
# Check if bundle command is available using Windows-compatible search
bundle_path = RubyLsp._find_executable_with_extensions(bundle_cmd[0] if len(bundle_cmd) == 1 else "bundle")
@@ -190,9 +183,8 @@ class RubyLsp(SolidLanguageServer):
break
if not bundle_path:
logger.log(
log.warning(
"Bundler project detected but 'bundle' command not found. Falling back to global ruby-lsp installation.",
logging.WARNING,
)
else:
# Check if ruby-lsp is in Gemfile.lock
@@ -203,29 +195,28 @@ class RubyLsp(SolidLanguageServer):
content = f.read()
ruby_lsp_in_bundle = "ruby-lsp" in content.lower()
except Exception as e:
logger.log(f"Warning: Could not read Gemfile.lock: {e}", logging.WARNING)
log.warning(f"Warning: Could not read Gemfile.lock: {e}")
if ruby_lsp_in_bundle:
logger.log("Found ruby-lsp in Gemfile.lock", logging.INFO)
log.info("Found ruby-lsp in Gemfile.lock")
return bundle_cmd + ["exec", "ruby-lsp"]
else:
logger.log(
log.info(
"ruby-lsp not found in Gemfile.lock. Consider adding 'gem \"ruby-lsp\"' to your Gemfile for better compatibility.",
logging.INFO,
)
# Fall through to global installation check
# Check if ruby-lsp is available globally using Windows-compatible search
ruby_lsp_path = RubyLsp._find_executable_with_extensions("ruby-lsp")
if ruby_lsp_path:
logger.log(f"Found ruby-lsp at: {ruby_lsp_path}", logging.INFO)
log.info(f"Found ruby-lsp at: {ruby_lsp_path}")
return [ruby_lsp_path]
# Try to install ruby-lsp globally
logger.log("ruby-lsp not found, attempting to install globally...", logging.INFO)
log.info("ruby-lsp not found, attempting to install globally...")
try:
subprocess.run(["gem", "install", "ruby-lsp"], check=True, capture_output=True, cwd=repository_root_path)
logger.log("Successfully installed ruby-lsp globally", logging.INFO)
log.info("Successfully installed ruby-lsp globally")
# Find the newly installed ruby-lsp executable
ruby_lsp_path = RubyLsp._find_executable_with_extensions("ruby-lsp")
return [ruby_lsp_path] if ruby_lsp_path else ["ruby-lsp"]
@@ -351,13 +342,13 @@ class RubyLsp(SolidLanguageServer):
def register_capability_handler(params: dict) -> None:
assert "registrations" in params
for registration in params["registrations"]:
self.logger.log(f"Registered capability: {registration['method']}", logging.INFO)
log.info(f"Registered capability: {registration['method']}")
return
def lang_status_handler(params: dict) -> None:
self.logger.log(f"LSP: language/status: {params}", logging.INFO)
log.info(f"LSP: language/status: {params}")
if params.get("type") == "ready":
self.logger.log("ruby-lsp service is ready.", logging.INFO)
log.info("ruby-lsp service is ready.")
self.analysis_complete.set()
self.completions_available.set()
@@ -368,36 +359,36 @@ class RubyLsp(SolidLanguageServer):
return
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
def progress_handler(params: dict) -> None:
# ruby-lsp sends progress notifications during indexing
self.logger.log(f"LSP: $/progress: {params}", logging.DEBUG)
log.debug(f"LSP: $/progress: {params}")
if "value" in params:
value = params["value"]
# Check for completion indicators
if value.get("kind") == "end":
self.logger.log("ruby-lsp indexing complete ($/progress end)", logging.INFO)
log.info("ruby-lsp indexing complete ($/progress end)")
self.analysis_complete.set()
self.completions_available.set()
elif value.get("kind") == "begin":
self.logger.log("ruby-lsp indexing started ($/progress begin)", logging.INFO)
log.info("ruby-lsp indexing started ($/progress begin)")
elif "percentage" in value:
percentage = value.get("percentage", 0)
self.logger.log(f"ruby-lsp indexing progress: {percentage}%", logging.DEBUG)
log.debug(f"ruby-lsp indexing progress: {percentage}%")
# Handle direct progress format (fallback)
elif "token" in params and "value" in params:
token = params.get("token")
if isinstance(token, str) and "indexing" in token.lower():
value = params.get("value", {})
if value.get("kind") == "end" or value.get("percentage") == 100:
self.logger.log("ruby-lsp indexing complete (token progress)", logging.INFO)
log.info("ruby-lsp indexing complete (token progress)")
self.analysis_complete.set()
self.completions_available.set()
def window_work_done_progress_create(params: dict) -> dict:
"""Handle workDoneProgress/create requests from ruby-lsp"""
self.logger.log(f"LSP: window/workDoneProgress/create: {params}", logging.DEBUG)
log.debug(f"LSP: window/workDoneProgress/create: {params}")
return {}
self.server.on_request("client/registerCapability", register_capability_handler)
@@ -408,17 +399,14 @@ class RubyLsp(SolidLanguageServer):
self.server.on_request("window/workDoneProgress/create", window_work_done_progress_create)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting ruby-lsp server process", logging.INFO)
log.info("Starting ruby-lsp server process")
self.server.start()
initialize_params = self._get_initialize_params()
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
self.logger.log(f"Sending init params: {json.dumps(initialize_params, indent=4)}", logging.INFO)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
log.info(f"Sending init params: {json.dumps(initialize_params, indent=4)}")
init_response = self.server.send.initialize(initialize_params)
self.logger.log(f"Received init response: {init_response}", logging.INFO)
log.info(f"Received init response: {init_response}")
# Verify expected capabilities
# Note: ruby-lsp may return textDocumentSync in different formats (number or object)
@@ -434,11 +422,11 @@ class RubyLsp(SolidLanguageServer):
self.server.notify.initialized({})
# Wait for ruby-lsp to complete its initial indexing
# ruby-lsp has fast indexing
self.logger.log("Waiting for ruby-lsp to complete initial indexing...", logging.INFO)
log.info("Waiting for ruby-lsp to complete initial indexing...")
if self.analysis_complete.wait(timeout=30.0):
self.logger.log("ruby-lsp initial indexing complete, server ready", logging.INFO)
log.info("ruby-lsp initial indexing complete, server ready")
else:
self.logger.log("Timeout waiting for ruby-lsp indexing completion, proceeding anyway", logging.WARNING)
log.warning("Timeout waiting for ruby-lsp indexing completion, proceeding anyway")
# Fallback: assume indexing is complete after timeout
self.analysis_complete.set()
self.completions_available.set()
@@ -472,7 +460,7 @@ class RubyLsp(SolidLanguageServer):
for cap in important_capabilities:
if cap in capabilities:
self.logger.log(f"ruby-lsp {cap}: available", logging.DEBUG)
log.debug(f"ruby-lsp {cap}: available")
# Signal that the service is ready
self.service_ready_event.set()
+7 -12
View File
@@ -13,11 +13,12 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
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__)
class RustAnalyzer(SolidLanguageServer):
"""
@@ -95,18 +96,15 @@ class RustAnalyzer(SolidLanguageServer):
return path
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
rustanalyzer_executable_path = self._ensure_rust_analyzer_installed()
logger.log(f"Using rust-analyzer at: {rustanalyzer_executable_path}", logging.INFO)
log.info(f"Using rust-analyzer at: {rustanalyzer_executable_path}")
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path),
"rust",
@@ -626,7 +624,7 @@ class RustAnalyzer(SolidLanguageServer):
self.server_ready.set()
def window_log_message(msg: dict) -> None:
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
log.info(f"LSP: window/logMessage: {msg}")
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("language/status", lang_status_handler)
@@ -637,14 +635,11 @@ class RustAnalyzer(SolidLanguageServer):
self.server.on_notification("language/actionableNotification", do_nothing)
self.server.on_notification("experimental/serverStatus", check_experimental_status)
self.logger.log("Starting RustAnalyzer server process", logging.INFO)
log.info("Starting RustAnalyzer server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore
assert "completionProvider" in init_response["capabilities"]
@@ -12,7 +12,6 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.ls_utils import PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -22,21 +21,21 @@ if not PlatformUtils.get_platform_id().value.startswith("win"):
pass
log = logging.getLogger(__name__)
class ScalaLanguageServer(SolidLanguageServer):
"""
Provides Scala specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Scala.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a ScalaLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
scala_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
scala_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=scala_lsp_executable_path, cwd=repository_root_path),
config.code_language.value,
@@ -52,26 +51,27 @@ class ScalaLanguageServer(SolidLanguageServer):
]
@classmethod
def _setup_runtime_dependencies(
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
) -> list[str]:
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]:
"""
Setup runtime dependencies for Scala Language Server and return the command to start the server.
"""
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")
assert coursier_command_path is not None, "coursier is not installed or not in PATH."
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."
if not os.path.exists(metals_executable):
if not cs_command_path:
logger.log("'cs' command not found. Trying to install it using 'coursier'.", logging.INFO)
assert coursier_command_path is not None
log.info("'cs' command not found. Trying to install it using 'coursier'.")
try:
logger.log("Running 'coursier setup --yes' to install 'cs'...", logging.INFO)
log.info("Running 'coursier setup --yes' to install 'cs'...")
subprocess.run([coursier_command_path, "setup", "--yes"], check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Failed to set up 'cs' command with 'coursier setup'. Stderr: {e.stderr}")
@@ -81,10 +81,11 @@ class ScalaLanguageServer(SolidLanguageServer):
raise RuntimeError(
"'cs' command not found after running 'coursier setup'. Please check your PATH or install it manually."
)
logger.log("'cs' command installed successfully.", logging.INFO)
log.info("'cs' command installed successfully.")
logger.log(f"metals executable not found at {metals_executable}, bootstrapping...", logging.INFO)
artifact = "org.scalameta:metals_2.13:1.6.2"
log.info(f"metals executable not found at {metals_executable}, bootstrapping...")
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",
@@ -103,9 +104,9 @@ class ScalaLanguageServer(SolidLanguageServer):
metals_executable,
"-f",
]
logger.log("Bootstrapping metals...", logging.INFO)
log.info("Bootstrapping metals...")
subprocess.run(cmd, cwd=metals_home, check=True)
logger.log("Bootstrapping metals finished.", logging.INFO)
log.info("Bootstrapping metals finished.")
return [metals_executable]
@staticmethod
@@ -159,13 +160,10 @@ class ScalaLanguageServer(SolidLanguageServer):
"""
Starts the Scala Language Server
"""
self.logger.log("Starting Scala server process", logging.INFO)
log.info("Starting Scala server process")
self.server.start()
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
initialize_params = self._get_initialize_params(self.repository_root_path)
self.server.send.initialize(initialize_params)
Loaded 100 of 186 files, more files were not shown because too many files have changed in this diff. Show more