mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 14:17:49 +00:00
Merge branch 'main' into mux
This commit is contained in:
commit
6dcaa3894a
186 files changed
+11604
-2301
No files matched your search
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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 $@"]
|
||||
|
||||
@@ -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
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 |
Whitespace-only changes.
+10
-3
@@ -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 |
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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({})
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,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()
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
Reference in new issue
Block a user