From 597c28c8e81cc8d55a0d1133e76be5d24e233b2d Mon Sep 17 00:00:00 2001 From: nikita2206 Date: Fri, 6 Jun 2025 11:10:36 +0200 Subject: [PATCH 001/174] feat: Add Java 21 support to multilspy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update Eclipse JDTLS to vscode-java v1.42.0 with Java 21.0.7 JRE - Update Kotlin language server dependencies to use Java 21 - Update test Maven project to use Java 21 compiler settings - Update runtime configuration from JavaSE-17 to JavaSE-21 - Update dependency versions: lombok 1.18.36, launcher 1.7.0 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../eclipse_jdtls/eclipse_jdtls.py | 4 +- .../eclipse_jdtls/initialize_params.json | 6 +-- .../eclipse_jdtls/runtime_dependencies.json | 44 +++++++++---------- .../runtime_dependencies.json | 30 ++++++------- test/resources/repos/java/test_repo/pom.xml | 19 +++++++- 5 files changed, 59 insertions(+), 44 deletions(-) diff --git a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py index 6753bf1..cf16ff3 100644 --- a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -300,10 +300,10 @@ class EclipseJDTLS(LanguageServer): d["initializationOptions"]["bundles"] = bundles assert d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] == [ - {"name": "JavaSE-17", "path": "static/vscode-java/extension/jre/17.0.8.1-linux-x86_64", "default": True} + {"name": "JavaSE-21", "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": True} ] d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [ - {"name": "JavaSE-17", "path": self.runtime_dependency_paths.jre_home_path, "default": True} + {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True} ] for runtime in d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]: diff --git a/src/multilspy/language_servers/eclipse_jdtls/initialize_params.json b/src/multilspy/language_servers/eclipse_jdtls/initialize_params.json index 5c4cd52..b65b7b6 100644 --- a/src/multilspy/language_servers/eclipse_jdtls/initialize_params.json +++ b/src/multilspy/language_servers/eclipse_jdtls/initialize_params.json @@ -510,8 +510,8 @@ "workspaceCacheLimit": 90, "runtimes": [ { - "name": "JavaSE-17", - "path": "static/vscode-java/extension/jre/17.0.8.1-linux-x86_64", + "name": "JavaSE-21", + "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": true } ] @@ -535,7 +535,7 @@ "version": null, "home": "abs(static/gradle-7.3.3)", "java": { - "home": "abs(static/launch_jres/17.0.6-linux-x86_64)" + "home": "abs(static/launch_jres/21.0.7-linux-x86_64)" }, "offline": { "enabled": false diff --git a/src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json b/src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json index c224c69..1b00040 100644 --- a/src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json +++ b/src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json @@ -9,53 +9,53 @@ }, "vscode-java": { "darwin-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-arm64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java" }, "osx-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/17.0.8.1-macosx-x86_64", - "jre_path": "extension/jre/17.0.8.1-macosx-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.30.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar", + "jre_home_path": "extension/jre/21.0.7-macosx-aarch64", + "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", "jdtls_readonly_config_path": "extension/server/config_mac_arm" }, "osx-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@darwin-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/17.0.8.1-macosx-x86_64", - "jre_path": "extension/jre/17.0.8.1-macosx-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.30.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar", + "jre_home_path": "extension/jre/21.0.7-macosx-x86_64", + "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", "jdtls_readonly_config_path": "extension/server/config_mac" }, "linux-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-arm64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java" }, "linux-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@linux-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/17.0.8.1-linux-x86_64", - "jre_path": "extension/jre/17.0.8.1-linux-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.30.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar", + "jre_home_path": "extension/jre/21.0.7-linux-x86_64", + "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", "jdtls_readonly_config_path": "extension/server/config_linux" }, "win-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@win32-x64-1.23.0.vsix", + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", "archiveType": "zip", "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/17.0.8.1-win32-x86_64", - "jre_path": "extension/jre/17.0.8.1-win32-x86_64/bin/java.exe", - "lombok_jar_path": "extension/lombok/lombok-1.18.30.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar", + "jre_home_path": "extension/jre/21.0.7-win32-x86_64", + "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", "jdtls_readonly_config_path": "extension/server/config_win" } }, diff --git a/src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json b/src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json index a7b64b9..3568c9d 100644 --- a/src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json +++ b/src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json @@ -8,34 +8,34 @@ }, "java": { "win-x64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.23.0/java@win32-x64-1.23.0.vsix", + "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/17.0.8.1-win32-x86_64", - "java_path": "extension/jre/17.0.8.1-win32-x86_64/bin/java.exe" + "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.23.0/java@linux-x64-1.23.0.vsix", + "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/17.0.8.1-linux-x86_64", - "java_path": "extension/jre/17.0.8.1-linux-x86_64/bin/java" + "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.23.0/java@linux-arm64-1.23.0.vsix", + "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/17.0.8.1-linux-aarch64", - "java_path": "extension/jre/17.0.8.1-linux-aarch64/bin/java" + "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.23.0/java@darwin-x64-1.23.0.vsix", + "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/17.0.8.1-macosx-x86_64", - "java_path": "extension/jre/17.0.8.1-macosx-x86_64/bin/java" + "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.23.0/java@darwin-arm64-1.23.0.vsix", + "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/17.0.8.1-macosx-aarch64", - "java_path": "extension/jre/17.0.8.1-macosx-aarch64/bin/java" + "java_home_path": "extension/jre/21.0.7-macosx-aarch64", + "java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java" } } } diff --git a/test/resources/repos/java/test_repo/pom.xml b/test/resources/repos/java/test_repo/pom.xml index 7df64e1..672f990 100644 --- a/test/resources/repos/java/test_repo/pom.xml +++ b/test/resources/repos/java/test_repo/pom.xml @@ -8,7 +8,22 @@ jar Java Test Repo - 17 - 17 + 21 + 21 + 3.13.0 + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.plugin.version} + + 21 + 21 + + + + From 9026ec5ef208bb4617530abd59ae6bb851ad604c Mon Sep 17 00:00:00 2001 From: Aaron Yordanyan Date: Fri, 6 Jun 2025 17:15:16 +0400 Subject: [PATCH 002/174] Add Docker support with GitHub Actions workflow and updated configurations - Introduced `.github/workflows/docker.yml` for building and pushing Docker images. - Updated `Dockerfile` to split stages for `base`, `development`, and `production`. - Added `compose.yaml` for Docker Compose setups. - Enhanced `README.md` with Docker usage instructions. --- .github/workflows/docker.yml | 68 ++++++++++++++++++++++++++++++++++++ Dockerfile | 30 ++++++++++++---- README.md | 29 ++++++++++++--- compose.yaml | 22 ++++++++++++ 4 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/docker.yml create mode 100644 compose.yaml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..7e15e4c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,68 @@ +name: Build and Push Docker Images + +on: + push: + branches: [ main ] + tags: [ 'v*' ] + pull_request: + branches: [ main ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push production image + uses: docker/build-push-action@v5 + with: + context: . + target: production + 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 + 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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4521867..d88d4e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Use the official Python image for the base image. -FROM python:3.11-slim +# Base stage with common dependencies +FROM python:3.11-slim AS base SHELL ["/bin/bash", "-c"] # Set environment variables to make Python print directly to the terminal and avoid .pyc files. @@ -21,18 +21,18 @@ RUN python3 -m pip install --no-cache-dir pipx \ # Add local bin to the path ENV PATH="${PATH}:/root/.local/bin" - # Install the latest version of uv RUN curl -LsSf https://astral.sh/uv/install.sh | sh # Set the working directory WORKDIR /workspaces/serena -# Copy required files into the image -COPY pyproject.toml /workspaces/serena/ -COPY README.md /workspaces/serena/ +# Development target +FROM base AS development +# Copy all files for development +COPY . /workspaces/serena/ -# Create virtual environment and install dependencies +# 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 . @@ -41,3 +41,19 @@ ENV PATH="/workspaces/serena/.venv/bin:${PATH}" # Entrypoint to ensure environment is activated ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"] +# 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) +RUN uv venv +RUN . .venv/bin/activate +RUN uv pip install -r pyproject.toml -e . +ENV PATH="/workspaces/serena/.venv/bin:${PATH}" + +# Entrypoint to ensure environment is activated +ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"] + diff --git a/README.md b/README.md index 6d48de7..fd07825 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,8 @@ Configure the MCP server in your client. For [Claude Desktop](https://claude.ai/download) (available for Windows and macOS), go to File / Settings / Developer / MCP Servers / Edit Config, which will let you open the JSON file `claude_desktop_config.json`. Add the following (with adjusted paths) to enable Serena: +#### Local Installation + ```json { "mcpServers": { @@ -254,6 +256,23 @@ which will let you open the JSON file `claude_desktop_config.json`. Add the foll } ``` +#### Docker Installation + +Alternatively, you can run Serena using Docker: + +```json +{ + "mcpServers": { + "serena": { + "command": "docker", + "args": ["run", "--rm", "-i", "--network", "host", "-v", "/path/to/your/projects:/workspaces/projects", "ghcr.io/oraios/serena:latest", "serena-mcp-server", "--transport", "stdio"] + } + } +} +``` + +Replace `/path/to/your/projects` with the absolute path to your projects directory. The Docker approach has the advantage of not requiring local installation of dependencies. + 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 (`\\`). @@ -265,16 +284,18 @@ That's it! Save the config and then restart Claude Desktop. You are ready for ac uv run serena-mcp-server --help ``` -â„šī¸ You can use Serena without cloning or configuring it explicitly by +â„šī¸ You can use Serena without cloning or configuring it explicitly by using the Docker image above or: +```json { "mcpServers": { "serena": { - "command": "/abs/path/to/uv", - "args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server"] + "command": "uvx", + "args": ["--from", "git+https://github.com/oraios/serena", "serena-mcp-server"] } } } +``` #### Troubleshooting @@ -399,7 +420,7 @@ Here's how it works (see also [Agno's documentation](https://docs.agno.com/intro 3. Copy `.env.example` to `.env` and fill in the API keys for the provider(s) you intend to use. -5. Start the agno agent app with +4. Start the agno agent app with ```shell uv run python scripts/agno_agent.py ``` diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..5150442 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,22 @@ +services: + serena: + image: serena:latest + build: + context: ./ + dockerfile: Dockerfile + target: production + tty: true + stdin_open: true + + serena-dev: + image: serena:dev + build: + context: ./ + dockerfile: Dockerfile + target: development + tty: true + stdin_open: true + volumes: + - .:/workspaces/serena + command: + - "uv run --directory . serena-mcp-server" From d9d40e89b545dfcba94155e2ee7e37526c75266f Mon Sep 17 00:00:00 2001 From: Aaron Yordanyan Date: Fri, 6 Jun 2025 17:37:05 +0400 Subject: [PATCH 003/174] Expose necessary ports and update host configuration for Dockerized services --- compose.yaml | 7 +++++-- src/serena/dashboard.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/compose.yaml b/compose.yaml index 5150442..17348bb 100644 --- a/compose.yaml +++ b/compose.yaml @@ -5,8 +5,11 @@ services: context: ./ dockerfile: Dockerfile target: production - tty: true - stdin_open: true + ports: + - "9121:9121" + - "24282:24282" + command: + - "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0" serena-dev: image: serena:dev diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 4b0419a..622bef1 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -109,7 +109,7 @@ class SerenaDashboardAPI: raise RuntimeError(f"No free ports found starting from {start_port}") - def run(self, host: str = "127.0.0.1", port: int = 0x5EDA) -> int: + def run(self, host: str = "0.0.0.0", port: int = 0x5EDA) -> int: """ Runs the dashboard on the given host and port and returns the port number. """ From e8fc6d4fa772bf95e7175ef656fcbac9479f20ce Mon Sep 17 00:00:00 2001 From: Aaron Yordanyan Date: Mon, 9 Jun 2025 10:09:30 +0400 Subject: [PATCH 004/174] Add experimental Docker support and update configuration handling - Introduced `DOCKER.md` with detailed setup instructions and limitations. - Enhanced `compose.yaml` with dynamic port and environment variable support. - Updated `agent.py` to detect Docker environments and disable GUI features. - Modified configuration paths to support both Docker and non-Docker setups. - Marked Docker support as experimental in the documentation. --- DOCKER.md | 161 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 11 ++- compose.yaml | 11 ++- src/serena/agent.py | 23 ++++++- 4 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 DOCKER.md diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..04f8ca6 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,161 @@ +# 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. + +## Overview + +Docker support allows you to run Serena in an isolated container environment, which provides better security isolation for the shell tool and consistent dependencies across different systems. + +## Benefits + +- **Safer shell tool execution**: Commands run in an isolated container environment +- **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 + +### 1. Configuration File Conflicts + +âš ī¸ **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 + +### 2. 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 + +### 3. GUI Window Disabled + +- The GUI log window option is automatically disabled in Docker environments +- Use the web dashboard instead (see below) + +### 4. Dashboard Port Configuration + +The web dashboard runs on port 24282 (0x5EDA) by default. You can configure this using environment variables: + +```bash +# Use default ports +docker-compose up serena + +# Use custom ports +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 + +âš ī¸ **Windows Users**: Be aware of potential line ending inconsistencies: +- Files edited within the Docker container may use Unix line endings (LF) +- Your Windows system may expect Windows line endings (CRLF) +- This can cause issues with version control and text editors +- Configure your Git settings appropriately: `git config core.autocrlf true` + +## Quick Start + +### Using Docker Compose (Recommended) + +1. **Production mode** (for using Serena as MCP server): + ```bash + docker-compose up serena + ``` + +2. **Development mode** (with source code mounted): + ```bash + docker-compose up serena-dev + ``` + +### Using Docker directly + +```bash +# Build the image +docker build -t serena . + +# Run with current directory mounted +docker run -it --rm \ + -v "$(pwd)":/workspace \ + -p 9121:9121 \ + -p 24282:24282 \ + -e SERENA_DOCKER=1 \ + serena +``` + +## Accessing the Dashboard + +Once running, access the web dashboard at: +- Default: http://localhost:24282/dashboard +- Custom port: http://localhost:${SERENA_DASHBOARD_PORT}/dashboard + +## Volume Mounting + +To work with projects, you must mount them as volumes: + +```yaml +# In compose.yaml +volumes: + - ./my-project:/workspace/my-project + - /path/to/another/project:/workspace/another-project +``` + +## Environment Variables + +- `SERENA_DOCKER=1`: Set automatically to indicate Docker environment +- `SERENA_PORT`: MCP server port (default: 9121) +- `SERENA_DASHBOARD_PORT`: Web dashboard port (default: 24282) + +## Troubleshooting + +### Port Already in Use + +If you see "port already in use" errors: +```bash +# Check what's using the port +lsof -i :24282 # macOS/Linux +netstat -ano | findstr :24282 # Windows + +# Use a different port +SERENA_DASHBOARD_PORT=8080 docker-compose up serena +``` + +### Configuration Issues + +If you need to reset Docker configuration: +```bash +# Remove Docker-specific config +rm serena_config.docker.yml + +# Serena will auto-generate a new one on next run +``` + +### Project Access Issues + +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 + +For updates and issues, please check the [GitHub repository](https://github.com/AbanteAI/serena). \ No newline at end of file diff --git a/README.md b/README.md index fd07825..9b832ad 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,9 @@ which will let you open the JSON file `claude_desktop_config.json`. Add the foll } ``` -#### Docker Installation +#### Docker Installation (Experimental) + +âš ī¸ **EXPERIMENTAL**: Docker support is currently experimental with several limitations. Please read the [Docker documentation](DOCKER.md) for important caveats before using. Alternatively, you can run Serena using Docker: @@ -271,7 +273,12 @@ Alternatively, you can run Serena using Docker: } ``` -Replace `/path/to/your/projects` with the absolute path to your projects directory. The Docker approach has the advantage of not requiring local installation of dependencies. +Replace `/path/to/your/projects` with the absolute path to your projects directory. The Docker approach provides: +- Better security isolation for shell command execution +- No need to install language servers and dependencies locally +- Consistent environment across different systems + +See the [Docker documentation](DOCKER.md) for detailed setup instructions, configuration options, and known limitations. 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 (`\\`). diff --git a/compose.yaml b/compose.yaml index 17348bb..bb8f683 100644 --- a/compose.yaml +++ b/compose.yaml @@ -6,8 +6,10 @@ services: dockerfile: Dockerfile target: production ports: - - "9121:9121" - - "24282:24282" + - "${SERENA_PORT:-9121}:9121" # MCP server port + - "${SERENA_DASHBOARD_PORT:-24282}:24282" # Dashboard port (default 0x5EDA = 24282) + environment: + - SERENA_DOCKER=1 command: - "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0" @@ -19,7 +21,12 @@ services: 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" diff --git a/src/serena/agent.py b/src/serena/agent.py index cde3bc1..c6021a1 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -102,6 +102,19 @@ def get_serena_managed_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', 'r') as f: + return 'docker' in f.read() + except: + return False + + @dataclass class ProjectConfig(ToStringMixin): project_name: str @@ -315,6 +328,7 @@ class SerenaConfig(SerenaConfigBase): loaded_commented_yaml: CommentedMap CONFIG_FILE = "serena_config.yml" + CONFIG_FILE_DOCKER = "serena_config.docker.yml" @classmethod def autogenerate(cls) -> None: @@ -328,7 +342,8 @@ class SerenaConfig(SerenaConfigBase): @classmethod def get_config_file_path(cls) -> str: - return os.path.join(REPO_ROOT, cls.CONFIG_FILE) + config_file = cls.CONFIG_FILE_DOCKER if is_running_in_docker() else cls.CONFIG_FILE + return os.path.join(REPO_ROOT, config_file) @classmethod def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig": @@ -370,7 +385,11 @@ class SerenaConfig(SerenaConfigBase): project = Project.load(path) instance.projects.append(project) - instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False) + # Force disable GUI in Docker environment + if is_running_in_docker(): + instance.gui_log_window_enabled = False + else: + instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False) 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.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT) From c987445faff528e654ea6e6f8aaf817fcf217b60 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 9 Jun 2025 15:07:40 +0200 Subject: [PATCH 005/174] LS: more logging, more robust server shutdown on stop Previously shutdown relied on garbage collection and occasionally failed (probably due to racing conditions) --- src/multilspy/language_server.py | 117 ++++++++++++++---- .../clangd_language_server.py | 3 - .../dart_language_server.py | 3 - .../eclipse_jdtls/eclipse_jdtls.py | 3 - src/multilspy/language_servers/gopls/gopls.py | 3 - .../intelephense/intelephense.py | 3 - .../jedi_language_server/jedi_server.py | 3 - .../kotlin_language_server.py | 7 -- .../language_servers/omnisharp/omnisharp.py | 3 - .../pyright_language_server/pyright_server.py | 3 - .../rust_analyzer/rust_analyzer.py | 3 - .../language_servers/solargraph/solargraph.py | 3 - .../typescript_language_server.py | 3 - src/multilspy/lsp_protocol_handler/server.py | 10 ++ src/serena/agent.py | 2 +- 15 files changed, 103 insertions(+), 66 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index dcf36ef..e1dd693 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -19,7 +19,7 @@ from collections import defaultdict from contextlib import asynccontextmanager, contextmanager from copy import copy from pathlib import Path, PurePath -from typing import AsyncIterator, Callable, Dict, Iterator, List, Optional, Tuple, Union, cast +from typing import AsyncIterator, Dict, Iterator, List, Optional, Tuple, Union, cast import pathspec @@ -336,29 +336,76 @@ class LanguageServer: return True return False + + async def _shutdown(self, timeout: int = 15): + """ + Performs the complete and robust shutdown sequence for the language server. + 1. Sends the graceful LSP shutdown/exit notifications. + 2. Stops the underlying process and cleans up tasks. + 3. Wraps the entire operation in a timeout to prevent hangs. + """ + if not self.server.is_running(): + self.logger.log("Server process not running, skipping shutdown.", logging.DEBUG) + return + self.logger.log(f"Initiating full shutdown sequence with a {timeout}s timeout...", logging.INFO) + + async def full_shutdown_sequence(): + """The sequence of shutdown operations.""" + # Step 1: Graceful LSP shutdown. + # This sends the `shutdown` and `exit` notifications. + # It's quick and just sends messages, so it's unlikely to hang. + self.logger.log("Sending LSP shutdown/exit notifications...", logging.DEBUG) + await self.server.shutdown() + + # Step 2: Stop the process and cleanup tasks. + # This is the part that can hang if the process is unresponsive. + # The LanguageServerHandler.stop() method has its own internal timeouts, + # but we wrap it in our own as a failsafe. + self.logger.log("Stopping process and cleaning up tasks...", logging.DEBUG) + await self.server.stop() + + try: + # Wrap the entire sequence in our own timeout. + # This ensures that even if a part of the handler's logic hangs + # unexpectedly, our application will not be blocked indefinitely. + await asyncio.wait_for(full_shutdown_sequence(), timeout=timeout) + self.logger.log("Language server shutdown sequence completed successfully.", logging.INFO) + + except asyncio.TimeoutError: + self.logger.log( + f"The full shutdown sequence did not complete within {timeout}s. " + "The process might be left in an orphaned state. This indicates a " + "potential hang in LanguageServerHandler.stop().", + logging.ERROR + ) + # At this point, the handler's internal kill logic has likely failed. + # We can make one last-ditch effort to kill the process if we know its PID. + if self.server.process and self.server.process.returncode is None: + self.logger.log("Attempting final forceful kill of the process.", logging.WARNING) + try: + self.server.process.kill() + await self.server.process.wait() + except ProcessLookupError: + pass # Process already gone + except Exception as e: + self.logger.log(f"Final kill attempt failed: {e}", logging.ERROR) + + except Exception as e: + self.logger.log(f"An unexpected error occurred during shutdown: {e}", logging.ERROR) @asynccontextmanager async def start_server(self) -> AsyncIterator["LanguageServer"]: """ Starts the Language Server and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` """ self.server_started = True - yield self - self.server_started = False - - # TODO: Add support for more LSP features - + try: + yield self + finally: + self.server_started = False + await self._shutdown() + @contextmanager def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]: """ @@ -1704,9 +1751,7 @@ class SyncLanguageServer: ctx = self.language_server.start_server() asyncio.run_coroutine_threadsafe(ctx.__aenter__(), loop=self.loop).result() yield self - asyncio.run_coroutine_threadsafe(ctx.__aexit__(None, None, None), loop=self.loop).result() - self.loop.call_soon_threadsafe(self.loop.stop) - self.loop_thread.join() + self.stop() def request_definition(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: """ @@ -2071,11 +2116,16 @@ class SyncLanguageServer: :return: self for method chaining """ + self.language_server.logger.log(f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}", logging.INFO) + self.language_server.logger.log("Creating new event loop", logging.DEBUG) self.loop = asyncio.new_event_loop() + self.language_server.logger.log("Creating new thread for event loop", logging.DEBUG) self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True) self.loop_thread.start() + self.language_server.logger.log("Starting server (async) context", logging.DEBUG) self._server_context = self.language_server.start_server() - asyncio.run_coroutine_threadsafe(self._server_context.__aenter__(), loop=self.loop).result() + self.language_server.logger.log("Entering server context", logging.DEBUG) + asyncio.run_coroutine_threadsafe(self._server_context.__aenter__(), loop=self.loop).result(timeout=self.timeout) return self def is_running(self) -> bool: @@ -2083,10 +2133,21 @@ class SyncLanguageServer: Check if the language server is running. """ return self.loop is not None and self.loop_thread is not None and self.loop_thread.is_alive() + + async def _shutdown_and_stop_loop(self): + """A coroutine that performs the full shutdown and then stops the event loop.""" + try: + if self._server_context: + await self._server_context.__aexit__(None, None, None) + except Exception as e: + self.language_server.logger.log(f"Exception during async shutdown: {e}", logging.ERROR) + finally: + if self.loop and self.loop.is_running(): + self.loop.stop() def stop(self) -> None: """ - Shuts down the language server process and cleans up resources. + Shuts down the language server process, saves the cache and cleans up resources. If the language server is not running, this method will log a warning and do nothing. """ @@ -2095,12 +2156,18 @@ class SyncLanguageServer: self.language_server.logger.log("Language server not running, skipping shutdown.", logging.INFO) return - assert self.loop - asyncio.run_coroutine_threadsafe(self._server_context.__aexit__(None, None, None), loop=self.loop).result() - self.loop.call_soon_threadsafe(self.loop.stop) - self.loop_thread.join() + assert self.loop is not None + self.language_server.logger.log(f"Initiating shutdown for {self.language_server.repository_root_path}", logging.INFO) + + asyncio.run_coroutine_threadsafe(self._shutdown_and_stop_loop(), loop=self.loop) + + if self.loop_thread: + self.loop_thread.join() + self.loop = None self.loop_thread = None + self._server_context = None + self.language_server.logger.log("Shutdown complete.", logging.INFO) def save_cache(self): """ diff --git a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py b/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py index 8cb4798..e28aa2c 100644 --- a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py @@ -188,6 +188,3 @@ class ClangdLanguageServer(LanguageServer): await self.server_ready.wait() yield self - - await self.server.shutdown() - await self.server.stop() diff --git a/src/multilspy/language_servers/dart_language_server/dart_language_server.py b/src/multilspy/language_servers/dart_language_server/dart_language_server.py index b814306..cd8ea51 100644 --- a/src/multilspy/language_servers/dart_language_server/dart_language_server.py +++ b/src/multilspy/language_servers/dart_language_server/dart_language_server.py @@ -141,6 +141,3 @@ class DartLanguageServer(LanguageServer): self.server.notify.initialized({}) yield self - - await self.server.shutdown() - await self.server.stop() diff --git a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py index cf16ff3..a88dc1b 100644 --- a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -420,6 +420,3 @@ class EclipseJDTLS(LanguageServer): await self.service_ready_event.wait() yield self - - await self.server.shutdown() - await self.server.stop() diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py index d17901a..0e5574a 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/multilspy/language_servers/gopls/gopls.py @@ -152,6 +152,3 @@ class Gopls(LanguageServer): await self.server_ready.wait() yield self - - await self.server.shutdown() - await self.server.stop() diff --git a/src/multilspy/language_servers/intelephense/intelephense.py b/src/multilspy/language_servers/intelephense/intelephense.py index 33382cf..d2ad911 100644 --- a/src/multilspy/language_servers/intelephense/intelephense.py +++ b/src/multilspy/language_servers/intelephense/intelephense.py @@ -181,9 +181,6 @@ class Intelephense(LanguageServer): await self.server_ready.wait() yield self - - await self.server.shutdown() - await self.server.stop() @override # For some reason, the LS may need longer to process this, so we just retry diff --git a/src/multilspy/language_servers/jedi_language_server/jedi_server.py b/src/multilspy/language_servers/jedi_language_server/jedi_server.py index 90d4376..28c9d91 100644 --- a/src/multilspy/language_servers/jedi_language_server/jedi_server.py +++ b/src/multilspy/language_servers/jedi_language_server/jedi_server.py @@ -121,6 +121,3 @@ class JediServer(LanguageServer): self.server.notify.initialized({}) yield self - - await self.server.shutdown() - await self.server.stop() diff --git a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py b/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py index 5edc0e7..ca4e866 100644 --- a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py @@ -225,10 +225,3 @@ class KotlinLanguageServer(LanguageServer): self.completions_available.set() yield self - - try: - await self.server.shutdown() - except Exception as e: - self.logger.log(f"Error during Kotlin server shutdown: {str(e)}", logging.WARNING) - finally: - await self.server.stop() diff --git a/src/multilspy/language_servers/omnisharp/omnisharp.py b/src/multilspy/language_servers/omnisharp/omnisharp.py index 08295a9..df3cf0c 100644 --- a/src/multilspy/language_servers/omnisharp/omnisharp.py +++ b/src/multilspy/language_servers/omnisharp/omnisharp.py @@ -404,6 +404,3 @@ class OmniSharp(LanguageServer): await self.references_available.wait() yield self - - await self.server.shutdown() - await self.server.stop() diff --git a/src/multilspy/language_servers/pyright_language_server/pyright_server.py b/src/multilspy/language_servers/pyright_language_server/pyright_server.py index cd6925e..2a8ee1b 100644 --- a/src/multilspy/language_servers/pyright_language_server/pyright_server.py +++ b/src/multilspy/language_servers/pyright_language_server/pyright_server.py @@ -198,6 +198,3 @@ class PyrightServer(LanguageServer): self.server.notify.initialized({}) yield self - - await self.server.shutdown() - await self.server.stop() diff --git a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py b/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py index 6a2dcee..65f3598 100644 --- a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py @@ -183,6 +183,3 @@ class RustAnalyzer(LanguageServer): await self.server_ready.wait() yield self - - await self.server.shutdown() - await self.server.stop() diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index 645f40a..099ff9d 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -187,6 +187,3 @@ class Solargraph(LanguageServer): await self.server_ready.wait() yield self - - await self.server.shutdown() - await self.server.stop() diff --git a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py index 6657ebb..122de61 100644 --- a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py @@ -221,9 +221,6 @@ class TypeScriptLanguageServer(LanguageServer): yield self - await self.server.shutdown() - await self.server.stop() - @override # For some reason, the LS may need longer to process this, so we just retry async def _send_references_request(self, relative_file_path: str, line: int, column: int): diff --git a/src/multilspy/lsp_protocol_handler/server.py b/src/multilspy/lsp_protocol_handler/server.py index 85e8ac5..8912c58 100644 --- a/src/multilspy/lsp_protocol_handler/server.py +++ b/src/multilspy/lsp_protocol_handler/server.py @@ -209,6 +209,12 @@ class LanguageServerHandler: self.task_counter = 0 self.loop = None self.start_independent_lsp_process = start_independent_lsp_process + + def is_running(self) -> bool: + """ + Checks if the language server process is currently running. + """ + return self.process is not None and self.process.returncode is None async def start(self) -> None: """ @@ -357,9 +363,13 @@ class LanguageServerHandler: """ Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit """ + self._log("Sending shutdown request to server") await self.send.shutdown() + self._log("Received shutdown response from server") self._received_shutdown = True + self._log("Sending exit notification to server") self.notify.exit() + self._log("Sent exit notification to server") if self.process and self.process.stdout: self.process.stdout.set_exception(StopLoopException()) # This yields the control to the event loop to allow the exception to be handled diff --git a/src/serena/agent.py b/src/serena/agent.py index cde3bc1..9e11d84 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -798,8 +798,8 @@ class SerenaAgent: """ # stop the language server if it is running if self.is_language_server_running(): - log.info(f"Stopping the current language server at {self.language_server.repository_root_path} ...") assert self.language_server is not None + log.info(f"Stopping the current language server at {self.language_server.repository_root_path} ...") self.language_server.stop() self.language_server = None From 9202924189d45837540704372bab6a533ebbf362 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 9 Jun 2025 17:58:02 +0200 Subject: [PATCH 006/174] LS: logging, thread-safe shutdown --- .github/workflows/pytest.yml | 10 ++-------- src/multilspy/language_server.py | 15 +++++++++++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 6d28afc..4a0e308 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -55,11 +55,5 @@ jobs: run: uv pip install -e ".[dev]" - name: Test with pytest shell: bash - # Currently, java and rust seem to cause problems in CI, and snapshot tests seem to cause problems on Windows - # probably due to the way the language servers are started and stopped - run: | - if [[ "$RUNNER_OS" == "Windows" ]]; then - uv run pytest -m "not java and not rust and not snapshot" - else - uv run pytest -m "not java and not rust" - fi + # Currently, java and rust seem to cause problems in CI + run: uv run pytest -m "not java and not rust" diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index e1dd693..9983a15 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -241,6 +241,7 @@ class LanguageServer: # cmd is obtained from the child classes, which provide the language specific command to start the language server # LanguageServerHandler provides the functionality to start the language server and communicate with it + self.logger.log(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) self.server = LanguageServerHandler( process_launch_info, logger=logging_fn, @@ -265,6 +266,7 @@ class LanguageServer: pattern = pattern.replace(os.path.sep, '/') processed_patterns.append(pattern) # Combine explicitly passed patterns with the content of the .gitignore file + self.logger.log(f"Processing gitignore content", logging.DEBUG) if config.gitignore_file_content is not None: for line in config.gitignore_file_content.splitlines(): if not line.startswith('#') and line.strip() != '': @@ -1677,6 +1679,9 @@ class SyncLanguageServer: self.timeout = timeout self._server_context = None + + self._shutdown_lock = threading.Lock() + self._is_shutting_down = False @classmethod def create( @@ -2151,11 +2156,13 @@ class SyncLanguageServer: If the language server is not running, this method will log a warning and do nothing. """ - self.save_cache() - if not self.is_running(): - self.language_server.logger.log("Language server not running, skipping shutdown.", logging.INFO) - return + with self._shutdown_lock: + if self._is_shutting_down or not self.is_running(): + self.language_server.logger.log("Language server is already shutting down or not running, skipping shutdown.", logging.INFO) + return + self._is_shutting_down = True + self.save_cache() assert self.loop is not None self.language_server.logger.log(f"Initiating shutdown for {self.language_server.repository_root_path}", logging.INFO) From ce0c82869b1867c0111f45ace33c0eccb1600391 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 9 Jun 2025 18:07:24 +0200 Subject: [PATCH 007/174] LS: thread-safe cache read/write through locking --- src/multilspy/language_server.py | 58 ++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 9983a15..903bf4c 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -252,6 +252,7 @@ class LanguageServer: self.open_file_buffers: Dict[str, LSPFileBuffer] = {} # --------------------------------- MODIFICATIONS BY ORAIOS --------------------------------- + self._cache_lock = threading.Lock() self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]]] = {} """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" self.load_cache() @@ -853,16 +854,17 @@ class LanguageServer: # Should be fixed in the future, it's a small performance optimization cache_key = f"{relative_file_path}-{include_body}" with self.open_file(relative_file_path) as file_data: - file_hash_and_result = self._document_symbols_cache.get(cache_key) - if file_hash_and_result is not None: - file_hash, result = file_hash_and_result - if file_hash == file_data.content_hash: - self.logger.log(f"Returning cached document symbols for {relative_file_path}", logging.DEBUG) - return result + with self._cache_lock: + file_hash_and_result = self._document_symbols_cache.get(cache_key) + if file_hash_and_result is not None: + file_hash, result = file_hash_and_result + if file_hash == file_data.content_hash: + self.logger.log(f"Returning cached document symbols for {relative_file_path}", logging.DEBUG) + return result + else: + self.logger.log(f"Content for {relative_file_path} has changed. Will overwrite in-memory cache", logging.DEBUG) else: - self.logger.log(f"Content for {relative_file_path} has changed. Will overwrite in-memory cache", logging.DEBUG) - else: - self.logger.log(f"No cache hit for symbols with {include_body=} in {relative_file_path}", logging.DEBUG) + self.logger.log(f"No cache hit for symbols with {include_body=} in {relative_file_path}", logging.DEBUG) self.logger.log(f"Requesting document symbols for {relative_file_path} from the Language Server", logging.DEBUG) response = await self.server.send.document_symbol( @@ -947,8 +949,9 @@ class LanguageServer: result = flat_all_symbol_list, root_nodes self.logger.log(f"Caching document symbols for {relative_file_path}", logging.DEBUG) - self._document_symbols_cache[cache_key] = (file_data.content_hash, result) - self._cache_has_changed = True + with self._cache_lock: + self._document_symbols_cache[cache_key] = (file_data.content_hash, result) + self._cache_has_changed = True return result async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: @@ -1603,35 +1606,40 @@ class LanguageServer: return Path(self.repository_root_path) / ".serena" / "cache" / "document_symbols_cache_v20-05-25.pkl" def save_cache(self): - if self._cache_has_changed: + with self._cache_lock: + if not self._cache_has_changed: + self.logger.log("No changes to document symbols cache, skipping save", logging.DEBUG) + return + self.logger.log(f"Saving updated document symbols cache to {self._cache_path}", logging.INFO) self._cache_path.parent.mkdir(parents=True, exist_ok=True) try: with open(self._cache_path, "wb") as f: pickle.dump(self._document_symbols_cache, f) + self._cache_has_changed = False except Exception as e: self.logger.log( - f"Failed to save document symbols cache to {self._cache_path}: {e}. " - "Note: this may have resulted in a corrupted cache file.", logging.ERROR - ) - else: - self.logger.log(f"No changes to document symbols cache, skipping save", logging.DEBUG) - self._cache_has_changed = False + f"Failed to save document symbols cache to {self._cache_path}: {e}. " + "Note: this may have resulted in a corrupted cache file.", + logging.ERROR, + ) def load_cache(self): if not self._cache_path.exists(): return - self.logger.log(f"Loading document symbols cache from {self._cache_path}", logging.INFO) - with open(self._cache_path, "rb") as f: + + with self._cache_lock: + self.logger.log(f"Loading document symbols cache from {self._cache_path}", logging.INFO) try: - self._document_symbols_cache = pickle.load(f) + with open(self._cache_path, "rb") as f: + self._document_symbols_cache = pickle.load(f) except Exception as e: # cache often becomes corrupt, so just skip loading it self.logger.log( - f"Failed to load document symbols cache from {self._cache_path}: {e}. Possible cause: the cache file is corrupted. " - "Check for any errors related to saving the cache in the logs.", - logging.ERROR - ) + f"Failed to load document symbols cache from {self._cache_path}: {e}. Possible cause: the cache file is corrupted. " + "Check for any errors related to saving the cache in the logs.", + logging.ERROR, + ) async def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]: From 1c6af8cfea3b8bbef35ca9148a6a738d343a15e2 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 9 Jun 2025 21:54:55 +0200 Subject: [PATCH 008/174] LS: attempt at more robust shutdown --- src/multilspy/language_server.py | 177 ++++++++++++------- src/multilspy/lsp_protocol_handler/server.py | 1 + 2 files changed, 114 insertions(+), 64 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 903bf4c..f308762 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -19,6 +19,7 @@ from collections import defaultdict from contextlib import asynccontextmanager, contextmanager from copy import copy from pathlib import Path, PurePath +import time from typing import AsyncIterator, Dict, Iterator, List, Optional, Tuple, Union, cast import pathspec @@ -124,15 +125,16 @@ class LanguageServer: gitignore_path = os.path.join(repository_root_path, ".gitignore") if not os.path.exists(gitignore_path): logger.log( - f"Should ignore all files in gitignore not not .gitignore found at {gitignore_path}. Skipping.", + f"Should ignore all files in gitignore but no .gitignore found at {gitignore_path}. Skipping.", logging.WARNING ) gitignore_file_content = None else: if config.gitignore_file_content is not None: raise ValueError( - f"Asked to add gitignore content to the config for {repository_root_path=} but there already is a non-empty entry" + f"Asked to add gitignore content to the config for {repository_root_path=} but there already is a non-empty entry there:\n{config.gitignore_file_content=}" ) + logger.log(f"Reading gitignore file from {gitignore_path}", logging.DEBUG) with open(gitignore_path, "r", encoding="utf-8") as f: gitignore_file_content = f.read() config.gitignore_file_content = gitignore_file_content @@ -228,10 +230,18 @@ class LanguageServer: ) self.logger = logger - self.server_started = False self.repository_root_path: str = repository_root_path - self.completions_available = asyncio.Event() + self.logger.log(f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) + + # load cache first to prevent any racing conditions due to asyncio stuff + self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]]] = {} + """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" + self._cache_lock = threading.Lock() + self._cache_has_changed: bool = False + self.load_cache() + self.server_started = False + self.completions_available = asyncio.Event() if config.trace_lsp_communication: def logging_fn(source: str, target: str, msg: StringDict | str): self.logger.log(f"LSP: {source} -> {target}: {str(msg)}", logging.DEBUG) @@ -251,12 +261,6 @@ class LanguageServer: self.language_id = language_id self.open_file_buffers: Dict[str, LSPFileBuffer] = {} - # --------------------------------- MODIFICATIONS BY ORAIOS --------------------------------- - self._cache_lock = threading.Lock() - self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]]] = {} - """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" - self.load_cache() - self._cache_has_changed: bool = False self.language = Language(language_id) # Set up the pathspec matcher for the ignored paths @@ -340,62 +344,78 @@ class LanguageServer: return False - async def _shutdown(self, timeout: int = 15): + async def _shutdown(self, timeout: float = 10.0): """ - Performs the complete and robust shutdown sequence for the language server. - 1. Sends the graceful LSP shutdown/exit notifications. - 2. Stops the underlying process and cleans up tasks. - 3. Wraps the entire operation in a timeout to prevent hangs. + A robust shutdown process designed to terminate cleanly on all platforms, including Windows, + by explicitly closing all I/O pipes. """ if not self.server.is_running(): self.logger.log("Server process not running, skipping shutdown.", logging.DEBUG) return - self.logger.log(f"Initiating full shutdown sequence with a {timeout}s timeout...", logging.INFO) - - async def full_shutdown_sequence(): - """The sequence of shutdown operations.""" - # Step 1: Graceful LSP shutdown. - # This sends the `shutdown` and `exit` notifications. - # It's quick and just sends messages, so it's unlikely to hang. - self.logger.log("Sending LSP shutdown/exit notifications...", logging.DEBUG) - await self.server.shutdown() - - # Step 2: Stop the process and cleanup tasks. - # This is the part that can hang if the process is unresponsive. - # The LanguageServerHandler.stop() method has its own internal timeouts, - # but we wrap it in our own as a failsafe. - self.logger.log("Stopping process and cleaning up tasks...", logging.DEBUG) - await self.server.stop() + self.logger.log(f"Initiating final robust shutdown with a {timeout}s timeout...", logging.INFO) + process = self.server.process + reader_tasks = list(self.server.tasks.values()) + # --- Main Shutdown Logic --- try: - # Wrap the entire sequence in our own timeout. - # This ensures that even if a part of the handler's logic hangs - # unexpectedly, our application will not be blocked indefinitely. - await asyncio.wait_for(full_shutdown_sequence(), timeout=timeout) - self.logger.log("Language server shutdown sequence completed successfully.", logging.INFO) + # Stage 1: Graceful Termination Request + # Send LSP shutdown and close stdin to signal no more input. + try: + await asyncio.wait_for(self.server.shutdown(), timeout=2.0) + if process.stdin and not process.stdin.is_closing(): + process.stdin.close() + except Exception: + pass # Ignore errors here, we are proceeding to terminate anyway. + + # Stage 2: Terminate and Concurrently Drain stdout/stderr + process.terminate() + + # Wait for the process to exit AND for the output pipes to be drained. + # The reader tasks will exit when they hit EOF. + await asyncio.wait_for( + asyncio.gather(process.wait(), *reader_tasks), + timeout=timeout - 2.0 + ) + self.logger.log("Process terminated and output pipes drained.", logging.INFO) except asyncio.TimeoutError: - self.logger.log( - f"The full shutdown sequence did not complete within {timeout}s. " - "The process might be left in an orphaned state. This indicates a " - "potential hang in LanguageServerHandler.stop().", - logging.ERROR - ) - # At this point, the handler's internal kill logic has likely failed. - # We can make one last-ditch effort to kill the process if we know its PID. - if self.server.process and self.server.process.returncode is None: - self.logger.log("Attempting final forceful kill of the process.", logging.WARNING) + # Stage 3: Forceful Kill + self.logger.log("Graceful termination failed. Forcefully killing process...", logging.WARNING) + if self.server.is_running(): try: - self.server.process.kill() - await self.server.process.wait() - except ProcessLookupError: - pass # Process already gone + process.kill() + # Wait for the killed process to be reaped by the OS. + await process.wait() except Exception as e: - self.logger.log(f"Final kill attempt failed: {e}", logging.ERROR) - + self.logger.log(f"Error during forceful kill: {e}", logging.ERROR) + except Exception as e: - self.logger.log(f"An unexpected error occurred during shutdown: {e}", logging.ERROR) + self.logger.log(f"An unexpected error occurred during shutdown logic: {e}", logging.ERROR) + + finally: + # === STAGE 4: EXPLICIT TASK & PIPE CLEANUP === + self.logger.log("Performing final task cancellation and pipe handle cleanup...", logging.DEBUG) + + # 1. Cancel any lingering reader tasks. + for task in reader_tasks: + if not task.done(): + task.cancel() + # Wait for cancellations to complete. + await asyncio.gather(*reader_tasks, return_exceptions=True) + self.server.tasks = {} + + # 2. Explicitly close each pipe to release OS handles. + for pipe in [process.stdin, process.stdout, process.stderr]: + if pipe and hasattr(pipe, 'is_closing') and not pipe.is_closing(): + try: + pipe.close() + except Exception: + pass + + # 3. Null out the process object in the handler. + self.server.process = None + self.logger.log("Shutdown sequence fully finished.", logging.DEBUG) @asynccontextmanager async def start_server(self) -> AsyncIterator["LanguageServer"]: @@ -1633,6 +1653,7 @@ class LanguageServer: try: with open(self._cache_path, "rb") as f: self._document_symbols_cache = pickle.load(f) + self.logger.log(f"Loaded {len(self._document_symbols_cache)} document symbols from cache.", logging.INFO) except Exception as e: # cache often becomes corrupt, so just skip loading it self.logger.log( @@ -2158,30 +2179,58 @@ class SyncLanguageServer: if self.loop and self.loop.is_running(): self.loop.stop() - def stop(self) -> None: + def stop(self, shutdown_timeout: float = 5.0) -> None: """ - Shuts down the language server process, saves the cache and cleans up resources. - - If the language server is not running, this method will log a warning and do nothing. + Stops the language server and robustly cleans up all associated resources, + including the asyncio event loop, to prevent hangs on process exit. """ + # 1. Use a lock and flag to make the shutdown call thread-safe and idempotent, + # ensuring the shutdown logic runs only once. with self._shutdown_lock: - if self._is_shutting_down or not self.is_running(): - self.language_server.logger.log("Language server is already shutting down or not running, skipping shutdown.", logging.INFO) + if self._is_shutting_down: + self.language_server.logger.log("Already shutting down or stopped, skipping", logging.DEBUG) return self._is_shutting_down = True + if not self.is_running(): + self.language_server.logger.log("Language server is not running, skipping", logging.DEBUG) + return + + self.language_server.logger.log("Initiating server shutdown...", logging.INFO) self.save_cache() - assert self.loop is not None - self.language_server.logger.log(f"Initiating shutdown for {self.language_server.repository_root_path}", logging.INFO) - - asyncio.run_coroutine_threadsafe(self._shutdown_and_stop_loop(), loop=self.loop) + # 2. From the main thread, schedule the async shutdown tasks to be run on + # the event loop. This coroutine will gracefully shut down the LSP + # connection and then call `loop.stop()`, which allows the background + # thread's `loop.run_forever()` call to finally return. + if self.loop and self.loop.is_running(): + asyncio.run_coroutine_threadsafe(self._shutdown_and_stop_loop(), self.loop) + + # 3. Wait for the background thread to exit. We use a timeout as a crucial + # safety net against deadlocks. if self.loop_thread: - self.loop_thread.join() + self.loop_thread.join(timeout=shutdown_timeout) + if self.loop_thread.is_alive(): + self.language_server.logger.log( + "Event loop thread did not terminate within timeout. The process may be stuck.", + logging.ERROR + ) + # 4. Perform the final, authoritative cleanup from the main thread. This is + # the most critical step to prevent hangs. + if self.loop and not self.loop.is_closed(): + # Schedule the loop's `close()` method to be run. This releases + # underlying OS resources (like the IocpProactor on Windows). + self.loop.call_soon_threadsafe(self.loop.close) + # Give the event loop a brief moment to process the close command + # before the main process exits. + time.sleep(0.1) + + # 5. Null out attributes for clean garbage collection. self.loop = None self.loop_thread = None self._server_context = None + self._is_shutting_down = False self.language_server.logger.log("Shutdown complete.", logging.INFO) def save_cache(self): diff --git a/src/multilspy/lsp_protocol_handler/server.py b/src/multilspy/lsp_protocol_handler/server.py index 8912c58..3cba4d4 100644 --- a/src/multilspy/lsp_protocol_handler/server.py +++ b/src/multilspy/lsp_protocol_handler/server.py @@ -248,6 +248,7 @@ class LanguageServerHandler: self.task_counter += 1 self.tasks[self.task_counter] = self.loop.create_task(self.run_forever_stderr()) self.task_counter += 1 + async def stop(self) -> None: """ From d2cd006007eae1fb8c5e6fa292b238581c89c956 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 9 Jun 2025 22:01:03 +0200 Subject: [PATCH 009/174] More logging in tests --- test/conftest.py | 8 +++++++- test/serena/test_symbol_editing.py | 24 +++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index a5ca841..2b45d36 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,11 +1,15 @@ +import logging from pathlib import Path import pytest +from sensai.util.logging import LOG_DEFAULT_FORMAT, configure from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger +configure(level=logging.DEBUG) + @pytest.fixture(scope="session") def resources_dir() -> Path: @@ -24,7 +28,9 @@ def get_repo_path(language: Language) -> Path: def create_ls(language: Language, repo_path: str): config = MultilspyConfig(code_language=language) - logger = MultilspyLogger() + logger = MultilspyLogger(log_level=logging.DEBUG) + logger.logger.setLevel(logging.DEBUG) + logger.logger.handlers[0].setFormatter(logging.Formatter(LOG_DEFAULT_FORMAT)) return SyncLanguageServer.create(config, logger, repo_path) diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index c569db5..986e465 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -1,6 +1,8 @@ +import logging import os import shutil import tempfile +import time from abc import abstractmethod from collections.abc import Iterator from contextlib import contextmanager @@ -16,6 +18,8 @@ from test.conftest import create_ls, get_repo_path pytestmark = pytest.mark.snapshot +log = logging.getLogger(__name__) + class EditingTest: def __init__(self, language: Language, rel_path: str): @@ -35,14 +39,32 @@ class EditingTest: self.repo_path = temp_dir / self.original_repo_path.name language_server = None # Initialize language_server try: + print(f"Copying repo from {self.original_repo_path} to {self.repo_path}") shutil.copytree(self.original_repo_path, self.repo_path) + # prevent deadlock on Windows due to file locks caused by antivirus or some other external software + # wait for a long time here + if os.name == "nt": + time.sleep(1) + log.info(f"Creating language server for {self.language} {self.rel_path}") language_server = create_ls(self.language, str(self.repo_path)) + log.info(f"Starting language server for {self.language} {self.rel_path}") language_server.start() + log.info(f"Language server started for {self.language} {self.rel_path}") yield SymbolManager(lang_server=language_server) finally: if language_server is not None and language_server.is_running(): + log.info(f"Stopping language server for {self.language} {self.rel_path}") language_server.stop() - shutil.rmtree(temp_dir) + # attempt at trigger of garbage collection + language_server = None + log.info(f"Language server stopped for {self.language} {self.rel_path}") + + # prevent deadlock on Windows due to lingering file locks + if os.name == "nt": + time.sleep(1) + log.info(f"Removing temp directory {temp_dir}") + shutil.rmtree(temp_dir, ignore_errors=True) + log.info(f"Temp directory {temp_dir} removed") def _read_file(self, rel_path: str) -> str: """Read the content of a file in the test repository.""" From 993c6f6dfd7852ac74acb5690bd776296ab353a4 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 9 Jun 2025 23:18:52 +0200 Subject: [PATCH 010/174] Mercilessly kill all asyncio stuff at shutdown --- src/multilspy/language_server.py | 119 +++++++++++++++++++++-------- test/conftest.py | 6 +- test/serena/test_symbol_editing.py | 4 +- 3 files changed, 92 insertions(+), 37 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index f308762..afdefc5 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -2176,8 +2176,23 @@ class SyncLanguageServer: except Exception as e: self.language_server.logger.log(f"Exception during async shutdown: {e}", logging.ERROR) finally: + # Clean up tasks but DON'T stop the loop from here + # Let the main thread handle loop stopping to avoid coroutine deadlocks if self.loop and self.loop.is_running(): - self.loop.stop() + current_task = asyncio.current_task(self.loop) + pending_tasks = [task for task in asyncio.all_tasks(self.loop) + if not task.done() and task is not current_task] + + if pending_tasks: + self.language_server.logger.log(f"Cancelling {len(pending_tasks)} pending tasks", logging.DEBUG) + for task in pending_tasks: + try: + task.cancel() + except Exception: + pass + + self.language_server.logger.log("Async shutdown tasks completed, returning to main thread", logging.DEBUG) + # Note: NOT calling loop.stop() here - let main thread do it def stop(self, shutdown_timeout: float = 5.0) -> None: """ @@ -2191,47 +2206,85 @@ class SyncLanguageServer: self.language_server.logger.log("Already shutting down or stopped, skipping", logging.DEBUG) return self._is_shutting_down = True - + if not self.is_running(): self.language_server.logger.log("Language server is not running, skipping", logging.DEBUG) return - + self.language_server.logger.log("Initiating server shutdown...", logging.INFO) self.save_cache() - - # 2. From the main thread, schedule the async shutdown tasks to be run on - # the event loop. This coroutine will gracefully shut down the LSP - # connection and then call `loop.stop()`, which allows the background - # thread's `loop.run_forever()` call to finally return. - if self.loop and self.loop.is_running(): - asyncio.run_coroutine_threadsafe(self._shutdown_and_stop_loop(), self.loop) - - # 3. Wait for the background thread to exit. We use a timeout as a crucial - # safety net against deadlocks. - if self.loop_thread: - self.loop_thread.join(timeout=shutdown_timeout) - if self.loop_thread.is_alive(): - self.language_server.logger.log( - "Event loop thread did not terminate within timeout. The process may be stuck.", - logging.ERROR - ) - - # 4. Perform the final, authoritative cleanup from the main thread. This is - # the most critical step to prevent hangs. - if self.loop and not self.loop.is_closed(): - # Schedule the loop's `close()` method to be run. This releases - # underlying OS resources (like the IocpProactor on Windows). - self.loop.call_soon_threadsafe(self.loop.close) - # Give the event loop a brief moment to process the close command - # before the main process exits. - time.sleep(0.1) - - # 5. Null out attributes for clean garbage collection. + + # Detect platform to choose shutdown strategy + is_windows = os.name == "nt" + + if not is_windows: + # 2. Graceful shutdown for Linux/Unix - try proper async cleanup first + shutdown_future = None + if self.loop and self.loop.is_running(): + shutdown_future = asyncio.run_coroutine_threadsafe(self._shutdown_and_stop_loop(), self.loop) + + # 3. Wait for the background thread to exit + if self.loop_thread: + self.loop_thread.join(timeout=shutdown_timeout) + if self.loop_thread.is_alive(): + self.language_server.logger.log("Event loop thread did not terminate within timeout", logging.WARNING) + + # 4. Wait for the shutdown coroutine to complete if it was scheduled + if shutdown_future: + try: + shutdown_future.result(timeout=2.0) + self.language_server.logger.log("Graceful async shutdown completed", logging.DEBUG) + except Exception as e: + self.language_server.logger.log(f"Async shutdown failed: {e}", logging.WARNING) + + # 5. Close the loop properly after graceful shutdown + if self.loop and not self.loop.is_closed(): + try: + self.loop.close() + self.language_server.logger.log("Event loop closed successfully", logging.DEBUG) + except Exception as e: + self.language_server.logger.log(f"Event loop close failed: {e}", logging.WARNING) + + else: + # WINDOWS NUCLEAR OPTION: Skip graceful shutdown - go straight to nuclear + # Windows IocpProactor has too many issues with proper cleanup + self.language_server.logger.log("Windows detected - using nuclear shutdown to prevent zombies", logging.WARNING) + + # Force stop the loop immediately without waiting for async cleanup + if self.loop and self.loop.is_running(): + try: + self.loop.call_soon_threadsafe(lambda: setattr(self.loop, '_stopping', True)) + self.loop.call_soon_threadsafe(self.loop.stop) + except Exception: + pass + + # Force mark loop as closed IMMEDIATELY to prevent zombies + try: + if hasattr(self.loop, '_closed'): + self.loop._closed = True + if hasattr(self.loop, '_stopping'): + self.loop._stopping = True + if hasattr(self.loop, '_ready'): + self.loop._ready.clear() + except Exception: + pass + + # Give thread minimal time to exit, then abandon it + if self.loop_thread: + self.loop_thread.join(timeout=1.0) # Only 1 second on Windows + if self.loop_thread.is_alive(): + self.language_server.logger.log("Thread stuck - abandoning to prevent GC deadlock", logging.WARNING) + + # 6. Null out everything immediately - same for both platforms self.loop = None self.loop_thread = None self._server_context = None self._is_shutting_down = False - self.language_server.logger.log("Shutdown complete.", logging.INFO) + + shutdown_type = "Nuclear" if is_windows else "Graceful" + self.language_server.logger.log(f"{shutdown_type} shutdown complete - all references cleared", logging.INFO) + + def save_cache(self): """ diff --git a/test/conftest.py b/test/conftest.py index 2b45d36..92b3430 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -29,8 +29,10 @@ def get_repo_path(language: Language) -> Path: def create_ls(language: Language, repo_path: str): config = MultilspyConfig(code_language=language) logger = MultilspyLogger(log_level=logging.DEBUG) - logger.logger.setLevel(logging.DEBUG) - logger.logger.handlers[0].setFormatter(logging.Formatter(LOG_DEFAULT_FORMAT)) + + # Simple way: Use basicConfig to set format for all handlers + logging.basicConfig(format=LOG_DEFAULT_FORMAT, level=logging.DEBUG, force=True) # Override existing configuration + return SyncLanguageServer.create(config, logger, repo_path) diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index 986e465..12be047 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -44,7 +44,7 @@ class EditingTest: # prevent deadlock on Windows due to file locks caused by antivirus or some other external software # wait for a long time here if os.name == "nt": - time.sleep(1) + time.sleep(0.1) log.info(f"Creating language server for {self.language} {self.rel_path}") language_server = create_ls(self.language, str(self.repo_path)) log.info(f"Starting language server for {self.language} {self.rel_path}") @@ -61,7 +61,7 @@ class EditingTest: # prevent deadlock on Windows due to lingering file locks if os.name == "nt": - time.sleep(1) + time.sleep(0.1) log.info(f"Removing temp directory {temp_dir}") shutil.rmtree(temp_dir, ignore_errors=True) log.info(f"Temp directory {temp_dir} removed") From d1537d682cda130096d9592806e41ce9daa2de83 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 10 Jun 2025 00:36:25 +0200 Subject: [PATCH 011/174] CI: maybe rust and java run? --- .github/workflows/pytest.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 4a0e308..fbb73c3 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -55,5 +55,4 @@ jobs: run: uv pip install -e ".[dev]" - name: Test with pytest shell: bash - # Currently, java and rust seem to cause problems in CI - run: uv run pytest -m "not java and not rust" + run: uv run pytest -vv From c14fff05172cedf5aabccc824380cc54d6c89e34 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 10 Jun 2025 01:03:45 +0200 Subject: [PATCH 012/174] No java tests on ubuntu for now --- .github/workflows/pytest.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index fbb73c3..ba193ee 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -55,4 +55,9 @@ jobs: run: uv pip install -e ".[dev]" - name: Test with pytest shell: bash - run: uv run pytest -vv + run: | + if [ "${{ runner.os }}" = "Linux" ]; then + uv run pytest -vv -m "not java" + else + uv run pytest -vv + fi From 6c03f10f8d9f8dbcf2f13d16e3e74c982067723b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 10 Jun 2025 01:09:36 +0200 Subject: [PATCH 013/174] No rust tests on ubuntu either for now --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ba193ee..6368dc2 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -57,7 +57,7 @@ jobs: shell: bash run: | if [ "${{ runner.os }}" = "Linux" ]; then - uv run pytest -vv -m "not java" + uv run pytest -vv -m "not java and not rust" else uv run pytest -vv fi From 9c7440861bd3a85d98b9a1c8d0e8037decaa66d1 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 11 Jun 2025 14:03:50 +0200 Subject: [PATCH 014/174] Gitignore handling and read-edit tools - important fixes and improvments 1. Use all gitignore files in SerenaAgent, not just top level 2. Listing and file finding tools now can find all non-ignored files (previously they would only find code files) 3. Read and edit tools will raise an error on attempting to operate on an ignored file or a path outside the project root --- src/multilspy/language_server.py | 47 +++--------- src/multilspy/multilspy_config.py | 2 - src/serena/agent.py | 119 ++++++++++++++++++++++-------- src/serena/util/file_system.py | 24 ++++-- 4 files changed, 113 insertions(+), 79 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index afdefc5..00374aa 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -105,7 +105,7 @@ class LanguageServer: return dirname.startswith('.') @classmethod - def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, add_gitignore_content_to_config: bool = True) -> "LanguageServer": + def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str) -> "LanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -115,30 +115,8 @@ class LanguageServer: :param repository_root_path: The root path of the repository. :param config: The Multilspy configuration. :param logger: The logger to use. - :param add_gitignore_content_to_config: whether to add the content of the .gitignore file (if any found) to the config, so that - the paths ignored there are also ignored by the language server - :return LanguageServer: A language specific LanguageServer instance. """ - config = copy(config) # prevent mutation - if add_gitignore_content_to_config: - gitignore_path = os.path.join(repository_root_path, ".gitignore") - if not os.path.exists(gitignore_path): - logger.log( - f"Should ignore all files in gitignore but no .gitignore found at {gitignore_path}. Skipping.", - logging.WARNING - ) - gitignore_file_content = None - else: - if config.gitignore_file_content is not None: - raise ValueError( - f"Asked to add gitignore content to the config for {repository_root_path=} but there already is a non-empty entry there:\n{config.gitignore_file_content=}" - ) - logger.log(f"Reading gitignore file from {gitignore_path}", logging.DEBUG) - with open(gitignore_path, "r", encoding="utf-8") as f: - gitignore_file_content = f.read() - config.gitignore_file_content = gitignore_file_content - if config.code_language == Language.PYTHON: from multilspy.language_servers.pyright_language_server.pyright_server import ( PyrightServer, @@ -270,12 +248,7 @@ class LanguageServer: # Normalize separators (pathspec expects forward slashes) pattern = pattern.replace(os.path.sep, '/') processed_patterns.append(pattern) - # Combine explicitly passed patterns with the content of the .gitignore file - self.logger.log(f"Processing gitignore content", logging.DEBUG) - if config.gitignore_file_content is not None: - for line in config.gitignore_file_content.splitlines(): - if not line.startswith('#') and line.strip() != '': - processed_patterns.append(line.strip()) + self.logger.log(f"Processing {len(processed_patterns)} ignored paths from the config", logging.DEBUG) # Create a pathspec matcher from the processed patterns self._ignore_spec = pathspec.PathSpec.from_lines( @@ -285,7 +258,7 @@ class LanguageServer: def get_ignore_spec(self) -> pathspec.PathSpec: """Returns the pathspec matcher for the paths that were configured to be ignored through - the multilspy config file and the .gitignore file. + the multilspy config. This is is a subset of the full language-specific ignore spec that determines which files are relevant for the language server. @@ -339,7 +312,7 @@ class LanguageServer: normalized_path = normalized_path + '/' # Use the pathspec matcher to check if the path matches any ignore pattern - if self._ignore_spec.match_file(normalized_path): + if self.get_ignore_spec().match_file(normalized_path): return True return False @@ -1697,7 +1670,7 @@ class SyncLanguageServer: It is used to communicate with Language Servers of different programming languages. """ - def __init__(self, language_server: LanguageServer, timeout: Optional[int] = None): + def __init__(self, language_server: LanguageServer, timeout: Optional[float] = None): """ :param language_server: the async language server being wrapped :param timeout: the timeout, in seconds, to use for requests to the language server. @@ -1714,8 +1687,8 @@ class SyncLanguageServer: @classmethod def create( - cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, add_gitignore_content_to_config=True, - timeout: Optional[int] = None + cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, + timeout: Optional[float] = None ) -> "SyncLanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -1725,13 +1698,11 @@ class SyncLanguageServer: :param repository_root_path: The root path of the repository (must be absolute). :param config: The Multilspy configuration. :param logger: The logger to use. - :param add_gitignore_content_to_config: whether to add the content of the .gitignore file (if any found) to the config, so that - the paths ignored there are also ignored by the language server :param timeout: the timeout, in seconds, to use for requests; if None, use no timeout :return SyncLanguageServer: A language specific LanguageServer instance. """ - return SyncLanguageServer(LanguageServer.create(config, logger, repository_root_path, add_gitignore_content_to_config=add_gitignore_content_to_config), timeout=timeout) + return SyncLanguageServer(LanguageServer.create(config, logger, repository_root_path), timeout=timeout) @property def repository_root_path(self) -> str: @@ -2313,7 +2284,7 @@ class SyncLanguageServer: def get_ignore_spec(self) -> pathspec.PathSpec: """Returns the pathspec matcher for the paths that were configured to be ignored through - the multilspy config file and the .gitignore file. + the multilspy config. This is is a subset of the full language-specific ignore spec that determines which files are relevant for the language server. diff --git a/src/multilspy/multilspy_config.py b/src/multilspy/multilspy_config.py index 2b4f7ca..c1d77ae 100644 --- a/src/multilspy/multilspy_config.py +++ b/src/multilspy/multilspy_config.py @@ -82,8 +82,6 @@ class MultilspyConfig: start_independent_lsp_process: bool = True ignored_paths: list[str] = field(default_factory=list) """Paths, dirs or glob-like patterns. The matching will follow the same logic as for .gitignore entries""" - gitignore_file_content: str | None = None - """Optional content of the gitignore file. If passed, will be used in addition to the explicitly passed ignored_paths for deciding which paths to ignore.""" @classmethod def from_dict(cls, env: dict): diff --git a/src/serena/agent.py b/src/serena/agent.py index 9e11d84..0795e83 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -25,6 +25,7 @@ from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast import yaml from overrides import override +from pathspec import PathSpec from ruamel.yaml.comments import CommentedMap from sensai.util import logging from sensai.util.logging import FallbackHandler @@ -41,7 +42,7 @@ from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI from serena.prompt_factory import PromptFactory, SerenaPromptFactory from serena.symbol import SymbolManager from serena.text_utils import search_files -from serena.util.file_system import scan_directory +from serena.util.file_system import GitignoreParser, match_path, scan_directory from serena.util.general import load_yaml, save_yaml from serena.util.inspection import determine_programming_language_composition, iter_subclasses from serena.util.shell import execute_shell_command @@ -583,6 +584,8 @@ class SerenaAgent: self.symbol_manager: SymbolManager | None = None self.memories_manager: MemoriesManager | None = None self.lines_read: LinesRead | None = None + self.ignore_spec: PathSpec # not set to None to avoid assert statements + """Ignore spec, extracted from the project's gitignore files and the explicitly configured ignored paths.""" # Apply context and mode tool configurations if context is None: @@ -615,6 +618,46 @@ class SerenaAgent: raise ValueError("Cannot get project root if no project is active.") return project.project_root + def path_is_inside_project(self, path: str | Path) -> bool: + """ + Checks if the given (absolute or relative) path is inside the project directory. + Note that even relative paths may be outside if the contain ".." or point to symlinks. + """ + path = Path(path) + _proj_root = Path(self.get_project_root()) + if not path.is_absolute(): + path = _proj_root / path + + path = path.resolve() + return path.is_relative_to(_proj_root) + + def path_is_gitignored(self, path: str | Path) -> bool: + """ + Checks if the given path is ignored by git. Non absolute paths are assumed to be relative to the project root. + """ + path = Path(path) + if path.is_absolute(): + relative_path = path.relative_to(self.get_project_root()) + else: + relative_path = path + + # always ignore paths inside .git + if len(relative_path.parts) > 0 and relative_path.parts[0] == ".git": + return True + + return match_path(str(relative_path), self.ignore_spec) + + def validate_relative_path(self, relative_path: str) -> None: + """ + Validates that the given relative path is safe to read or edit, + meaning it's inside the project directory and is not ignored by git. + """ + if not self.path_is_inside_project(relative_path): + raise ValueError(f"{relative_path=} points to path outside of the repository root, can't use it for safety reasons") + + if self.path_is_gitignored(relative_path): + raise ValueError(f"File {relative_path} is gitignored, can't read or edit it for safety reasons") + def get_exposed_tool_instances(self) -> list["Tool"]: """ :return: all tool instances, including the non-active ones. For MCP clients, we need to expose them all since typical @@ -685,6 +728,7 @@ class SerenaAgent: # start the language server self.reset_language_server() assert self.language_server is not None + self.ignore_spec = self.language_server.get_ignore_spec() # initialize project-specific instances self.symbol_manager = SymbolManager(self.language_server, self) @@ -805,9 +849,21 @@ class SerenaAgent: # instantiate and start the language server assert self._active_project is not None + ignored_paths = self._active_project.project_config.ignored_paths + if len(ignored_paths) > 0: + log.info(f"Using {len(ignored_paths)} ignored paths from the explicit project configuration.") + log.debug(f"Ignored paths: {ignored_paths}") + if self._active_project.project_config.ignore_all_files_in_gitignore: + log.info(f"Parsing all gitignore files in {self._active_project.project_root}") + gitignore_parser = GitignoreParser(self._active_project.project_root) + log.info(f"Found {len(gitignore_parser.get_ignore_specs())} gitignore files.") + for spec in gitignore_parser.get_ignore_specs(): + log.debug(f"Adding {len(spec.patterns)} patterns from {spec.file_path} to the ignored paths.") + ignored_paths.extend(spec.patterns) + log.debug(f"Using {len(ignored_paths)} ignored paths in total.") multilspy_config = MultilspyConfig( code_language=self._active_project.project_config.language, - ignored_paths=self._active_project.project_config.ignored_paths, + ignored_paths=ignored_paths, trace_lsp_communication=self.serena_config.trace_lsp_communication, ) ls_logger = MultilspyLogger(log_level=self.serena_config.log_level) @@ -816,7 +872,6 @@ class SerenaAgent: multilspy_config, ls_logger, self._active_project.project_root, - add_gitignore_content_to_config=self._active_project.project_config.ignore_all_files_in_gitignore, ) self.language_server.start() if not self.language_server.is_running(): @@ -1061,6 +1116,8 @@ class ReadFileTool(Tool): required for the task. :return: the full text of the file at the given relative path """ + self.agent.validate_relative_path(relative_path) + result = self.language_server.retrieve_full_file_content(relative_path) result_lines = result.splitlines() if end_line is None: @@ -1093,11 +1150,17 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit): :param content: the (utf-8-encoded) content to write to the file :return: a message indicating success or failure """ - absolute_path = os.path.join(self.get_project_root(), relative_path) - os.makedirs(os.path.dirname(absolute_path), exist_ok=True) - with open(absolute_path, "w", encoding="utf-8") as f: - f.write(content) - return f"File created: {relative_path}" + self.agent.validate_relative_path(relative_path) + + abs_path = (Path(self.get_project_root()) / relative_path).resolve() + will_overwrite_existing = abs_path.exists() + + abs_path.parent.mkdir(parents=True, exist_ok=True) + abs_path.write_text(content, encoding="utf-8") + answer = f"File created: {relative_path}." + if will_overwrite_existing: + answer += " Overwrote existing file." + return answer class ListDirTool(Tool): @@ -1107,7 +1170,7 @@ class ListDirTool(Tool): def apply(self, relative_path: str, recursive: bool, max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH) -> str: """ - Lists files and directories in the given directory (optionally with recursion). + Lists all non-gitignored files and directories in the given directory (optionally with recursion). :param relative_path: the relative path to the directory to list; pass "." to scan the project root :param recursive: whether to scan subdirectories recursively @@ -1116,17 +1179,14 @@ class ListDirTool(Tool): required for the task. :return: a JSON object with the names of directories and files within the given directory """ - - def is_ignored_path(abs_path: str) -> bool: - rel_path = os.path.relpath(abs_path, self.get_project_root()) - return self.language_server.is_ignored_path(rel_path, ignore_unsupported_files=False) + self.agent.validate_relative_path(relative_path) dirs, files = scan_directory( os.path.join(self.get_project_root(), relative_path), relative_to=self.get_project_root(), recursive=recursive, - is_ignored_dir=is_ignored_path, - is_ignored_file=is_ignored_path, + is_ignored_dir=self.agent.path_is_gitignored, + is_ignored_file=self.agent.path_is_gitignored, ) result = json.dumps({"dirs": dirs, "files": files}) @@ -1140,31 +1200,27 @@ class FindFileTool(Tool): def apply(self, file_mask: str, relative_path: str) -> str: """ - Finds files matching the given file mask within the given relative path + Finds non-gitignored files matching the given file mask within the given relative path :param file_mask: the filename or file mask (using the wildcards * or ?) to search for :param relative_path: the relative path to the directory to search in; pass "." to scan the project root :return: a JSON object with the list of matching files """ + self.agent.validate_relative_path(relative_path) - def is_ignored_path(abs_path: str) -> bool: - rel_path = os.path.relpath(abs_path, self.get_project_root()) - return self.language_server.is_ignored_path(rel_path, ignore_unsupported_files=False) + dir_to_scan = os.path.join(self.get_project_root(), relative_path) + # find the files by ignoring everything that doesn't match def is_ignored_file(abs_path: str) -> bool: - if is_ignored_path(abs_path): + if self.agent.path_is_gitignored(abs_path): return True filename = os.path.basename(abs_path) - is_ignored = not fnmatch(filename, file_mask) - if not is_ignored: - is_ignored = not fnmatch(filename, file_mask) - return is_ignored + return not fnmatch(filename, file_mask) dirs, files = scan_directory( - os.path.join(self.get_project_root(), relative_path), - relative_to=self.get_project_root(), + path=dir_to_scan, recursive=True, - is_ignored_dir=is_ignored_path, + is_ignored_dir=self.agent.path_is_gitignored, is_ignored_file=is_ignored_file, ) @@ -1512,6 +1568,7 @@ class ReplaceRegexTool(Tool, ToolMarkerCanEdit): If this is set to False and the regex matches multiple occurrences, an error will be returned (and you may retry with a revised, more specific regex). """ + self.agent.validate_relative_path(relative_path) with EditedFileContext(relative_path, self.agent) as context: original_content = context.get_original_content() updated_content, n = re.subn(regex, repl, original_content, flags=re.DOTALL | re.MULTILINE) @@ -1833,16 +1890,16 @@ class SearchForPatternTool(Tool): else: # we walk through all files in the project starting from the root files_to_search = [] - ignore_spec = self.language_server.get_ignore_spec() for root, dirs, files in os.walk(self.get_project_root()): # Don't go into directories that are ignored by modifying dirs inplace # Explanation for the + "/" part: # pathspec can't handle the matching of directories if they don't end with a slash! # see https://github.com/cpburnz/python-pathspec/issues/89 - dirs[:] = [d for d in dirs if not ignore_spec.match_file(d + "/")] + dirs[:] = [d for d in dirs if not self.agent.path_is_gitignored(os.path.join(root, d))] for file in files: - if not ignore_spec.match_file(os.path.join(root, file)): - files_to_search.append(os.path.join(root, file)) + file_path = os.path.join(root, file) + if not self.agent.path_is_gitignored(file_path): + files_to_search.append(file_path) # TODO (maybe): not super efficient to walk through the files again and filter if glob patterns are provided # but it probably never matters and this version required no further refactoring matches = search_files( diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index e7f773a..9ded41d 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -78,17 +78,14 @@ def find_all_non_ignored_files(repo_root: str) -> list[str]: @dataclass class GitignoreSpec: - """ - Represents a single gitignore file and its parsed patterns. - - :param file_path: Path to the gitignore file - :param patterns: List of adjusted patterns from the gitignore file - :param pathspec: Compiled PathSpec object for pattern matching - """ - file_path: str + """Path to the gitignore file.""" patterns: list[str] = field(default_factory=list) + """List of patterns from the gitignore file. + The patterns are adjusted based on the gitignore file location. + """ pathspec: PathSpec = field(init=False) + """Compiled PathSpec object for pattern matching.""" def __post_init__(self) -> None: """Initialize the PathSpec from patterns.""" @@ -276,3 +273,14 @@ class GitignoreParser: """Reload all gitignore files from the repository.""" self.ignore_specs.clear() self._load_gitignore_files() + + +def match_path(path: str, path_spec: PathSpec) -> bool: + path = os.path.abspath(path) + normalized_path = str(path).replace(os.path.sep, "/") + + # pathspec can't handle the matching of directories if they don't end with a slash! + # see https://github.com/cpburnz/python-pathspec/issues/89 + if os.path.isdir(normalized_path) and not normalized_path.endswith("/"): + normalized_path = normalized_path + "/" + return path_spec.match_file(normalized_path) From 40b9a246f07d8bd3162773c961c1e421d8121d5c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 11 Jun 2025 17:08:32 +0200 Subject: [PATCH 015/174] Multilspy: fix dotnet version detection don't just use first line of output, instead use latest available and allowed version --- src/multilspy/multilspy_utils.py | 37 ++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/multilspy/multilspy_utils.py b/src/multilspy/multilspy_utils.py index c649ba9..3655456 100644 --- a/src/multilspy/multilspy_utils.py +++ b/src/multilspy/multilspy_utils.py @@ -313,23 +313,31 @@ class PlatformUtils: """ try: result = subprocess.run(["dotnet", "--list-runtimes"], capture_output=True, check=True) - version = '' + available_version_cmd_output = [] for line in result.stdout.decode('utf-8').split('\n'): if line.startswith('Microsoft.NETCore.App'): - version = line.split(' ')[1] - break - if version == '': + version_cmd_output = line.split(' ')[1] + available_version_cmd_output.append(version_cmd_output) + + if not available_version_cmd_output: raise MultilspyException("dotnet not found on the system") - if version.startswith("8"): - return DotnetVersion.V8 - elif version.startswith("7"): - return DotnetVersion.V7 - elif version.startswith("6"): - return DotnetVersion.V6 - elif version.startswith("4"): - return DotnetVersion.V4 - else: - raise MultilspyException("Unknown dotnet version: " + version) + + # Check for supported versions in order of preference (latest first) + for version_cmd_output in available_version_cmd_output: + if version_cmd_output.startswith("8"): + return DotnetVersion.V8 + for version_cmd_output in available_version_cmd_output: + if version_cmd_output.startswith("7"): + return DotnetVersion.V7 + for version_cmd_output in available_version_cmd_output: + if version_cmd_output.startswith("6"): + return DotnetVersion.V6 + for version_cmd_output in available_version_cmd_output: + if version_cmd_output.startswith("4"): + return DotnetVersion.V4 + + # If no supported version found, raise exception with all available versions + raise MultilspyException(f"No supported dotnet version found. Available versions: {', '.join(available_version_cmd_output)}. Supported versions: 4, 6, 7, 8") except (FileNotFoundError, subprocess.CalledProcessError): try: result = subprocess.run(["mono", "--version"], capture_output=True, check=True) @@ -338,6 +346,7 @@ class PlatformUtils: raise MultilspyException("dotnet or mono not found on the system") + class SymbolUtils: @staticmethod def symbol_tree_contains_name(roots: list[UnifiedSymbolInformation], name: str) -> bool: From 0c5a98236f01589a041ba5514726d0a469425441 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 11 Jun 2025 17:09:33 +0200 Subject: [PATCH 016/174] Gitignore: fix handling of anchored ignores Previously anchored ignores would be erroneosly ingored everywhere --- src/serena/util/file_system.py | 13 ++- test/serena/util/test_file_system.py | 125 ++++++++++++++++++++++++++- 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index 9ded41d..fa8ee32 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -225,7 +225,13 @@ class GitignoreParser: # Add the directory prefix but also allow matching in subdirectories adjusted_pattern = os.path.join(rel_dir, "**", line) else: - adjusted_pattern = line + if is_anchored: + # Anchored patterns in root should only match at root level + # Add leading slash back to indicate root-only matching + adjusted_pattern = "/" + line + else: + # Non-anchored patterns can match anywhere + adjusted_pattern = line # Re-add negation if needed if is_negation: @@ -251,9 +257,14 @@ class GitignoreParser: else: rel_path = path + abs_path = os.path.join(self.repo_root, rel_path) + # Normalize path separators rel_path = rel_path.replace(os.sep, "/") + if os.path.exists(abs_path) and os.path.isdir(abs_path) and not rel_path.endswith("/"): + rel_path = rel_path + "/" + # Check against each ignore spec for spec in self.ignore_specs: if spec.matches(rel_path): diff --git a/test/serena/util/test_file_system.py b/test/serena/util/test_file_system.py index 48d983a..3c9fd14 100644 --- a/test/serena/util/test_file_system.py +++ b/test/serena/util/test_file_system.py @@ -133,7 +133,7 @@ build/ assert "*.log" in patterns assert "build/" in patterns - assert "temp.txt" in patterns + assert "/temp.txt" in patterns def test_parse_patterns_subdirectory(self): """Test parsing gitignore patterns in subdirectory.""" @@ -227,6 +227,127 @@ data.json assert parser.should_ignore("src/subdir/data.json") assert parser.should_ignore("src/subdir/deep/data.json") + def test_root_anchored_patterns(self): + """Test anchored patterns in root .gitignore only match root-level files.""" + # Create new test structure for root anchored patterns + test_dir = self.repo_path / "test_root_anchored" + test_dir.mkdir() + (test_dir / "src").mkdir() + (test_dir / "docs").mkdir() + (test_dir / "src" / "nested").mkdir() + + # Create root .gitignore with anchored patterns + gitignore = test_dir / ".gitignore" + gitignore.write_text( + """/config.json +/temp.log +/build +*.pyc +""" + ) + + # Create test files at root level + (test_dir / "config.json").touch() + (test_dir / "temp.log").touch() + (test_dir / "build").mkdir() + (test_dir / "file.pyc").touch() + + # Create same-named files in subdirectories + (test_dir / "src" / "config.json").touch() + (test_dir / "src" / "temp.log").touch() + (test_dir / "src" / "build").mkdir() + (test_dir / "src" / "file.pyc").touch() + (test_dir / "docs" / "config.json").touch() + (test_dir / "docs" / "temp.log").touch() + (test_dir / "src" / "nested" / "config.json").touch() + (test_dir / "src" / "nested" / "temp.log").touch() + (test_dir / "src" / "nested" / "build").mkdir() + + parser = GitignoreParser(str(test_dir)) + + # Anchored patterns should only match root-level files + assert parser.should_ignore("config.json") + assert not parser.should_ignore("src/config.json") + assert not parser.should_ignore("docs/config.json") + assert not parser.should_ignore("src/nested/config.json") + + assert parser.should_ignore("temp.log") + assert not parser.should_ignore("src/temp.log") + assert not parser.should_ignore("docs/temp.log") + assert not parser.should_ignore("src/nested/temp.log") + + assert parser.should_ignore("build") + assert not parser.should_ignore("src/build") + assert not parser.should_ignore("src/nested/build") + + # Non-anchored patterns should match everywhere + assert parser.should_ignore("file.pyc") + assert parser.should_ignore("src/file.pyc") + + def test_mixed_anchored_and_non_anchored_root_patterns(self): + """Test mix of anchored and non-anchored patterns in root .gitignore.""" + test_dir = self.repo_path / "test_mixed_patterns" + test_dir.mkdir() + (test_dir / "app").mkdir() + (test_dir / "tests").mkdir() + (test_dir / "app" / "modules").mkdir() + + # Create root .gitignore with mixed patterns + gitignore = test_dir / ".gitignore" + gitignore.write_text( + """/secrets.env +/dist/ +node_modules/ +*.tmp +/app/local.config +debug.log +""" + ) + + # Create test files and directories + (test_dir / "secrets.env").touch() + (test_dir / "dist").mkdir() + (test_dir / "node_modules").mkdir() + (test_dir / "file.tmp").touch() + (test_dir / "app" / "local.config").touch() + (test_dir / "debug.log").touch() + + # Create same files in subdirectories + (test_dir / "app" / "secrets.env").touch() + (test_dir / "app" / "dist").mkdir() + (test_dir / "app" / "node_modules").mkdir() + (test_dir / "app" / "file.tmp").touch() + (test_dir / "app" / "debug.log").touch() + (test_dir / "tests" / "secrets.env").touch() + (test_dir / "tests" / "node_modules").mkdir() + (test_dir / "tests" / "debug.log").touch() + (test_dir / "app" / "modules" / "local.config").touch() + + parser = GitignoreParser(str(test_dir)) + + # Anchored patterns should only match at root + assert parser.should_ignore("secrets.env") + assert not parser.should_ignore("app/secrets.env") + assert not parser.should_ignore("tests/secrets.env") + + assert parser.should_ignore("dist") + assert not parser.should_ignore("app/dist") + + assert parser.should_ignore("app/local.config") + assert not parser.should_ignore("app/modules/local.config") + + # Non-anchored patterns should match everywhere + assert parser.should_ignore("node_modules") + assert parser.should_ignore("app/node_modules") + assert parser.should_ignore("tests/node_modules") + + assert parser.should_ignore("file.tmp") + assert parser.should_ignore("app/file.tmp") + + assert parser.should_ignore("debug.log") + assert parser.should_ignore("app/debug.log") + assert parser.should_ignore("tests/debug.log") + def test_negation_patterns(self): """Test negation patterns are parsed correctly.""" test_dir = self.repo_path / "test_negation" @@ -248,7 +369,7 @@ data.json assert "*.log" in patterns assert "!important.log" in patterns - assert "!src/keep.log" in patterns + assert "!/src/keep.log" in patterns def test_comments_and_empty_lines(self): """Test that comments and empty lines are ignored.""" From bdaedfd3e538d6687ee121e525f3bced8cc88a1c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 12 Jun 2025 14:57:19 +0200 Subject: [PATCH 017/174] Added Claude.md --- CLAUDE.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ae5217f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Essential Commands + +This project uses `uv` for dependency management and `poe` for task orchestration: + +- **Linting**: `uv run poe lint` (only allowed command for linting) +- **Formatting**: `uv run poe format` (only allowed command for formatting) +- **Type checking**: `uv run poe type-check` (only allowed command for type checking) +- **Testing**: `uv run poe test [args]` (preferred) or `uv run pytest [args]` + +**Important**: Always run `format`, `type-check` and `test` at the end of tasks to ensure code quality. Fix any issues and re-run until they pass. + +## Architecture Overview + +Serena is a powerful coding agent toolkit that turns LLMs into fully-featured agents working directly on codebases through semantic code retrieval and editing tools. + +### Core Components + +- **src/serena/**: Main package containing core functionality + - `mcp.py`: Model Context Protocol server implementation + - `agno.py`: Agno framework integration for model-agnostic agents + - `agent.py`: Core agent implementation with semantic tools + - **llm/**: LLM integration modules + - **util/**: Utility functions and helpers + +- **src/multilspy/**: Language server integration layer + - **language_servers/**: Language-specific implementations + - Python (pyright/jedi), Java (Eclipse JDTLS), TypeScript/JavaScript + - C#, Rust, Go, Ruby, C++, PHP support + - Provides semantic code analysis through Language Server Protocol (LSP) + +- **src/interprompt/**: Template and prompt management system + +### Key Design Principles + +- **Semantic Code Understanding**: Uses language servers (LSP) for symbol-level code analysis rather than text-based approaches +- **Multiple Integration Methods**: Can be used as MCP server, Agno agent, or integrated into custom frameworks +- **Language Agnostic**: Supports multiple programming languages through language server adapters +- **Context and Mode System**: Configurable behavior for different environments (desktop-app, ide-assistant, agent) + +### Integration Patterns + +- **MCP Server**: Primary integration method for Claude Desktop and other MCP clients +- **Agno Agent**: Model-agnostic agent framework for any LLM with GUI support +- **Framework Adapter**: Tools can be adapted to any agent framework (example: SerenaAgnoToolkit) + +### Configuration System + +Four-layer configuration hierarchy: +1. `serena_config.yml` - Global settings +2. CLI arguments - Client-specific overrides +3. `.serena/project.yml` - Project-specific settings +4. Active modes - Runtime behavior modification + +The codebase implements sophisticated semantic code operations through language servers, enabling precise symbol-level editing and code understanding that goes beyond simple text manipulation. + +## Working on Serena's Code + +**Important**: When working on this codebase, remember that you are using Serena's own tools to improve Serena itself. This means: + +- You can use Serena's semantic tools (`find_symbol`, `replace_symbol_body`, `search_for_pattern`, etc.) to analyze and edit Serena's own source code +- When asked to "perform a task on Serena", you're being asked to modify/improve the current Serena codebase +- You have access to the full power of Serena's semantic code understanding to work on Serena's code \ No newline at end of file From 36dbf5c681d92eb933f657f441da61c384afe54b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 12 Jun 2025 14:59:33 +0200 Subject: [PATCH 018/174] Possibility to override tool timeout through CLI --- src/serena/agent.py | 4 ++++ src/serena/mcp.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/serena/agent.py b/src/serena/agent.py index 0795e83..0a07da2 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -506,6 +506,7 @@ class SerenaAgent: enable_gui_log_window: bool | None = None, log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, ): """ :param project: the project to load immediately or None to not load any project; may be a path to the project or a name of @@ -520,6 +521,7 @@ class SerenaAgent: :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. :param gui_log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. + :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. """ # obtain serena configuration self.serena_config = serena_config or SerenaConfig.from_config_file() @@ -533,6 +535,8 @@ class SerenaAgent: self.serena_config.log_level = logging.getLevelNamesMapping()[log_level] if trace_lsp_communication is not None: self.serena_config.trace_lsp_communication = trace_lsp_communication + if tool_timeout is not None: + self.serena_config.tool_timeout = tool_timeout # adjust log level serena_log_level = self.serena_config.log_level diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 3f9d4ef..73c04fc 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -106,6 +106,7 @@ def create_mcp_server_and_agent( enable_gui_log_window: bool | None = None, log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, ) -> tuple[FastMCP, SerenaAgent]: """ Create an MCP server. @@ -123,6 +124,7 @@ def create_mcp_server_and_agent( :param log_level: Log level. If not specified, will take the value from the serena configuration. :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. This is useful for debugging language server issues. + :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. """ mcp: FastMCP | None = None context_instance = SerenaAgentContext.load(context) @@ -139,6 +141,7 @@ def create_mcp_server_and_agent( enable_gui_log_window=enable_gui_log_window, log_level=log_level, trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, ) except Exception as e: show_fatal_exception_safe(e) @@ -280,6 +283,12 @@ PROJECT_TYPE = ProjectType() default=None, help="Whether to trace the communication between Serena and the language servers. This is useful for debugging language server issues.", ) +@click.option( + "--tool-timeout", + type=float, + default=None, + help="Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration.", +) def start_mcp_server( project_file_opt: str | None, project_file_arg: str | None, @@ -292,6 +301,7 @@ def start_mcp_server( enable_gui_log_window: bool | None = None, log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, ) -> None: """Starts the Serena MCP server. By default, will not activate any project at startup. If you want to start with an already active project, use --project to pass the project name or path. @@ -313,6 +323,7 @@ def start_mcp_server( enable_gui_log_window=enable_gui_log_window, log_level=log_level, trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, ) # log after server creation such that the log appears in the GUI From 3b04d0b8676b09292d412bdd5e3ecbf372068cb3 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Thu, 12 Jun 2025 15:05:53 +0200 Subject: [PATCH 019/174] Advice to restart LS in tool failure message --- src/serena/agent.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 0a07da2..2093365 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -1071,7 +1071,11 @@ class Tool(Component): if not catch_exceptions: raise msg = f"Error executing tool: {e}\n{traceback.format_exc()}" - log.error(f"Error executing tool: {e}", exc_info=e) + log.error( + f"Error executing tool: {e}. " + f"Consider restarting the language server to solve this (especially, if it's a timeout of a symbolic operation)", + exc_info=e, + ) result = msg if log_call: From e19b0b6d34dca5d91b56aa57da1f1b8573f9564b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 13 Jun 2025 11:54:56 +0200 Subject: [PATCH 020/174] Fix glob pattern escaping --- src/serena/text_utils.py | 16 +++------------- test/serena/test_text_utils.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/serena/text_utils.py b/src/serena/text_utils.py index 338b295..380eb91 100644 --- a/src/serena/text_utils.py +++ b/src/serena/text_utils.py @@ -153,19 +153,9 @@ def search_text( # Convert pattern to a compiled regex if it's a string if is_glob and isinstance(pattern, str): - # Convert glob pattern to regex - # Escape all regex special characters except * and ? - regex_special_chars = r"\^$.|+()[{" - escaped_pattern = "" - for char in pattern: - if char in regex_special_chars: - escaped_pattern += "\\" + char - elif char == "*": - escaped_pattern += ".*" - elif char == "?": - escaped_pattern += "." - else: - escaped_pattern += char + # Convert glob pattern to regex. Use re.escape to handle special + # characters and replace glob wildcards afterwards. + escaped_pattern = "".join(".*" if ch == "*" else "." if ch == "?" else re.escape(ch) for ch in pattern) # For glob patterns, don't anchor with ^ and $ to allow partial line matches compiled_pattern = re.compile(escaped_pattern) elif isinstance(pattern, str): diff --git a/test/serena/test_text_utils.py b/test/serena/test_text_utils.py index 1fa6929..860ce6a 100644 --- a/test/serena/test_text_utils.py +++ b/test/serena/test_text_utils.py @@ -182,6 +182,24 @@ class TestSearchText: assert any("isinstance(item, dict)" in line for line in instance_matches) assert any("isinstance(item, list)" in line for line in instance_matches) + def test_search_text_glob_with_special_chars(self): + """Glob patterns containing regex special characters should match literally.""" + content = """ + def func_square(): + print("value[42]") + + def func_curly(): + print("value{bar}") + """ + + matches_square = search_text(r"*\[42\]*", content=content, is_glob=True) + assert len(matches_square) == 1 + assert "[42]" in matches_square[0].lines[0].line_content + + matches_curly = search_text("*{bar}*", content=content, is_glob=True) + assert len(matches_curly) == 1 + assert "{bar}" in matches_curly[0].lines[0].line_content + def test_search_text_no_matches(self): """Test searching with a pattern that doesn't match anything.""" content = """ From 34cef92c6cd681fd0294788ee907c2f431e5c894 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 13 Jun 2025 12:51:53 +0200 Subject: [PATCH 021/174] Fix glob escaping for search_text --- src/serena/text_utils.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/serena/text_utils.py b/src/serena/text_utils.py index 380eb91..75cc7dd 100644 --- a/src/serena/text_utils.py +++ b/src/serena/text_utils.py @@ -153,9 +153,28 @@ def search_text( # Convert pattern to a compiled regex if it's a string if is_glob and isinstance(pattern, str): - # Convert glob pattern to regex. Use re.escape to handle special - # characters and replace glob wildcards afterwards. - escaped_pattern = "".join(".*" if ch == "*" else "." if ch == "?" else re.escape(ch) for ch in pattern) + # Convert glob pattern with optional backslash escaping to regex + def glob_to_regex(glob_pat: str) -> str: + regex_parts: list[str] = [] + i = 0 + while i < len(glob_pat): + ch = glob_pat[i] + if ch == "*": + regex_parts.append(".*") + elif ch == "?": + regex_parts.append(".") + elif ch == "\\": + i += 1 + if i < len(glob_pat): + regex_parts.append(re.escape(glob_pat[i])) + else: + regex_parts.append("\\") + else: + regex_parts.append(re.escape(ch)) + i += 1 + return "".join(regex_parts) + + escaped_pattern = glob_to_regex(pattern) # For glob patterns, don't anchor with ^ and $ to allow partial line matches compiled_pattern = re.compile(escaped_pattern) elif isinstance(pattern, str): From b0ade9e9fa5df6fe221bea028d0cb387bb1b1372 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 12:50:33 +0200 Subject: [PATCH 022/174] Fix tests to correctly use gitignore for LS setup --- .../test_retrieval_with_ignored_dirs.py | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 5150d53..5bbb372 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -6,23 +6,43 @@ import pytest from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger +from serena.util.file_system import GitignoreParser from test.conftest import get_repo_path # This mark will be applied to all tests in this module pytestmark = pytest.mark.python +def create_ls_with_ignored_dirs( + ignored_paths: list[str] | None = None, + repo_path: Path | None = None, + language: Language = Language.PYTHON, +) -> SyncLanguageServer: + """Helper function to create a SyncLanguageServer with ignored directories.""" + if ignored_paths is None: + ignored_paths = [] + + if repo_path is None: + repo_path = get_repo_path(language) + + gitignore_parser = GitignoreParser(str(repo_path)) + for spec in gitignore_parser.get_ignore_specs(): + ignored_paths.extend(spec.patterns) + + config = MultilspyConfig( + code_language=language, + trace_lsp_communication=False, + ignored_paths=ignored_paths, # Configure the relative path to be ignored + ) + logger = MultilspyLogger() + return SyncLanguageServer.create(config, logger, str(repo_path)) + + @pytest.fixture(scope="module") def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" - config = MultilspyConfig( - code_language=Language.PYTHON, - trace_lsp_communication=False, - ignored_paths=["scripts", "custom_test"], # Configure the relative path to be ignored - ) - logger = MultilspyLogger() - repo_path = get_repo_path(Language.PYTHON) - ls = SyncLanguageServer.create(config, logger, str(repo_path)) + ignored_paths = ["scripts", "custom_test_repo"] + ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, language=Language.PYTHON) ls.start() try: yield ls @@ -56,13 +76,8 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): @pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True) def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: """Tests that refs and symbols with glob patterns are ignored.""" - config = MultilspyConfig( - code_language=Language.PYTHON, - trace_lsp_communication=False, - ignored_paths=["*ipts", "custom_t*"], - ) - logger = MultilspyLogger() - ls = SyncLanguageServer.create(config, logger, str(repo_path)) + ignored_paths = ["*ipts", "custom_t*"] + ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, repo_path=repo_path, language=Language.PYTHON) ls.start() # same as in the above tests root = ls.request_full_symbol_tree()[0] From 8cd50fab168472d5a72aea2b22e835fee1a65f97 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:58:12 +0200 Subject: [PATCH 023/174] Force-added ingored file in test resources Needed for tests involving ignored patterns --- .../ignored_module.py | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py diff --git a/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py b/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py new file mode 100644 index 0000000..1e6da93 --- /dev/null +++ b/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py @@ -0,0 +1,141 @@ +""" +Example demonstrating user management with the test_repo module. + +This example showcases: +- Creating and managing users +- Using various object types and relationships +- Type annotations and complex Python patterns +""" + +import logging +from dataclasses import dataclass +from typing import Any + +from test_repo.models import User, create_user_object +from test_repo.services import UserService + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@dataclass +class UserStats: + """Statistics about user activity.""" + + user_id: str + login_count: int = 0 + last_active_days: int = 0 + engagement_score: float = 0.0 + + def is_active(self) -> bool: + """Check if the user is considered active.""" + return self.last_active_days < 30 + + +class UserManager: + """Example class demonstrating complex user management.""" + + def __init__(self, service: UserService): + self.service = service + self.active_users: dict[str, User] = {} + self.user_stats: dict[str, UserStats] = {} + + def register_user(self, name: str, email: str, roles: list[str] | None = None) -> User: + """Register a new user.""" + logger.info(f"Registering new user: {name} ({email})") + user = self.service.create_user(name=name, email=email, roles=roles) + self.active_users[user.id] = user + self.user_stats[user.id] = UserStats(user_id=user.id) + return user + + def get_user(self, user_id: str) -> User | None: + """Get a user by ID.""" + if user_id in self.active_users: + return self.active_users[user_id] + + # Try to fetch from service + user = self.service.get_user(user_id) + if user: + self.active_users[user.id] = user + return user + + def update_user_stats(self, user_id: str, login_count: int, days_since_active: int) -> None: + """Update statistics for a user.""" + if user_id not in self.user_stats: + self.user_stats[user_id] = UserStats(user_id=user_id) + + stats = self.user_stats[user_id] + stats.login_count = login_count + stats.last_active_days = days_since_active + + # Calculate engagement score based on activity + engagement = (100 - min(days_since_active, 100)) * 0.8 + engagement += min(login_count, 20) * 0.2 + stats.engagement_score = engagement + + def get_active_users(self) -> list[User]: + """Get all active users.""" + active_user_ids = [user_id for user_id, stats in self.user_stats.items() if stats.is_active()] + return [self.active_users[user_id] for user_id in active_user_ids if user_id in self.active_users] + + def get_user_by_email(self, email: str) -> User | None: + """Find a user by their email address.""" + for user in self.active_users.values(): + if user.email == email: + return user + return None + + +# Example function demonstrating type annotations +def process_user_data(users: list[User], include_inactive: bool = False, transform_func: callable | None = None) -> dict[str, Any]: + """Process user data with optional transformations.""" + result: dict[str, Any] = {"users": [], "total": 0, "admin_count": 0} + + for user in users: + if transform_func: + user_data = transform_func(user.to_dict()) + else: + user_data = user.to_dict() + + result["users"].append(user_data) + result["total"] += 1 + + if "admin" in user.roles: + result["admin_count"] += 1 + + return result + + +def main(): + """Main function demonstrating the usage of UserManager.""" + # Initialize service and manager + service = UserService() + manager = UserManager(service) + + # Register some users + admin = manager.register_user("Admin User", "admin@example.com", ["admin"]) + user1 = manager.register_user("Regular User", "user@example.com", ["user"]) + user2 = manager.register_user("Another User", "another@example.com", ["user"]) + + # Update some stats + manager.update_user_stats(admin.id, 100, 5) + manager.update_user_stats(user1.id, 50, 10) + manager.update_user_stats(user2.id, 10, 45) # Inactive user + + # Get active users + active_users = manager.get_active_users() + logger.info(f"Active users: {len(active_users)}") + + # Process user data + user_data = process_user_data(active_users, transform_func=lambda u: {**u, "full_name": u.get("name", "")}) + + logger.info(f"Processed {user_data['total']} users, {user_data['admin_count']} admins") + + # Example of calling create_user directly + external_user = create_user_object(id="ext123", name="External User", email="external@example.org", roles=["external"]) + logger.info(f"Created external user: {external_user.name}") + + +if __name__ == "__main__": + main() From 099ab412037beef0cd89df0d435d3c5a7e7f04f9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:23:04 +0200 Subject: [PATCH 024/174] Typo --- test/multilspy/python/test_retrieval_with_ignored_dirs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 5bbb372..70f0e37 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -41,7 +41,7 @@ def create_ls_with_ignored_dirs( @pytest.fixture(scope="module") def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" - ignored_paths = ["scripts", "custom_test_repo"] + ignored_paths = ["scripts", "custom_test"] ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, language=Language.PYTHON) ls.start() try: From b185a9f595e35999ec7c13e33bfc7becd2d31751 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:55:31 +0200 Subject: [PATCH 025/174] Tests: use gitignore parser in LS setup, remove duplication --- test/conftest.py | 11 ++++-- .../test_retrieval_with_ignored_dirs.py | 35 +++---------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 92b3430..c0bbe0a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -7,6 +7,7 @@ from sensai.util.logging import LOG_DEFAULT_FORMAT, configure from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger +from serena.util.file_system import GitignoreParser configure(level=logging.DEBUG) @@ -26,8 +27,14 @@ def get_repo_path(language: Language) -> Path: return Path(__file__).parent / "resources" / "repos" / language / "test_repo" -def create_ls(language: Language, repo_path: str): - config = MultilspyConfig(code_language=language) +def create_ls(language: Language, repo_path: str | None = None, ignored_paths: list[str] | None = None) -> SyncLanguageServer: + ignored_paths = ignored_paths or [] + if repo_path is None: + repo_path = str(get_repo_path(language)) + gitignore_parser = GitignoreParser(str(repo_path)) + for spec in gitignore_parser.get_ignore_specs(): + ignored_paths.extend(spec.patterns) + config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths) logger = MultilspyLogger(log_level=logging.DEBUG) # Simple way: Use basicConfig to set format for all handlers diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 70f0e37..0db668e 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -4,45 +4,18 @@ from pathlib import Path import pytest from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language, MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from serena.util.file_system import GitignoreParser -from test.conftest import get_repo_path +from multilspy.multilspy_config import Language +from test.conftest import create_ls # This mark will be applied to all tests in this module pytestmark = pytest.mark.python -def create_ls_with_ignored_dirs( - ignored_paths: list[str] | None = None, - repo_path: Path | None = None, - language: Language = Language.PYTHON, -) -> SyncLanguageServer: - """Helper function to create a SyncLanguageServer with ignored directories.""" - if ignored_paths is None: - ignored_paths = [] - - if repo_path is None: - repo_path = get_repo_path(language) - - gitignore_parser = GitignoreParser(str(repo_path)) - for spec in gitignore_parser.get_ignore_specs(): - ignored_paths.extend(spec.patterns) - - config = MultilspyConfig( - code_language=language, - trace_lsp_communication=False, - ignored_paths=ignored_paths, # Configure the relative path to be ignored - ) - logger = MultilspyLogger() - return SyncLanguageServer.create(config, logger, str(repo_path)) - - @pytest.fixture(scope="module") def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" ignored_paths = ["scripts", "custom_test"] - ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, language=Language.PYTHON) + ls = create_ls(ignored_paths=ignored_paths, language=Language.PYTHON) ls.start() try: yield ls @@ -77,7 +50,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: """Tests that refs and symbols with glob patterns are ignored.""" ignored_paths = ["*ipts", "custom_t*"] - ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, repo_path=repo_path, language=Language.PYTHON) + ls = create_ls(ignored_paths=ignored_paths, repo_path=str(repo_path), language=Language.PYTHON) ls.start() # same as in the above tests root = ls.request_full_symbol_tree()[0] From 3c4e9d77d7b47bd09da63c3d9703ca220490edbf Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:56:19 +0200 Subject: [PATCH 026/174] Agent: force log format through basicConfig Needed to make multilspy logs respect our log format --- src/serena/agent.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 2093365..1d8acd4 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -28,7 +28,7 @@ from overrides import override from pathspec import PathSpec from ruamel.yaml.comments import CommentedMap from sensai.util import logging -from sensai.util.logging import FallbackHandler +from sensai.util.logging import LOG_DEFAULT_FORMAT, FallbackHandler from sensai.util.string import ToStringMixin, dict_string from multilspy import SyncLanguageServer @@ -543,6 +543,8 @@ class SerenaAgent: if Logger.root.level > serena_log_level: log.info(f"Changing the root logger level to {serena_log_level}") Logger.root.setLevel(serena_log_level) + # Override existing configuration (needed to configure multilspy logger as desired) + logging.basicConfig(format=LOG_DEFAULT_FORMAT, level=serena_log_level, force=True) # open GUI log window if enabled self._gui_log_handler: Union["GuiLogViewerHandler", None] = None # noqa From 3ca27e47d57e35200316cbf18a07fca1bbdd0b68 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 12:50:33 +0200 Subject: [PATCH 027/174] Fix tests to correctly use gitignore for LS setup --- .../test_retrieval_with_ignored_dirs.py | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 5150d53..5bbb372 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -6,23 +6,43 @@ import pytest from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger +from serena.util.file_system import GitignoreParser from test.conftest import get_repo_path # This mark will be applied to all tests in this module pytestmark = pytest.mark.python +def create_ls_with_ignored_dirs( + ignored_paths: list[str] | None = None, + repo_path: Path | None = None, + language: Language = Language.PYTHON, +) -> SyncLanguageServer: + """Helper function to create a SyncLanguageServer with ignored directories.""" + if ignored_paths is None: + ignored_paths = [] + + if repo_path is None: + repo_path = get_repo_path(language) + + gitignore_parser = GitignoreParser(str(repo_path)) + for spec in gitignore_parser.get_ignore_specs(): + ignored_paths.extend(spec.patterns) + + config = MultilspyConfig( + code_language=language, + trace_lsp_communication=False, + ignored_paths=ignored_paths, # Configure the relative path to be ignored + ) + logger = MultilspyLogger() + return SyncLanguageServer.create(config, logger, str(repo_path)) + + @pytest.fixture(scope="module") def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" - config = MultilspyConfig( - code_language=Language.PYTHON, - trace_lsp_communication=False, - ignored_paths=["scripts", "custom_test"], # Configure the relative path to be ignored - ) - logger = MultilspyLogger() - repo_path = get_repo_path(Language.PYTHON) - ls = SyncLanguageServer.create(config, logger, str(repo_path)) + ignored_paths = ["scripts", "custom_test_repo"] + ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, language=Language.PYTHON) ls.start() try: yield ls @@ -56,13 +76,8 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): @pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True) def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: """Tests that refs and symbols with glob patterns are ignored.""" - config = MultilspyConfig( - code_language=Language.PYTHON, - trace_lsp_communication=False, - ignored_paths=["*ipts", "custom_t*"], - ) - logger = MultilspyLogger() - ls = SyncLanguageServer.create(config, logger, str(repo_path)) + ignored_paths = ["*ipts", "custom_t*"] + ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, repo_path=repo_path, language=Language.PYTHON) ls.start() # same as in the above tests root = ls.request_full_symbol_tree()[0] From 47a133a7c0dffd2e8fef1097ccb7a18bea1c60cb Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:23:04 +0200 Subject: [PATCH 028/174] Typo --- test/multilspy/python/test_retrieval_with_ignored_dirs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 5bbb372..70f0e37 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -41,7 +41,7 @@ def create_ls_with_ignored_dirs( @pytest.fixture(scope="module") def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" - ignored_paths = ["scripts", "custom_test_repo"] + ignored_paths = ["scripts", "custom_test"] ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, language=Language.PYTHON) ls.start() try: From 40e9bb87c1aa0379989df6f0fd142282e7c7a4e1 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:55:31 +0200 Subject: [PATCH 029/174] Tests: use gitignore parser in LS setup, remove duplication --- test/conftest.py | 11 ++++-- .../test_retrieval_with_ignored_dirs.py | 35 +++---------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 92b3430..c0bbe0a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -7,6 +7,7 @@ from sensai.util.logging import LOG_DEFAULT_FORMAT, configure from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger +from serena.util.file_system import GitignoreParser configure(level=logging.DEBUG) @@ -26,8 +27,14 @@ def get_repo_path(language: Language) -> Path: return Path(__file__).parent / "resources" / "repos" / language / "test_repo" -def create_ls(language: Language, repo_path: str): - config = MultilspyConfig(code_language=language) +def create_ls(language: Language, repo_path: str | None = None, ignored_paths: list[str] | None = None) -> SyncLanguageServer: + ignored_paths = ignored_paths or [] + if repo_path is None: + repo_path = str(get_repo_path(language)) + gitignore_parser = GitignoreParser(str(repo_path)) + for spec in gitignore_parser.get_ignore_specs(): + ignored_paths.extend(spec.patterns) + config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths) logger = MultilspyLogger(log_level=logging.DEBUG) # Simple way: Use basicConfig to set format for all handlers diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 70f0e37..0db668e 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -4,45 +4,18 @@ from pathlib import Path import pytest from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language, MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from serena.util.file_system import GitignoreParser -from test.conftest import get_repo_path +from multilspy.multilspy_config import Language +from test.conftest import create_ls # This mark will be applied to all tests in this module pytestmark = pytest.mark.python -def create_ls_with_ignored_dirs( - ignored_paths: list[str] | None = None, - repo_path: Path | None = None, - language: Language = Language.PYTHON, -) -> SyncLanguageServer: - """Helper function to create a SyncLanguageServer with ignored directories.""" - if ignored_paths is None: - ignored_paths = [] - - if repo_path is None: - repo_path = get_repo_path(language) - - gitignore_parser = GitignoreParser(str(repo_path)) - for spec in gitignore_parser.get_ignore_specs(): - ignored_paths.extend(spec.patterns) - - config = MultilspyConfig( - code_language=language, - trace_lsp_communication=False, - ignored_paths=ignored_paths, # Configure the relative path to be ignored - ) - logger = MultilspyLogger() - return SyncLanguageServer.create(config, logger, str(repo_path)) - - @pytest.fixture(scope="module") def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" ignored_paths = ["scripts", "custom_test"] - ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, language=Language.PYTHON) + ls = create_ls(ignored_paths=ignored_paths, language=Language.PYTHON) ls.start() try: yield ls @@ -77,7 +50,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: """Tests that refs and symbols with glob patterns are ignored.""" ignored_paths = ["*ipts", "custom_t*"] - ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, repo_path=repo_path, language=Language.PYTHON) + ls = create_ls(ignored_paths=ignored_paths, repo_path=str(repo_path), language=Language.PYTHON) ls.start() # same as in the above tests root = ls.request_full_symbol_tree()[0] From 71ab18fddfa28b8552b3b34f1b85718c12b44884 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:56:19 +0200 Subject: [PATCH 030/174] Agent: force log format through basicConfig Needed to make multilspy logs respect our log format --- src/serena/agent.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 2093365..1d8acd4 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -28,7 +28,7 @@ from overrides import override from pathspec import PathSpec from ruamel.yaml.comments import CommentedMap from sensai.util import logging -from sensai.util.logging import FallbackHandler +from sensai.util.logging import LOG_DEFAULT_FORMAT, FallbackHandler from sensai.util.string import ToStringMixin, dict_string from multilspy import SyncLanguageServer @@ -543,6 +543,8 @@ class SerenaAgent: if Logger.root.level > serena_log_level: log.info(f"Changing the root logger level to {serena_log_level}") Logger.root.setLevel(serena_log_level) + # Override existing configuration (needed to configure multilspy logger as desired) + logging.basicConfig(format=LOG_DEFAULT_FORMAT, level=serena_log_level, force=True) # open GUI log window if enabled self._gui_log_handler: Union["GuiLogViewerHandler", None] = None # noqa From b1e14d82678e5ed7c997c4b461bfb3394344f051 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:58:12 +0200 Subject: [PATCH 031/174] Force-added ingored file in test resources Needed for tests involving ignored patterns --- .../ignored_module.py | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py diff --git a/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py b/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py new file mode 100644 index 0000000..1e6da93 --- /dev/null +++ b/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py @@ -0,0 +1,141 @@ +""" +Example demonstrating user management with the test_repo module. + +This example showcases: +- Creating and managing users +- Using various object types and relationships +- Type annotations and complex Python patterns +""" + +import logging +from dataclasses import dataclass +from typing import Any + +from test_repo.models import User, create_user_object +from test_repo.services import UserService + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@dataclass +class UserStats: + """Statistics about user activity.""" + + user_id: str + login_count: int = 0 + last_active_days: int = 0 + engagement_score: float = 0.0 + + def is_active(self) -> bool: + """Check if the user is considered active.""" + return self.last_active_days < 30 + + +class UserManager: + """Example class demonstrating complex user management.""" + + def __init__(self, service: UserService): + self.service = service + self.active_users: dict[str, User] = {} + self.user_stats: dict[str, UserStats] = {} + + def register_user(self, name: str, email: str, roles: list[str] | None = None) -> User: + """Register a new user.""" + logger.info(f"Registering new user: {name} ({email})") + user = self.service.create_user(name=name, email=email, roles=roles) + self.active_users[user.id] = user + self.user_stats[user.id] = UserStats(user_id=user.id) + return user + + def get_user(self, user_id: str) -> User | None: + """Get a user by ID.""" + if user_id in self.active_users: + return self.active_users[user_id] + + # Try to fetch from service + user = self.service.get_user(user_id) + if user: + self.active_users[user.id] = user + return user + + def update_user_stats(self, user_id: str, login_count: int, days_since_active: int) -> None: + """Update statistics for a user.""" + if user_id not in self.user_stats: + self.user_stats[user_id] = UserStats(user_id=user_id) + + stats = self.user_stats[user_id] + stats.login_count = login_count + stats.last_active_days = days_since_active + + # Calculate engagement score based on activity + engagement = (100 - min(days_since_active, 100)) * 0.8 + engagement += min(login_count, 20) * 0.2 + stats.engagement_score = engagement + + def get_active_users(self) -> list[User]: + """Get all active users.""" + active_user_ids = [user_id for user_id, stats in self.user_stats.items() if stats.is_active()] + return [self.active_users[user_id] for user_id in active_user_ids if user_id in self.active_users] + + def get_user_by_email(self, email: str) -> User | None: + """Find a user by their email address.""" + for user in self.active_users.values(): + if user.email == email: + return user + return None + + +# Example function demonstrating type annotations +def process_user_data(users: list[User], include_inactive: bool = False, transform_func: callable | None = None) -> dict[str, Any]: + """Process user data with optional transformations.""" + result: dict[str, Any] = {"users": [], "total": 0, "admin_count": 0} + + for user in users: + if transform_func: + user_data = transform_func(user.to_dict()) + else: + user_data = user.to_dict() + + result["users"].append(user_data) + result["total"] += 1 + + if "admin" in user.roles: + result["admin_count"] += 1 + + return result + + +def main(): + """Main function demonstrating the usage of UserManager.""" + # Initialize service and manager + service = UserService() + manager = UserManager(service) + + # Register some users + admin = manager.register_user("Admin User", "admin@example.com", ["admin"]) + user1 = manager.register_user("Regular User", "user@example.com", ["user"]) + user2 = manager.register_user("Another User", "another@example.com", ["user"]) + + # Update some stats + manager.update_user_stats(admin.id, 100, 5) + manager.update_user_stats(user1.id, 50, 10) + manager.update_user_stats(user2.id, 10, 45) # Inactive user + + # Get active users + active_users = manager.get_active_users() + logger.info(f"Active users: {len(active_users)}") + + # Process user data + user_data = process_user_data(active_users, transform_func=lambda u: {**u, "full_name": u.get("name", "")}) + + logger.info(f"Processed {user_data['total']} users, {user_data['admin_count']} admins") + + # Example of calling create_user directly + external_user = create_user_object(id="ext123", name="External User", email="external@example.org", roles=["external"]) + logger.info(f"Created external user: {external_user.name}") + + +if __name__ == "__main__": + main() From 386a8df54e7ac48a5eaa3ca1502115c3dd97909d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 17:04:28 +0200 Subject: [PATCH 032/174] Timeout fixup --- src/serena/agent.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/serena/agent.py b/src/serena/agent.py index 1d8acd4..864efed 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -9,6 +9,8 @@ import platform import re import shutil import sys +import threading +from time import time import traceback import webbrowser from abc import ABC, abstractmethod @@ -846,6 +848,15 @@ class SerenaAgent: """ Starts/resets the language server for the current project """ + + tool_timeout = self.serena_config.tool_timeout + if tool_timeout is None or tool_timeout < 0: + ls_timeout = None + else: + if tool_timeout < 10: + raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds") + ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout + # stop the language server if it is running if self.is_language_server_running(): assert self.language_server is not None @@ -878,6 +889,7 @@ class SerenaAgent: multilspy_config, ls_logger, self._active_project.project_root, + timeout=ls_timeout, ) self.language_server.start() if not self.language_server.is_running(): From 8c465b53f73d608c8ae98afd584b2d778104582f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 20:39:49 +0200 Subject: [PATCH 033/174] LS: minor deduplication, logging --- src/multilspy/language_server.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 00374aa..ffc1613 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -317,7 +317,7 @@ class LanguageServer: return False - async def _shutdown(self, timeout: float = 10.0): + async def _shutdown(self, timeout: float = 5.0): """ A robust shutdown process designed to terminate cleanly on all platforms, including Windows, by explicitly closing all I/O pipes. @@ -946,7 +946,7 @@ class LanguageServer: self._document_symbols_cache[cache_key] = (file_data.content_hash, result) self._cache_has_changed = True return result - + async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: """ Will go through all files in the project or within a relative path and build a tree of symbols. @@ -1750,12 +1750,9 @@ class SyncLanguageServer: :return: None """ - self.loop = asyncio.new_event_loop() - self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True) - self.loop_thread.start() - ctx = self.language_server.start_server() - asyncio.run_coroutine_threadsafe(ctx.__aenter__(), loop=self.loop).result() + self.start() yield self + self.language_server.logger.log("SyncLS Startup: exiting LS context", logging.DEBUG) self.stop() def request_definition(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: From 5c530fa642ecdd6d2734089f4353bdbb64a52564 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 20:40:15 +0200 Subject: [PATCH 034/174] Multilspy logger: default stacklevel to 2 --- src/multilspy/multilspy_logger.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/multilspy/multilspy_logger.py b/src/multilspy/multilspy_logger.py index e604e1a..495c8a7 100644 --- a/src/multilspy/multilspy_logger.py +++ b/src/multilspy/multilspy_logger.py @@ -30,7 +30,7 @@ class MultilspyLogger: self.logger.setLevel(log_level) self.json_format = json_format - def log(self, debug_message: str, level: int, sanitized_error_message: str = "") -> None: + def log(self, debug_message: str, level: int, sanitized_error_message: str = "", stacklevel: int = 2) -> None: """ Log the debug and santized messages using the logger """ @@ -59,6 +59,7 @@ class MultilspyLogger: self.logger.log( level=level, msg=debug_log_line.json(), + stacklevel=stacklevel, ) else: - self.logger.log(level, debug_message) + self.logger.log(level, debug_message, stacklevel=stacklevel) From 2fba67f2310aa3d41bae8569175ae8540bdd2eab Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 20:41:52 +0200 Subject: [PATCH 035/174] LS: hacky solution for waiting till pyright and ts servers are ready --- .../pyright_language_server/pyright_server.py | 64 ++++++++++++++++--- .../typescript_language_server.py | 22 +++++-- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/multilspy/language_servers/pyright_language_server/pyright_server.py b/src/multilspy/language_servers/pyright_language_server/pyright_server.py index 2a8ee1b..4885583 100644 --- a/src/multilspy/language_servers/pyright_language_server/pyright_server.py +++ b/src/multilspy/language_servers/pyright_language_server/pyright_server.py @@ -2,10 +2,12 @@ Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ +import asyncio import json import logging import os import pathlib +import re from contextlib import asynccontextmanager from typing import AsyncIterator, Tuple @@ -38,6 +40,10 @@ class PyrightServer(LanguageServer): "python", ) + # Event to signal when initial workspace analysis is complete + self.analysis_complete = asyncio.Event() + self.found_source_files = False + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] @@ -145,16 +151,19 @@ class PyrightServer(LanguageServer): @asynccontextmanager async def start_server(self) -> AsyncIterator["PyrightServer"]: """ - Starts the Pyright Language Server, waits for the server to be ready and yields the LanguageServer instance. + Starts the Pyright Language Server and waits for initial workspace analysis to complete. + + This prevents zombie processes by ensuring Pyright has finished its initial background + tasks before we consider the server ready. Usage: ``` async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests + # LanguageServer has been initialized and workspace analysis is complete await lsp.request_definition(...) await lsp.request_references(...) # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown + # LanguageServer has been shutdown cleanly ``` """ @@ -164,13 +173,33 @@ class PyrightServer(LanguageServer): async def do_nothing(params): return - async def check_experimental_status(params): - if params["quiescent"] == True: + async def window_log_message(msg): + """ + Monitor Pyright's log messages to detect when initial analysis is complete. + 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) + + # 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) + self.found_source_files = True + self.analysis_complete.set() self.completions_available.set() - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + async def check_experimental_status(params): + """ + 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) + if not self.found_source_files: + self.analysis_complete.set() + self.completions_available.set() + # Set up notification handlers 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) @@ -195,6 +224,23 @@ class PyrightServer(LanguageServer): self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) # Verify that the server supports our required features - self.server.notify.initialized({}) + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] - yield self + # Complete the initialization handshake + self.server.notify.initialized({}) + + # 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) + try: + await asyncio.wait_for(self.analysis_complete.wait(), timeout=1.0) + self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) + except asyncio.TimeoutError: + self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) + # Fallback: assume analysis is complete after timeout + self.analysis_complete.set() + self.completions_available.set() + + yield self \ No newline at end of file diff --git a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py index 122de61..479936c 100644 --- a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py @@ -186,12 +186,22 @@ class TypeScriptLanguageServer(LanguageServer): async def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + + async def check_experimental_status(params): + """ + Also listen for experimental/serverStatus as a backup signal + """ + if params.get("quiescent") == True: + self.server_ready.set() + self.completions_available.set() self.server.on_request("client/registerCapability", register_capability_handler) 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("experimental/serverStatus", check_experimental_status) async with super().start_server(): self.logger.log("Starting TypeScript server process", logging.INFO) @@ -213,11 +223,13 @@ class TypeScriptLanguageServer(LanguageServer): } self.server.notify.initialized({}) - self.completions_available.set() - - # TypeScript server is typically ready immediately after initialization - self.server_ready.set() - await self.server_ready.wait() + try: + await asyncio.wait_for(self.server_ready.wait(), timeout=1.0) + except asyncio.TimeoutError: + self.logger.log("Timeout waiting for TypeScript server to become ready, proceeding anyway", logging.INFO) + # Fallback: assume server is ready after timeout + self.server_ready.set() + self.completions_available.set() yield self From 3f5b4da532c2363ba1d59ba0079e07f4cf95f6e7 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 20:43:10 +0200 Subject: [PATCH 036/174] More config options for logging in tests, formating --- src/serena/agent.py | 2 +- test/conftest.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 864efed..ded70f7 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -10,7 +10,6 @@ import re import shutil import sys import threading -from time import time import traceback import webbrowser from abc import ABC, abstractmethod @@ -22,6 +21,7 @@ from fnmatch import fnmatch from functools import cached_property from logging import Logger from pathlib import Path +from time import time from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast diff --git a/test/conftest.py b/test/conftest.py index c0bbe0a..994034e 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -2,7 +2,7 @@ import logging from pathlib import Path import pytest -from sensai.util.logging import LOG_DEFAULT_FORMAT, configure +from sensai.util.logging import configure from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig @@ -27,18 +27,21 @@ def get_repo_path(language: Language) -> Path: return Path(__file__).parent / "resources" / "repos" / language / "test_repo" -def create_ls(language: Language, repo_path: str | None = None, ignored_paths: list[str] | None = None) -> SyncLanguageServer: +def create_ls( + language: Language, + repo_path: str | None = None, + ignored_paths: list[str] | None = None, + trace_lsp_communication: bool = False, + log_level: int = logging.INFO, +) -> SyncLanguageServer: ignored_paths = ignored_paths or [] if repo_path is None: repo_path = str(get_repo_path(language)) gitignore_parser = GitignoreParser(str(repo_path)) for spec in gitignore_parser.get_ignore_specs(): ignored_paths.extend(spec.patterns) - config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths) - logger = MultilspyLogger(log_level=logging.DEBUG) - - # Simple way: Use basicConfig to set format for all handlers - logging.basicConfig(format=LOG_DEFAULT_FORMAT, level=logging.DEBUG, force=True) # Override existing configuration + config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) + logger = MultilspyLogger(log_level=log_level) return SyncLanguageServer.create(config, logger, repo_path) From cd10d2f056edf6ba49756f9bc1d90fdb5ade0d94 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 23:33:15 +0200 Subject: [PATCH 037/174] Minor --- src/multilspy/language_server.py | 2 -- src/serena/agent.py | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index ffc1613..c7f921d 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -2252,8 +2252,6 @@ class SyncLanguageServer: shutdown_type = "Nuclear" if is_windows else "Graceful" self.language_server.logger.log(f"{shutdown_type} shutdown complete - all references cleared", logging.INFO) - - def save_cache(self): """ Save the cache to a file. diff --git a/src/serena/agent.py b/src/serena/agent.py index ded70f7..0584ec4 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -522,7 +522,7 @@ class SerenaAgent: :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. - :param gui_log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. + :param log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. """ # obtain serena configuration @@ -804,6 +804,8 @@ class SerenaAgent: :return: a string overview of the current configuration, including the active and available configuration options """ result_str = "Current configuration:\n" + result_str += f"Serena version: {serena_version()}\n" + result_str += f"Loglevel: {self.serena_config.log_level}, trace_lsp_communication={self.serena_config.trace_lsp_communication}\n" if self._active_project is not None: result_str += f"Active project: {self._active_project.project_name}\n" else: From f3901dfec24c97a27ef38693f792d2b1d9689ba6 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 00:01:10 +0200 Subject: [PATCH 038/174] Fix: trace_lsp_communication in yaml was ignored --- src/serena/agent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/serena/agent.py b/src/serena/agent.py index 0584ec4..db300c5 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -377,6 +377,7 @@ class SerenaConfig(SerenaConfigBase): 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.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT) + instance.trace_lsp_communication = loaded_commented_yaml.get("trace_lsp_communication", False) # re-save the configuration file if any migrations were performed if num_project_migrations > 0: From 4514d04c90631dd5496fdcd0daf7299065732d55 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 00:04:54 +0200 Subject: [PATCH 039/174] Minor (docstrings, validation) --- src/multilspy/language_server.py | 33 ++++++++++++------ src/serena/agent.py | 58 +++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index c7f921d..5bd0ae5 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -1760,12 +1760,15 @@ class SyncLanguageServer: Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server for the symbol at the given line and column in the given file. Wait for the response and return the result. - :param relative_file_path: The relative path of the file that has the symbol for which definition should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol + :param file_path: The path of the file relative to the repository root. + :param line: The line number (zero-indexed). + :param column: The column number (zero-indexed). - :return List[multilspy_types.Location]: A list of locations where the symbol is defined + :return: A list of Locations defining the symbol. """ + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + result = asyncio.run_coroutine_threadsafe( self.language_server.request_definition(file_path, line, column), self.loop ).result(timeout=self.timeout) @@ -1773,15 +1776,18 @@ class SyncLanguageServer: def request_references(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: """ - Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server - to find references to the symbol at the given line and column in the given file. Wait for the response and return the result. + Raises a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server + for the symbol at the given line and column in the given file. Wait for the response and return the result. - :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol + :param file_path: The path of the file relative to the repository root. + :param line: The line number (zero-indexed). + :param column: The column number (zero-indexed). - :return List[multilspy_types.Location]: A list of locations where the symbol is referenced + :return: A list of Locations referencing the symbol. """ + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + try: result = asyncio.run_coroutine_threadsafe( self.language_server.request_references(file_path, line, column), self.loop @@ -1867,6 +1873,9 @@ class SyncLanguageServer: :return: A list of root symbols representing the top-level packages/modules in the project. """ + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + result = asyncio.run_coroutine_threadsafe( self.language_server.request_full_symbol_tree(within_relative_path, include_body), self.loop ).result(timeout=self.timeout) @@ -1879,7 +1888,9 @@ class SyncLanguageServer: Maps relative paths of all contained files to info about top-level symbols in the file (name, kind, line, column). """ - assert self.loop + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + result = asyncio.run_coroutine_threadsafe( self.language_server.request_dir_overview(relative_dir_path), self.loop ).result(timeout=self.timeout) diff --git a/src/serena/agent.py b/src/serena/agent.py index db300c5..b8c177f 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -9,7 +9,6 @@ import platform import re import shutil import sys -import threading import traceback import webbrowser from abc import ABC, abstractmethod @@ -21,7 +20,6 @@ from fnmatch import fnmatch from functools import cached_property from logging import Logger from pathlib import Path -from time import time from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast @@ -306,6 +304,59 @@ class SerenaConfigBase(ABC): else: raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}") + def to_dict(self) -> dict: + """Convert configuration to dictionary for serialization.""" + return { + "projects": [ + { + "project_root": project.project_root, + "project_config": { + "project_name": project.project_config.project_name, + "language": project.project_config.language, + "ignored_paths": project.project_config.ignored_paths, + "ignore_all_files_in_gitignore": project.project_config.ignore_all_files_in_gitignore, + }, + } + for project in self.projects + ], + "gui_log_window_enabled": self.gui_log_window_enabled, + "log_level": self.log_level, + "trace_lsp_communication": self.trace_lsp_communication, + "web_dashboard": self.web_dashboard, + "tool_timeout": self.tool_timeout, + } + + @classmethod + def from_dict(cls, data: dict) -> "SerenaConfigBase": + """Create configuration from dictionary.""" + projects = [] + for project_data in data.get("projects", []): + project_config = ProjectConfig( + project_name=project_data["project_config"]["project_name"], + language=project_data["project_config"]["language"], + ignored_paths=project_data["project_config"]["ignored_paths"], + ignore_all_files_in_gitignore=project_data["project_config"]["ignore_all_files_in_gitignore"], + ) + project = Project(project_root=project_data["project_root"], project_config=project_config) + projects.append(project) + + # Create a basic config class that can be instantiated + from dataclasses import dataclass + + @dataclass(kw_only=True) + class DeserializedConfig(SerenaConfigBase): + def _add_new_project(self, project: Project) -> None: + self.projects.append(project) + + return DeserializedConfig( + projects=projects, + gui_log_window_enabled=data.get("gui_log_window_enabled", False), + log_level=data.get("log_level", logging.INFO), + trace_lsp_communication=data.get("trace_lsp_communication", False), + web_dashboard=data.get("web_dashboard", True), + tool_timeout=data.get("tool_timeout", DEFAULT_TOOL_TIMEOUT), + ) + @dataclass(kw_only=True) class SerenaConfig(SerenaConfigBase): @@ -851,14 +902,13 @@ class SerenaAgent: """ Starts/resets the language server for the current project """ - tool_timeout = self.serena_config.tool_timeout if tool_timeout is None or tool_timeout < 0: ls_timeout = None else: if tool_timeout < 10: raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds") - ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout + ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout # stop the language server if it is running if self.is_language_server_running(): From 9c28f901398446244079418222e631a7f1ac4caf Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 00:31:47 +0200 Subject: [PATCH 040/174] Major: Implement process isolation for MCP server to prevent asyncio deadlocks ROOT CAUSE: Asyncio event loop contamination between MCP server and SerenaAgent was causing intermittent deadlocks. MCP server runs in its own asyncio loop, but SerenaAgent creates coroutines that leak into the MCP context, leading to unawaited coroutines and eventual event loop blocking. SOLUTION: Complete process isolation with clean architecture Process Isolation Changes: - Add ProcessIsolatedSerenaAgent wrapper that runs SerenaAgent in separate process - Implement JSON-RPC based IPC for communication between MCP server and agent - Add SerenaAgentWorker class to handle requests in the isolated process - Centralize error handling in _make_request_with_result() helper method - Use serena_config.tool_timeout instead of hardcoded timeout parameters Architecture Improvements: - Define ToolProtocol interface for clean tool contracts - Implement ProcessIsolatedTool with complete Tool interface (apply + apply_ex) - Remove hacky make_process_isolated_tool function - use single make_tool path - Eliminate inheritance abuse - ProcessIsolatedTool implements protocol cleanly - Preserve full docstring parsing functionality through process boundary MCP Server Updates: - Update start_mcp_server() to use process isolation by default - Maintain backward compatibility with create_mcp_server_and_agent() - Retrieve tool metadata from isolated process for proper MCP tool creation - Ensure proper process lifecycle management in server lifespan handlers Key Benefits: - Prevents asyncio contamination deadlocks completely - Maintains full docstring parsing and parameter schema generation - Clean protocol-based architecture with no inheritance hacks - Centralized configuration-driven timeout handling - All tools implement consistent apply_ex interface - Process isolation is transparent to MCP clients This fix ensures that MCP symbolic tools no longer create deadlocks --- src/serena/agent.py | 18 +- src/serena/mcp.py | 122 +++++++-- src/serena/process_isolated_agent.py | 361 +++++++++++++++++++++++++++ 3 files changed, 474 insertions(+), 27 deletions(-) create mode 100644 src/serena/process_isolated_agent.py diff --git a/src/serena/agent.py b/src/serena/agent.py index b8c177f..75d0e77 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -21,7 +21,7 @@ from functools import cached_property from logging import Logger from pathlib import Path from types import TracebackType -from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Literal, Protocol, Self, TypeVar, Union, cast import yaml from overrides import override @@ -1026,6 +1026,22 @@ class ToolMarkerDoesNotRequireActiveProject: pass +class ToolProtocol(Protocol): + """Protocol defining the complete interface that make_tool() expects from a tool.""" + + def get_name(self) -> str: + """Get the tool name.""" + ... + + def apply(self, **kwargs: Any) -> str: + """The application logic of the tool, docstring will be set here and used for MCP tool description.""" + ... + + def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: + """Apply the tool with logging and exception handling.""" + ... + + class Tool(Component): # NOTE: each tool should implement the apply method, which is then used in # the central method of the Tool class `apply_ex`. diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 73c04fc..f499b3d 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -19,9 +19,10 @@ from mcp.server.fastmcp.utilities.func_metadata import func_metadata from sensai.util import logging from sensai.util.helper import mark_used -from serena.agent import SerenaAgent, Tool, show_fatal_exception_safe +from serena.agent import SerenaAgent, SerenaConfig, ToolProtocol, show_fatal_exception_safe from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES +from serena.process_isolated_agent import ProcessIsolatedSerenaAgent, ProcessIsolatedTool log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -46,7 +47,7 @@ class SerenaMCPRequestContext: def make_tool( - tool: Tool, + tool: ToolProtocol, ) -> MCPTool: func_name = tool.get_name() @@ -68,17 +69,17 @@ def make_tool( func_doc = f"{docstring.description.strip().strip('.')}." else: func_doc = "" - if (docstring.returns) and (docstring_returns := docstring.returns.description): + if docstring.returns and (docstring_returns_descr := docstring.returns.description): # Only add a space before "Returns" if func_doc is not empty prefix = " " if func_doc else "" - func_doc = f"{func_doc}{prefix}Returns {docstring_returns.strip().strip('.')}." + func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." # Parse the parameter descriptions from the docstring and add pass its description # to the parameters schema. docstring_params = {param.arg_name: param for param in docstring.params} parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] for parameter, properties in parameters_properties.items(): - if (param_doc := docstring_params.get(parameter)) and (param_doc.description): + if (param_doc := docstring_params.get(parameter)) and param_doc.description: param_desc = f"{param_doc.description.strip().strip('.') + '.'}" properties["description"] = param_desc[0].upper() + param_desc[1:] @@ -109,7 +110,54 @@ def create_mcp_server_and_agent( tool_timeout: float | None = None, ) -> tuple[FastMCP, SerenaAgent]: """ - Create an MCP server. + Create an MCP server (legacy - now uses process isolation). + + :param project: "Either an absolute path to the project directory or a name of an already registered project. " + "If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name " + "afterwards. + :param host: The host to bind to + :param port: The port to bind to + :param context: The context name or path to context file + :param modes: List of mode names or paths to mode files + :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. + :param log_level: Log level. If not specified, will take the value from the serena configuration. + :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. + This is useful for debugging language server issues. + :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + """ + # Delegate to process-isolated version for safety + mcp, process_agent = create_mcp_server_and_process_isolated_agent( + project=project, + host=host, + port=port, + context=context, + modes=modes, + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) + # Return a dummy agent for compatibility + return mcp, process_agent # type: ignore + + +def create_mcp_server_and_process_isolated_agent( + project: str | None, + host: str = "0.0.0.0", + port: int = 8000, + context: str = DEFAULT_CONTEXT, + modes: Sequence[str] = DEFAULT_MODES, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, +) -> tuple[FastMCP, ProcessIsolatedSerenaAgent]: + """ + Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. :param project: "Either an absolute path to the project directory or a name of an already registered project. " "If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name " @@ -131,10 +179,9 @@ def create_mcp_server_and_agent( modes_instances = [SerenaAgentMode.load(mode) for mode in modes] try: - agent = SerenaAgent( + # First create a regular agent to get the config + temp_agent = SerenaAgent( project=project, - # Callback disabled for the time being (see above) - # project_activation_callback=update_tools context=context_instance, modes=modes_instances, enable_web_dashboard=enable_web_dashboard, @@ -143,39 +190,61 @@ def create_mcp_server_and_agent( trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, ) + + # Create process-isolated agent with the same config + if not isinstance(temp_agent.serena_config, SerenaConfig): + raise TypeError("Expected SerenaConfig instance") + process_agent = ProcessIsolatedSerenaAgent(temp_agent.serena_config) + + # Clean up temp agent + if temp_agent.language_server is not None: + temp_agent.language_server.stop() + del temp_agent + except Exception as e: show_fatal_exception_safe(e) raise def update_tools() -> None: - """Update the tools in the MCP server.""" - # Tools may change as a result of project activation. - # NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not, - # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, - # so we need to register all tools at startup, unfortunately. - nonlocal mcp, agent - tools = agent.get_exposed_tool_instances() + """Update the tools in the MCP server - adapted for process isolation.""" + nonlocal mcp, process_agent + + # Get tool instances data from process-isolated agent + try: + tool_instances_data = process_agent.get_tool_instances() + except Exception as e: + log.error(f"Failed to get tool instances from process agent: {e}") + return + if mcp is not None: mcp._tool_manager._tools = {} - for tool in tools: - # noinspection PyProtectedMember - mcp._tool_manager._tools[tool.get_name()] = make_tool(tool) + for tool_data in tool_instances_data: + # Create ProcessIsolatedTool instance that can be used with make_tool + tool_instance = ProcessIsolatedTool( + process_agent=process_agent, tool_name=tool_data["name"], tool_doc=tool_data["apply_method_doc"] + ) + # Use the original make_tool function to get full docstring parsing + mcp_tool = make_tool(tool_instance) + mcp._tool_manager._tools[tool_data["name"]] = mcp_tool @asynccontextmanager async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" - nonlocal agent + nonlocal process_agent mark_used(mcp_server) - yield - if agent.language_server is not None: - agent.language_server.stop() + + try: + process_agent.start() + yield + finally: + process_agent.stop() mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) mcp = FastMCP(**mcp_settings.model_dump()) update_tools() - return mcp, agent + return mcp, process_agent class ProjectType(click.ParamType): @@ -313,7 +382,8 @@ def start_mcp_server( # This is for backward compatibility with the old CLI, should be removed in the future! project_file = project_file_arg if project_file_arg is not None else project_file_opt - mcp_server, agent = create_mcp_server_and_agent( + # Use process isolation by default to prevent asyncio event loop contamination + mcp_server, agent = create_mcp_server_and_process_isolated_agent( project=project_file, host=host, port=port, @@ -335,7 +405,7 @@ def start_mcp_server( ) log.info( - f"Starting serena agent in MCP server with config:\n{agent.get_current_config_overview()}." + f"Starting process-isolated serena agent in MCP server with config:\n{agent.serena_config}." f"\n Log level: {agent.serena_config.log_level}" ) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py new file mode 100644 index 0000000..de562a1 --- /dev/null +++ b/src/serena/process_isolated_agent.py @@ -0,0 +1,361 @@ +""" +Process-isolated SerenaAgent to prevent asyncio event loop contamination between MCP server and language server. + +This module provides: +1. ProcessIsolatedSerenaAgent - A wrapper that runs SerenaAgent in a separate process +2. SerenaAgentWorker - The worker process that hosts the actual SerenaAgent +3. JSON-RPC based IPC for communication between processes +""" + +import logging +import multiprocessing +import queue +import threading +import time +import traceback +from typing import Any, Self + +from serena.agent import SerenaAgent, SerenaConfig, ToolProtocol + +log = logging.getLogger(__name__) + + +class SerenaAgentWorker: + """Worker process that hosts the actual SerenaAgent.""" + + def __init__(self, request_queue: multiprocessing.Queue, response_queue: multiprocessing.Queue): + self.request_queue = request_queue + self.response_queue = response_queue + self.agent: SerenaAgent | None = None + self.running = True + + def run(self) -> None: + """Main worker loop - runs in separate process.""" + try: + log.info("SerenaAgent worker process started") + + while self.running: + try: + # Wait for requests with timeout to allow clean shutdown + request = self.request_queue.get(timeout=1.0) + if request is None: # Shutdown signal + break + + response = self._handle_request(request) + self.response_queue.put(response) + + except queue.Empty: + continue + except Exception as e: + log.error(f"Error in worker process: {e}") + response = { + "id": getattr(request, "id", None), + "error": f"Worker process error: {e!s}", + "traceback": traceback.format_exc(), + } + self.response_queue.put(response) + + except Exception as e: + log.error(f"Fatal error in worker process: {e}") + finally: + self._cleanup() + log.info("SerenaAgent worker process stopped") + + def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]: + """Handle a single request.""" + try: + method = request["method"] + params = request.get("params", {}) + request_id = request["id"] + + if method == "initialize": + return self._initialize(request_id, params) + elif method == "tool_call": + return self._tool_call(request_id, params) + elif method == "get_active_tool_names": + return self._get_active_tool_names(request_id) + elif method == "is_language_server_running": + return self._is_language_server_running(request_id) + elif method == "reset_language_server": + return self._reset_language_server(request_id) + elif method == "get_tool_instances": + return self._get_tool_instances(request_id) + else: + return {"id": request_id, "error": f"Unknown method: {method}"} + + except Exception as e: + return {"id": request.get("id"), "error": str(e), "traceback": traceback.format_exc()} + + def _initialize(self, request_id: str, params: dict[str, Any]) -> dict[str, Any]: + """Initialize the SerenaAgent.""" + try: + config_dict = params["config"] + serena_config = SerenaConfig.from_dict(config_dict) + + self.agent = SerenaAgent(serena_config=serena_config) + + return {"id": request_id, "result": "SerenaAgent initialized successfully"} + except Exception as e: + return {"id": request_id, "error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} + + def _tool_call(self, request_id: str, params: dict[str, Any]) -> dict[str, Any]: + """Execute a tool call.""" + if self.agent is None: + return {"id": request_id, "error": "SerenaAgent not initialized"} + + try: + tool_name = params["tool_name"] + tool_params = params["tool_params"] + + # Get the tool by name + tool = None + for tool_instance in self.agent._active_tools.values(): + if tool_instance.get_name() == tool_name: + tool = tool_instance + break + + if tool is None: + return {"id": request_id, "error": f"Tool '{tool_name}' not found or not active"} + + # Execute the tool + result = tool.apply_ex(**tool_params) + + return {"id": request_id, "result": result} + + except Exception as e: + return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + + def _get_active_tool_names(self, request_id: str) -> dict[str, Any]: + """Get list of active tool names.""" + if self.agent is None: + return {"id": request_id, "error": "SerenaAgent not initialized"} + + try: + tool_names = self.agent.get_active_tool_names() + return {"id": request_id, "result": tool_names} + except Exception as e: + return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + + def _is_language_server_running(self, request_id: str) -> dict[str, Any]: + """Check if language server is running.""" + if self.agent is None: + return {"id": request_id, "error": "SerenaAgent not initialized"} + + try: + is_running = self.agent.is_language_server_running() + return {"id": request_id, "result": is_running} + except Exception as e: + return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + + def _reset_language_server(self, request_id: str) -> dict[str, Any]: + """Reset the language server.""" + if self.agent is None: + return {"id": request_id, "error": "SerenaAgent not initialized"} + + try: + self.agent.reset_language_server() + return {"id": request_id, "result": "Language server reset successfully"} + except Exception as e: + return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + + def _get_tool_instances(self, request_id: str) -> dict[str, Any]: + """Get exposed tool instances for MCP tool creation.""" + if self.agent is None: + return {"id": request_id, "error": "SerenaAgent not initialized"} + + try: + tool_instances = self.agent.get_exposed_tool_instances() + # Serialize tool information that can be used to recreate tools on the MCP side + tool_data = [] + for tool in tool_instances: + tool_data.append( + { + "name": tool.get_name(), + "class_name": tool.__class__.__name__, + "apply_method_doc": getattr(tool, "apply").__doc__, + } + ) + return {"id": request_id, "result": tool_data} + except Exception as e: + return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + + def _cleanup(self) -> None: + """Clean up resources.""" + if self.agent is not None: + try: + if self.agent.is_language_server_running() and self.agent.language_server is not None: + self.agent.language_server.stop() + except Exception as e: + log.error(f"Error stopping language server: {e}") + self.agent = None + + +class ProcessIsolatedSerenaAgent: + """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" + + def __init__(self, serena_config: SerenaConfig): + self.serena_config = serena_config + self.process: multiprocessing.Process | None = None + self.request_queue: multiprocessing.Queue | None = None + self.response_queue: multiprocessing.Queue | None = None + self.request_id_counter = 0 + self._lock = threading.Lock() + + def start(self) -> None: + """Start the worker process.""" + if self.process is not None: + raise RuntimeError("ProcessIsolatedSerenaAgent already started") + + log.info("Starting process-isolated SerenaAgent") + + # Create communication queues + self.request_queue = multiprocessing.Queue() + self.response_queue = multiprocessing.Queue() + + # Create and start worker process + worker = SerenaAgentWorker(self.request_queue, self.response_queue) + self.process = multiprocessing.Process(target=worker.run) + self.process.start() + + # Initialize the agent in the worker process + try: + self._make_request_with_result("initialize", {"config": self.serena_config.to_dict()}) + except Exception as e: + self.stop() + raise RuntimeError(f"Failed to initialize SerenaAgent: {e}") from e + + log.info("Process-isolated SerenaAgent started successfully") + + def stop(self) -> None: + """Stop the worker process.""" + if self.process is None: + return + + log.info("Stopping process-isolated SerenaAgent") + + try: + # Send shutdown signal + if self.request_queue is not None: + self.request_queue.put(None) + + # Wait for process to terminate + if self.process.is_alive(): + self.process.join(timeout=10.0) + + if self.process.is_alive(): + log.warning("Worker process did not terminate gracefully, forcing termination") + self.process.terminate() + self.process.join(timeout=5.0) + + if self.process.is_alive(): + log.error("Worker process could not be terminated, killing it") + self.process.kill() + self.process.join() + + except Exception as e: + log.error(f"Error stopping worker process: {e}") + + finally: + self.process = None + self.request_queue = None + self.response_queue = None + + log.info("Process-isolated SerenaAgent stopped") + + def _make_request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]: + """Make a request to the worker process.""" + if self.process is None or not self.process.is_alive(): + raise RuntimeError("Worker process is not running") + + with self._lock: + self.request_id_counter += 1 + request_id = str(self.request_id_counter) + + request = {"id": request_id, "method": method, "params": params or {}} + + # Send request + if self.request_queue is not None: + self.request_queue.put(request) + + # Wait for response + timeout = self.serena_config.tool_timeout + start_time = time.time() + while time.time() - start_time < timeout: + try: + if self.response_queue is not None: + response = self.response_queue.get(timeout=1.0) + else: + raise RuntimeError("Response queue is not initialized") + if response["id"] == request_id: + return response + else: + # Put back response for different request + if self.response_queue is not None: + self.response_queue.put(response) + except queue.Empty: + continue + + raise TimeoutError(f"Request {request_id} timed out after {timeout} seconds") + + def _make_request_with_result(self, method: str, params: dict[str, Any] | None = None) -> Any: + """Make a request and return the result, raising an exception if there's an error.""" + response = self._make_request(method, params) + if "error" in response: + raise RuntimeError(f"Request {method} failed: {response['error']}") + return response["result"] + + def tool_call(self, tool_name: str, **tool_params: Any) -> str: + """Call a tool in the worker process.""" + return self._make_request_with_result("tool_call", {"tool_name": tool_name, "tool_params": tool_params}) + + def get_active_tool_names(self) -> list[str]: + """Get list of active tool names.""" + return self._make_request_with_result("get_active_tool_names") + + def is_language_server_running(self) -> bool: + """Check if language server is running.""" + return self._make_request_with_result("is_language_server_running") + + def reset_language_server(self) -> None: + """Reset the language server.""" + self._make_request_with_result("reset_language_server") + + def get_tool_instances(self) -> list[dict[str, Any]]: + """Get tool instances data for MCP tool creation.""" + return self._make_request_with_result("get_tool_instances") + + def __enter__(self) -> Self: + self.start() + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.stop() + + +class ProcessIsolatedTool(ToolProtocol): + """A clean tool wrapper that delegates to ProcessIsolatedSerenaAgent.""" + + def __init__(self, process_agent: ProcessIsolatedSerenaAgent, tool_name: str, tool_doc: str | None = None): + self.process_agent = process_agent + self.tool_name = tool_name + self._tool_doc = tool_doc + # Set the docstring for the apply method if provided + if tool_doc: + self.apply.__doc__ = tool_doc + + def apply(self, **kwargs: Any) -> str: + """Apply the tool through the process-isolated agent.""" + return self.process_agent.tool_call(self.tool_name, **kwargs) + + def get_name(self) -> str: + """Get the tool name for this process-isolated tool.""" + return self.tool_name + + def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: + """Apply the tool with logging and exception handling.""" + try: + return self.apply(**kwargs) + except Exception as e: + if catch_exceptions: + return f"Error executing tool {self.tool_name}: {e!s}" + raise From 4a4b665d60fd0670e4d45b0d080ddf260e88b519 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 01:24:14 +0200 Subject: [PATCH 041/174] Testing: allow setting PYTEST_MARKERS to control which tests to run by default --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3ce1ed4..ec13ddd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,7 +107,9 @@ exclude = "^build/|^docs/" PYDEVD_DISABLE_FILE_VALIDATION = "1" [tool.poe.tasks] -test = "pytest test --color=yes" +# Uses PYTEST_MARKERS env var for default markers (defaults to "not java and not rust") +# Set PYTEST_MARKERS="" to run all tests +test = "pytest test --color=yes -m \"${PYTEST_MARKERS:-not java and not rust}\"" _black_check = "black --check --exclude src/multilspy/ src scripts test" _ruff_check = "ruff check --exclude .venv/ --exclude src/multilspy/ src scripts test" _black_format = "black --exclude .venv/|src/multilspy/ src scripts test" From 04011230081cfb54cd3b652d709273b8de837d9f Mon Sep 17 00:00:00 2001 From: Aaron Yordanyan Date: Sat, 14 Jun 2025 10:19:12 +0400 Subject: [PATCH 042/174] Refine Docker configuration handling and documentation - Updated exception handling in `agent.py` for better error specificity (catching `FileNotFoundError`). - Added contextual comment for `CONFIG_FILE_DOCKER` in `agent.py`. - Updated `DOCKER.md` to reflect the new GitHub repository link. --- DOCKER.md | 2 +- src/serena/agent.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 04f8ca6..45e0f5c 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -158,4 +158,4 @@ We're working on: - Dynamic port allocation - Windows line-ending handling -For updates and issues, please check the [GitHub repository](https://github.com/AbanteAI/serena). \ No newline at end of file +For updates and issues, please check the [GitHub repository](https://github.com/diazoxide/serena). diff --git a/src/serena/agent.py b/src/serena/agent.py index c6021a1..512a8fe 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -111,7 +111,7 @@ def is_running_in_docker() -> bool: try: with open('/proc/self/cgroup', 'r') as f: return 'docker' in f.read() - except: + except FileNotFoundError: return False @@ -328,7 +328,7 @@ class SerenaConfig(SerenaConfigBase): loaded_commented_yaml: CommentedMap CONFIG_FILE = "serena_config.yml" - CONFIG_FILE_DOCKER = "serena_config.docker.yml" + CONFIG_FILE_DOCKER = "serena_config.docker.yml" # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization @classmethod def autogenerate(cls) -> None: From 37d100446057d8c9a3a568c87bfa880c0af9fd74 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 01:59:29 +0200 Subject: [PATCH 043/174] Fixes in SerenaAgent isolation in process creation make_tool previously depended on tool instances in the main process, but the tool instances are no longer available there To solve this, the tool interface was adjusted to provide static information which is used in the ProcessIsolatedTool wrapper. I also tried another approach before, where ProcessIsolatedTool would accept tool metadata at init, but this lead to serialization issues that couldn't be easily solved (mcp's FuncMetadata is not serializable, despite being a BaseModel...). Tests for checking that the wrapped tool instance has the same metadata as the wrapper have been added. --- src/serena/agent.py | 189 ++++++++++++++---- src/serena/agno.py | 4 +- src/serena/mcp.py | 68 +++---- src/serena/process_isolated_agent.py | 60 +++--- .../test_make_tool_process_isolation.py | 160 +++++++++++++++ test/serena/test_mcp.py | 4 +- test/serena/test_serena_agent.py | 37 +++- 7 files changed, 414 insertions(+), 108 deletions(-) create mode 100644 test/serena/test_make_tool_process_isolation.py diff --git a/src/serena/agent.py b/src/serena/agent.py index 75d0e77..ebb8a36 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -21,9 +21,10 @@ from functools import cached_property from logging import Logger from pathlib import Path from types import TracebackType -from typing import TYPE_CHECKING, Any, Literal, Protocol, Self, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast import yaml +from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata from overrides import override from pathspec import PathSpec from ruamel.yaml.comments import CommentedMap @@ -548,6 +549,77 @@ class MemoriesManagerMDFilesInProject(MemoriesManager): return f"Memory {name} deleted." +def create_serena_config( + project: str | None = None, + serena_config: SerenaConfigBase | None = None, + context: SerenaAgentContext | None = None, + modes: list[SerenaAgentMode] | 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, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, +) -> SerenaConfig: + """ + Create a SerenaConfig instance without instantiating a full SerenaAgent. + + This function extracts the configuration creation logic from SerenaAgent.__init__ + to allow creating configurations independently for process isolation and other use cases. + + :param project: the project to register/activate or None + :param serena_config: the base Serena configuration or None to read from default location + :param context: the context in which the agent will operate + :param modes: list of modes in which the agent will operate + :param enable_web_dashboard: Whether to enable the web dashboard + :param enable_gui_log_window: Whether to enable the GUI log window + :param log_level: Log level + :param trace_lsp_communication: Whether to trace LSP communication + :param tool_timeout: Timeout in seconds for tool execution + :return: A fully configured SerenaConfig instance + """ + # obtain serena configuration + if serena_config is not None: + # If a complete SerenaConfig is provided, use it directly + if isinstance(serena_config, SerenaConfig): + config = serena_config + else: + # For SerenaConfigBase instances (like test configs), create an in-memory SerenaConfig + # that preserves the base config attributes without loading from file + from ruamel.yaml.comments import CommentedMap + + config = SerenaConfig.__new__(SerenaConfig) # Create without calling __init__ + # Initialize basic attributes from base config + config.projects = getattr(serena_config, "projects", []) + config.gui_log_window_enabled = serena_config.gui_log_window_enabled + config.log_level = serena_config.log_level + config.trace_lsp_communication = serena_config.trace_lsp_communication + config.web_dashboard = serena_config.web_dashboard + config.tool_timeout = serena_config.tool_timeout + # Set empty yaml for in-memory config + config.loaded_commented_yaml = CommentedMap() + else: + config = SerenaConfig.from_config_file() + + # Apply parameter overrides + if enable_web_dashboard is not None: + config.web_dashboard = enable_web_dashboard + if enable_gui_log_window is not None: + config.gui_log_window_enabled = enable_gui_log_window + if log_level is not None: + log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], log_level.upper()) + # transform to int + config.log_level = logging.getLevelNamesMapping()[log_level] + if trace_lsp_communication is not None: + config.trace_lsp_communication = trace_lsp_communication + if tool_timeout is not None: + config.tool_timeout = tool_timeout + + # Note: Project registration/activation is handled separately by the caller + # since it involves complex logic that may require the full agent context + + return config + + class SerenaAgent: def __init__( self, @@ -577,20 +649,18 @@ class SerenaAgent: :param log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. """ - # obtain serena configuration - self.serena_config = serena_config or SerenaConfig.from_config_file() - if enable_web_dashboard is not None: - self.serena_config.web_dashboard = enable_web_dashboard - if enable_gui_log_window is not None: - self.serena_config.gui_log_window_enabled = enable_gui_log_window - if log_level is not None: - log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], log_level.upper()) - # transform to int - self.serena_config.log_level = logging.getLevelNamesMapping()[log_level] - if trace_lsp_communication is not None: - self.serena_config.trace_lsp_communication = trace_lsp_communication - if tool_timeout is not None: - self.serena_config.tool_timeout = tool_timeout + # obtain serena configuration using the decoupled factory function + self.serena_config = create_serena_config( + project=None, # Don't pass project here, handle activation separately + serena_config=serena_config, + context=context, + modes=modes, + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) # adjust log level serena_log_level = self.serena_config.log_level @@ -617,7 +687,7 @@ class SerenaAgent: # instantiate all tool classes self._all_tools: dict[type[Tool], Tool] = {tool_class: tool_class(self) for tool_class in ToolRegistry.get_all_tool_classes()} - tool_names = [tool.get_name() for tool in self._all_tools.values()] + tool_names = [tool.get_name_from_cls() for tool in self._all_tools.values()] # If GUI log window is enabled, set the tool names for highlighting if self._gui_log_handler is not None: @@ -654,7 +724,7 @@ class SerenaAgent: modes = SerenaAgentMode.load_default_modes() self._context = context self._modes = modes - log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name() for tool in self._all_tools.values()])}") + log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name_from_cls() for tool in self._all_tools.values()])}") self._active_tools: dict[type[Tool], Tool] = {} self._update_active_tools() @@ -839,7 +909,7 @@ class SerenaAgent: """ :return: the list of names of the active tools for the current project """ - return sorted([tool.get_name() for tool in self.get_active_tool_classes()]) + return sorted([tool.get_name_from_cls() for tool in self.get_active_tool_classes()]) def tool_is_active(self, tool_class: type["Tool"] | str) -> bool: """ @@ -885,7 +955,7 @@ class SerenaAgent: result_str += " " + ", ".join(chunk) + "\n" # Available but not active tools - all_tool_names = sorted([tool.get_name() for tool in self._all_tools.values()]) + all_tool_names = sorted([tool.get_name_from_cls() for tool in self._all_tools.values()]) inactive_tool_names = [tool for tool in all_tool_names if tool not in active_tool_names] if inactive_tool_names: result_str += "Available but not active tools:\n" @@ -1026,23 +1096,31 @@ class ToolMarkerDoesNotRequireActiveProject: pass -class ToolProtocol(Protocol): +class ToolInterface(ABC): """Protocol defining the complete interface that make_tool() expects from a tool.""" + @abstractmethod def get_name(self) -> str: """Get the tool name.""" ... - def apply(self, **kwargs: Any) -> str: - """The application logic of the tool, docstring will be set here and used for MCP tool description.""" + @abstractmethod + def get_apply_docstring(self) -> str: + """Get the docstring for the tool application, used by the MCP server.""" ... + @abstractmethod + def get_apply_fn_metadata(self) -> FuncMetadata: + """Get the metadata for the tool application function, used by the MCP server.""" + ... + + @abstractmethod def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: """Apply the tool with logging and exception handling.""" ... -class Tool(Component): +class Tool(Component, ToolInterface): # NOTE: each tool should implement the apply method, which is then used in # the central method of the Tool class `apply_ex`. # Failure to do so will result in a RuntimeError at tool execution time. @@ -1054,7 +1132,7 @@ class Tool(Component): # and to validate the tool call arguments. @classmethod - def get_name(cls) -> str: + def get_name_from_cls(cls) -> str: name = cls.__name__ if name.endswith("Tool"): name = name[:-4] @@ -1062,6 +1140,9 @@ class Tool(Component): name = "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_") return name + def get_name(self) -> str: + return self.get_name_from_cls() + def get_apply_fn(self) -> Callable: apply_fn = getattr(self, "apply") if apply_fn is None: @@ -1084,12 +1165,48 @@ class Tool(Component): return "" return docstring.strip() - def get_function_description(self) -> str: - apply_fn = self.get_apply_fn() + @classmethod + def get_apply_docstring_from_cls(cls) -> str: + """Get the docstring for the apply method from the class (static metadata). + Needed for creating MCP tools in a separate process without running into serialization issues. + """ + # First try to get from __dict__ to handle dynamic docstring changes + if "apply" in cls.__dict__: + apply_fn = cls.__dict__["apply"] + else: + # Fall back to getattr for inherited methods + apply_fn = getattr(cls, "apply", None) + if apply_fn is None: + raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?") + docstring = apply_fn.__doc__ - if docstring is None: - raise Exception(f"Missing docstring for {self}") - return docstring + if not docstring: + raise AttributeError(f"apply method has no (or empty) docstring in {cls}. Did you forget to implement it?") + return docstring.strip() + + def get_apply_docstring(self) -> str: + """Get the docstring for the apply method (instance method implementing ToolProtocol).""" + return self.get_apply_docstring_from_cls() + + def get_apply_fn_metadata(self) -> FuncMetadata: + """Get the metadata for the apply method (instance method implementing ToolProtocol).""" + return self.get_apply_fn_metadata_from_cls() + + @classmethod + def get_apply_fn_metadata_from_cls(cls) -> FuncMetadata: + """Get the metadata for the apply method from the class (static metadata). + Needed for creating MCP tools in a separate process without running into serialization issues. + """ + # First try to get from __dict__ to handle dynamic docstring changes + if "apply" in cls.__dict__: + apply_fn = cls.__dict__["apply"] + else: + # Fall back to getattr for inherited methods + apply_fn = getattr(cls, "apply", None) + if apply_fn is None: + raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?") + + return func_metadata(apply_fn, skip_names=["self", "cls"]) def _log_tool_application(self, frame: Any) -> None: params = {} @@ -1101,7 +1218,7 @@ class Tool(Component): params.update(value) else: params[param] = value - log.info(f"{self.get_name()}: {dict_string(params)}") + log.info(f"{self.get_name_from_cls()}: {dict_string(params)}") @staticmethod def _limit_length(result: str, max_answer_chars: int) -> str: @@ -1123,9 +1240,9 @@ class Tool(Component): try: if not self.is_active(): - return f"Error: Tool '{self.get_name()}' is not active. Active tools: {self.agent.get_active_tool_names()}" + return f"Error: Tool '{self.get_name_from_cls()}' is not active. Active tools: {self.agent.get_active_tool_names()}" except Exception as e: - return f"RuntimeError while checking if tool {self.get_name()} is active: {e}" + return f"RuntimeError while checking if tool {self.get_name_from_cls()} is active: {e}" if log_call: self._log_tool_application(inspect.currentframe()) @@ -1143,7 +1260,7 @@ class Tool(Component): # apply the actual tool with a timeout execution_fn = lambda: apply_fn(**kwargs) - execution_result = execute_with_timeout(execution_fn, self.agent.serena_config.tool_timeout, self.get_name()) + execution_result = execute_with_timeout(execution_fn, self.agent.serena_config.tool_timeout, self.get_name_from_cls()) if execution_result.status == ExecutionResult.Status.SUCCESS: result = cast(str, execution_result.result_value) else: @@ -1696,7 +1813,7 @@ class DeleteLinesTool(Tool, ToolMarkerCanEdit): """ if not self.lines_read.were_lines_read(relative_path, (start_line, end_line)): read_lines_tool = self.agent.get_tool(ReadFileTool) - return f"Error: Must call `{read_lines_tool.get_name()}` first to read exactly the affected lines." + return f"Error: Must call `{read_lines_tool.get_name_from_cls()}` first to read exactly the affected lines." self.symbol_manager.delete_lines(relative_path, start_line, end_line) return SUCCESS_RESULT @@ -2158,7 +2275,7 @@ def _iter_tool_classes(same_module_only: bool = True) -> Generator[type[Tool], N yield tool_class -_TOOL_REGISTRY_DICT: dict[str, type[Tool]] = {tool_class.get_name(): tool_class for tool_class in _iter_tool_classes()} +_TOOL_REGISTRY_DICT: dict[str, type[Tool]] = {tool_class.get_name_from_cls(): tool_class for tool_class in _iter_tool_classes()} """maps tool name to the corresponding tool class""" @@ -2194,7 +2311,7 @@ class ToolRegistry: tool_dict: dict[str, type[Tool] | Tool] = {} for tool_class in tools: - tool_dict[tool_class.get_name()] = tool_class + tool_dict[tool_class.get_name_from_cls()] = tool_class for tool_name in sorted(tool_dict.keys()): tool_class = tool_dict[tool_name] print(f" * `{tool_name}`: {tool_class.get_tool_description().strip()}") diff --git a/src/serena/agno.py b/src/serena/agno.py index c915069..0bb0e70 100644 --- a/src/serena/agno.py +++ b/src/serena/agno.py @@ -25,7 +25,7 @@ class SerenaAgnoToolkit(Toolkit): def __init__(self, serena_agent: SerenaAgent): super().__init__("Serena") for tool in serena_agent.get_exposed_tool_instances(): - self.functions[tool.get_name()] = self._create_agno_function(tool) + self.functions[tool.get_name_from_cls()] = self._create_agno_function(tool) log.info("Agno agent functions: %s", list(self.functions.keys())) @staticmethod @@ -39,7 +39,7 @@ class SerenaAgnoToolkit(Toolkit): return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) function = Function.from_callable(tool.get_apply_fn()) - function.name = tool.get_name() + function.name = tool.get_name_from_cls() function.entrypoint = entrypoint function.skip_entrypoint_processing = True return function diff --git a/src/serena/mcp.py b/src/serena/mcp.py index f499b3d..db188a2 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -8,21 +8,23 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from logging import Formatter, Logger, StreamHandler from pathlib import Path -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal import click # Add click import 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.server.fastmcp.utilities.func_metadata import func_metadata from sensai.util import logging from sensai.util.helper import mark_used -from serena.agent import SerenaAgent, SerenaConfig, ToolProtocol, show_fatal_exception_safe +from serena.agent import SerenaAgent, ToolInterface, create_serena_config, show_fatal_exception_safe from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES -from serena.process_isolated_agent import ProcessIsolatedSerenaAgent, ProcessIsolatedTool + +# Import for type annotations +if TYPE_CHECKING: + from serena.process_isolated_agent import ProcessIsolatedSerenaAgent log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -47,18 +49,12 @@ class SerenaMCPRequestContext: def make_tool( - tool: ToolProtocol, + tool: ToolInterface, ) -> MCPTool: func_name = tool.get_name() - - apply_fn = getattr(tool, "apply") - if apply_fn is None: - raise ValueError(f"Tool does not have an apply method: {tool}") - - func_doc = apply_fn.__doc__ or "" + func_doc = tool.get_apply_docstring() or "" + func_arg_metadata = tool.get_apply_fn_metadata() is_async = False - - func_arg_metadata = func_metadata(apply_fn) parameters = func_arg_metadata.arg_model.model_json_schema() docstring = docstring_parser.parse(func_doc) @@ -94,6 +90,7 @@ def make_tool( fn_metadata=func_arg_metadata, is_async=is_async, context_kwarg=None, + annotations=None, ) @@ -108,7 +105,7 @@ def create_mcp_server_and_agent( log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, -) -> tuple[FastMCP, SerenaAgent]: +) -> tuple[FastMCP, "ProcessIsolatedSerenaAgent"]: """ Create an MCP server (legacy - now uses process isolation). @@ -141,7 +138,7 @@ def create_mcp_server_and_agent( tool_timeout=tool_timeout, ) # Return a dummy agent for compatibility - return mcp, process_agent # type: ignore + return mcp, process_agent def create_mcp_server_and_process_isolated_agent( @@ -155,7 +152,7 @@ def create_mcp_server_and_process_isolated_agent( log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, -) -> tuple[FastMCP, ProcessIsolatedSerenaAgent]: +) -> tuple[FastMCP, "ProcessIsolatedSerenaAgent"]: """ Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. @@ -179,8 +176,8 @@ def create_mcp_server_and_process_isolated_agent( modes_instances = [SerenaAgentMode.load(mode) for mode in modes] try: - # First create a regular agent to get the config - temp_agent = SerenaAgent( + # Create configuration without instantiating a full SerenaAgent + serena_config = create_serena_config( project=project, context=context_instance, modes=modes_instances, @@ -191,15 +188,10 @@ def create_mcp_server_and_process_isolated_agent( tool_timeout=tool_timeout, ) - # Create process-isolated agent with the same config - if not isinstance(temp_agent.serena_config, SerenaConfig): - raise TypeError("Expected SerenaConfig instance") - process_agent = ProcessIsolatedSerenaAgent(temp_agent.serena_config) + # Create process-isolated agent with the configuration + from serena.process_isolated_agent import ProcessIsolatedSerenaAgent - # Clean up temp agent - if temp_agent.language_server is not None: - temp_agent.language_server.stop() - del temp_agent + process_agent = ProcessIsolatedSerenaAgent(serena_config) except Exception as e: show_fatal_exception_safe(e) @@ -209,23 +201,29 @@ def create_mcp_server_and_process_isolated_agent( """Update the tools in the MCP server - adapted for process isolation.""" nonlocal mcp, process_agent - # Get tool instances data from process-isolated agent + # Check if process agent is running + if process_agent.process is None or not process_agent.process.is_alive(): + log.debug("Process agent not running yet, skipping tool update") + return + + # Get tool names from process-isolated agent try: - tool_instances_data = process_agent.get_tool_instances() + tool_names = process_agent.get_tool_instances() except Exception as e: - log.error(f"Failed to get tool instances from process agent: {e}") + log.error(f"Failed to get tool names from process agent: {e}") return if mcp is not None: + from serena.process_isolated_agent import ProcessIsolatedTool + mcp._tool_manager._tools = {} - for tool_data in tool_instances_data: + for tool_name in tool_names: # Create ProcessIsolatedTool instance that can be used with make_tool - tool_instance = ProcessIsolatedTool( - process_agent=process_agent, tool_name=tool_data["name"], tool_doc=tool_data["apply_method_doc"] - ) + # Metadata is extracted from ToolRegistry using classmethods + tool_instance = ProcessIsolatedTool(process_agent=process_agent, tool_name=tool_name) # Use the original make_tool function to get full docstring parsing mcp_tool = make_tool(tool_instance) - mcp._tool_manager._tools[tool_data["name"]] = mcp_tool + mcp._tool_manager._tools[tool_name] = mcp_tool @asynccontextmanager async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: @@ -235,6 +233,8 @@ def create_mcp_server_and_process_isolated_agent( try: process_agent.start() + # Update tools now that the process agent is running + update_tools() yield finally: process_agent.stop() diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index de562a1..543b5ec 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -15,7 +15,9 @@ import time import traceback from typing import Any, Self -from serena.agent import SerenaAgent, SerenaConfig, ToolProtocol +from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata + +from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry log = logging.getLogger(__name__) @@ -110,7 +112,7 @@ class SerenaAgentWorker: # Get the tool by name tool = None for tool_instance in self.agent._active_tools.values(): - if tool_instance.get_name() == tool_name: + if tool_instance.get_name_from_cls() == tool_name: tool = tool_instance break @@ -159,23 +161,15 @@ class SerenaAgentWorker: return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} def _get_tool_instances(self, request_id: str) -> dict[str, Any]: - """Get exposed tool instances for MCP tool creation.""" + """Get exposed tool names for MCP tool creation.""" if self.agent is None: return {"id": request_id, "error": "SerenaAgent not initialized"} try: tool_instances = self.agent.get_exposed_tool_instances() - # Serialize tool information that can be used to recreate tools on the MCP side - tool_data = [] - for tool in tool_instances: - tool_data.append( - { - "name": tool.get_name(), - "class_name": tool.__class__.__name__, - "apply_method_doc": getattr(tool, "apply").__doc__, - } - ) - return {"id": request_id, "result": tool_data} + # Return only tool names - metadata will be reconstructed from ToolRegistry + tool_names = [tool.get_name_from_cls() for tool in tool_instances] + return {"id": request_id, "result": tool_names} except Exception as e: return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} @@ -193,7 +187,7 @@ class SerenaAgentWorker: class ProcessIsolatedSerenaAgent: """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" - def __init__(self, serena_config: SerenaConfig): + def __init__(self, serena_config: SerenaConfigBase): self.serena_config = serena_config self.process: multiprocessing.Process | None = None self.request_queue: multiprocessing.Queue | None = None @@ -320,8 +314,8 @@ class ProcessIsolatedSerenaAgent: """Reset the language server.""" self._make_request_with_result("reset_language_server") - def get_tool_instances(self) -> list[dict[str, Any]]: - """Get tool instances data for MCP tool creation.""" + def get_tool_instances(self) -> list[str]: + """Get tool names for MCP tool creation.""" return self._make_request_with_result("get_tool_instances") def __enter__(self) -> Self: @@ -332,30 +326,36 @@ class ProcessIsolatedSerenaAgent: self.stop() -class ProcessIsolatedTool(ToolProtocol): +class ProcessIsolatedTool(ToolInterface): """A clean tool wrapper that delegates to ProcessIsolatedSerenaAgent.""" - def __init__(self, process_agent: ProcessIsolatedSerenaAgent, tool_name: str, tool_doc: str | None = None): + def __init__(self, process_agent: ProcessIsolatedSerenaAgent, tool_name: str): self.process_agent = process_agent - self.tool_name = tool_name - self._tool_doc = tool_doc - # Set the docstring for the apply method if provided - if tool_doc: - self.apply.__doc__ = tool_doc + self._tool_name = tool_name - def apply(self, **kwargs: Any) -> str: - """Apply the tool through the process-isolated agent.""" - return self.process_agent.tool_call(self.tool_name, **kwargs) + @property + def _tool_class(self) -> type[Tool]: + return ToolRegistry.get_tool_class_by_name(self._tool_name) def get_name(self) -> str: """Get the tool name for this process-isolated tool.""" - return self.tool_name + return self._tool_name + + def get_apply_docstring(self) -> str: + """Get the docstring for the apply method.""" + # in the actual tool, this is a classmethod + return self._tool_class.get_apply_docstring_from_cls() + + def get_apply_fn_metadata(self) -> FuncMetadata: + """Get the metadata for the apply method.""" + # in the actual tool, this is a classmethod + return self._tool_class.get_apply_fn_metadata_from_cls() def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: """Apply the tool with logging and exception handling.""" try: - return self.apply(**kwargs) + return self.process_agent.tool_call(self._tool_name, **kwargs) except Exception as e: if catch_exceptions: - return f"Error executing tool {self.tool_name}: {e!s}" + return f"Error executing tool {self._tool_name}: {e!s}" raise diff --git a/test/serena/test_make_tool_process_isolation.py b/test/serena/test_make_tool_process_isolation.py new file mode 100644 index 0000000..f0fdda2 --- /dev/null +++ b/test/serena/test_make_tool_process_isolation.py @@ -0,0 +1,160 @@ +"""Tests for make_tool consistency between regular tools and ProcessIsolatedTool.""" + +import pytest +from mcp.server.fastmcp.tools.base import Tool as MCPTool + +from serena.agent import SerenaAgent, ToolRegistry +from serena.mcp import make_tool +from serena.process_isolated_agent import ProcessIsolatedSerenaAgent, ProcessIsolatedTool +from test.serena.test_serena_agent import SerenaConfigForTests + + +@pytest.fixture +def in_memory_config(): + """Create an in-memory configuration for tests.""" + return SerenaConfigForTests() + + +@pytest.fixture +def regular_agent(in_memory_config): + """Create a regular SerenaAgent for comparison.""" + return SerenaAgent(serena_config=in_memory_config) + + +@pytest.fixture +def process_isolated_agent(in_memory_config): + """Create a ProcessIsolatedSerenaAgent for comparison.""" + agent = ProcessIsolatedSerenaAgent(in_memory_config) + agent.start() + yield agent + agent.stop() + + +class TestMakeToolProcessIsolation: + """Test that make_tool produces identical metadata for regular and process-isolated tools.""" + + @pytest.mark.parametrize("tool_name", ToolRegistry.get_tool_names()) + def test_make_tool_metadata_consistency( + self, tool_name: str, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent + ): + """Test that make_tool produces identical metadata for regular and process-isolated tools.""" + # Get regular tool instance + tool_class = ToolRegistry.get_tool_class_by_name(tool_name) + regular_tool = regular_agent.get_tool(tool_class) + + # Get ProcessIsolatedTool instance + isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) + + # Create MCP tools from both + regular_mcp_tool = make_tool(regular_tool) + isolated_mcp_tool = make_tool(isolated_tool) + + # Verify both are MCPTool instances + assert isinstance(regular_mcp_tool, MCPTool) + assert isinstance(isolated_mcp_tool, MCPTool) + + # Test name consistency + assert regular_mcp_tool.name == isolated_mcp_tool.name + assert regular_mcp_tool.name == tool_name + + # Test description consistency + assert regular_mcp_tool.description == isolated_mcp_tool.description, ( + f"Tool {tool_name}: descriptions differ\n" + f"Regular: {regular_mcp_tool.description}\n" + f"Isolated: {isolated_mcp_tool.description}" + ) + + # Test parameters schema consistency + assert regular_mcp_tool.parameters == isolated_mcp_tool.parameters, ( + f"Tool {tool_name}: parameter schemas differ\n" + f"Regular: {regular_mcp_tool.parameters}\n" + f"Isolated: {isolated_mcp_tool.parameters}" + ) + + # Test function metadata consistency (compare schemas, not class objects) + regular_schema = regular_mcp_tool.fn_metadata.arg_model.model_json_schema() + isolated_schema = isolated_mcp_tool.fn_metadata.arg_model.model_json_schema() + assert ( + regular_schema == isolated_schema + ), f"Tool {tool_name}: function metadata schemas differ\nRegular: {regular_schema}\nIsolated: {isolated_schema}" + + # Test async flag consistency + assert regular_mcp_tool.is_async == isolated_mcp_tool.is_async + + # Test context kwarg consistency + assert regular_mcp_tool.context_kwarg == isolated_mcp_tool.context_kwarg + + @pytest.mark.parametrize("tool_name", ToolRegistry.get_tool_names()[:5]) # Test first 5 tools for faster execution + def test_tool_protocol_methods_consistency( + self, tool_name: str, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent + ): + """Test that Tool methods return identical results for regular and process-isolated tools.""" + # Get regular tool instance + tool_class = ToolRegistry.get_tool_class_by_name(tool_name) + regular_tool = regular_agent.get_tool(tool_class) + + # Get ProcessIsolatedTool instance + isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) + + # Test get_name() + assert regular_tool.get_name_from_cls() == isolated_tool.get_name() + assert regular_tool.get_name_from_cls() == tool_name + + # Test get_apply_docstring() + regular_docstring = regular_tool.get_apply_docstring() + isolated_docstring = isolated_tool.get_apply_docstring() + assert ( + regular_docstring == isolated_docstring + ), f"Tool {tool_name}: docstrings differ\nRegular: {regular_docstring}\nIsolated: {isolated_docstring}" + + # Test get_apply_fn_metadata() + regular_metadata = regular_tool.get_apply_fn_metadata() + isolated_metadata = isolated_tool.get_apply_fn_metadata() + + # Compare metadata properties (compare schemas, not class objects) + regular_schema = regular_metadata.arg_model.model_json_schema() + isolated_schema = isolated_metadata.arg_model.model_json_schema() + assert ( + regular_schema == isolated_schema + ), f"Tool {tool_name}: metadata schemas differ\nRegular: {regular_schema}\nIsolated: {isolated_schema}" + + def test_process_isolated_tool_uses_tool_registry(self, process_isolated_agent: ProcessIsolatedSerenaAgent): + """Test that ProcessIsolatedTool correctly uses ToolRegistry for metadata.""" + tool_name = ToolRegistry.get_tool_names()[0] # Use first available tool + isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) + + # Verify that the tool uses ToolRegistry + assert isolated_tool._tool_class == ToolRegistry.get_tool_class_by_name(tool_name) + + # Verify that metadata comes from the tool class + expected_docstring = isolated_tool._tool_class.get_apply_docstring_from_cls() + expected_metadata = isolated_tool._tool_class.get_apply_fn_metadata_from_cls() + + assert isolated_tool.get_apply_docstring() == expected_docstring + # Compare schemas, not class objects + isolated_schema = isolated_tool.get_apply_fn_metadata().arg_model.model_json_schema() + expected_schema = expected_metadata.arg_model.model_json_schema() + assert isolated_schema == expected_schema + + def test_tool_registry_completeness(self, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent): + """Test that all tools are available in both agents and the registry.""" + # Get tool names from both agents + regular_active_tools = set(regular_agent.get_active_tool_names()) + regular_all_tools = set(tool.get_name_from_cls() for tool in regular_agent.get_exposed_tool_instances()) + isolated_tool_names = set(process_isolated_agent.get_tool_instances()) + + # The process isolated agent should have all tools (exposed, not just active) + assert regular_all_tools == isolated_tool_names, ( + f"Tool sets differ:\n" + f"Regular exposed only: {regular_all_tools - isolated_tool_names}\n" + f"Isolated only: {isolated_tool_names - regular_all_tools}" + ) + + # Active tools should be a subset of all tools + assert regular_active_tools.issubset( + regular_all_tools + ), f"Some active tools not in exposed tools: {regular_active_tools - regular_all_tools}" + + # All tools should be in the registry + registry_tool_names = set(ToolRegistry.get_tool_names()) + assert regular_all_tools.issubset(registry_tool_names), f"Some tools not in registry: {regular_all_tools - registry_tool_names}" diff --git a/test/serena/test_mcp.py b/test/serena/test_mcp.py index f179b85..eed27a7 100644 --- a/test/serena/test_mcp.py +++ b/test/serena/test_mcp.py @@ -224,7 +224,7 @@ def test_make_tool_missing_apply() -> None: """, "", ), - ("", ""), + ("Description without params.", "Description without params."), ], ) def test_make_tool_descriptions(docstring, expected_description) -> None: @@ -287,7 +287,7 @@ def test_make_tool_all_tools(tool_class) -> None: # Basic validation assert isinstance(mcp_tool, MCPTool) - assert mcp_tool.name == tool_class.get_name() + assert mcp_tool.name == tool_class.get_name_from_cls() # The description should be a string (either from docstring or default) assert isinstance(mcp_tool.description, str) diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index f1208dd..49a47d8 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -6,7 +6,7 @@ from dataclasses import dataclass import pytest from multilspy.multilspy_config import Language -from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, SerenaAgent, SerenaConfigBase +from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase from test.conftest import LanguageParamRequest, get_repo_path @@ -19,17 +19,46 @@ class SerenaConfigForTests(SerenaConfigBase): gui_log_window_enabled: bool = False web_dashboard: bool = False + def __post_init__(self): + # Initialize with empty projects list if not already set + if not hasattr(self, "projects") or self.projects is None: + self.projects = [] + @pytest.fixture def serena_config(): - return SerenaConfigForTests() + """Create an in-memory configuration for tests with test repositories pre-registered.""" + # Create test projects for all supported languages + test_projects = [] + for language in [Language.PYTHON, Language.GO, Language.JAVA, Language.RUST, Language.TYPESCRIPT, Language.PHP]: + repo_path = get_repo_path(language) + if repo_path.exists(): + project_name = f"test_repo_{language}" + project = Project( + project_root=str(repo_path), + project_config=ProjectConfig( + project_name=project_name, + language=language, + ignored_paths=[], + excluded_tools=set(), + read_only=False, + ignore_all_files_in_gitignore=True, + initial_prompt="", + encoding="utf-8", + ), + ) + test_projects.append(project) + + config = SerenaConfigForTests() + config.projects = test_projects + return config @pytest.fixture def serena_agent(request: LanguageParamRequest, serena_config) -> SerenaAgent: language = Language(request.param) - repo = get_repo_path(language) - return SerenaAgent(project=str(repo), serena_config=serena_config) + project_name = f"test_repo_{language}" + return SerenaAgent(project=project_name, serena_config=serena_config) class TestSerenaAgent: From 8c76eb0adf0fe575684e9a32dccfd6cc937ea56e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 15:14:50 +0200 Subject: [PATCH 044/174] Update mcp dependency, remove not needed fastmcp --- pyproject.toml | 1 - uv.lock | 760 ++++++++++++++++++++++++------------------------- 2 files changed, 372 insertions(+), 389 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ec13ddd..4efceca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ dependencies = [ "python-dotenv>=1.0.0, <2", "mcp>=1.5.0", "fastapi>=0.115.12", - "fastmcp>=0.4.1", "sensai-utils>=1.4.0", "pydantic>=2.10.6", "types-pyyaml>=6.0.12.20241230", diff --git a/uv.lock b/uv.lock index 8be9884..540c126 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,10 @@ version = 1 -revision = 2 +revision = 1 requires-python = "==3.11.*" [[package]] name = "agno" -version = "1.2.15" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -20,23 +20,23 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/e6/5f747db61e35500750b9f595e8836649bd0d7a22ed0c2ec41167e5dec8bc/agno-1.2.15.tar.gz", hash = "sha256:b589d1277c756e5d8ed5c5ad2052d7910e1c2ad007fa3c830a4e8332809f2099", size = 467123, upload-time = "2025-04-09T00:13:42.792Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/8b/20a6d88f0740d7a17c45016048cb4d278eb5c78a2f75c443a566159420fd/agno-1.6.2.tar.gz", hash = "sha256:f78492021e84ef3ec730d502a89963d11f42cbefd872dfe1f353975489dd9cc9", size = 635136 } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/4b/05c42f236c18d11b29af368910df398a8a67e4f5c5681ead30ebde07a548/agno-1.2.15-py3-none-any.whl", hash = "sha256:e82b57f2980bff1ce7574a16ed963485f2efa3519e9322e17fa6ba07719bd5e2", size = 616626, upload-time = "2025-04-09T00:13:40.805Z" }, + { url = "https://files.pythonhosted.org/packages/72/b9/45456c06f034553327ddcd2e8db6fd224144d0fd2e62fd1e7b07acc18353/agno-1.6.2-py3-none-any.whl", hash = "sha256:7f831893f7b70a931438390650843096da92f42961b5ce63660b35a79a94d0a7", size = 851217 }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] name = "anthropic" -version = "0.49.0" +version = "0.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -47,9 +47,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/e3/a88c8494ce4d1a88252b9e053607e885f9b14d0a32273d47b727cbee4228/anthropic-0.49.0.tar.gz", hash = "sha256:c09e885b0f674b9119b4f296d8508907f6cff0009bc20d5cf6b35936c40b4398", size = 210016, upload-time = "2025-02-28T19:35:47.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/28/80cb9bb6e7ce77d404145b51da4257455805c17f0a6be528ff3286e3882f/anthropic-0.54.0.tar.gz", hash = "sha256:5e6f997d97ce8e70eac603c3ec2e7f23addeff953fbbb76b19430562bb6ba815", size = 312376 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/74/5d90ad14d55fbe3f9c474fdcb6e34b4bed99e3be8efac98734a5ddce88c1/anthropic-0.49.0-py3-none-any.whl", hash = "sha256:bbc17ad4e7094988d2fa86b87753ded8dce12498f4b85fe5810f208f454a8375", size = 243368, upload-time = "2025-02-28T19:35:44.963Z" }, + { url = "https://files.pythonhosted.org/packages/de/b9/6ffb48e82c5e97b03cecee872d134a6b6666c2767b2d32ed709f3a60a8fe/anthropic-0.54.0-py3-none-any.whl", hash = "sha256:c1062a0a905daeec17ca9c06c401e4b3f24cb0495841d29d752568a1d4018d56", size = 288774 }, ] [[package]] @@ -61,18 +61,18 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, ] [[package]] name = "asttokens" version = "3.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, ] [[package]] @@ -86,13 +86,13 @@ dependencies = [ { name = "pathspec" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, ] [package.optional-dependencies] @@ -105,88 +105,88 @@ jupyter = [ name = "cachetools" version = "5.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, ] [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.6.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753 } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650 }, ] [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload-time = "2024-12-24T18:10:12.838Z" }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload-time = "2024-12-24T18:10:14.101Z" }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload-time = "2024-12-24T18:10:15.512Z" }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335, upload-time = "2024-12-24T18:10:18.369Z" }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862, upload-time = "2024-12-24T18:10:19.743Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673, upload-time = "2024-12-24T18:10:21.139Z" }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211, upload-time = "2024-12-24T18:10:22.382Z" }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039, upload-time = "2024-12-24T18:10:24.802Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939, upload-time = "2024-12-24T18:10:26.124Z" }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075, upload-time = "2024-12-24T18:10:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340, upload-time = "2024-12-24T18:10:32.679Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205, upload-time = "2024-12-24T18:10:34.724Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441, upload-time = "2024-12-24T18:10:37.574Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, ] [[package]] name = "click" -version = "8.1.8" +version = "8.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, ] [[package]] name = "distro" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, ] [[package]] name = "docstring-parser" version = "0.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565, upload-time = "2024-03-15T10:39:44.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533, upload-time = "2024-03-15T10:39:41.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 }, ] [[package]] @@ -197,16 +197,16 @@ dependencies = [ { name = "python-dotenv" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892 }, ] [[package]] name = "executing" version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, ] [[package]] @@ -218,26 +218,9 @@ dependencies = [ { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, -] - -[[package]] -name = "fastmcp" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "mcp" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-dotenv" }, - { name = "typer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/84/17b549133263d7ee77141970769bbc401525526bf1af043ea6842bce1a55/fastmcp-0.4.1.tar.gz", hash = "sha256:713ad3b8e4e04841c9e2f3ca022b053adb89a286ceffad0d69ae7b56f31cbe64", size = 785575, upload-time = "2024-12-09T13:33:11.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0b/008a340435fe8f0879e9d608f48af2737ad48440e09bd33b83b3fd03798b/fastmcp-0.4.1-py3-none-any.whl", hash = "sha256:664b42c376fb89ec90a50c9433f5a1f4d24f36696d6c41b024b427ae545f9619", size = 35282, upload-time = "2024-12-09T13:33:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, ] [[package]] @@ -247,9 +230,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, ] [[package]] @@ -259,28 +242,28 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, ] [[package]] name = "google-auth" -version = "2.38.0" +version = "2.40.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866, upload-time = "2025-01-23T01:05:29.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770, upload-time = "2025-01-23T01:05:26.572Z" }, + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137 }, ] [[package]] name = "google-genai" -version = "1.10.0" +version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -291,48 +274,48 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/7a/224e2f70c835202042969685ee3da00a6475508d1b64f0f1e90144f96beb/google_genai-1.10.0.tar.gz", hash = "sha256:f59423e0f155dc66b7792c8a0e6724c75c72dc699d1eb7907d4d0006d4f6186f", size = 156355, upload-time = "2025-04-09T03:48:21.961Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/12/ad9f08be2ca85122ca50ac69ae70454f18a3c7d840bcc4ed43f517ab47be/google_genai-1.20.0.tar.gz", hash = "sha256:dccca78f765233844b1bd4f1f7a2237d9a76fe6038cf9aa72c0cd991e3c107b5", size = 201550 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/a0/56839a2e202d79c773edd1c1db124da8eb2a7b657267a888080b678d0369/google_genai-1.10.0-py3-none-any.whl", hash = "sha256:41b105a2fcf8a027fc45cc16694cd559b8cd1272eab7345ad58cfa2c353bf34f", size = 154705, upload-time = "2025-04-09T03:48:20.49Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/08f3ea414060a7e7d4436c08bb22d03dabef74cc05ef13ef8cd846156d5b/google_genai-1.20.0-py3-none-any.whl", hash = "sha256:ccd61d6ebcb14f5c778b817b8010e3955ae4f6ddfeaabf65f42f6d5e3e5a8125", size = 203039 }, ] [[package]] name = "greenlet" -version = "3.1.1" +version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" }, - { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" }, - { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload-time = "2024-09-20T17:44:24.101Z" }, - { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload-time = "2024-09-20T17:08:31.728Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload-time = "2024-09-20T17:09:23.903Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload-time = "2024-09-20T17:25:18.656Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219 }, + { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422 }, + { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375 }, + { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627 }, + { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502 }, + { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498 }, + { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977 }, + { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017 }, ] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload-time = "2022-09-25T15:40:01.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, ] [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196, upload-time = "2024-11-15T12:30:47.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551, upload-time = "2024-11-15T12:30:45.782Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, ] [[package]] @@ -345,41 +328,41 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] [[package]] name = "httpx-sse" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] [[package]] name = "ipython" -version = "9.1.0" +version = "9.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -394,9 +377,9 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/9a/6b8984bedc990f3a4aa40ba8436dea27e23d26a64527de7c2e5e12e76841/ipython-9.1.0.tar.gz", hash = "sha256:a47e13a5e05e02f3b8e1e7a0f9db372199fe8c3763532fe7a1e0379e4e135f16", size = 4373688, upload-time = "2025-04-07T10:18:28.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/09/4c7e06b96fbd203e06567b60fb41b06db606b6a82db6db7b2c85bb72a15c/ipython-9.3.0.tar.gz", hash = "sha256:79eb896f9f23f50ad16c3bc205f686f6e030ad246cc309c6279a242b14afe9d8", size = 4426460 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl", hash = "sha256:2df07257ec2f84a6b346b8d83100bcf8fa501c6e01ab75cd3799b0bb253b3d2a", size = 604053, upload-time = "2025-04-07T10:18:24.869Z" }, + { url = "https://files.pythonhosted.org/packages/3c/99/9ed3d52d00f1846679e3aa12e2326ac7044b5e7f90dc822b60115fa533ca/ipython-9.3.0-py3-none-any.whl", hash = "sha256:1a0b6dd9221a1f5dddf725b57ac0cb6fddc7b5f470576231ae9162b9b3455a04", size = 605320 }, ] [[package]] @@ -406,9 +389,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, ] [[package]] @@ -418,9 +401,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, ] [[package]] @@ -430,38 +413,38 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, ] [[package]] name = "jiter" -version = "0.9.0" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/e4562507f52f0af7036da125bb699602ead37a2332af0788f8e0a3417f36/jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893", size = 162604, upload-time = "2025-03-10T21:37:03.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/44/e241a043f114299254e44d7e777ead311da400517f179665e59611ab0ee4/jiter-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6c4d99c71508912a7e556d631768dcdef43648a93660670986916b297f1c54af", size = 314654, upload-time = "2025-03-10T21:35:23.939Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/a7e5e42db9fa262baaa9489d8d14ca93f8663e7f164ed5e9acc9f467fc00/jiter-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f60fb8ce7df529812bf6c625635a19d27f30806885139e367af93f6e734ef58", size = 320909, upload-time = "2025-03-10T21:35:26.127Z" }, - { url = "https://files.pythonhosted.org/packages/60/bf/8ebdfce77bc04b81abf2ea316e9c03b4a866a7d739cf355eae4d6fd9f6fe/jiter-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51c4e1a4f8ea84d98b7b98912aa4290ac3d1eabfde8e3c34541fae30e9d1f08b", size = 341733, upload-time = "2025-03-10T21:35:27.94Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4e/754ebce77cff9ab34d1d0fa0fe98f5d42590fd33622509a3ba6ec37ff466/jiter-0.9.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f4c677c424dc76684fea3e7285a7a2a7493424bea89ac441045e6a1fb1d7b3b", size = 365097, upload-time = "2025-03-10T21:35:29.605Z" }, - { url = "https://files.pythonhosted.org/packages/32/2c/6019587e6f5844c612ae18ca892f4cd7b3d8bbf49461ed29e384a0f13d98/jiter-0.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2221176dfec87f3470b21e6abca056e6b04ce9bff72315cb0b243ca9e835a4b5", size = 406603, upload-time = "2025-03-10T21:35:31.696Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/c9e6546c817ab75a1a7dab6dcc698e62e375e1017113e8e983fccbd56115/jiter-0.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c7adb66f899ffa25e3c92bfcb593391ee1947dbdd6a9a970e0d7e713237d572", size = 396625, upload-time = "2025-03-10T21:35:33.182Z" }, - { url = "https://files.pythonhosted.org/packages/be/bd/976b458add04271ebb5a255e992bd008546ea04bb4dcadc042a16279b4b4/jiter-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98d27330fdfb77913c1097a7aab07f38ff2259048949f499c9901700789ac15", size = 351832, upload-time = "2025-03-10T21:35:35.394Z" }, - { url = "https://files.pythonhosted.org/packages/07/51/fe59e307aaebec9265dbad44d9d4381d030947e47b0f23531579b9a7c2df/jiter-0.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eda3f8cc74df66892b1d06b5d41a71670c22d95a1ca2cbab73654745ce9d0419", size = 384590, upload-time = "2025-03-10T21:35:37.171Z" }, - { url = "https://files.pythonhosted.org/packages/db/55/5dcd2693794d8e6f4889389ff66ef3be557a77f8aeeca8973a97a7c00557/jiter-0.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dd5ab5ddc11418dce28343123644a100f487eaccf1de27a459ab36d6cca31043", size = 520690, upload-time = "2025-03-10T21:35:38.717Z" }, - { url = "https://files.pythonhosted.org/packages/54/d5/9f51dc90985e9eb251fbbb747ab2b13b26601f16c595a7b8baba964043bd/jiter-0.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42f8a68a69f047b310319ef8e2f52fdb2e7976fb3313ef27df495cf77bcad965", size = 512649, upload-time = "2025-03-10T21:35:40.157Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e5/4e385945179bcf128fa10ad8dca9053d717cbe09e258110e39045c881fe5/jiter-0.9.0-cp311-cp311-win32.whl", hash = "sha256:a25519efb78a42254d59326ee417d6f5161b06f5da827d94cf521fed961b1ff2", size = 206920, upload-time = "2025-03-10T21:35:41.72Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/5e0b94c603d8e54dd1faab439b40b832c277d3b90743e7835879ab663757/jiter-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:923b54afdd697dfd00d368b7ccad008cccfeb1efb4e621f32860c75e9f25edbd", size = 210119, upload-time = "2025-03-10T21:35:43.46Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473 }, + { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971 }, + { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574 }, + { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028 }, + { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821 }, + { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174 }, + { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869 }, + { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741 }, + { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527 }, + { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765 }, + { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234 }, ] [[package]] name = "joblib" version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746 }, ] [[package]] @@ -471,27 +454,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, ] [[package]] @@ -501,14 +484,14 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, ] [[package]] name = "mcp" -version = "1.6.0" +version = "1.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -516,104 +499,106 @@ dependencies = [ { name = "httpx-sse" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "python-multipart" }, { name = "sse-starlette" }, { name = "starlette" }, - { name = "uvicorn" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031, upload-time = "2025-03-27T16:46:32.336Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f2/dc2450e566eeccf92d89a00c3e813234ad58e2ba1e31d11467a09ac4f3b9/mcp-1.9.4.tar.gz", hash = "sha256:cfb0bcd1a9535b42edaef89947b9e18a8feb49362e1cc059d6e7fc636f2cb09f", size = 333294 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077, upload-time = "2025-03-27T16:46:29.919Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232 }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] [[package]] name = "mypy" -version = "1.15.0" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, + { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139 } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload-time = "2025-02-05T03:50:17.287Z" }, - { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload-time = "2025-02-05T03:49:51.21Z" }, - { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload-time = "2025-02-05T03:50:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload-time = "2025-02-05T03:49:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload-time = "2025-02-05T03:49:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload-time = "2025-02-05T03:49:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, + { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498 }, + { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755 }, + { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138 }, + { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156 }, + { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426 }, + { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319 }, + { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773 }, ] [[package]] name = "mypy-extensions" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] [[package]] name = "overrides" version = "7.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, ] [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] [[package]] name = "parso" version = "0.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, ] [[package]] name = "pastel" version = "0.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955 }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, ] [[package]] @@ -623,94 +608,94 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, ] [[package]] name = "platformdirs" -version = "4.3.7" +version = "4.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, ] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] [[package]] name = "poethepoet" -version = "0.33.1" +version = "0.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/1d/ec87271390cc5fafdd5996137331ad3a7ce99b715e4ee68db554d202817f/poethepoet-0.33.1.tar.gz", hash = "sha256:8775e09b64f773278b5483659ff238a708723491efadeedd1c2cbf773558cb4c", size = 62536, upload-time = "2025-03-15T20:38:56.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/b1/d4f4361b278fae10f6074675385ce3acf53c647f8e6eeba22c652f8ba985/poethepoet-0.35.0.tar.gz", hash = "sha256:b396ae862d7626e680bbd0985b423acf71634ce93a32d8b5f38340f44f5fbc3e", size = 66006 } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/ea/c476bfec360eb6831ce46df2719f76d1132b9a87da11c302081a9def5fce/poethepoet-0.33.1-py3-none-any.whl", hash = "sha256:b86d80a81b2ca4e4ce8e8f716cc6004a1a1cdead027778bc07d1c26cb3664770", size = 83512, upload-time = "2025-03-15T20:38:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/38/08/abc2d7e2400dd8906e3208f9b88ac610f097d7ee0c7a1fa4a157b49a9e86/poethepoet-0.35.0-py3-none-any.whl", hash = "sha256:bed5ae1fd63f179dfa67aabb93fa253d79695c69667c927d8b24ff378799ea75", size = 87164 }, ] [[package]] name = "prompt-toolkit" -version = "3.0.50" +version = "3.0.51" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087, upload-time = "2025-01-20T15:55:35.072Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816, upload-time = "2025-01-20T15:55:29.98Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, ] [[package]] name = "psutil" version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, ] [[package]] name = "pure-eval" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, ] [[package]] @@ -720,14 +705,14 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, ] [[package]] name = "pydantic" -version = "2.11.3" +version = "2.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -735,133 +720,135 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513, upload-time = "2025-04-08T13:27:06.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591, upload-time = "2025-04-08T13:27:03.789Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 }, ] [[package]] name = "pydantic-core" -version = "2.33.1" +version = "2.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395, upload-time = "2025-04-02T09:49:41.8Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224, upload-time = "2025-04-02T09:47:04.199Z" }, - { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845, upload-time = "2025-04-02T09:47:05.686Z" }, - { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029, upload-time = "2025-04-02T09:47:07.042Z" }, - { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784, upload-time = "2025-04-02T09:47:08.63Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075, upload-time = "2025-04-02T09:47:10.267Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849, upload-time = "2025-04-02T09:47:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794, upload-time = "2025-04-02T09:47:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237, upload-time = "2025-04-02T09:47:14.355Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351, upload-time = "2025-04-02T09:47:15.676Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914, upload-time = "2025-04-02T09:47:17Z" }, - { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385, upload-time = "2025-04-02T09:47:18.631Z" }, - { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765, upload-time = "2025-04-02T09:47:20.34Z" }, - { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688, upload-time = "2025-04-02T09:47:22.029Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185, upload-time = "2025-04-02T09:47:23.385Z" }, - { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858, upload-time = "2025-04-02T09:49:03.419Z" }, - { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745, upload-time = "2025-04-02T09:49:05.391Z" }, - { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188, upload-time = "2025-04-02T09:49:07.352Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479, upload-time = "2025-04-02T09:49:09.304Z" }, - { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415, upload-time = "2025-04-02T09:49:11.25Z" }, - { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623, upload-time = "2025-04-02T09:49:13.292Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175, upload-time = "2025-04-02T09:49:15.597Z" }, - { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674, upload-time = "2025-04-02T09:49:17.61Z" }, - { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951, upload-time = "2025-04-02T09:49:19.559Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, ] [[package]] name = "pydantic-settings" -version = "2.8.1" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550, upload-time = "2025-02-27T10:10:32.338Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839, upload-time = "2025-02-27T10:10:30.711Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] [[package]] name = "pyright" -version = "1.1.399" +version = "1.1.402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954, upload-time = "2025-04-10T04:40:25.703Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/04/ce0c132d00e20f2d2fb3b3e7c125264ca8b909e693841210534b1ea1752f/pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683", size = 3888207 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584, upload-time = "2025-04-10T04:40:23.502Z" }, + { url = "https://files.pythonhosted.org/packages/fe/37/1a1c62d955e82adae588be8e374c7f77b165b6cb4203f7d581269959abbc/pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982", size = 5624004 }, ] [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797 }, ] [[package]] name = "python-dotenv" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] [[package]] name = "python-multipart" version = "0.0.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -869,9 +856,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, ] [[package]] @@ -882,75 +869,75 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, ] [[package]] name = "rsa" -version = "4.9" +version = "4.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711, upload-time = "2022-07-20T10:28:36.115Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315, upload-time = "2022-07-20T10:28:34.978Z" }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, ] [[package]] name = "ruamel-yaml" -version = "0.18.10" +version = "0.18.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447, upload-time = "2025-01-06T14:08:51.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/87/6da0df742a4684263261c253f00edd5829e6aca970fff69e75028cccc547/ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7", size = 145511 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729, upload-time = "2025-01-06T14:08:47.471Z" }, + { url = "https://files.pythonhosted.org/packages/af/6d/6fe4805235e193aad4aaf979160dd1f3c487c57d48b810c816e6e842171b/ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2", size = 118570 }, ] [[package]] name = "ruamel-yaml-clib" version = "0.2.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224, upload-time = "2024-10-20T10:12:45.162Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480, upload-time = "2024-10-20T10:12:46.758Z" }, - { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068, upload-time = "2024-10-20T10:12:48.605Z" }, - { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012, upload-time = "2024-10-20T10:12:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352, upload-time = "2024-10-21T11:26:41.438Z" }, - { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344, upload-time = "2024-10-21T11:26:43.62Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498, upload-time = "2024-12-11T19:58:15.592Z" }, - { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205, upload-time = "2024-10-20T10:12:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185, upload-time = "2024-10-20T10:12:54.652Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224 }, + { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480 }, + { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068 }, + { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012 }, + { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352 }, + { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344 }, + { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498 }, + { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205 }, + { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185 }, ] [[package]] name = "ruff" -version = "0.11.5" +version = "0.11.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488, upload-time = "2025-04-10T17:13:29.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150, upload-time = "2025-04-10T17:12:37.886Z" }, - { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637, upload-time = "2025-04-10T17:12:41.602Z" }, - { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012, upload-time = "2025-04-10T17:12:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338, upload-time = "2025-04-10T17:12:47.172Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277, upload-time = "2025-04-10T17:12:50.628Z" }, - { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614, upload-time = "2025-04-10T17:12:53.783Z" }, - { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873, upload-time = "2025-04-10T17:12:56.956Z" }, - { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190, upload-time = "2025-04-10T17:13:00.194Z" }, - { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301, upload-time = "2025-04-10T17:13:03.246Z" }, - { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132, upload-time = "2025-04-10T17:13:06.209Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937, upload-time = "2025-04-10T17:13:08.855Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683, upload-time = "2025-04-10T17:13:11.378Z" }, - { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217, upload-time = "2025-04-10T17:13:14.565Z" }, - { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521, upload-time = "2025-04-10T17:13:17.8Z" }, - { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697, upload-time = "2025-04-10T17:13:20.582Z" }, - { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665, upload-time = "2025-04-10T17:13:23.349Z" }, - { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287, upload-time = "2025-04-10T17:13:26.538Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516 }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083 }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024 }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324 }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416 }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197 }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615 }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080 }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315 }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640 }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364 }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462 }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028 }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992 }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944 }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669 }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928 }, ] [[package]] @@ -960,9 +947,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/b1/9dba08e2d8ea739b58d1ac393c066169f1ca6506c86f0322d6e56e600ae2/sensai_utils-1.4.0.tar.gz", hash = "sha256:2d32bdcc91fd1428c5cae0181e98623142d2d5f7e115e23d585a842dd9dc59ba", size = 56317, upload-time = "2025-01-27T22:47:51.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/b1/9dba08e2d8ea739b58d1ac393c066169f1ca6506c86f0322d6e56e600ae2/sensai_utils-1.4.0.tar.gz", hash = "sha256:2d32bdcc91fd1428c5cae0181e98623142d2d5f7e115e23d585a842dd9dc59ba", size = 56317 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/df/49cc942b653427df0bbd3b9a2fb1e85652df3d4c1d9f6dcbfce1b6155716/sensai_utils-1.4.0-py3-none-any.whl", hash = "sha256:ed6fc57552620e43b33cf364ea0bc0fd7df39391069dd7b621b113ef55547507", size = 63214, upload-time = "2025-01-27T22:47:49.606Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/49cc942b653427df0bbd3b9a2fb1e85652df3d4c1d9f6dcbfce1b6155716/sensai_utils-1.4.0-py3-none-any.whl", hash = "sha256:ed6fc57552620e43b33cf364ea0bc0fd7df39391069dd7b621b113ef55547507", size = 63214 }, ] [[package]] @@ -973,7 +960,6 @@ dependencies = [ { name = "docstring-parser" }, { name = "dotenv" }, { name = "fastapi" }, - { name = "fastmcp" }, { name = "jinja2" }, { name = "joblib" }, { name = "mcp" }, @@ -1021,7 +1007,6 @@ requires-dist = [ { name = "docstring-parser", specifier = ">=0.16" }, { name = "dotenv", specifier = ">=0.9.9" }, { name = "fastapi", specifier = ">=0.115.12" }, - { name = "fastmcp", specifier = ">=0.4.1" }, { name = "google-genai", marker = "extra == 'google'", specifier = ">=1.8.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", marker = "extra == 'dev'" }, @@ -1053,61 +1038,60 @@ provides-extras = ["dev", "agno", "anthropic", "google"] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] name = "sqlalchemy" -version = "2.0.40" +version = "2.0.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299, upload-time = "2025-03-27T17:52:31.876Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/7e/55044a9ec48c3249bb38d5faae93f09579c35e862bb318ebd1ed7a1994a5/sqlalchemy-2.0.40-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6bacab7514de6146a1976bc56e1545bee247242fab030b89e5f70336fc0003e", size = 2114025, upload-time = "2025-03-27T18:49:29.456Z" }, - { url = "https://files.pythonhosted.org/packages/77/0f/dcf7bba95f847aec72f638750747b12d37914f71c8cc7c133cf326ab945c/sqlalchemy-2.0.40-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5654d1ac34e922b6c5711631f2da497d3a7bffd6f9f87ac23b35feea56098011", size = 2104419, upload-time = "2025-03-27T18:49:30.75Z" }, - { url = "https://files.pythonhosted.org/packages/75/70/c86a5c20715e4fe903dde4c2fd44fc7e7a0d5fb52c1b954d98526f65a3ea/sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35904d63412db21088739510216e9349e335f142ce4a04b69e2528020ee19ed4", size = 3222720, upload-time = "2025-03-27T18:44:29.871Z" }, - { url = "https://files.pythonhosted.org/packages/12/cf/b891a8c1d0c27ce9163361664c2128c7a57de3f35000ea5202eb3a2917b7/sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7a80ed86d6aaacb8160a1caef6680d4ddd03c944d985aecee940d168c411d1", size = 3222682, upload-time = "2025-03-27T18:55:20.097Z" }, - { url = "https://files.pythonhosted.org/packages/15/3f/7709d8c8266953d945435a96b7f425ae4172a336963756b58e996fbef7f3/sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:519624685a51525ddaa7d8ba8265a1540442a2ec71476f0e75241eb8263d6f51", size = 3159542, upload-time = "2025-03-27T18:44:31.333Z" }, - { url = "https://files.pythonhosted.org/packages/85/7e/717eaabaf0f80a0132dc2032ea8f745b7a0914451c984821a7c8737fb75a/sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5f9999a5b0e9689bed96e60ee53c3384f1a05c2dd8068cc2e8361b0df5b7a", size = 3179864, upload-time = "2025-03-27T18:55:21.784Z" }, - { url = "https://files.pythonhosted.org/packages/e4/cc/03eb5dfcdb575cbecd2bd82487b9848f250a4b6ecfb4707e834b4ce4ec07/sqlalchemy-2.0.40-cp311-cp311-win32.whl", hash = "sha256:c0cae71e20e3c02c52f6b9e9722bca70e4a90a466d59477822739dc31ac18b4b", size = 2084675, upload-time = "2025-03-27T18:48:55.915Z" }, - { url = "https://files.pythonhosted.org/packages/9a/48/440946bf9dc4dc231f4f31ef0d316f7135bf41d4b86aaba0c0655150d370/sqlalchemy-2.0.40-cp311-cp311-win_amd64.whl", hash = "sha256:574aea2c54d8f1dd1699449f332c7d9b71c339e04ae50163a3eb5ce4c4325ee4", size = 2110099, upload-time = "2025-03-27T18:48:57.45Z" }, - { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894, upload-time = "2025-03-27T18:40:43.796Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232 }, + { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897 }, + { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313 }, + { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807 }, + { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632 }, + { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642 }, + { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475 }, + { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903 }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224 }, ] [[package]] name = "sse-starlette" -version = "2.2.1" +version = "2.3.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376, upload-time = "2024-12-25T09:09:30.616Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120, upload-time = "2024-12-25T09:09:26.761Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606 }, ] [[package]] @@ -1119,21 +1103,21 @@ dependencies = [ { name = "executing" }, { name = "pure-eval" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, ] [[package]] name = "starlette" -version = "0.46.1" +version = "0.46.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102, upload-time = "2025-03-08T10:55:34.504Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995, upload-time = "2025-03-08T10:55:32.662Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, ] [[package]] @@ -1143,18 +1127,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/f8/022d8704a3314f3e96dbd6bbd16ebe119ce30e35f41aabfa92345652fceb/syrupy-4.9.1.tar.gz", hash = "sha256:b7d0fcadad80a7d2f6c4c71917918e8ebe2483e8c703dfc8d49cdbb01081f9a4", size = 52492, upload-time = "2025-03-24T01:36:37.225Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/f8/022d8704a3314f3e96dbd6bbd16ebe119ce30e35f41aabfa92345652fceb/syrupy-4.9.1.tar.gz", hash = "sha256:b7d0fcadad80a7d2f6c4c71917918e8ebe2483e8c703dfc8d49cdbb01081f9a4", size = 52492 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214, upload-time = "2025-03-24T01:36:35.278Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214 }, ] [[package]] name = "tokenize-rt" -version = "6.1.0" +version = "6.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/0a/5854d8ced8c1e00193d1353d13db82d7f813f99bd5dcb776ce3e2a4c0d19/tokenize_rt-6.1.0.tar.gz", hash = "sha256:e8ee836616c0877ab7c7b54776d2fefcc3bde714449a206762425ae114b53c86", size = 5506, upload-time = "2024-10-22T00:14:59.189Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/ed/8f07e893132d5051d86a553e749d5c89b2a4776eb3a579b72ed61f8559ca/tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6", size = 5476 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/ba/576aac29b10dfa49a6ce650001d1bb31f81e734660555eaf144bfe5b8995/tokenize_rt-6.1.0-py2.py3-none-any.whl", hash = "sha256:d706141cdec4aa5f358945abe36b911b8cbdc844545da99e811250c0cee9b6fc", size = 6015, upload-time = "2024-10-22T00:14:57.469Z" }, + { url = "https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44", size = 6004 }, ] [[package]] @@ -1164,51 +1148,51 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/55/b128ee446606e9f2e49582bb1e462a255317401fd7adb0b2114e50006d01/toml_sort-0.24.2.tar.gz", hash = "sha256:20cb7c5e9de9c871990f1594f028aaf8bd0f78d7ce37995a22289dc157a45b79", size = 17973, upload-time = "2024-11-19T14:03:05.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/55/b128ee446606e9f2e49582bb1e462a255317401fd7adb0b2114e50006d01/toml_sort-0.24.2.tar.gz", hash = "sha256:20cb7c5e9de9c871990f1594f028aaf8bd0f78d7ce37995a22289dc157a45b79", size = 17973 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/55/7a7c02de848eef670bd018b62122ce32a6e227203e5a626c96d4935958a6/toml_sort-0.24.2-py3-none-any.whl", hash = "sha256:d81d299789a1fd9dd306a4021951eab5fc0c5486599e277fcf8142c7735f3308", size = 20162, upload-time = "2024-11-19T14:03:03.338Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/7a7c02de848eef670bd018b62122ce32a6e227203e5a626c96d4935958a6/toml_sort-0.24.2-py3-none-any.whl", hash = "sha256:d81d299789a1fd9dd306a4021951eab5fc0c5486599e277fcf8142c7735f3308", size = 20162 }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] [[package]] name = "tomlkit" -version = "0.13.2" +version = "0.13.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885, upload-time = "2024-08-14T08:19:41.488Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload-time = "2024-08-14T08:19:40.05Z" }, + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901 }, ] [[package]] name = "traitlets" version = "5.14.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, ] [[package]] name = "typer" -version = "0.15.2" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1216,88 +1200,88 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711, upload-time = "2025-02-27T19:17:34.807Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061, upload-time = "2025-02-27T19:17:32.111Z" }, + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317 }, ] [[package]] name = "types-pyyaml" -version = "6.0.12.20250402" +version = "6.0.12.20250516" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282, upload-time = "2025-04-02T02:56:00.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329, upload-time = "2025-04-02T02:55:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312 }, ] [[package]] name = "typing-extensions" -version = "4.13.2" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, ] [[package]] name = "typing-inspection" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, ] [[package]] name = "urllib3" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.34.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568, upload-time = "2024-12-15T13:33:30.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431 }, ] [[package]] name = "wcwidth" version = "0.2.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, ] From 9d51c0cfff1be48f2d28152ea6ddb859768c4ae4 Mon Sep 17 00:00:00 2001 From: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> Date: Sun, 15 Jun 2025 15:21:08 +0200 Subject: [PATCH 045/174] Update DOCKER.md - remove reference to fork [ci skip] --- DOCKER.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 45e0f5c..5ee8829 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -156,6 +156,4 @@ We're working on: - Automatic config migration between environments - Better project path handling - Dynamic port allocation -- Windows line-ending handling - -For updates and issues, please check the [GitHub repository](https://github.com/diazoxide/serena). +- Windows line-ending handling. From a0c820cc33d9b0048bacfdc4cf602d0a86f730ec Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 15:51:41 +0200 Subject: [PATCH 046/174] Removed unused method --- src/serena/agent.py | 6 ++--- src/serena/mcp.py | 56 +++------------------------------------------ 2 files changed, 6 insertions(+), 56 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 98b5fc2..a169e2f 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -107,12 +107,12 @@ def get_serena_managed_dir(project_root: str | Path) -> str: 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'): + if os.path.exists("/.dockerenv"): return True # Check cgroup for docker references try: - with open('/proc/self/cgroup', 'r') as f: - return 'docker' in f.read() + with open("/proc/self/cgroup") as f: + return "docker" in f.read() except FileNotFoundError: return False diff --git a/src/serena/mcp.py b/src/serena/mcp.py index db188a2..e307301 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -105,53 +105,6 @@ def create_mcp_server_and_agent( log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, -) -> tuple[FastMCP, "ProcessIsolatedSerenaAgent"]: - """ - Create an MCP server (legacy - now uses process isolation). - - :param project: "Either an absolute path to the project directory or a name of an already registered project. " - "If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name " - "afterwards. - :param host: The host to bind to - :param port: The port to bind to - :param context: The context name or path to context file - :param modes: List of mode names or paths to mode files - :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. - :param log_level: Log level. If not specified, will take the value from the serena configuration. - :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. - This is useful for debugging language server issues. - :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. - """ - # Delegate to process-isolated version for safety - mcp, process_agent = create_mcp_server_and_process_isolated_agent( - project=project, - host=host, - port=port, - context=context, - modes=modes, - enable_web_dashboard=enable_web_dashboard, - enable_gui_log_window=enable_gui_log_window, - log_level=log_level, - trace_lsp_communication=trace_lsp_communication, - tool_timeout=tool_timeout, - ) - # Return a dummy agent for compatibility - return mcp, process_agent - - -def create_mcp_server_and_process_isolated_agent( - project: str | None, - host: str = "0.0.0.0", - port: int = 8000, - context: str = DEFAULT_CONTEXT, - modes: Sequence[str] = DEFAULT_MODES, - enable_web_dashboard: bool | None = None, - enable_gui_log_window: bool | None = None, - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, - trace_lsp_communication: bool | None = None, - tool_timeout: float | None = None, ) -> tuple[FastMCP, "ProcessIsolatedSerenaAgent"]: """ Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. @@ -218,11 +171,8 @@ def create_mcp_server_and_process_isolated_agent( mcp._tool_manager._tools = {} for tool_name in tool_names: - # Create ProcessIsolatedTool instance that can be used with make_tool - # Metadata is extracted from ToolRegistry using classmethods - tool_instance = ProcessIsolatedTool(process_agent=process_agent, tool_name=tool_name) - # Use the original make_tool function to get full docstring parsing - mcp_tool = make_tool(tool_instance) + process_isolated_tool = ProcessIsolatedTool(process_agent=process_agent, tool_name=tool_name) + mcp_tool = make_tool(process_isolated_tool) mcp._tool_manager._tools[tool_name] = mcp_tool @asynccontextmanager @@ -383,7 +333,7 @@ def start_mcp_server( project_file = project_file_arg if project_file_arg is not None else project_file_opt # Use process isolation by default to prevent asyncio event loop contamination - mcp_server, agent = create_mcp_server_and_process_isolated_agent( + mcp_server, agent = create_mcp_server_and_agent( project=project_file, host=host, port=port, From ad96a7b25567f28a8e1bc689d4d56f54bf74c81d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 16:14:40 +0200 Subject: [PATCH 047/174] Minor renaming --- src/serena/mcp.py | 6 +++++- src/serena/process_isolated_agent.py | 15 ++++++++++----- test/serena/test_make_tool_process_isolation.py | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index e307301..a7f9ea2 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -160,8 +160,12 @@ def create_mcp_server_and_agent( return # Get tool names from process-isolated agent + # Tools may change as a result of project activation. + # NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not, + # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, + # so we need to register all tools at startup, unfortunately. try: - tool_names = process_agent.get_tool_instances() + tool_names = process_agent.get_exposed_tool_names() except Exception as e: log.error(f"Failed to get tool names from process agent: {e}") return diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 543b5ec..741e848 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -80,8 +80,8 @@ class SerenaAgentWorker: return self._is_language_server_running(request_id) elif method == "reset_language_server": return self._reset_language_server(request_id) - elif method == "get_tool_instances": - return self._get_tool_instances(request_id) + elif method == "get_exposed_tool_names": + return self._get_exposed_tool_names(request_id) else: return {"id": request_id, "error": f"Unknown method: {method}"} @@ -160,7 +160,7 @@ class SerenaAgentWorker: except Exception as e: return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} - def _get_tool_instances(self, request_id: str) -> dict[str, Any]: + def _get_exposed_tool_names(self, request_id: str) -> dict[str, Any]: """Get exposed tool names for MCP tool creation.""" if self.agent is None: return {"id": request_id, "error": "SerenaAgent not initialized"} @@ -314,9 +314,14 @@ class ProcessIsolatedSerenaAgent: """Reset the language server.""" self._make_request_with_result("reset_language_server") - def get_tool_instances(self) -> list[str]: + def get_exposed_tool_names(self) -> list[str]: """Get tool names for MCP tool creation.""" - return self._make_request_with_result("get_tool_instances") + return self._make_request_with_result("get_exposed_tool_names") + + def get_exposed_tool_instances(self) -> list[ToolInterface]: + """Get exposed tool instances for MCP tool creation.""" + tool_names = self.get_exposed_tool_names() + return [ProcessIsolatedTool(self, tool_name) for tool_name in tool_names] def __enter__(self) -> Self: self.start() diff --git a/test/serena/test_make_tool_process_isolation.py b/test/serena/test_make_tool_process_isolation.py index f0fdda2..3404aa1 100644 --- a/test/serena/test_make_tool_process_isolation.py +++ b/test/serena/test_make_tool_process_isolation.py @@ -141,7 +141,7 @@ class TestMakeToolProcessIsolation: # Get tool names from both agents regular_active_tools = set(regular_agent.get_active_tool_names()) regular_all_tools = set(tool.get_name_from_cls() for tool in regular_agent.get_exposed_tool_instances()) - isolated_tool_names = set(process_isolated_agent.get_tool_instances()) + isolated_tool_names = set(process_isolated_agent.get_exposed_tool_names()) # The process isolated agent should have all tools (exposed, not just active) assert regular_all_tools == isolated_tool_names, ( From b284eed69c8fe24f07240ffd449508bfb6253ac4 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 16:36:31 +0200 Subject: [PATCH 048/174] Replaced string interfaces by enum --- src/serena/process_isolated_agent.py | 63 ++++++++++++++++------------ 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 741e848..12a8071 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -13,6 +13,7 @@ import queue import threading import time import traceback +from enum import StrEnum from typing import Any, Self from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata @@ -70,20 +71,21 @@ class SerenaAgentWorker: params = request.get("params", {}) request_id = request["id"] - if method == "initialize": - return self._initialize(request_id, params) - elif method == "tool_call": - return self._tool_call(request_id, params) - elif method == "get_active_tool_names": - return self._get_active_tool_names(request_id) - elif method == "is_language_server_running": - return self._is_language_server_running(request_id) - elif method == "reset_language_server": - return self._reset_language_server(request_id) - elif method == "get_exposed_tool_names": - return self._get_exposed_tool_names(request_id) - else: - return {"id": request_id, "error": f"Unknown method: {method}"} + match method: + case self.RequestMethod.INITIALIZE: + return self._initialize(request_id, params) + case self.RequestMethod.TOOL_CALL: + return self._tool_call(request_id, params) + case self.RequestMethod.GET_ACTIVE_TOOL_NAMES: + return self._get_active_tool_names(request_id) + case self.RequestMethod.IS_LANGUAGE_SERVER_RUNNING: + return self._is_language_server_running(request_id) + case self.RequestMethod.RESET_LANGUAGE_SERVER: + return self._reset_language_server(request_id) + case self.RequestMethod.GET_EXPOSED_TOOL_NAMES: + return self._get_exposed_tool_names(request_id) + case _: + return {"id": request_id, "error": f"Unknown method: {method}"} except Exception as e: return {"id": request.get("id"), "error": str(e), "traceback": traceback.format_exc()} @@ -183,6 +185,16 @@ class SerenaAgentWorker: log.error(f"Error stopping language server: {e}") self.agent = None + class RequestMethod(StrEnum): + """Enumeration of available request methods for type-safe IPC.""" + + INITIALIZE = "initialize" + TOOL_CALL = "tool_call" + GET_ACTIVE_TOOL_NAMES = "get_active_tool_names" + IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running" + RESET_LANGUAGE_SERVER = "reset_language_server" + GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names" + class ProcessIsolatedSerenaAgent: """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" @@ -213,7 +225,7 @@ class ProcessIsolatedSerenaAgent: # Initialize the agent in the worker process try: - self._make_request_with_result("initialize", {"config": self.serena_config.to_dict()}) + self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, {"config": self.serena_config.to_dict()}) except Exception as e: self.stop() raise RuntimeError(f"Failed to initialize SerenaAgent: {e}") from e @@ -256,7 +268,7 @@ class ProcessIsolatedSerenaAgent: log.info("Process-isolated SerenaAgent stopped") - def _make_request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]: + def _make_request(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> dict[str, Any]: """Make a request to the worker process.""" if self.process is None or not self.process.is_alive(): raise RuntimeError("Worker process is not running") @@ -291,7 +303,7 @@ class ProcessIsolatedSerenaAgent: raise TimeoutError(f"Request {request_id} timed out after {timeout} seconds") - def _make_request_with_result(self, method: str, params: dict[str, Any] | None = None) -> Any: + def _make_request_with_result(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> Any: """Make a request and return the result, raising an exception if there's an error.""" response = self._make_request(method, params) if "error" in response: @@ -300,28 +312,25 @@ class ProcessIsolatedSerenaAgent: def tool_call(self, tool_name: str, **tool_params: Any) -> str: """Call a tool in the worker process.""" - return self._make_request_with_result("tool_call", {"tool_name": tool_name, "tool_params": tool_params}) + return self._make_request_with_result( + SerenaAgentWorker.RequestMethod.TOOL_CALL, {"tool_name": tool_name, "tool_params": tool_params} + ) def get_active_tool_names(self) -> list[str]: """Get list of active tool names.""" - return self._make_request_with_result("get_active_tool_names") + return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_ACTIVE_TOOL_NAMES) def is_language_server_running(self) -> bool: """Check if language server is running.""" - return self._make_request_with_result("is_language_server_running") + return self._make_request_with_result(SerenaAgentWorker.RequestMethod.IS_LANGUAGE_SERVER_RUNNING) def reset_language_server(self) -> None: """Reset the language server.""" - self._make_request_with_result("reset_language_server") + self._make_request_with_result(SerenaAgentWorker.RequestMethod.RESET_LANGUAGE_SERVER) def get_exposed_tool_names(self) -> list[str]: """Get tool names for MCP tool creation.""" - return self._make_request_with_result("get_exposed_tool_names") - - def get_exposed_tool_instances(self) -> list[ToolInterface]: - """Get exposed tool instances for MCP tool creation.""" - tool_names = self.get_exposed_tool_names() - return [ProcessIsolatedTool(self, tool_name) for tool_name in tool_names] + return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_EXPOSED_TOOL_NAMES) def __enter__(self) -> Self: self.start() From fd90ec471df1a4ab306c4160fcf6441d7f3d265d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 17:47:36 +0200 Subject: [PATCH 049/174] Brought normal and isolated agent interfaces closer together --- src/serena/mcp.py | 2 +- src/serena/process_isolated_agent.py | 92 +++++++++++++++++++++++++--- 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index a7f9ea2..e8f4ec6 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -144,7 +144,7 @@ def create_mcp_server_and_agent( # Create process-isolated agent with the configuration from serena.process_isolated_agent import ProcessIsolatedSerenaAgent - process_agent = ProcessIsolatedSerenaAgent(serena_config) + process_agent = ProcessIsolatedSerenaAgent(serena_config=serena_config) except Exception as e: show_fatal_exception_safe(e) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 12a8071..d8e3ec5 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -13,12 +13,14 @@ import queue import threading import time import traceback +from collections.abc import Callable from enum import StrEnum -from typing import Any, Self +from typing import Any, Literal, Self from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry +from serena.config import SerenaAgentContext, SerenaAgentMode log = logging.getLogger(__name__) @@ -93,10 +95,37 @@ class SerenaAgentWorker: def _initialize(self, request_id: str, params: dict[str, Any]) -> dict[str, Any]: """Initialize the SerenaAgent.""" try: - config_dict = params["config"] - serena_config = SerenaConfig.from_dict(config_dict) + # Extract all possible initialization parameters + project = params.get("project") + project_activation_callback = params.get("project_activation_callback") + serena_config = params.get("serena_config") + context = params.get("context") + modes = params.get("modes") + enable_web_dashboard = params.get("enable_web_dashboard") + enable_gui_log_window = params.get("enable_gui_log_window") + log_level = params.get("log_level") + trace_lsp_communication = params.get("trace_lsp_communication") + tool_timeout = params.get("tool_timeout") - self.agent = SerenaAgent(serena_config=serena_config) + # Handle legacy config_dict parameter for backward compatibility + if "config" in params and serena_config is None: + config_dict = params["config"] + serena_config = SerenaConfig.from_dict(config_dict) + elif serena_config is not None and isinstance(serena_config, dict): + serena_config = SerenaConfig.from_dict(serena_config) + + self.agent = SerenaAgent( + project=project, + project_activation_callback=project_activation_callback, + serena_config=serena_config, + context=context, + modes=modes, + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) return {"id": request_id, "result": "SerenaAgent initialized successfully"} except Exception as e: @@ -199,8 +228,39 @@ class SerenaAgentWorker: class ProcessIsolatedSerenaAgent: """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" - def __init__(self, serena_config: SerenaConfigBase): - self.serena_config = serena_config + def __init__( + self, + project: str | None = None, + project_activation_callback: Callable[[], None] | None = None, + serena_config: SerenaConfigBase | None = None, + context: SerenaAgentContext | None = None, + modes: list[SerenaAgentMode] | 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, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, + ): + # Store all initialization parameters to pass to worker + self._init_params = { + "project": project, + "project_activation_callback": project_activation_callback, + "serena_config": serena_config, + "context": context, + "modes": modes, + "enable_web_dashboard": enable_web_dashboard, + "enable_gui_log_window": enable_gui_log_window, + "log_level": log_level, + "trace_lsp_communication": trace_lsp_communication, + "tool_timeout": tool_timeout, + } + + # Keep serena_config for compatibility + if serena_config is not None: + self.serena_config = serena_config + else: + # Use the default config loader + self.serena_config = SerenaConfig.from_config_file() self.process: multiprocessing.Process | None = None self.request_queue: multiprocessing.Queue | None = None self.response_queue: multiprocessing.Queue | None = None @@ -225,7 +285,20 @@ class ProcessIsolatedSerenaAgent: # Initialize the agent in the worker process try: - self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, {"config": self.serena_config.to_dict()}) + # Prepare initialization parameters, converting complex objects to dict if present + init_params = self._init_params.copy() + if init_params["serena_config"] is not None and hasattr(init_params["serena_config"], "to_dict"): + # Convert SerenaConfigBase to dict for serialization + serena_config_obj = init_params["serena_config"] + init_params["serena_config"] = serena_config_obj.to_dict() # type: ignore + # Note: context and modes are not serializable across processes, + # so they will be None and SerenaAgent will use defaults + init_params["context"] = None + init_params["modes"] = None + # project_activation_callback cannot be serialized across processes + init_params["project_activation_callback"] = None + + self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, init_params) except Exception as e: self.stop() raise RuntimeError(f"Failed to initialize SerenaAgent: {e}") from e @@ -316,6 +389,11 @@ class ProcessIsolatedSerenaAgent: SerenaAgentWorker.RequestMethod.TOOL_CALL, {"tool_name": tool_name, "tool_params": tool_params} ) + def get_tool(self, tool_cls: type[Tool]) -> "ProcessIsolatedTool": + """Get a process-isolated tool that delegates to this agent.""" + tool_name = tool_cls.get_name_from_cls() + return ProcessIsolatedTool(self, tool_name) + def get_active_tool_names(self) -> list[str]: """Get list of active tool names.""" return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_ACTIVE_TOOL_NAMES) From 1327c7a118d91952959200331808dd62af3e3ce3 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 17:47:46 +0200 Subject: [PATCH 050/174] Tests for isolated agent --- pyproject.toml | 1 + test/serena/test_serena_agent.py | 75 ++++++++++++++++++++++++-------- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4efceca..4132dd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -260,4 +260,5 @@ markers = [ "typescript: language server running for TypeScript", "php: language server running for PHP", "snapshot: snapshot tests", + "isolated_process: test runs with process isolated agent", ] diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 49a47d8..853d86a 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -7,7 +7,8 @@ import pytest from multilspy.multilspy_config import Language from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase -from test.conftest import LanguageParamRequest, get_repo_path +from serena.process_isolated_agent import ProcessIsolatedSerenaAgent +from test.conftest import get_repo_path @dataclass @@ -55,13 +56,35 @@ def serena_config(): @pytest.fixture -def serena_agent(request: LanguageParamRequest, serena_config) -> SerenaAgent: +def serena_agent(request: pytest.FixtureRequest, serena_config): language = Language(request.param) project_name = f"test_repo_{language}" - return SerenaAgent(project=project_name, serena_config=serena_config) + + # Check if this test should use process isolation by looking at the test parameters + isolated_process = False + if hasattr(request, "node") and hasattr(request.node, "callspec"): + # Get the isolated_process parameter value from the test + params = request.node.callspec.params + isolated_process = params.get("isolated_process", False) + + if isolated_process: + agent = ProcessIsolatedSerenaAgent(project=project_name, serena_config=serena_config) + agent.start() + + # Add cleanup to stop the process + def cleanup(): + agent.stop() + + request.addfinalizer(cleanup) + return agent + else: + return SerenaAgent(project=project_name, serena_config=serena_config) class TestSerenaAgent: + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,symbol_name,expected_kind,expected_file", [ @@ -74,16 +97,20 @@ class TestSerenaAgent: ], indirect=["serena_agent"], ) - def test_find_symbol(self, serena_agent: SerenaAgent, symbol_name: str, expected_kind: str, expected_file: str): + def test_find_symbol(self, serena_agent, symbol_name: str, expected_kind: str, expected_file: str, isolated_process: bool): agent = serena_agent find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply(symbol_name) + result = find_symbol_tool.apply_ex(name_path=symbol_name) + symbols = json.loads(result) assert any( symbol_name in s["name_path"] and expected_kind.lower() in s["kind"].lower() and expected_file in s["relative_path"] for s in symbols - ), f"Expected to find {symbol_name} ({expected_kind}) in {expected_file} for {agent.get_active_project().language.name}" + ), f"Expected to find {symbol_name} ({expected_kind}) in {expected_file}" + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,symbol_name,def_file,ref_file", [ @@ -108,23 +135,30 @@ class TestSerenaAgent: ], indirect=["serena_agent"], ) - def test_find_symbol_references(self, serena_agent: SerenaAgent, symbol_name: str, def_file: str, ref_file: str) -> None: + def test_find_symbol_references(self, serena_agent, symbol_name: str, def_file: str, ref_file: str, isolated_process: bool) -> None: agent = serena_agent + # Find the symbol location first find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply(symbol_name, relative_path=def_file) + result = find_symbol_tool.apply_ex(name_path=symbol_name, relative_path=def_file) + time.sleep(1) symbols = json.loads(result) # Find the definition def_symbol = symbols[0] + # Now find references find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) - result = find_refs_tool.apply(name_path=def_symbol["name_path"], relative_path=def_symbol["relative_path"]) + result = find_refs_tool.apply_ex(name_path=def_symbol["name_path"], relative_path=def_symbol["relative_path"]) + refs = json.loads(result) assert any( ref["relative_path"] == ref_file for ref in refs - ), f"Expected to find reference to {symbol_name} in {ref_file} for {agent._active_project.language.name}. refs={refs}" + ), f"Expected to find reference to {symbol_name} in {ref_file}. refs={refs}" + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,name_path,substring_matching,expected_symbol_name,expected_kind,expected_file", [ @@ -193,16 +227,18 @@ class TestSerenaAgent: ) def test_find_symbol_name_path( self, - serena_agent: SerenaAgent, + serena_agent, name_path: str, substring_matching: bool, expected_symbol_name: str, expected_kind: str, expected_file: str, + isolated_process: bool, ): agent = serena_agent + find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply( + result = find_symbol_tool.apply_ex( name_path=name_path, depth=0, relative_path=None, @@ -211,6 +247,7 @@ class TestSerenaAgent: exclude_kinds=None, substring_matching=substring_matching, ) + symbols = json.loads(result) assert any( expected_symbol_name == s["name_path"].split("/")[-1] @@ -219,6 +256,9 @@ class TestSerenaAgent: for s in symbols ), f"Expected to find {name_path} ({expected_kind}) in {expected_file} for {agent._active_project.language.name}. Symbols: {symbols}" + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,name_path", [ @@ -239,17 +279,18 @@ class TestSerenaAgent: ) def test_find_symbol_name_path_no_match( self, - serena_agent: SerenaAgent, + serena_agent, name_path: str, + isolated_process: bool, ): agent = serena_agent + find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply( + result = find_symbol_tool.apply_ex( name_path=name_path, depth=0, substring_matching=True, ) + symbols = json.loads(result) - assert ( - not symbols - ), f"Expected to find no symbols for {name_path} for {agent._active_project.language.name}. Symbols found: {symbols}" + assert not symbols, f"Expected to find no symbols for {name_path}. Symbols found: {symbols}" From bc6b0f2ebaffe6b5b0866a5d5be1dbba67d9ffff Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 17:57:36 +0200 Subject: [PATCH 051/174] Replaced queue-based approach to worker with pipe-based one --- src/serena/process_isolated_agent.py | 166 +++++++++++++-------------- 1 file changed, 77 insertions(+), 89 deletions(-) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index d8e3ec5..be6234b 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -9,12 +9,10 @@ This module provides: import logging import multiprocessing -import queue -import threading -import time import traceback from collections.abc import Callable from enum import StrEnum +from multiprocessing.connection import Connection from typing import Any, Literal, Self from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata @@ -28,37 +26,40 @@ log = logging.getLogger(__name__) class SerenaAgentWorker: """Worker process that hosts the actual SerenaAgent.""" - def __init__(self, request_queue: multiprocessing.Queue, response_queue: multiprocessing.Queue): - self.request_queue = request_queue - self.response_queue = response_queue + def __init__(self, conn: Connection): + self.conn = conn self.agent: SerenaAgent | None = None - self.running = True def run(self) -> None: """Main worker loop - runs in separate process.""" try: log.info("SerenaAgent worker process started") - while self.running: + while True: try: - # Wait for requests with timeout to allow clean shutdown - request = self.request_queue.get(timeout=1.0) + # Block until request received or connection closed + request = self.conn.recv() if request is None: # Shutdown signal break response = self._handle_request(request) - self.response_queue.put(response) + self.conn.send(response) - except queue.Empty: - continue + except EOFError: + # Connection closed - parent process terminated + log.info("Connection closed, worker shutting down") + break except Exception as e: log.error(f"Error in worker process: {e}") - response = { - "id": getattr(request, "id", None), - "error": f"Worker process error: {e!s}", - "traceback": traceback.format_exc(), - } - self.response_queue.put(response) + try: + response = { + "error": f"Worker process error: {e!s}", + "traceback": traceback.format_exc(), + } + self.conn.send(response) + except (EOFError, BrokenPipeError): + # Connection is broken, can't send error response + break except Exception as e: log.error(f"Fatal error in worker process: {e}") @@ -71,28 +72,27 @@ class SerenaAgentWorker: try: method = request["method"] params = request.get("params", {}) - request_id = request["id"] match method: case self.RequestMethod.INITIALIZE: - return self._initialize(request_id, params) + return self._initialize(params) case self.RequestMethod.TOOL_CALL: - return self._tool_call(request_id, params) + return self._tool_call(params) case self.RequestMethod.GET_ACTIVE_TOOL_NAMES: - return self._get_active_tool_names(request_id) + return self._get_active_tool_names() case self.RequestMethod.IS_LANGUAGE_SERVER_RUNNING: - return self._is_language_server_running(request_id) + return self._is_language_server_running() case self.RequestMethod.RESET_LANGUAGE_SERVER: - return self._reset_language_server(request_id) + return self._reset_language_server() case self.RequestMethod.GET_EXPOSED_TOOL_NAMES: - return self._get_exposed_tool_names(request_id) + return self._get_exposed_tool_names() case _: - return {"id": request_id, "error": f"Unknown method: {method}"} + return {"error": f"Unknown method: {method}"} except Exception as e: - return {"id": request.get("id"), "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} - def _initialize(self, request_id: str, params: dict[str, Any]) -> dict[str, Any]: + def _initialize(self, params: dict[str, Any]) -> dict[str, Any]: """Initialize the SerenaAgent.""" try: # Extract all possible initialization parameters @@ -127,14 +127,14 @@ class SerenaAgentWorker: tool_timeout=tool_timeout, ) - return {"id": request_id, "result": "SerenaAgent initialized successfully"} + return {"result": "SerenaAgent initialized successfully"} except Exception as e: - return {"id": request_id, "error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} + return {"error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} - def _tool_call(self, request_id: str, params: dict[str, Any]) -> dict[str, Any]: + def _tool_call(self, params: dict[str, Any]) -> dict[str, Any]: """Execute a tool call.""" if self.agent is None: - return {"id": request_id, "error": "SerenaAgent not initialized"} + return {"error": "SerenaAgent not initialized"} try: tool_name = params["tool_name"] @@ -148,61 +148,61 @@ class SerenaAgentWorker: break if tool is None: - return {"id": request_id, "error": f"Tool '{tool_name}' not found or not active"} + return {"error": f"Tool '{tool_name}' not found or not active"} # Execute the tool result = tool.apply_ex(**tool_params) - return {"id": request_id, "result": result} + return {"result": result} except Exception as e: - return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} - def _get_active_tool_names(self, request_id: str) -> dict[str, Any]: + def _get_active_tool_names(self) -> dict[str, Any]: """Get list of active tool names.""" if self.agent is None: - return {"id": request_id, "error": "SerenaAgent not initialized"} + return {"error": "SerenaAgent not initialized"} try: tool_names = self.agent.get_active_tool_names() - return {"id": request_id, "result": tool_names} + return {"result": tool_names} except Exception as e: - return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} - def _is_language_server_running(self, request_id: str) -> dict[str, Any]: + def _is_language_server_running(self) -> dict[str, Any]: """Check if language server is running.""" if self.agent is None: - return {"id": request_id, "error": "SerenaAgent not initialized"} + return {"error": "SerenaAgent not initialized"} try: is_running = self.agent.is_language_server_running() - return {"id": request_id, "result": is_running} + return {"result": is_running} except Exception as e: - return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} - def _reset_language_server(self, request_id: str) -> dict[str, Any]: + def _reset_language_server(self) -> dict[str, Any]: """Reset the language server.""" if self.agent is None: - return {"id": request_id, "error": "SerenaAgent not initialized"} + return {"error": "SerenaAgent not initialized"} try: self.agent.reset_language_server() - return {"id": request_id, "result": "Language server reset successfully"} + return {"result": "Language server reset successfully"} except Exception as e: - return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} - def _get_exposed_tool_names(self, request_id: str) -> dict[str, Any]: + def _get_exposed_tool_names(self) -> dict[str, Any]: """Get exposed tool names for MCP tool creation.""" if self.agent is None: - return {"id": request_id, "error": "SerenaAgent not initialized"} + return {"error": "SerenaAgent not initialized"} try: tool_instances = self.agent.get_exposed_tool_instances() # Return only tool names - metadata will be reconstructed from ToolRegistry tool_names = [tool.get_name_from_cls() for tool in tool_instances] - return {"id": request_id, "result": tool_names} + return {"result": tool_names} except Exception as e: - return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} def _cleanup(self) -> None: """Clean up resources.""" @@ -215,7 +215,7 @@ class SerenaAgentWorker: self.agent = None class RequestMethod(StrEnum): - """Enumeration of available request methods for type-safe IPC.""" + """Enumeration of available request methods.""" INITIALIZE = "initialize" TOOL_CALL = "tool_call" @@ -262,10 +262,7 @@ class ProcessIsolatedSerenaAgent: # Use the default config loader self.serena_config = SerenaConfig.from_config_file() self.process: multiprocessing.Process | None = None - self.request_queue: multiprocessing.Queue | None = None - self.response_queue: multiprocessing.Queue | None = None - self.request_id_counter = 0 - self._lock = threading.Lock() + self.conn: Connection | None = None def start(self) -> None: """Start the worker process.""" @@ -274,12 +271,12 @@ class ProcessIsolatedSerenaAgent: log.info("Starting process-isolated SerenaAgent") - # Create communication queues - self.request_queue = multiprocessing.Queue() - self.response_queue = multiprocessing.Queue() + # Create communication pipe + parent_conn, child_conn = multiprocessing.Pipe() + self.conn = parent_conn # Create and start worker process - worker = SerenaAgentWorker(self.request_queue, self.response_queue) + worker = SerenaAgentWorker(child_conn) self.process = multiprocessing.Process(target=worker.run) self.process.start() @@ -313,9 +310,9 @@ class ProcessIsolatedSerenaAgent: log.info("Stopping process-isolated SerenaAgent") try: - # Send shutdown signal - if self.request_queue is not None: - self.request_queue.put(None) + # Close connection to signal worker to shutdown + if self.conn is not None: + self.conn.close() # Wait for process to terminate if self.process.is_alive(): @@ -336,8 +333,7 @@ class ProcessIsolatedSerenaAgent: finally: self.process = None - self.request_queue = None - self.response_queue = None + self.conn = None log.info("Process-isolated SerenaAgent stopped") @@ -346,35 +342,27 @@ class ProcessIsolatedSerenaAgent: if self.process is None or not self.process.is_alive(): raise RuntimeError("Worker process is not running") - with self._lock: - self.request_id_counter += 1 - request_id = str(self.request_id_counter) + if self.conn is None: + raise RuntimeError("Connection is not initialized") - request = {"id": request_id, "method": method, "params": params or {}} + request = {"method": method, "params": params or {}} # Send request - if self.request_queue is not None: - self.request_queue.put(request) + try: + self.conn.send(request) + except (EOFError, BrokenPipeError) as e: + raise RuntimeError("Failed to send request: worker process may have crashed") from e - # Wait for response + # Wait for response with timeout timeout = self.serena_config.tool_timeout - start_time = time.time() - while time.time() - start_time < timeout: + if self.conn.poll(timeout): try: - if self.response_queue is not None: - response = self.response_queue.get(timeout=1.0) - else: - raise RuntimeError("Response queue is not initialized") - if response["id"] == request_id: - return response - else: - # Put back response for different request - if self.response_queue is not None: - self.response_queue.put(response) - except queue.Empty: - continue - - raise TimeoutError(f"Request {request_id} timed out after {timeout} seconds") + response = self.conn.recv() + return response + except (EOFError, BrokenPipeError) as e: + raise RuntimeError("Failed to receive response: worker process may have crashed") from e + else: + raise TimeoutError(f"Request {method} timed out after {timeout} seconds") def _make_request_with_result(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> Any: """Make a request and return the result, raising an exception if there's an error.""" From 343c00f6052a228c970e69d8b0c7b4d9ec2ffd2e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 18:31:17 +0200 Subject: [PATCH 052/174] Fix process termination issues and allow terminating through dashboard --- src/serena/dashboard.py | 22 +++++-- src/serena/mcp.py | 21 +++++++ src/serena/process_isolated_agent.py | 61 +++++++++++++++++++ .../test_make_tool_process_isolation.py | 2 +- 4 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 622bef1..549c003 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -3,6 +3,7 @@ import queue import socket import sys import threading +from collections.abc import Callable import uvicorn from fastapi import FastAPI @@ -68,9 +69,12 @@ class ResponseToolNames(BaseModel): class SerenaDashboardAPI: log = logging.getLogger(__qualname__) - def __init__(self, memory_log_handler: MemoryLogHandler, tool_names: list[str]) -> None: + def __init__( + self, memory_log_handler: MemoryLogHandler, tool_names: list[str], shutdown_callback: Callable[[], None] | None = None + ) -> None: self._memory_log_handler = memory_log_handler self._tool_names = tool_names + self._shutdown_callback = shutdown_callback self._app = FastAPI(title="Serena Dashboard") self._setup_routes() @@ -92,9 +96,19 @@ class SerenaDashboardAPI: async def _shutdown(self) -> None: print("Shutdown initiated by dashbaord ...", file=sys.stderr) log.info("Shutting down Serena") - # noinspection PyUnresolvedReferences - # noinspection PyProtectedMember - os._exit(0) + if self._shutdown_callback: + self._shutdown_callback() + else: + # Try to use the global shutdown function from process_isolated_agent + try: + from serena.process_isolated_agent import call_global_shutdown + + call_global_shutdown() + except ImportError: + # Fallback to the old behavior if not in process-isolated mode + # noinspection PyUnresolvedReferences + # noinspection PyProtectedMember + os._exit(0) @staticmethod def _find_first_free_port(start_port: int) -> int: diff --git a/src/serena/mcp.py b/src/serena/mcp.py index e8f4ec6..a5ade13 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -183,14 +183,35 @@ def create_mcp_server_and_agent( async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" nonlocal process_agent + import asyncio + mark_used(mcp_server) + async def monitor_worker_process() -> None: + """Monitor the worker process and shutdown server if it exits.""" + while True: + if process_agent.process is None or not process_agent.process.is_alive(): + log.info("Worker process has exited, shutting down MCP server") + # Trigger server shutdown + import os + + os._exit(0) + await asyncio.sleep(1) + try: process_agent.start() # Update tools now that the process agent is running update_tools() + # Start monitoring task + monitor_task = asyncio.create_task(monitor_worker_process()) yield finally: + if "monitor_task" in locals(): + monitor_task.cancel() + import contextlib + + with contextlib.suppress(asyncio.CancelledError): + await monitor_task process_agent.stop() mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index be6234b..6ff4468 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -23,6 +23,36 @@ from serena.config import SerenaAgentContext, SerenaAgentMode log = logging.getLogger(__name__) +# Global shutdown function that can be called by the dashboard +class _GlobalShutdownRegistry: + """Registry for global shutdown function.""" + + def __init__(self) -> None: + self.shutdown_func: Callable[[], None] | None = None + + def set_shutdown_func(self, func: Callable[[], None]) -> None: + """Set the global shutdown function.""" + self.shutdown_func = func + + def call_shutdown(self) -> None: + """Call the global shutdown function if it exists.""" + if self.shutdown_func: + self.shutdown_func() + + +_shutdown_registry = _GlobalShutdownRegistry() + + +def set_global_shutdown_func(func: Callable[[], None]) -> None: + """Set the global shutdown function.""" + _shutdown_registry.set_shutdown_func(func) + + +def call_global_shutdown() -> None: + """Call the global shutdown function if it exists.""" + _shutdown_registry.call_shutdown() + + class SerenaAgentWorker: """Worker process that hosts the actual SerenaAgent.""" @@ -86,6 +116,8 @@ class SerenaAgentWorker: return self._reset_language_server() case self.RequestMethod.GET_EXPOSED_TOOL_NAMES: return self._get_exposed_tool_names() + case self.RequestMethod.SHUTDOWN: + return self._shutdown() case _: return {"error": f"Unknown method: {method}"} @@ -127,6 +159,16 @@ class SerenaAgentWorker: tool_timeout=tool_timeout, ) + # Set up global shutdown function for dashboard + def shutdown_worker() -> None: + log.info("Global shutdown function called") + self._cleanup() + import sys + + sys.exit(0) + + set_global_shutdown_func(shutdown_worker) + return {"result": "SerenaAgent initialized successfully"} except Exception as e: return {"error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} @@ -204,6 +246,20 @@ class SerenaAgentWorker: except Exception as e: return {"error": str(e), "traceback": traceback.format_exc()} + def _shutdown(self) -> dict[str, Any]: + """Handle shutdown request from dashboard.""" + try: + log.info("Shutdown requested from dashboard") + # Clean up resources before exiting + self._cleanup() + # Exit the worker process - this will cause the main process to detect the termination + # and shut down gracefully through the server lifespan manager + import sys + + sys.exit(0) + except Exception as e: + return {"error": str(e), "traceback": traceback.format_exc()} + def _cleanup(self) -> None: """Clean up resources.""" if self.agent is not None: @@ -223,6 +279,7 @@ class SerenaAgentWorker: IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running" RESET_LANGUAGE_SERVER = "reset_language_server" GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names" + SHUTDOWN = "shutdown" class ProcessIsolatedSerenaAgent: @@ -394,6 +451,10 @@ class ProcessIsolatedSerenaAgent: """Reset the language server.""" self._make_request_with_result(SerenaAgentWorker.RequestMethod.RESET_LANGUAGE_SERVER) + def shutdown_from_dashboard(self) -> None: + """Request shutdown from dashboard.""" + self._make_request_with_result(SerenaAgentWorker.RequestMethod.SHUTDOWN) + def get_exposed_tool_names(self) -> list[str]: """Get tool names for MCP tool creation.""" return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_EXPOSED_TOOL_NAMES) diff --git a/test/serena/test_make_tool_process_isolation.py b/test/serena/test_make_tool_process_isolation.py index 3404aa1..fb13f86 100644 --- a/test/serena/test_make_tool_process_isolation.py +++ b/test/serena/test_make_tool_process_isolation.py @@ -24,7 +24,7 @@ def regular_agent(in_memory_config): @pytest.fixture def process_isolated_agent(in_memory_config): """Create a ProcessIsolatedSerenaAgent for comparison.""" - agent = ProcessIsolatedSerenaAgent(in_memory_config) + agent = ProcessIsolatedSerenaAgent(serena_config=in_memory_config) agent.start() yield agent agent.stop() From 7b35b77bc920e5450cf1f5df1e9907075d70119f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 18:56:45 +0200 Subject: [PATCH 053/174] Isolating dashboard process, solving shutdown issues --- src/serena/mcp.py | 160 ++++++++++-- src/serena/process_isolated_agent.py | 355 +++++++++++++++++++++++++-- 2 files changed, 470 insertions(+), 45 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index a5ade13..6c91d0b 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -142,7 +142,7 @@ def create_mcp_server_and_agent( ) # Create process-isolated agent with the configuration - from serena.process_isolated_agent import ProcessIsolatedSerenaAgent + from serena.process_isolated_agent import ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent process_agent = ProcessIsolatedSerenaAgent(serena_config=serena_config) @@ -184,35 +184,159 @@ def create_mcp_server_and_agent( """Manage server startup and shutdown lifecycle.""" nonlocal process_agent import asyncio + import webbrowser mark_used(mcp_server) + dashboard: ProcessIsolatedDashboard | None = None + shutdown_event = asyncio.Event() + async def monitor_worker_process() -> None: """Monitor the worker process and shutdown server if it exits.""" - while True: - if process_agent.process is None or not process_agent.process.is_alive(): - log.info("Worker process has exited, shutting down MCP server") - # Trigger server shutdown - import os + while not shutdown_event.is_set(): + try: + if process_agent.process is None or not process_agent.process.is_alive(): + log.info("Worker process has exited, shutting down MCP server") + shutdown_event.set() + break + await asyncio.sleep(1) + except asyncio.CancelledError: + break + except Exception as e: + log.error(f"Error monitoring worker process: {e}") + await asyncio.sleep(1) - os._exit(0) - await asyncio.sleep(1) + async def monitor_dashboard_shutdown() -> None: + """Monitor the dashboard shutdown queue.""" + if dashboard is None or dashboard.shutdown_queue is None: + return + + while not shutdown_event.is_set(): + try: + # Check if dashboard sent shutdown signal + if not dashboard.shutdown_queue.empty(): + dashboard.shutdown_queue.get_nowait() + log.info("Dashboard shutdown signal received via queue") + shutdown_event.set() + # Force immediate exit since MCP server doesn't respond properly + import os + import sys + + log.info("Forcing immediate process exit from dashboard signal") + sys.stdout.flush() + sys.stderr.flush() + os._exit(0) + await asyncio.sleep(0.1) # Check more frequently + except asyncio.CancelledError: + break + except Exception as e: + log.error(f"Error monitoring dashboard shutdown: {e}") + await asyncio.sleep(1) + + def shutdown_server() -> None: + """Shutdown the server.""" + log.info("Shutdown initiated by dashboard") + shutdown_event.set() + # Force immediate exit since the MCP server doesn't respond to shutdown events + import os + import sys + + log.info("Dashboard shutdown - forcing process exit immediately") + sys.stdout.flush() + sys.stderr.flush() + os._exit(0) try: process_agent.start() + + # Set up global shutdown function for dashboard + from serena.process_isolated_agent import set_global_shutdown_func + + set_global_shutdown_func(shutdown_server) + # Update tools now that the process agent is running update_tools() - # Start monitoring task - monitor_task = asyncio.create_task(monitor_worker_process()) - yield - finally: - if "monitor_task" in locals(): - monitor_task.cancel() - import contextlib - with contextlib.suppress(asyncio.CancelledError): - await monitor_task - process_agent.stop() + # Start process-isolated dashboard if enabled + if serena_config.web_dashboard: + try: + tool_names = process_agent.get_exposed_tool_names() + dashboard = ProcessIsolatedDashboard(tool_names) + port = dashboard.start() + webbrowser.open(f"http://localhost:{port}/dashboard/index.html") + log.info(f"Dashboard started on port {port}") + except Exception as e: + log.error(f"Failed to start dashboard: {e}") + dashboard = None + + # Start monitoring tasks + tasks = [] + monitor_task = asyncio.create_task(monitor_worker_process()) + tasks.append(monitor_task) + + # Start dashboard shutdown monitor if dashboard is running + if dashboard is not None: + dashboard_monitor_task = asyncio.create_task(monitor_dashboard_shutdown()) + tasks.append(dashboard_monitor_task) + + # Set up signal handlers in main process + def signal_handler(signum: int, frame: Any) -> None: + log.info(f"Received signal {signum} in main process") + shutdown_event.set() + # Give a short delay for graceful shutdown, then force exit + import os + import threading + + def force_exit() -> None: + import time + + time.sleep(2.0) # Wait 2 seconds for graceful shutdown + log.warning("Forcing exit after timeout") + os._exit(1) + + threading.Thread(target=force_exit, daemon=True).start() + + import signal + + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + try: + # Start the server and wait for shutdown + yield + except (KeyboardInterrupt, SystemExit): + log.info("Received shutdown signal") + shutdown_event.set() + + except Exception as e: + log.error(f"Error in server lifespan: {e}") + shutdown_event.set() + finally: + log.info("Starting server shutdown cleanup") + + # Cancel monitoring tasks + import contextlib + + if "tasks" in locals(): + for task in tasks: + task.cancel() + with contextlib.suppress(TimeoutError, asyncio.CancelledError): + await asyncio.wait_for(task, timeout=1.0) + + # Stop dashboard first + if dashboard is not None: + try: + dashboard.stop() + except Exception as e: + log.error(f"Error stopping dashboard: {e}") + + # Stop process agent + try: + process_agent.stop() + except Exception as e: + log.error(f"Error stopping process agent: {e}") + + log.info("Server shutdown cleanup completed") mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) mcp = FastMCP(**mcp_settings.model_dump()) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 6ff4468..a9b59a7 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -7,8 +7,10 @@ This module provides: 3. JSON-RPC based IPC for communication between processes """ +import asyncio import logging import multiprocessing +import signal import traceback from collections.abc import Callable from enum import StrEnum @@ -19,6 +21,7 @@ from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry from serena.config import SerenaAgentContext, SerenaAgentMode +from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI log = logging.getLogger(__name__) @@ -53,43 +56,326 @@ def call_global_shutdown() -> None: _shutdown_registry.call_shutdown() +class ProcessIsolatedDashboard: + """Process-isolated dashboard to prevent asyncio contamination with MCP server and worker.""" + + def __init__(self, tool_names: list[str]): + self.tool_names = tool_names + self.process: multiprocessing.Process | None = None + self.log_queue: multiprocessing.Queue[str] | None = None + self.shutdown_queue: multiprocessing.Queue[bool] | None = None + self.port_queue: multiprocessing.Queue[int] | None = None + self._log_handler: ProcessDashboardLogHandler | None = None + + def start(self) -> int: + """Start the dashboard process and return the port number.""" + if self.process is not None: + raise RuntimeError("Dashboard process already started") + + log.info("Starting process-isolated dashboard") + + # Create communication queues + self.log_queue = multiprocessing.Queue() + self.shutdown_queue = multiprocessing.Queue() + self.port_queue = multiprocessing.Queue() + + # Create and start dashboard process + dashboard_worker = DashboardWorker( + log_queue=self.log_queue, shutdown_queue=self.shutdown_queue, port_queue=self.port_queue, tool_names=self.tool_names + ) + self.process = multiprocessing.Process(target=dashboard_worker.run) + self.process.start() + + # Set up log handler to send logs to dashboard process + self._log_handler = ProcessDashboardLogHandler(self.log_queue) + logging.getLogger().addHandler(self._log_handler) + + # Wait for dashboard to start and return port + try: + port = self.port_queue.get(timeout=10) # Wait up to 10 seconds for startup + log.info(f"Dashboard started on port {port}") + return port + except Exception as e: + self.stop() + raise RuntimeError(f"Failed to start dashboard: {e}") from e + + def stop(self) -> None: + """Stop the dashboard process.""" + if self.process is None: + return + + log.info("Stopping process-isolated dashboard") + + try: + # Remove log handler + if self._log_handler is not None: + logging.getLogger().removeHandler(self._log_handler) + self._log_handler = None + + # Signal shutdown + if self.shutdown_queue is not None: + import contextlib + + with contextlib.suppress(Exception): + self.shutdown_queue.put_nowait(True) + + # Wait for process to terminate + if self.process.is_alive(): + self.process.join(timeout=5.0) + + if self.process.is_alive(): + log.warning("Dashboard process did not terminate gracefully, forcing termination") + self.process.terminate() + self.process.join(timeout=3.0) + + if self.process.is_alive(): + log.error("Dashboard process could not be terminated, killing it") + self.process.kill() + self.process.join() + + except Exception as e: + log.error(f"Error stopping dashboard process: {e}") + + finally: + self.process = None + self.log_queue = None + self.shutdown_queue = None + self.port_queue = None + + log.info("Process-isolated dashboard stopped") + + def update_tool_names(self, tool_names: list[str]) -> None: + """Update tool names (for future enhancement).""" + self.tool_names = tool_names + # TODO: Could send update to dashboard process if needed + + +class ProcessDashboardLogHandler(logging.Handler): + """Log handler that sends log messages to dashboard process via queue.""" + + def __init__(self, log_queue: "multiprocessing.Queue[str]", level: int = logging.NOTSET): + super().__init__(level=level) + self.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s")) + self.log_queue = log_queue + + def emit(self, record: logging.LogRecord) -> None: + try: + msg = self.format(record) + # Non-blocking put to avoid deadlocks + self.log_queue.put_nowait(msg) + except Exception: + # Silently ignore queue errors to avoid recursive logging + pass + + +class DashboardWorker: + """Worker process that hosts the dashboard with its own asyncio loop.""" + + def __init__( + self, + log_queue: "multiprocessing.Queue[str]", + shutdown_queue: "multiprocessing.Queue[bool]", + port_queue: "multiprocessing.Queue[int]", + tool_names: list[str], + ): + self.log_queue = log_queue + self.shutdown_queue = shutdown_queue + self.port_queue = port_queue + self.tool_names = tool_names + self.memory_log_handler: MemoryLogHandler | None = None + self.dashboard_api: SerenaDashboardAPI | None = None + self.log_processor_task: asyncio.Task | None = None + + def run(self) -> None: + """Main dashboard worker loop - runs in separate process.""" + try: + log.info("Dashboard worker process started") + asyncio.run(self._async_main()) + except Exception as e: + log.error(f"Fatal error in dashboard worker process: {e}") + finally: + log.info("Dashboard worker process stopped") + + async def _async_main(self) -> None: + """Main async loop for dashboard process.""" + try: + # Set up memory log handler + self.memory_log_handler = MemoryLogHandler() + + # Set up dashboard API with shutdown callback + self.dashboard_api = SerenaDashboardAPI( + memory_log_handler=self.memory_log_handler, tool_names=self.tool_names, shutdown_callback=self._handle_shutdown + ) + + # Start log processor task + self.log_processor_task = asyncio.create_task(self._process_log_queue()) + + # Find free port and signal to parent + port = self.dashboard_api._find_first_free_port(0x5EDA) + self.port_queue.put(port) + + # Start dashboard server + import uvicorn + + config = uvicorn.Config( + app=self.dashboard_api._app, host="0.0.0.0", port=port, workers=1, log_config=None, log_level="critical" + ) + server = uvicorn.Server(config) + + # Start server and shutdown monitor concurrently + shutdown_task = asyncio.create_task(self._monitor_shutdown()) + server_task = asyncio.create_task(server.serve()) + + # Wait for either server to complete or shutdown signal + done, pending = await asyncio.wait([server_task, shutdown_task], return_when=asyncio.FIRST_COMPLETED) + + # Cancel pending tasks + for task in pending: + task.cancel() + import contextlib + + with contextlib.suppress(asyncio.CancelledError): + await task + + except Exception as e: + log.error(f"Error in dashboard worker: {e}") + finally: + # Clean up + if self.log_processor_task and not self.log_processor_task.done(): + self.log_processor_task.cancel() + import contextlib + + with contextlib.suppress(asyncio.CancelledError): + await self.log_processor_task + + async def _process_log_queue(self) -> None: + """Process log messages from the queue and forward to memory handler.""" + while True: + try: + # Check for log messages + while not self.log_queue.empty(): + try: + log_msg = self.log_queue.get_nowait() + if self.memory_log_handler: + # Create a dummy log record and emit it + record = logging.LogRecord( + name="forwarded", level=logging.INFO, pathname="", lineno=0, msg=log_msg, args=(), exc_info=None + ) + self.memory_log_handler.emit(record) + except Exception: + break + + # Small delay to avoid busy waiting + await asyncio.sleep(0.1) + + except asyncio.CancelledError: + break + except Exception as e: + log.error(f"Error processing log queue: {e}") + await asyncio.sleep(1) + + async def _monitor_shutdown(self) -> None: + """Monitor for shutdown signals.""" + while True: + try: + # Check for shutdown signal + if not self.shutdown_queue.empty(): + self.shutdown_queue.get_nowait() + log.info("Dashboard shutdown signal received") + break + + await asyncio.sleep(0.5) + + except asyncio.CancelledError: + break + except Exception as e: + log.error(f"Error monitoring shutdown: {e}") + await asyncio.sleep(1) + + def _handle_shutdown(self) -> None: + """Handle shutdown request from dashboard UI.""" + log.info("Dashboard UI shutdown requested") + # Signal shutdown to the main process through the parent dashboard + if self.shutdown_queue is not None: + try: + self.shutdown_queue.put_nowait(True) + log.info("Sent shutdown signal to main process via queue") + except Exception as e: + log.error(f"Failed to send shutdown signal: {e}") + + # Also try the global shutdown as fallback + try: + call_global_shutdown() + except Exception as e: + log.error(f"Global shutdown failed: {e}") + + # Force exit this dashboard process + import os + import threading + + def delayed_exit() -> None: + import time + + time.sleep(0.5) # Give time for shutdown signal to be sent + log.info("Dashboard process forcing exit") + os._exit(0) + + threading.Thread(target=delayed_exit, daemon=True).start() + + class SerenaAgentWorker: """Worker process that hosts the actual SerenaAgent.""" def __init__(self, conn: Connection): self.conn = conn self.agent: SerenaAgent | None = None + self._shutdown_requested = False def run(self) -> None: """Main worker loop - runs in separate process.""" try: + # Set up signal handler for clean shutdown + signal.signal(signal.SIGTERM, self._signal_handler) + signal.signal(signal.SIGINT, self._signal_handler) + log.info("SerenaAgent worker process started") - while True: + while not self._shutdown_requested: try: - # Block until request received or connection closed - request = self.conn.recv() - if request is None: # Shutdown signal - break + # Use polling to avoid blocking indefinitely + if self.conn.poll(timeout=0.5): # Poll every 500ms + try: + request = self.conn.recv() + if request is None: # Explicit shutdown signal + break - response = self._handle_request(request) - self.conn.send(response) + response = self._handle_request(request) + self.conn.send(response) + except EOFError: + # Connection closed - parent process terminated + log.info("Connection closed, worker shutting down") + break + except (BrokenPipeError, ConnectionResetError): + # Connection broken, can't communicate + log.info("Connection broken, worker shutting down") + break + except Exception as e: + log.error(f"Error processing request: {e}") + try: + response = { + "error": f"Worker process error: {e!s}", + "traceback": traceback.format_exc(), + } + self.conn.send(response) + except (EOFError, BrokenPipeError, ConnectionResetError): + # Connection is broken, can't send error response + log.info("Connection broken during error response, shutting down") + break + # Continue polling if no data available - except EOFError: - # Connection closed - parent process terminated - log.info("Connection closed, worker shutting down") - break except Exception as e: - log.error(f"Error in worker process: {e}") - try: - response = { - "error": f"Worker process error: {e!s}", - "traceback": traceback.format_exc(), - } - self.conn.send(response) - except (EOFError, BrokenPipeError): - # Connection is broken, can't send error response - break + log.error(f"Error in worker main loop: {e}") + break except Exception as e: log.error(f"Fatal error in worker process: {e}") @@ -97,6 +383,11 @@ class SerenaAgentWorker: self._cleanup() log.info("SerenaAgent worker process stopped") + def _signal_handler(self, signum: int, frame: Any) -> None: + """Handle shutdown signals gracefully.""" + log.info(f"Received signal {signum}, initiating shutdown") + self._shutdown_requested = True + def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]: """Handle a single request.""" try: @@ -351,6 +642,8 @@ class ProcessIsolatedSerenaAgent: init_params["modes"] = None # project_activation_callback cannot be serialized across processes init_params["project_activation_callback"] = None + # Disable web dashboard in worker process - it will run in MCP process instead + init_params["enable_web_dashboard"] = False self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, init_params) except Exception as e: @@ -371,20 +664,28 @@ class ProcessIsolatedSerenaAgent: if self.conn is not None: self.conn.close() - # Wait for process to terminate + # Wait for process to terminate with shorter timeout if self.process.is_alive(): - self.process.join(timeout=10.0) + self.process.join(timeout=3.0) # Reduced from 10s to 3s if self.process.is_alive(): - log.warning("Worker process did not terminate gracefully, forcing termination") + log.warning("Worker process did not terminate gracefully, sending SIGTERM") self.process.terminate() - self.process.join(timeout=5.0) + self.process.join(timeout=2.0) # Reduced from 5s to 2s if self.process.is_alive(): - log.error("Worker process could not be terminated, killing it") + log.error("Worker process did not respond to SIGTERM, sending SIGKILL") self.process.kill() - self.process.join() + self.process.join(timeout=1.0) # Add timeout for kill as well + if self.process.is_alive(): + log.error("Worker process could not be killed - forcing cleanup") + + except KeyboardInterrupt: + log.warning("Keyboard interrupt during shutdown - forcing termination") + if self.process and self.process.is_alive(): + self.process.kill() + self.process.join(timeout=1.0) except Exception as e: log.error(f"Error stopping worker process: {e}") From cbf4933c04959f423f1d037199c514d50d3cd775 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 20:47:36 +0200 Subject: [PATCH 054/174] Forwarding logs from agent process, WIP --- src/serena/dashboard.py | 4 + src/serena/mcp.py | 61 +++++++-------- src/serena/process_isolated_agent.py | 106 +++++++++++---------------- 3 files changed, 72 insertions(+), 99 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 549c003..63bf79a 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -78,6 +78,10 @@ class SerenaDashboardAPI: self._app = FastAPI(title="Serena Dashboard") self._setup_routes() + @property + def memory_log_handler(self) -> MemoryLogHandler: + return self._memory_log_handler + def _setup_routes(self) -> None: self._app.mount("/dashboard", StaticFiles(directory=SERENA_DASHBOARD_DIR), name="dashboard") diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 6c91d0b..ff4d4c8 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -2,13 +2,14 @@ The Serena Model Context Protocol (MCP) Server """ +import contextlib import sys from collections.abc import AsyncIterator, Sequence from contextlib import asynccontextmanager from dataclasses import dataclass from logging import Formatter, Logger, StreamHandler from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal +from typing import Any, Literal import click # Add click import import docstring_parser @@ -21,10 +22,7 @@ from sensai.util.helper import mark_used from serena.agent import SerenaAgent, ToolInterface, create_serena_config, show_fatal_exception_safe from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES - -# Import for type annotations -if TYPE_CHECKING: - from serena.process_isolated_agent import ProcessIsolatedSerenaAgent +from serena.process_isolated_agent import ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -141,10 +139,7 @@ def create_mcp_server_and_agent( tool_timeout=tool_timeout, ) - # Create process-isolated agent with the configuration - from serena.process_isolated_agent import ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent - - process_agent = ProcessIsolatedSerenaAgent(serena_config=serena_config) + serena_agent_process = ProcessIsolatedSerenaAgent(serena_config=serena_config) except Exception as e: show_fatal_exception_safe(e) @@ -152,10 +147,10 @@ def create_mcp_server_and_agent( def update_tools() -> None: """Update the tools in the MCP server - adapted for process isolation.""" - nonlocal mcp, process_agent + nonlocal mcp, serena_agent_process # Check if process agent is running - if process_agent.process is None or not process_agent.process.is_alive(): + if serena_agent_process.process is None or not serena_agent_process.process.is_alive(): log.debug("Process agent not running yet, skipping tool update") return @@ -165,7 +160,7 @@ def create_mcp_server_and_agent( # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, # so we need to register all tools at startup, unfortunately. try: - tool_names = process_agent.get_exposed_tool_names() + tool_names = serena_agent_process.get_exposed_tool_names() except Exception as e: log.error(f"Failed to get tool names from process agent: {e}") return @@ -175,27 +170,27 @@ def create_mcp_server_and_agent( mcp._tool_manager._tools = {} for tool_name in tool_names: - process_isolated_tool = ProcessIsolatedTool(process_agent=process_agent, tool_name=tool_name) + process_isolated_tool = ProcessIsolatedTool(process_agent=serena_agent_process, tool_name=tool_name) mcp_tool = make_tool(process_isolated_tool) mcp._tool_manager._tools[tool_name] = mcp_tool @asynccontextmanager async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" - nonlocal process_agent + nonlocal serena_agent_process import asyncio import webbrowser mark_used(mcp_server) - dashboard: ProcessIsolatedDashboard | None = None + serena_dashboard_process: ProcessIsolatedDashboard | None = None shutdown_event = asyncio.Event() async def monitor_worker_process() -> None: """Monitor the worker process and shutdown server if it exits.""" while not shutdown_event.is_set(): try: - if process_agent.process is None or not process_agent.process.is_alive(): + if serena_agent_process.process is None or not serena_agent_process.process.is_alive(): log.info("Worker process has exited, shutting down MCP server") shutdown_event.set() break @@ -208,14 +203,14 @@ def create_mcp_server_and_agent( async def monitor_dashboard_shutdown() -> None: """Monitor the dashboard shutdown queue.""" - if dashboard is None or dashboard.shutdown_queue is None: + if serena_dashboard_process is None or serena_dashboard_process.shutdown_queue is None: return while not shutdown_event.is_set(): try: # Check if dashboard sent shutdown signal - if not dashboard.shutdown_queue.empty(): - dashboard.shutdown_queue.get_nowait() + if not serena_dashboard_process.shutdown_queue.empty(): + serena_dashboard_process.shutdown_queue.get_nowait() log.info("Dashboard shutdown signal received via queue") shutdown_event.set() # Force immediate exit since MCP server doesn't respond properly @@ -247,27 +242,26 @@ def create_mcp_server_and_agent( os._exit(0) try: - process_agent.start() # Set up global shutdown function for dashboard from serena.process_isolated_agent import set_global_shutdown_func set_global_shutdown_func(shutdown_server) - # Update tools now that the process agent is running - update_tools() - # Start process-isolated dashboard if enabled if serena_config.web_dashboard: try: - tool_names = process_agent.get_exposed_tool_names() - dashboard = ProcessIsolatedDashboard(tool_names) - port = dashboard.start() + serena_dashboard_process = ProcessIsolatedDashboard(tool_names=[]) + port = serena_dashboard_process.start() webbrowser.open(f"http://localhost:{port}/dashboard/index.html") log.info(f"Dashboard started on port {port}") except Exception as e: log.error(f"Failed to start dashboard: {e}") - dashboard = None + serena_dashboard_process = None + + serena_agent_process.start() + # Update tools now that the process agent is running + update_tools() # Start monitoring tasks tasks = [] @@ -275,7 +269,7 @@ def create_mcp_server_and_agent( tasks.append(monitor_task) # Start dashboard shutdown monitor if dashboard is running - if dashboard is not None: + if serena_dashboard_process is not None: dashboard_monitor_task = asyncio.create_task(monitor_dashboard_shutdown()) tasks.append(dashboard_monitor_task) @@ -314,9 +308,6 @@ def create_mcp_server_and_agent( finally: log.info("Starting server shutdown cleanup") - # Cancel monitoring tasks - import contextlib - if "tasks" in locals(): for task in tasks: task.cancel() @@ -324,15 +315,15 @@ def create_mcp_server_and_agent( await asyncio.wait_for(task, timeout=1.0) # Stop dashboard first - if dashboard is not None: + if serena_dashboard_process is not None: try: - dashboard.stop() + serena_dashboard_process.stop() except Exception as e: log.error(f"Error stopping dashboard: {e}") # Stop process agent try: - process_agent.stop() + serena_agent_process.stop() except Exception as e: log.error(f"Error stopping process agent: {e}") @@ -343,7 +334,7 @@ def create_mcp_server_and_agent( update_tools() - return mcp, process_agent + return mcp, serena_agent_process class ProjectType(click.ParamType): diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index a9b59a7..7b16767 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -8,16 +8,20 @@ This module provides: """ import asyncio +import contextlib import logging import multiprocessing import signal import traceback from collections.abc import Callable from enum import StrEnum +from logging.handlers import QueueHandler from multiprocessing.connection import Connection from typing import Any, Literal, Self +import uvicorn from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata +from sensai.util.logging import LOG_DEFAULT_FORMAT from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry from serena.config import SerenaAgentContext, SerenaAgentMode @@ -25,6 +29,8 @@ from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI log = logging.getLogger(__name__) +_global_log_q: "multiprocessing.Queue[str]" = multiprocessing.Queue() + # Global shutdown function that can be called by the dashboard class _GlobalShutdownRegistry: @@ -62,7 +68,6 @@ class ProcessIsolatedDashboard: def __init__(self, tool_names: list[str]): self.tool_names = tool_names self.process: multiprocessing.Process | None = None - self.log_queue: multiprocessing.Queue[str] | None = None self.shutdown_queue: multiprocessing.Queue[bool] | None = None self.port_queue: multiprocessing.Queue[int] | None = None self._log_handler: ProcessDashboardLogHandler | None = None @@ -75,25 +80,21 @@ class ProcessIsolatedDashboard: log.info("Starting process-isolated dashboard") # Create communication queues - self.log_queue = multiprocessing.Queue() self.shutdown_queue = multiprocessing.Queue() self.port_queue = multiprocessing.Queue() # Create and start dashboard process - dashboard_worker = DashboardWorker( - log_queue=self.log_queue, shutdown_queue=self.shutdown_queue, port_queue=self.port_queue, tool_names=self.tool_names - ) + dashboard_worker = DashboardWorker(shutdown_queue=self.shutdown_queue, port_queue=self.port_queue, tool_names=self.tool_names) self.process = multiprocessing.Process(target=dashboard_worker.run) self.process.start() # Set up log handler to send logs to dashboard process - self._log_handler = ProcessDashboardLogHandler(self.log_queue) - logging.getLogger().addHandler(self._log_handler) + logging.Logger.root.addHandler(ProcessDashboardLogHandler()) # Wait for dashboard to start and return port try: port = self.port_queue.get(timeout=10) # Wait up to 10 seconds for startup - log.info(f"Dashboard started on port {port}") + log.debug(f"Dashboard started on port {port}") return port except Exception as e: self.stop() @@ -107,11 +108,6 @@ class ProcessIsolatedDashboard: log.info("Stopping process-isolated dashboard") try: - # Remove log handler - if self._log_handler is not None: - logging.getLogger().removeHandler(self._log_handler) - self._log_handler = None - # Signal shutdown if self.shutdown_queue is not None: import contextlib @@ -138,7 +134,6 @@ class ProcessIsolatedDashboard: finally: self.process = None - self.log_queue = None self.shutdown_queue = None self.port_queue = None @@ -153,19 +148,14 @@ class ProcessIsolatedDashboard: class ProcessDashboardLogHandler(logging.Handler): """Log handler that sends log messages to dashboard process via queue.""" - def __init__(self, log_queue: "multiprocessing.Queue[str]", level: int = logging.NOTSET): + def __init__(self, level: int = logging.NOTSET): super().__init__(level=level) - self.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s")) - self.log_queue = log_queue + self.setFormatter(logging.Formatter(LOG_DEFAULT_FORMAT)) def emit(self, record: logging.LogRecord) -> None: - try: - msg = self.format(record) - # Non-blocking put to avoid deadlocks - self.log_queue.put_nowait(msg) - except Exception: - # Silently ignore queue errors to avoid recursive logging - pass + msg = self.format(record) + # Non-blocking put to avoid deadlocks + _global_log_q.put_nowait(msg) class DashboardWorker: @@ -173,18 +163,13 @@ class DashboardWorker: def __init__( self, - log_queue: "multiprocessing.Queue[str]", shutdown_queue: "multiprocessing.Queue[bool]", port_queue: "multiprocessing.Queue[int]", tool_names: list[str], ): - self.log_queue = log_queue self.shutdown_queue = shutdown_queue self.port_queue = port_queue self.tool_names = tool_names - self.memory_log_handler: MemoryLogHandler | None = None - self.dashboard_api: SerenaDashboardAPI | None = None - self.log_processor_task: asyncio.Task | None = None def run(self) -> None: """Main dashboard worker loop - runs in separate process.""" @@ -198,28 +183,21 @@ class DashboardWorker: async def _async_main(self) -> None: """Main async loop for dashboard process.""" + log_processor_task = None try: - # Set up memory log handler - self.memory_log_handler = MemoryLogHandler() - # Set up dashboard API with shutdown callback - self.dashboard_api = SerenaDashboardAPI( - memory_log_handler=self.memory_log_handler, tool_names=self.tool_names, shutdown_callback=self._handle_shutdown + dashboard_api = SerenaDashboardAPI( + memory_log_handler=MemoryLogHandler(), tool_names=self.tool_names, shutdown_callback=self._handle_shutdown ) # Start log processor task - self.log_processor_task = asyncio.create_task(self._process_log_queue()) + log_processor_task = asyncio.create_task(self._process_log_queue(dashboard_api)) # Find free port and signal to parent - port = self.dashboard_api._find_first_free_port(0x5EDA) + port = dashboard_api._find_first_free_port(0x5EDA) self.port_queue.put(port) - # Start dashboard server - import uvicorn - - config = uvicorn.Config( - app=self.dashboard_api._app, host="0.0.0.0", port=port, workers=1, log_config=None, log_level="critical" - ) + config = uvicorn.Config(app=dashboard_api._app, host="0.0.0.0", port=port, workers=1, log_config=None, log_level="critical") server = uvicorn.Server(config) # Start server and shutdown monitor concurrently @@ -228,12 +206,9 @@ class DashboardWorker: # Wait for either server to complete or shutdown signal done, pending = await asyncio.wait([server_task, shutdown_task], return_when=asyncio.FIRST_COMPLETED) - # Cancel pending tasks for task in pending: task.cancel() - import contextlib - with contextlib.suppress(asyncio.CancelledError): await task @@ -241,33 +216,31 @@ class DashboardWorker: log.error(f"Error in dashboard worker: {e}") finally: # Clean up - if self.log_processor_task and not self.log_processor_task.done(): - self.log_processor_task.cancel() - import contextlib + if log_processor_task and not log_processor_task.done(): + log_processor_task.cancel() with contextlib.suppress(asyncio.CancelledError): - await self.log_processor_task + await log_processor_task - async def _process_log_queue(self) -> None: + @staticmethod + async def _process_log_queue(dashboard_api: SerenaDashboardAPI) -> None: """Process log messages from the queue and forward to memory handler.""" while True: try: # Check for log messages - while not self.log_queue.empty(): + while not _global_log_q.empty(): try: - log_msg = self.log_queue.get_nowait() - if self.memory_log_handler: - # Create a dummy log record and emit it - record = logging.LogRecord( - name="forwarded", level=logging.INFO, pathname="", lineno=0, msg=log_msg, args=(), exc_info=None - ) - self.memory_log_handler.emit(record) - except Exception: - break + log_msg = _global_log_q.get_nowait() + record = logging.LogRecord( + name="forwarded", level=logging.INFO, pathname="", lineno=0, msg=log_msg, args=(), exc_info=None + ) + dashboard_api.memory_log_handler.emit(record) + except Exception as e: + log.error(f"Error processing log message: {e}") + await asyncio.sleep(1) # Small delay to avoid busy waiting await asyncio.sleep(0.1) - except asyncio.CancelledError: break except Exception as e: @@ -331,8 +304,13 @@ class SerenaAgentWorker: self.agent: SerenaAgent | None = None self._shutdown_requested = False - def run(self) -> None: + def run(self, q: "multiprocessing.Queue[str]") -> None: """Main worker loop - runs in separate process.""" + qh = QueueHandler(q) + root = logging.getLogger() + root.setLevel(logging.DEBUG) + root.addHandler(qh) + try: # Set up signal handler for clean shutdown signal.signal(signal.SIGTERM, self._signal_handler) @@ -623,9 +601,9 @@ class ProcessIsolatedSerenaAgent: parent_conn, child_conn = multiprocessing.Pipe() self.conn = parent_conn - # Create and start worker process + # Create and start worker process, passing along the dashboard's queue if available worker = SerenaAgentWorker(child_conn) - self.process = multiprocessing.Process(target=worker.run) + self.process = multiprocessing.Process(target=worker.run, args=[_global_log_q]) self.process.start() # Initialize the agent in the worker process From beb6b4e67de4a2e6c4c06ca19e66390ed6afe221 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 00:52:00 +0200 Subject: [PATCH 055/174] Simplify dashboard process management --- src/serena/dashboard.py | 14 +- src/serena/mcp.py | 224 ++++--------- src/serena/process_isolated_agent.py | 484 +++++++++------------------ 3 files changed, 224 insertions(+), 498 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 63bf79a..22e3495 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -1,7 +1,6 @@ import os import queue import socket -import sys import threading from collections.abc import Callable @@ -98,21 +97,16 @@ class SerenaDashboardAPI: return ResponseToolNames(tool_names=self._tool_names) async def _shutdown(self) -> None: - print("Shutdown initiated by dashbaord ...", file=sys.stderr) log.info("Shutting down Serena") if self._shutdown_callback: self._shutdown_callback() else: # Try to use the global shutdown function from process_isolated_agent - try: - from serena.process_isolated_agent import call_global_shutdown + from serena.process_isolated_agent import request_global_shutdown - call_global_shutdown() - except ImportError: - # Fallback to the old behavior if not in process-isolated mode - # noinspection PyUnresolvedReferences - # noinspection PyProtectedMember - os._exit(0) + request_global_shutdown() + # noinspection PyProtectedMember + os._exit(0) @staticmethod def _find_first_free_port(start_port: int) -> int: diff --git a/src/serena/mcp.py b/src/serena/mcp.py index ff4d4c8..3551ec1 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -2,8 +2,13 @@ The Serena Model Context Protocol (MCP) Server """ +import asyncio import contextlib +import os +import signal import sys +import threading +import time from collections.abc import AsyncIterator, Sequence from contextlib import asynccontextmanager from dataclasses import dataclass @@ -11,7 +16,7 @@ from logging import Formatter, Logger, StreamHandler from pathlib import Path from typing import Any, Literal -import click # Add click import +import click import docstring_parser from mcp.server.fastmcp import server from mcp.server.fastmcp.server import FastMCP, Settings @@ -22,7 +27,13 @@ from sensai.util.helper import mark_used from serena.agent import SerenaAgent, ToolInterface, create_serena_config, show_fatal_exception_safe from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES -from serena.process_isolated_agent import ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent +from serena.process_isolated_agent import ( + ProcessIsolatedDashboard, + ProcessIsolatedSerenaAgent, + ProcessIsolatedTool, + global_shutdown_event, + request_global_shutdown, +) log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -103,7 +114,7 @@ def create_mcp_server_and_agent( log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, -) -> tuple[FastMCP, "ProcessIsolatedSerenaAgent"]: +) -> tuple[FastMCP, ProcessIsolatedSerenaAgent]: """ Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. @@ -127,7 +138,6 @@ def create_mcp_server_and_agent( modes_instances = [SerenaAgentMode.load(mode) for mode in modes] try: - # Create configuration without instantiating a full SerenaAgent serena_config = create_serena_config( project=project, context=context_instance, @@ -138,9 +148,12 @@ def create_mcp_server_and_agent( trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, ) - serena_agent_process = ProcessIsolatedSerenaAgent(serena_config=serena_config) + # Start process-isolated dashboard if enabled + serena_dashboard_process = None + if serena_config.web_dashboard: + serena_dashboard_process = ProcessIsolatedDashboard(tool_names=[]) except Exception as e: show_fatal_exception_safe(e) raise @@ -149,25 +162,13 @@ def create_mcp_server_and_agent( """Update the tools in the MCP server - adapted for process isolation.""" nonlocal mcp, serena_agent_process - # Check if process agent is running - if serena_agent_process.process is None or not serena_agent_process.process.is_alive(): - log.debug("Process agent not running yet, skipping tool update") - return - # Get tool names from process-isolated agent # Tools may change as a result of project activation. # NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not, # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, # so we need to register all tools at startup, unfortunately. - try: - tool_names = serena_agent_process.get_exposed_tool_names() - except Exception as e: - log.error(f"Failed to get tool names from process agent: {e}") - return - + tool_names = serena_agent_process.get_exposed_tool_names() if mcp is not None: - from serena.process_isolated_agent import ProcessIsolatedTool - mcp._tool_manager._tools = {} for tool_name in tool_names: process_isolated_tool = ProcessIsolatedTool(process_agent=serena_agent_process, tool_name=tool_name) @@ -177,163 +178,69 @@ def create_mcp_server_and_agent( @asynccontextmanager async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" - nonlocal serena_agent_process - import asyncio - import webbrowser - mark_used(mcp_server) - serena_dashboard_process: ProcessIsolatedDashboard | None = None - shutdown_event = asyncio.Event() + def signal_handler(signum: int, frame: Any) -> None: + log.info(f"Received signal {signum} in main process") + request_global_shutdown() - async def monitor_worker_process() -> None: - """Monitor the worker process and shutdown server if it exits.""" - while not shutdown_event.is_set(): - try: - if serena_agent_process.process is None or not serena_agent_process.process.is_alive(): - log.info("Worker process has exited, shutting down MCP server") - shutdown_event.set() - break - await asyncio.sleep(1) - except asyncio.CancelledError: - break - except Exception as e: - log.error(f"Error monitoring worker process: {e}") - await asyncio.sleep(1) + def force_exit() -> None: + time.sleep(2.0) # Wait 2 seconds for graceful shutdown + log.warning("Forcing exit after timeout") + os._exit(1) - async def monitor_dashboard_shutdown() -> None: - """Monitor the dashboard shutdown queue.""" - if serena_dashboard_process is None or serena_dashboard_process.shutdown_queue is None: - return + threading.Thread(target=force_exit, daemon=True).start() - while not shutdown_event.is_set(): - try: - # Check if dashboard sent shutdown signal - if not serena_dashboard_process.shutdown_queue.empty(): - serena_dashboard_process.shutdown_queue.get_nowait() - log.info("Dashboard shutdown signal received via queue") - shutdown_event.set() - # Force immediate exit since MCP server doesn't respond properly - import os - import sys + # Install signal handlers + sigint_singal = signal.signal(signal.SIGINT, signal_handler) + sigterm_signal = signal.signal(signal.SIGTERM, signal_handler) - log.info("Forcing immediate process exit from dashboard signal") - sys.stdout.flush() - sys.stderr.flush() - os._exit(0) - await asyncio.sleep(0.1) # Check more frequently - except asyncio.CancelledError: - break - except Exception as e: - log.error(f"Error monitoring dashboard shutdown: {e}") - await asyncio.sleep(1) + if serena_dashboard_process is not None: + log.info("Starting dashboard process") + serena_dashboard_process.start() + log.info("Starting serena agent process") + serena_agent_process.start() + update_tools() - def shutdown_server() -> None: - """Shutdown the server.""" - log.info("Shutdown initiated by dashboard") - shutdown_event.set() - # Force immediate exit since the MCP server doesn't respond to shutdown events - import os - import sys + async def monitor_global_shutdown() -> None: + """Monitor the global shutdown event and trigger local shutdown.""" + while not global_shutdown_event.is_set(): + # Poll the multiprocessing Event in async context + await asyncio.sleep(0.1) + continue + log.info("Global shutdown event detected, initiating server shutdown") + request_global_shutdown() + # Send SIGTERM to self to trigger graceful shutdown + os.kill(os.getpid(), signal.SIGTERM) - log.info("Dashboard shutdown - forcing process exit immediately") - sys.stdout.flush() - sys.stderr.flush() - os._exit(0) + # Start monitoring task + monitor_task = asyncio.create_task(monitor_global_shutdown()) try: - - # Set up global shutdown function for dashboard - from serena.process_isolated_agent import set_global_shutdown_func - - set_global_shutdown_func(shutdown_server) - - # Start process-isolated dashboard if enabled - if serena_config.web_dashboard: - try: - serena_dashboard_process = ProcessIsolatedDashboard(tool_names=[]) - port = serena_dashboard_process.start() - webbrowser.open(f"http://localhost:{port}/dashboard/index.html") - log.info(f"Dashboard started on port {port}") - except Exception as e: - log.error(f"Failed to start dashboard: {e}") - serena_dashboard_process = None - - serena_agent_process.start() - # Update tools now that the process agent is running - update_tools() - - # Start monitoring tasks - tasks = [] - monitor_task = asyncio.create_task(monitor_worker_process()) - tasks.append(monitor_task) - - # Start dashboard shutdown monitor if dashboard is running - if serena_dashboard_process is not None: - dashboard_monitor_task = asyncio.create_task(monitor_dashboard_shutdown()) - tasks.append(dashboard_monitor_task) - - # Set up signal handlers in main process - def signal_handler(signum: int, frame: Any) -> None: - log.info(f"Received signal {signum} in main process") - shutdown_event.set() - # Give a short delay for graceful shutdown, then force exit - import os - import threading - - def force_exit() -> None: - import time - - time.sleep(2.0) # Wait 2 seconds for graceful shutdown - log.warning("Forcing exit after timeout") - os._exit(1) - - threading.Thread(target=force_exit, daemon=True).start() - - import signal - - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGINT, signal_handler) - - try: - # Start the server and wait for shutdown - yield - except (KeyboardInterrupt, SystemExit): - log.info("Received shutdown signal") - shutdown_event.set() - + yield + except (KeyboardInterrupt, SystemExit): + log.info("Received shutdown signal") + request_global_shutdown() except Exception as e: log.error(f"Error in server lifespan: {e}") - shutdown_event.set() + request_global_shutdown() finally: - log.info("Starting server shutdown cleanup") + # Cancel monitor task + monitor_task.cancel() - if "tasks" in locals(): - for task in tasks: - task.cancel() - with contextlib.suppress(TimeoutError, asyncio.CancelledError): - await asyncio.wait_for(task, timeout=1.0) + with contextlib.suppress(asyncio.CancelledError): + await monitor_task - # Stop dashboard first + serena_agent_process.stop() if serena_dashboard_process is not None: - try: - serena_dashboard_process.stop() - except Exception as e: - log.error(f"Error stopping dashboard: {e}") - - # Stop process agent - try: - serena_agent_process.stop() - except Exception as e: - log.error(f"Error stopping process agent: {e}") - - log.info("Server shutdown cleanup completed") + serena_dashboard_process.stop() + request_global_shutdown() + log.info("Shutting down all processes") + signal.signal(signal.SIGINT, sigint_singal) + signal.signal(signal.SIGTERM, sigterm_signal) mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) mcp = FastMCP(**mcp_settings.model_dump()) - - update_tools() - return mcp, serena_agent_process @@ -494,9 +401,6 @@ def start_mcp_server( f"Used path: {project_file}" ) - log.info( - f"Starting process-isolated serena agent in MCP server with config:\n{agent.serena_config}." - f"\n Log level: {agent.serena_config.log_level}" - ) + log.info(f"Starting MCP server with config:\n{agent.serena_config}.\n Log level: {agent.serena_config.log_level}") mcp_server.run(transport=transport) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 7b16767..768e45f 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -1,27 +1,20 @@ -""" -Process-isolated SerenaAgent to prevent asyncio event loop contamination between MCP server and language server. - -This module provides: -1. ProcessIsolatedSerenaAgent - A wrapper that runs SerenaAgent in a separate process -2. SerenaAgentWorker - The worker process that hosts the actual SerenaAgent -3. JSON-RPC based IPC for communication between processes -""" - import asyncio import contextlib import logging import multiprocessing -import signal +import os import traceback +import webbrowser from collections.abc import Callable from enum import StrEnum from logging.handlers import QueueHandler from multiprocessing.connection import Connection +from multiprocessing.sharedctypes import Synchronized +from multiprocessing.synchronize import Event as EventClass from typing import Any, Literal, Self import uvicorn from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata -from sensai.util.logging import LOG_DEFAULT_FORMAT from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry from serena.config import SerenaAgentContext, SerenaAgentMode @@ -29,296 +22,182 @@ from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI log = logging.getLogger(__name__) -_global_log_q: "multiprocessing.Queue[str]" = multiprocessing.Queue() +# Global synchronization primitives +_global_log_queue: multiprocessing.Queue = multiprocessing.Queue() +_dashboard_ready_event = multiprocessing.Event() +_dashboard_port_value = multiprocessing.Value("i", 0) +global_shutdown_event = multiprocessing.Event() -# Global shutdown function that can be called by the dashboard -class _GlobalShutdownRegistry: - """Registry for global shutdown function.""" - - def __init__(self) -> None: - self.shutdown_func: Callable[[], None] | None = None - - def set_shutdown_func(self, func: Callable[[], None]) -> None: - """Set the global shutdown function.""" - self.shutdown_func = func - - def call_shutdown(self) -> None: - """Call the global shutdown function if it exists.""" - if self.shutdown_func: - self.shutdown_func() +def request_global_shutdown() -> None: + """Signal the global shutdown event.""" + global_shutdown_event.set() + log.info("Global shutdown event set") -_shutdown_registry = _GlobalShutdownRegistry() +def _dashboard_worker( + tool_names: list[str], + log_q: "multiprocessing.Queue[Any]", + dashboard_ready_event: EventClass, + port_value: "Synchronized[int]", + shutdown_evt: EventClass, +) -> None: + """Entry point for the dashboard process.""" + # Route all logging to the shared queue + root = logging.getLogger() + root.handlers.clear() + root.setLevel(logging.DEBUG) + root.addHandler(QueueHandler(log_q)) + + async def _process_logs(api: SerenaDashboardAPI) -> None: + while not shutdown_evt.is_set(): + while not log_q.empty(): + record = log_q.get_nowait() + if record is None: + break + api.memory_log_handler.emit(record) + # Small delay to avoid busy waiting + await asyncio.sleep(0.1) + + async def _monitor_shutdown() -> None: + # Poll the multiprocessing Event in async context + # Check every 100ms until shutdown is requested + loop = asyncio.get_event_loop() + while True: + # Check in executor to avoid blocking + result = await loop.run_in_executor(None, shutdown_evt.is_set) + if result: + break + await asyncio.sleep(0.1) + + async def _async_main() -> None: + api = SerenaDashboardAPI( + memory_log_handler=MemoryLogHandler(), + tool_names=tool_names, + shutdown_callback=shutdown_evt.set, + ) + # Pick a free port and signal readiness + port = api._find_first_free_port(0x5EDA) + port_value.value = port + + # Start server first, then log processing + config = uvicorn.Config( + app=api._app, + host="0.0.0.0", + port=port, + workers=1, + log_config=None, + log_level="critical", + ) + server = uvicorn.Server(config) + + # Serve and await shutdown signal + server_task = asyncio.create_task(server.serve()) + shutdown_task = asyncio.create_task(_monitor_shutdown()) + logging_loop_task = asyncio.create_task(_process_logs(api)) + dashboard_ready_event.set() + + done, pending = await asyncio.wait( + [server_task, shutdown_task, logging_loop_task], + return_when=asyncio.FIRST_COMPLETED, + ) + if shutdown_task in done: + server.should_exit = True + await server_task + + # Cancel remaining tasks + for task in pending: + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task + + try: + asyncio.run(_async_main()) + except BaseException: + logging.exception("Dashboard worker crashed") + finally: + logging.info("Dashboard worker exiting") -def set_global_shutdown_func(func: Callable[[], None]) -> None: - """Set the global shutdown function.""" - _shutdown_registry.set_shutdown_func(func) - - -def call_global_shutdown() -> None: - """Call the global shutdown function if it exists.""" - _shutdown_registry.call_shutdown() +def _shutdown_process(proc: multiprocessing.Process, timeout: float = 1.0) -> None: + """Helper to shutdown a process gracefully.""" + if proc.is_alive(): + proc.terminate() + proc.join(timeout=timeout) + if proc.is_alive(): + log.error("Process did not terminate gracefully, forcing kill") + proc.kill() + proc.join(timeout=timeout) + if proc.is_alive(): + log.error("Process did not respond to kill") class ProcessIsolatedDashboard: - """Process-isolated dashboard to prevent asyncio contamination with MCP server and worker.""" + """Dashboard running in a separate process to avoid asyncio contamination.""" def __init__(self, tool_names: list[str]): self.tool_names = tool_names self.process: multiprocessing.Process | None = None - self.shutdown_queue: multiprocessing.Queue[bool] | None = None - self.port_queue: multiprocessing.Queue[int] | None = None - self._log_handler: ProcessDashboardLogHandler | None = None - def start(self) -> int: - """Start the dashboard process and return the port number.""" + def start(self, timeout: float = 10.0) -> None: + """Start the dashboard process.""" if self.process is not None: - raise RuntimeError("Dashboard process already started") + raise RuntimeError("Dashboard already started") - log.info("Starting process-isolated dashboard") - - # Create communication queues - self.shutdown_queue = multiprocessing.Queue() - self.port_queue = multiprocessing.Queue() - - # Create and start dashboard process - dashboard_worker = DashboardWorker(shutdown_queue=self.shutdown_queue, port_queue=self.port_queue, tool_names=self.tool_names) - self.process = multiprocessing.Process(target=dashboard_worker.run) + self.process = multiprocessing.Process( + target=_dashboard_worker, + args=(self.tool_names, _global_log_queue, _dashboard_ready_event, _dashboard_port_value, global_shutdown_event), + daemon=True, + ) self.process.start() - # Set up log handler to send logs to dashboard process - logging.Logger.root.addHandler(ProcessDashboardLogHandler()) + if not _dashboard_ready_event.wait(timeout): + self.process.terminate() + self.process.join(timeout=1.0) + self.process = None + raise RuntimeError("Dashboard failed to start within timeout") - # Wait for dashboard to start and return port - try: - port = self.port_queue.get(timeout=10) # Wait up to 10 seconds for startup - log.debug(f"Dashboard started on port {port}") - return port - except Exception as e: - self.stop() - raise RuntimeError(f"Failed to start dashboard: {e}") from e + port = _dashboard_port_value.value + log.info(f"Dashboard started on port {port}") + webbrowser.open(f"http://localhost:{port}/dashboard/index.html") - def stop(self) -> None: - """Stop the dashboard process.""" + def stop(self, timeout: float = 1.0) -> None: + """Signal shutdown and wait for the dashboard process to exit.""" if self.process is None: return - log.info("Stopping process-isolated dashboard") - - try: - # Signal shutdown - if self.shutdown_queue is not None: - import contextlib - - with contextlib.suppress(Exception): - self.shutdown_queue.put_nowait(True) - - # Wait for process to terminate - if self.process.is_alive(): - self.process.join(timeout=5.0) - - if self.process.is_alive(): - log.warning("Dashboard process did not terminate gracefully, forcing termination") - self.process.terminate() - self.process.join(timeout=3.0) - - if self.process.is_alive(): - log.error("Dashboard process could not be terminated, killing it") - self.process.kill() - self.process.join() - - except Exception as e: - log.error(f"Error stopping dashboard process: {e}") - - finally: - self.process = None - self.shutdown_queue = None - self.port_queue = None - - log.info("Process-isolated dashboard stopped") - - def update_tool_names(self, tool_names: list[str]) -> None: - """Update tool names (for future enhancement).""" - self.tool_names = tool_names - # TODO: Could send update to dashboard process if needed - - -class ProcessDashboardLogHandler(logging.Handler): - """Log handler that sends log messages to dashboard process via queue.""" - - def __init__(self, level: int = logging.NOTSET): - super().__init__(level=level) - self.setFormatter(logging.Formatter(LOG_DEFAULT_FORMAT)) - - def emit(self, record: logging.LogRecord) -> None: - msg = self.format(record) - # Non-blocking put to avoid deadlocks - _global_log_q.put_nowait(msg) - - -class DashboardWorker: - """Worker process that hosts the dashboard with its own asyncio loop.""" - - def __init__( - self, - shutdown_queue: "multiprocessing.Queue[bool]", - port_queue: "multiprocessing.Queue[int]", - tool_names: list[str], - ): - self.shutdown_queue = shutdown_queue - self.port_queue = port_queue - self.tool_names = tool_names - - def run(self) -> None: - """Main dashboard worker loop - runs in separate process.""" - try: - log.info("Dashboard worker process started") - asyncio.run(self._async_main()) - except Exception as e: - log.error(f"Fatal error in dashboard worker process: {e}") - finally: - log.info("Dashboard worker process stopped") - - async def _async_main(self) -> None: - """Main async loop for dashboard process.""" - log_processor_task = None - try: - # Set up dashboard API with shutdown callback - dashboard_api = SerenaDashboardAPI( - memory_log_handler=MemoryLogHandler(), tool_names=self.tool_names, shutdown_callback=self._handle_shutdown - ) - - # Start log processor task - log_processor_task = asyncio.create_task(self._process_log_queue(dashboard_api)) - - # Find free port and signal to parent - port = dashboard_api._find_first_free_port(0x5EDA) - self.port_queue.put(port) - - config = uvicorn.Config(app=dashboard_api._app, host="0.0.0.0", port=port, workers=1, log_config=None, log_level="critical") - server = uvicorn.Server(config) - - # Start server and shutdown monitor concurrently - shutdown_task = asyncio.create_task(self._monitor_shutdown()) - server_task = asyncio.create_task(server.serve()) - - # Wait for either server to complete or shutdown signal - done, pending = await asyncio.wait([server_task, shutdown_task], return_when=asyncio.FIRST_COMPLETED) - # Cancel pending tasks - for task in pending: - task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await task - - except Exception as e: - log.error(f"Error in dashboard worker: {e}") - finally: - # Clean up - if log_processor_task and not log_processor_task.done(): - log_processor_task.cancel() - - with contextlib.suppress(asyncio.CancelledError): - await log_processor_task - - @staticmethod - async def _process_log_queue(dashboard_api: SerenaDashboardAPI) -> None: - """Process log messages from the queue and forward to memory handler.""" - while True: - try: - # Check for log messages - while not _global_log_q.empty(): - try: - log_msg = _global_log_q.get_nowait() - record = logging.LogRecord( - name="forwarded", level=logging.INFO, pathname="", lineno=0, msg=log_msg, args=(), exc_info=None - ) - dashboard_api.memory_log_handler.emit(record) - except Exception as e: - log.error(f"Error processing log message: {e}") - await asyncio.sleep(1) - - # Small delay to avoid busy waiting - await asyncio.sleep(0.1) - except asyncio.CancelledError: - break - except Exception as e: - log.error(f"Error processing log queue: {e}") - await asyncio.sleep(1) - - async def _monitor_shutdown(self) -> None: - """Monitor for shutdown signals.""" - while True: - try: - # Check for shutdown signal - if not self.shutdown_queue.empty(): - self.shutdown_queue.get_nowait() - log.info("Dashboard shutdown signal received") - break - - await asyncio.sleep(0.5) - - except asyncio.CancelledError: - break - except Exception as e: - log.error(f"Error monitoring shutdown: {e}") - await asyncio.sleep(1) - - def _handle_shutdown(self) -> None: - """Handle shutdown request from dashboard UI.""" - log.info("Dashboard UI shutdown requested") - # Signal shutdown to the main process through the parent dashboard - if self.shutdown_queue is not None: - try: - self.shutdown_queue.put_nowait(True) - log.info("Sent shutdown signal to main process via queue") - except Exception as e: - log.error(f"Failed to send shutdown signal: {e}") - - # Also try the global shutdown as fallback - try: - call_global_shutdown() - except Exception as e: - log.error(f"Global shutdown failed: {e}") - - # Force exit this dashboard process - import os - import threading - - def delayed_exit() -> None: - import time - - time.sleep(0.5) # Give time for shutdown signal to be sent - log.info("Dashboard process forcing exit") - os._exit(0) - - threading.Thread(target=delayed_exit, daemon=True).start() + log.info("Stopping dashboard process") + request_global_shutdown() + _shutdown_process(self.process, timeout=timeout) + self.process = None class SerenaAgentWorker: """Worker process that hosts the actual SerenaAgent.""" + class RequestMethod(StrEnum): + INITIALIZE = "initialize" + TOOL_CALL = "tool_call" + GET_ACTIVE_TOOL_NAMES = "get_active_tool_names" + IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running" + RESET_LANGUAGE_SERVER = "reset_language_server" + GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names" + SHUTDOWN = "shutdown" + def __init__(self, conn: Connection): self.conn = conn self.agent: SerenaAgent | None = None - self._shutdown_requested = False - def run(self, q: "multiprocessing.Queue[str]") -> None: + def run(self, log_queue: "multiprocessing.Queue[str]") -> None: """Main worker loop - runs in separate process.""" - qh = QueueHandler(q) + qh = QueueHandler(log_queue) root = logging.getLogger() root.setLevel(logging.DEBUG) root.addHandler(qh) + log.info("SerenaAgent worker process started") try: - # Set up signal handler for clean shutdown - signal.signal(signal.SIGTERM, self._signal_handler) - signal.signal(signal.SIGINT, self._signal_handler) - - log.info("SerenaAgent worker process started") - - while not self._shutdown_requested: + while not global_shutdown_event.is_set(): try: # Use polling to avoid blocking indefinitely if self.conn.poll(timeout=0.5): # Poll every 500ms @@ -354,17 +233,12 @@ class SerenaAgentWorker: except Exception as e: log.error(f"Error in worker main loop: {e}") break - except Exception as e: log.error(f"Fatal error in worker process: {e}") finally: self._cleanup() log.info("SerenaAgent worker process stopped") - - def _signal_handler(self, signum: int, frame: Any) -> None: - """Handle shutdown signals gracefully.""" - log.info(f"Received signal {signum}, initiating shutdown") - self._shutdown_requested = True + os._exit(0) # Exit without raising any further exceptions def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]: """Handle a single request.""" @@ -386,10 +260,9 @@ class SerenaAgentWorker: case self.RequestMethod.GET_EXPOSED_TOOL_NAMES: return self._get_exposed_tool_names() case self.RequestMethod.SHUTDOWN: - return self._shutdown() + return self.shutdown() case _: return {"error": f"Unknown method: {method}"} - except Exception as e: return {"error": str(e), "traceback": traceback.format_exc()} @@ -427,17 +300,6 @@ class SerenaAgentWorker: trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, ) - - # Set up global shutdown function for dashboard - def shutdown_worker() -> None: - log.info("Global shutdown function called") - self._cleanup() - import sys - - sys.exit(0) - - set_global_shutdown_func(shutdown_worker) - return {"result": "SerenaAgent initialized successfully"} except Exception as e: return {"error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} @@ -515,18 +377,15 @@ class SerenaAgentWorker: except Exception as e: return {"error": str(e), "traceback": traceback.format_exc()} - def _shutdown(self) -> dict[str, Any]: - """Handle shutdown request from dashboard.""" + def shutdown(self) -> dict[str, Any]: try: - log.info("Shutdown requested from dashboard") - # Clean up resources before exiting + log.info("Shutting down SerenaAgent worker process on request") self._cleanup() - # Exit the worker process - this will cause the main process to detect the termination - # and shut down gracefully through the server lifespan manager - import sys - - sys.exit(0) + request_global_shutdown() + # Return successful response before exiting + return {"result": "Shutdown initiated"} except Exception as e: + log.error(f"Error during shutdown: {e}") return {"error": str(e), "traceback": traceback.format_exc()} def _cleanup(self) -> None: @@ -539,17 +398,6 @@ class SerenaAgentWorker: log.error(f"Error stopping language server: {e}") self.agent = None - class RequestMethod(StrEnum): - """Enumeration of available request methods.""" - - INITIALIZE = "initialize" - TOOL_CALL = "tool_call" - GET_ACTIVE_TOOL_NAMES = "get_active_tool_names" - IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running" - RESET_LANGUAGE_SERVER = "reset_language_server" - GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names" - SHUTDOWN = "shutdown" - class ProcessIsolatedSerenaAgent: """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" @@ -603,7 +451,7 @@ class ProcessIsolatedSerenaAgent: # Create and start worker process, passing along the dashboard's queue if available worker = SerenaAgentWorker(child_conn) - self.process = multiprocessing.Process(target=worker.run, args=[_global_log_q]) + self.process = multiprocessing.Process(target=worker.run, args=[_global_log_queue]) self.process.start() # Initialize the agent in the worker process @@ -634,31 +482,13 @@ class ProcessIsolatedSerenaAgent: """Stop the worker process.""" if self.process is None: return - - log.info("Stopping process-isolated SerenaAgent") - + log.info("Stopping SerenaAgent process") try: # Close connection to signal worker to shutdown if self.conn is not None: self.conn.close() - - # Wait for process to terminate with shorter timeout - if self.process.is_alive(): - self.process.join(timeout=3.0) # Reduced from 10s to 3s - - if self.process.is_alive(): - log.warning("Worker process did not terminate gracefully, sending SIGTERM") - self.process.terminate() - self.process.join(timeout=2.0) # Reduced from 5s to 2s - - if self.process.is_alive(): - log.error("Worker process did not respond to SIGTERM, sending SIGKILL") - self.process.kill() - self.process.join(timeout=1.0) # Add timeout for kill as well - - if self.process.is_alive(): - log.error("Worker process could not be killed - forcing cleanup") - + _shutdown_process(self.process, timeout=2.0) + self.process = None except KeyboardInterrupt: log.warning("Keyboard interrupt during shutdown - forcing termination") if self.process and self.process.is_alive(): @@ -666,12 +496,11 @@ class ProcessIsolatedSerenaAgent: self.process.join(timeout=1.0) except Exception as e: log.error(f"Error stopping worker process: {e}") - finally: self.process = None self.conn = None - log.info("Process-isolated SerenaAgent stopped") + log.info("SerenaAgent stopped") def _make_request(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> dict[str, Any]: """Make a request to the worker process.""" @@ -693,8 +522,7 @@ class ProcessIsolatedSerenaAgent: timeout = self.serena_config.tool_timeout if self.conn.poll(timeout): try: - response = self.conn.recv() - return response + return self.conn.recv() except (EOFError, BrokenPipeError) as e: raise RuntimeError("Failed to receive response: worker process may have crashed") from e else: From 3d4892394a75fb01f14745ed401f7e753defde7c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 02:40:04 +0200 Subject: [PATCH 056/174] Fixed project activation in process-isolated agent --- src/serena/agent.py | 3 --- src/serena/mcp.py | 6 ++++-- src/serena/process_isolated_agent.py | 3 +++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index a169e2f..1933d8e 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -569,7 +569,6 @@ class MemoriesManagerMDFilesInProject(MemoriesManager): def create_serena_config( - project: str | None = None, serena_config: SerenaConfigBase | None = None, context: SerenaAgentContext | None = None, modes: list[SerenaAgentMode] | None = None, @@ -585,7 +584,6 @@ def create_serena_config( This function extracts the configuration creation logic from SerenaAgent.__init__ to allow creating configurations independently for process isolation and other use cases. - :param project: the project to register/activate or None :param serena_config: the base Serena configuration or None to read from default location :param context: the context in which the agent will operate :param modes: list of modes in which the agent will operate @@ -670,7 +668,6 @@ class SerenaAgent: """ # obtain serena configuration using the decoupled factory function self.serena_config = create_serena_config( - project=None, # Don't pass project here, handle activation separately serena_config=serena_config, context=context, modes=modes, diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 3551ec1..3eab295 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -137,9 +137,11 @@ def create_mcp_server_and_agent( context_instance = SerenaAgentContext.load(context) modes_instances = [SerenaAgentMode.load(mode) for mode in modes] + if project is not None: + log.info(f"Will use project at mcp server startup: {project}") + try: serena_config = create_serena_config( - project=project, context=context_instance, modes=modes_instances, enable_web_dashboard=enable_web_dashboard, @@ -148,7 +150,7 @@ def create_mcp_server_and_agent( trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, ) - serena_agent_process = ProcessIsolatedSerenaAgent(serena_config=serena_config) + serena_agent_process = ProcessIsolatedSerenaAgent(project=project, serena_config=serena_config) # Start process-isolated dashboard if enabled serena_dashboard_process = None diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 768e45f..f87856b 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -436,6 +436,7 @@ class ProcessIsolatedSerenaAgent: # Use the default config loader self.serena_config = SerenaConfig.from_config_file() self.process: multiprocessing.Process | None = None + self.project = project self.conn: Connection | None = None def start(self) -> None: @@ -456,12 +457,14 @@ class ProcessIsolatedSerenaAgent: # Initialize the agent in the worker process try: + # TODO: instead pass config and project to worker, use at startup of agent there # Prepare initialization parameters, converting complex objects to dict if present init_params = self._init_params.copy() if init_params["serena_config"] is not None and hasattr(init_params["serena_config"], "to_dict"): # Convert SerenaConfigBase to dict for serialization serena_config_obj = init_params["serena_config"] init_params["serena_config"] = serena_config_obj.to_dict() # type: ignore + # init_params["project"] = self.project # Note: context and modes are not serializable across processes, # so they will be None and SerenaAgent will use defaults init_params["context"] = None From 21de3c377eb106060db2f2dd3d11dcdfd2df1c38 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 02:58:32 +0200 Subject: [PATCH 057/174] Minor [ci skip] --- src/serena/mcp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 3eab295..326ff9b 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -99,7 +99,6 @@ def make_tool( fn_metadata=func_arg_metadata, is_async=is_async, context_kwarg=None, - annotations=None, ) From afea349d67805662e6dc9226ea4ea87a351eb28f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 13:51:57 +0200 Subject: [PATCH 058/174] CI, added some memories --- .github/workflows/pytest.yml | 7 +-- .../deadlock_analysis_mcp_symbolic_tools.md | 63 +++++++++++++++++++ .serena/memories/suggested_commands.md | 18 +++++- pyproject.toml | 9 +-- 4 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 .serena/memories/deadlock_analysis_mcp_symbolic_tools.md diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 6368dc2..64dd29f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -55,9 +55,4 @@ jobs: run: uv pip install -e ".[dev]" - name: Test with pytest shell: bash - run: | - if [ "${{ runner.os }}" = "Linux" ]; then - uv run pytest -vv -m "not java and not rust" - else - uv run pytest -vv - fi + run: uv run poe test \ No newline at end of file diff --git a/.serena/memories/deadlock_analysis_mcp_symbolic_tools.md b/.serena/memories/deadlock_analysis_mcp_symbolic_tools.md new file mode 100644 index 0000000..fa3fc20 --- /dev/null +++ b/.serena/memories/deadlock_analysis_mcp_symbolic_tools.md @@ -0,0 +1,63 @@ +# Deadlock Analysis: MCP Symbolic Tools Timeout Issue + +## Problem Summary +Intermittent deadlocks occur when using symbolic tools (particularly `find_symbol`) through MCP clients. Successful calls are followed by calls that hang for 240 seconds before timing out. Once a timeout occurs, the language server calls will no longer respond. + +## ROOT CAUSE DISCOVERED: Asyncio Event Loop Interference + +### Critical Evidence from MCP Server Logs +The smoking gun was found in the MCP server logs showing **unawaited coroutines**: + +``` +INFO mcp.server.lowlevel.server:_handle_message:524 - Warning: RuntimeWarning: coroutine 'LanguageServer.request_full_symbol_tree' was never awaited +``` + +This warning is being logged by the **MCP server's `_handle_message` method**, not SerenaAgent code, indicating coroutines are being created in the MCP server's asyncio context but never properly awaited. + +### The Real Issue: Dual Asyncio Context Contamination + +**Architecture Problem:** +1. **MCP Server**: Runs its own asyncio event loop to handle incoming requests +2. **SerenaAgent**: Creates its own asyncio event loop in separate thread for language serve, and even a second loop for the dashboard if so configured +3. **Conflict**: When MCP server calls SerenaAgent tools, we have **two or three asyncio contexts interacting** + +**Deadlock Mechanism:** +``` +MCP Server (asyncio loop A) + → handles tool request + → calls SerenaAgent.find_symbol() + → SerenaAgent uses asyncio.run_coroutine_threadsafe() + → Creates coroutine in language server loop (loop B) + → BUT: Coroutine gets leaked into MCP's context and never awaited + → Dangling coroutines accumulate in MCP server's loop + → Eventually causes resource exhaustion/event loop blocking + → Language server appears to "hang" but it's actually MCP loop contamination +``` + +### Discarded Hypotheses +- **Not a timeout issue**: Language server actually works fine in isolation +- **Not abandoned threads**: The threading mechanism works correctly +- **Not LSP deadlock**: The TypeScript language server itself isn't hanging + +The actual issue is **asyncio context bleeding** between MCP server and SerenaAgent. + +### Why We Can't Reproduce Outside MCP +- **Direct script calls**: No MCP server, no dual asyncio context +- **Single event loop**: Only SerenaAgent's language server loop exists +- **No async interference**: Clean, isolated execution +- **MCP-specific**: Requires the exact async context interaction pattern + +## Technical Details + +### Key Files and Locations +- `src/multilspy/language_server.py:1870` - `run_coroutine_threadsafe` creates coroutines that leak to MCP context +- MCP server `_handle_message` - Where unawaited coroutine warnings appear + +## Solution +**Process Isolation** is the fundamental fix: +1. **Separate processes**: MCP server, SerenaAgent and Dashboard in different processes +2. **IPC communication**: Replace direct method calls with inter-process communication +3. **Clean async boundaries**: Each process manages its own asyncio context +4. **No coroutine leakage**: Complete isolation prevents context contamination + +This explains why the deadlock is **MCP-specific** and doesn't occur in direct tool execution - it's fundamentally about asyncio context contamination between the MCP server and SerenaAgent. diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md index 920b4d0..9ce3d46 100644 --- a/.serena/memories/suggested_commands.md +++ b/.serena/memories/suggested_commands.md @@ -4,10 +4,24 @@ The following tasks should generally be executed using `uv run poe `. -- `lint`: This is the **only** allowed command for linting. Run as `uv run poe lint`. - `format`: This is the **only** allowed command for formatting. Run as `uv run poe format`. - `type-check`: This is the **only** allowed command for type checking. Run as `uv run poe type-check`. -- `test`: This is the preferred command for running tests (`uv run poe test [args]`). However, running tests directly with `uv run pytest [args]` is also permitted. +- `test`: This is the preferred command for running tests (`uv run poe test [args]`). You can select subsets of tests with markers, + the current markers are + ```toml + markers = [ + "python: language server running for Python", + "go: language server running for Go", + "java: language server running for Java", + "rust: language server running for Rust", + "typescript: language server running for TypeScript", + "php: language server running for PHP", + "snapshot: snapshot tests for symbolic editing operations", + "isolated_process: test runs with process isolated agent", + ] + ``` + By default, `uv run poe test` uses the markes set in the env var `PYTEST_MARKERS`, or, if it unset, uses `-m "not java and not rust and not isolated process"`. + You can override this behavior by simply passing the `-m` option to `uv run poe test`, e.g. `uv run poe test -m "python or go"`. For finishing a task, make sure format, type-check and test pass! Run them at the end of the task and if needed fix any issues that come up and run them again until they pass. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4132dd7..2fccdf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,9 +106,10 @@ exclude = "^build/|^docs/" PYDEVD_DISABLE_FILE_VALIDATION = "1" [tool.poe.tasks] -# Uses PYTEST_MARKERS env var for default markers (defaults to "not java and not rust") -# Set PYTEST_MARKERS="" to run all tests -test = "pytest test --color=yes -m \"${PYTEST_MARKERS:-not java and not rust}\"" +# 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 java and not rust and not isolated process}\"" _black_check = "black --check --exclude src/multilspy/ src scripts test" _ruff_check = "ruff check --exclude .venv/ --exclude src/multilspy/ src scripts test" _black_format = "black --exclude .venv/|src/multilspy/ src scripts test" @@ -259,6 +260,6 @@ markers = [ "rust: language server running for Rust", "typescript: language server running for TypeScript", "php: language server running for PHP", - "snapshot: snapshot tests", + "snapshot: snapshot tests for symbolic editing operations", "isolated_process: test runs with process isolated agent", ] From 4bd8ab83ad4e9853910b5c4b18bfabfbc6a6dfe1 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 13:54:38 +0200 Subject: [PATCH 059/174] LS: make cache path language dependent This means existing cache files will no longer be used, they have to be moved or deleted --- src/multilspy/language_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 5bd0ae5..9801735 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -1596,7 +1596,7 @@ class LanguageServer: @property def _cache_path(self) -> Path: - return Path(self.repository_root_path) / ".serena" / "cache" / "document_symbols_cache_v20-05-25.pkl" + return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v20-05-25.pkl" def save_cache(self): with self._cache_lock: From fe6669c8c0e645ea8c29a3d9826590357d8cd16f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 14:41:47 +0200 Subject: [PATCH 060/174] Added tqdm, typo in test command --- pyproject.toml | 3 ++- uv.lock | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2fccdf2..a12c10c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "psutil>=7.0.0", "docstring_parser>=0.16", "joblib>=1.5.1", + "tqdm>=4.67.1", ] [project.scripts] @@ -109,7 +110,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 java and not rust and not isolated process}\"" +test = "pytest test -vv -m \"${PYTEST_MARKERS:-not java and not rust and not isolated_process}\"" _black_check = "black --check --exclude src/multilspy/ src scripts test" _ruff_check = "ruff check --exclude .venv/ --exclude src/multilspy/ src scripts test" _black_format = "black --exclude .venv/|src/multilspy/ src scripts test" diff --git a/uv.lock b/uv.lock index 540c126..05834d9 100644 --- a/uv.lock +++ b/uv.lock @@ -973,6 +973,7 @@ dependencies = [ { name = "requests" }, { name = "ruamel-yaml" }, { name = "sensai-utils" }, + { name = "tqdm" }, { name = "types-pyyaml" }, ] @@ -1029,6 +1030,7 @@ requires-dist = [ { name = "sqlalchemy", marker = "extra == 'agno'", specifier = ">=2.0.40" }, { name = "syrupy", marker = "extra == 'dev'", specifier = ">=4.9.1" }, { name = "toml-sort", marker = "extra == 'dev'", specifier = ">=0.24.2" }, + { name = "tqdm", specifier = ">=4.67.1" }, { name = "types-pyyaml", specifier = ">=6.0.12.20241230" }, { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20241230" }, ] @@ -1181,6 +1183,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901 }, ] +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + [[package]] name = "traitlets" version = "5.14.3" From 3d65db8286247b3faacfdbc0da7f3cd528501cdb Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 15:33:02 +0200 Subject: [PATCH 061/174] LS: add index repository command --- src/multilspy/language_server.py | 40 ++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 9801735..f45c8f4 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -23,6 +23,7 @@ import time from typing import AsyncIterator, Dict, Iterator, List, Optional, Tuple, Union, cast import pathspec +import tqdm from . import multilspy_types from .lsp_protocol_handler import lsp_types as LSPTypes @@ -211,6 +212,10 @@ class LanguageServer: self.repository_root_path: str = repository_root_path self.logger.log(f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) + self.language_id = language_id + self.open_file_buffers: Dict[str, LSPFileBuffer] = {} + self.language = Language(language_id) + # load cache first to prevent any racing conditions due to asyncio stuff self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]]] = {} """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" @@ -236,10 +241,6 @@ class LanguageServer: start_independent_lsp_process=config.start_independent_lsp_process, ) - self.language_id = language_id - self.open_file_buffers: Dict[str, LSPFileBuffer] = {} - - self.language = Language(language_id) # Set up the pathspec matcher for the ignored paths # for all absolute paths in ignored_paths, convert them to relative paths @@ -1598,6 +1599,22 @@ class LanguageServer: def _cache_path(self) -> Path: return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v20-05-25.pkl" + async def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None: + """Will go through the entire repository and "index" all files, meaning save their symbols to the cache. + + :param progress_bar: Whether to show a progress bar while indexing the repository. + :param save_after_n_files: How many files to process before saving a checkpoint of the cache. + """ + parsed_files = await self.request_parsed_files() + files_processed = 0 + for relative_file_path in tqdm.tqdm(parsed_files, disable=not progress_bar): + await self.request_document_symbols(relative_file_path, include_body=False) + await self.request_document_symbols(relative_file_path, include_body=True) + files_processed += 1 + if files_processed % save_after_n_files == 0: + self.save_cache() + self.save_cache() + def save_cache(self): with self._cache_lock: if not self._cache_has_changed: @@ -2123,6 +2140,21 @@ class SyncLanguageServer: ).result(timeout=self.timeout) return result + def index_repository(self, progress_bar: bool = True, timeout: float | None = None, save_after_n_files: int = 10) -> None: + """ + Will go through the entire repository and "index" all files, meaning save their symbols to the cache. + + :param progress_bar: Whether to show a progress bar while indexing the repository. + :param save_after_n_files: How many files to process before saving a checkpoint of the cache. + :param timeout: since this operation can take a long time, the default timeout (the attribute of the LS instance) is not used. + If you want to provide a timeout for indexing, you can pass it here explicitly. + :return: + """ + assert self.loop + asyncio.run_coroutine_threadsafe( + self.language_server.index_repository(progress_bar=progress_bar, save_after_n_files=save_after_n_files), self.loop + ).result(timeout=timeout) + def start(self) -> "SyncLanguageServer": """ Starts the language server process and connects to it. Call shutdown when ready. From 2e3eda72ba4d23e69bf63ae352811711f7ea94a9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 15:44:38 +0200 Subject: [PATCH 062/174] Refactoring and fixes in agent 1. Separated out a method create_ls_for_project 2. Fix serialization and IPC of config objects --- src/serena/agent.py | 219 ++++++++++++++------------- src/serena/config.py | 25 ++- src/serena/mcp.py | 15 +- src/serena/process_isolated_agent.py | 78 ++++------ 4 files changed, 171 insertions(+), 166 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 1933d8e..00760c1 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -15,7 +15,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Callable, Generator, Iterable, Sequence from copy import copy, deepcopy -from dataclasses import dataclass, field +from dataclasses import asdict, dataclass, field from fnmatch import fnmatch from functools import cached_property from logging import Logger @@ -159,45 +159,47 @@ class ProjectConfig(ToStringMixin): config_with_comments["language"] = dominant_language if save_to_disk: save_yaml(str(project_root / cls.rel_path_to_project_yml()), config_with_comments, preserve_comments=True) - return cls._from_yml_data(config_with_comments) + return cls.from_json_dict(config_with_comments) @classmethod def rel_path_to_project_yml(cls) -> str: return os.path.join(SERENA_MANAGED_DIR_NAME, cls.SERENA_DEFAULT_PROJECT_FILE) @classmethod - def _from_yml_data(cls, yaml_data: dict[str, Any]) -> Self: + def from_json_dict(cls, data: dict[str, Any]) -> Self: """ Create a ProjectConfig instance from a configuration dictionary """ + data = copy(data) try: - yaml_data["language"] = Language(yaml_data["language"].lower()) + data["language"] = Language(data["language"].lower()) except ValueError as e: - raise ValueError(f"Invalid language: {yaml_data['language']}.\nValid languages are: {[l.value for l in Language]}") from e - return cls( - project_name=yaml_data["project_name"], - language=yaml_data["language"], - ignored_paths=yaml_data.get("ignored_paths", []), - excluded_tools=set(yaml_data.get("excluded_tools", [])), - read_only=yaml_data.get("read_only", False), - ignore_all_files_in_gitignore=yaml_data.get("ignore_all_files_in_gitignore", True), - initial_prompt=yaml_data.get("initial_prompt", ""), - ) + raise ValueError(f"Invalid language: {data['language']}.\nValid languages are: {[l.value for l in Language]}") from e + return cls(**data) + + def to_json_dict(self) -> dict[str, Any]: + result = asdict(self) + result["language"] = result["language"].value + result["excluded_tools"] = list(result["excluded_tools"]) + return result @classmethod - def load(cls, project_root: Path | str) -> Self: + def load(cls, project_root: Path | str, autogenerate: bool = True) -> Self: """ Load a ProjectConfig instance from the path to the project root. """ project_root = Path(project_root) yaml_path = project_root / cls.rel_path_to_project_yml() if not yaml_path.exists(): - raise FileNotFoundError(f"Project configuration file not found: {yaml_path}") + if autogenerate: + return cls.autogenerate(project_root) + else: + raise FileNotFoundError(f"Project configuration file not found: {yaml_path}") with open(yaml_path, encoding="utf-8") as f: yaml_data = yaml.safe_load(f) if "project_name" not in yaml_data: yaml_data["project_name"] = project_root.name - return cls._from_yml_data(yaml_data) + return cls.from_json_dict(yaml_data) def get_excluded_tool_classes(self) -> set[type["Tool"]]: return set(ToolRegistry.get_tool_class_by_name(tool_name) for tool_name in self.excluded_tools) @@ -217,13 +219,20 @@ class Project: return self.project_config.language @classmethod - def load(cls, project_root: str | Path) -> Self: + def load(cls, project_root: str | Path, autogenerate: bool = True) -> Self: project_root = Path(project_root).resolve() if not project_root.exists(): raise FileNotFoundError(f"Project root not found: {project_root}") - project_config = ProjectConfig.load(project_root) + project_config = ProjectConfig.load(project_root, autogenerate=autogenerate) return cls(project_root=str(project_root), project_config=project_config) + @classmethod + def from_json_dict(cls, data: dict) -> Self: + return cls(project_root=data["project_root"], project_config=ProjectConfig.from_json_dict(data["project_config"])) + + def to_json_dict(self) -> dict: + return {"project_root": self.project_root, "project_config": self.project_config.to_json_dict()} + def path_to_project_yml(self) -> str: return os.path.join(self.project_root, self.project_config.rel_path_to_project_yml()) @@ -318,58 +327,18 @@ class SerenaConfigBase(ABC): else: raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}") - def to_dict(self) -> dict: + def to_json_dict(self) -> dict: """Convert configuration to dictionary for serialization.""" - return { - "projects": [ - { - "project_root": project.project_root, - "project_config": { - "project_name": project.project_config.project_name, - "language": project.project_config.language, - "ignored_paths": project.project_config.ignored_paths, - "ignore_all_files_in_gitignore": project.project_config.ignore_all_files_in_gitignore, - }, - } - for project in self.projects - ], - "gui_log_window_enabled": self.gui_log_window_enabled, - "log_level": self.log_level, - "trace_lsp_communication": self.trace_lsp_communication, - "web_dashboard": self.web_dashboard, - "tool_timeout": self.tool_timeout, - } + result = asdict(self) + result["projects"] = [project.to_json_dict() for project in self.projects] + return result @classmethod - def from_dict(cls, data: dict) -> "SerenaConfigBase": + def from_json_dict(cls, data: dict) -> Self: """Create configuration from dictionary.""" - projects = [] - for project_data in data.get("projects", []): - project_config = ProjectConfig( - project_name=project_data["project_config"]["project_name"], - language=project_data["project_config"]["language"], - ignored_paths=project_data["project_config"]["ignored_paths"], - ignore_all_files_in_gitignore=project_data["project_config"]["ignore_all_files_in_gitignore"], - ) - project = Project(project_root=project_data["project_root"], project_config=project_config) - projects.append(project) - - # Create a basic config class that can be instantiated - from dataclasses import dataclass - - @dataclass(kw_only=True) - class DeserializedConfig(SerenaConfigBase): - def _add_new_project(self, project: Project) -> None: - self.projects.append(project) - - return DeserializedConfig( - projects=projects, - gui_log_window_enabled=data.get("gui_log_window_enabled", False), - log_level=data.get("log_level", logging.INFO), - trace_lsp_communication=data.get("trace_lsp_communication", False), - web_dashboard=data.get("web_dashboard", True), - tool_timeout=data.get("tool_timeout", DEFAULT_TOOL_TIMEOUT), - ) + data = copy(data) + data["projects"] = [Project.from_json_dict(project_data) for project_data in data["projects"]] + return cls(**data) @dataclass(kw_only=True) @@ -400,23 +369,26 @@ class SerenaConfig(SerenaConfigBase): config_file = cls.CONFIG_FILE_DOCKER if is_running_in_docker() else cls.CONFIG_FILE return os.path.join(REPO_ROOT, config_file) + @classmethod + def _load_commented_yaml(cls, config_file: str, generate_if_missing: bool = True) -> CommentedMap: + if not os.path.exists(config_file): + if not generate_if_missing: + raise FileNotFoundError(f"Serena configuration file not found: {config_file}") + log.info(f"Serena configuration file not found at {config_file}, autogenerating...") + cls.autogenerate() + try: + return load_yaml(config_file, preserve_comments=True) + except Exception as e: + raise ValueError(f"Error loading Serena configuration from {config_file}: {e}") from e + @classmethod def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig": """ Static constructor to create SerenaConfig from the configuration file """ config_file = cls.get_config_file_path() - if not os.path.exists(config_file): - if not generate_if_missing: - raise FileNotFoundError(f"Serena configuration file not found: {config_file}") - cls.autogenerate() - log.info(f"Loading Serena configuration from {config_file}") - try: - loaded_commented_yaml = load_yaml(config_file, preserve_comments=True) - except Exception as e: - raise ValueError(f"Error loading Serena configuration from {config_file}: {e}") from e - + loaded_commented_yaml = cls._load_commented_yaml(config_file, generate_if_missing) # Create instance instance = cls(loaded_commented_yaml=loaded_commented_yaml) @@ -500,6 +472,16 @@ class SerenaConfig(SerenaConfigBase): super().remove_project(project_name) self.save() + def to_json_dict(self) -> dict: + result = super().to_json_dict() + result.pop("loaded_commented_yaml", None) + return result + + @classmethod + def from_json_dict(cls, data: dict) -> Self: + data["loaded_commented_yaml"] = cls._load_commented_yaml(cls.get_config_file_path()) + return super().from_json_dict(data) + class LinesRead: def __init__(self) -> None: @@ -570,8 +552,6 @@ class MemoriesManagerMDFilesInProject(MemoriesManager): def create_serena_config( serena_config: SerenaConfigBase | None = None, - context: SerenaAgentContext | None = None, - modes: list[SerenaAgentMode] | 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, @@ -585,8 +565,6 @@ def create_serena_config( to allow creating configurations independently for process isolation and other use cases. :param serena_config: the base Serena configuration or None to read from default location - :param context: the context in which the agent will operate - :param modes: list of modes in which the agent will operate :param enable_web_dashboard: Whether to enable the web dashboard :param enable_gui_log_window: Whether to enable the GUI log window :param log_level: Log level @@ -637,6 +615,56 @@ def create_serena_config( return config +def create_ls_for_project( + project: str | Project, + log_level: int = logging.INFO, + ls_timeout: float | None = DEFAULT_TOOL_TIMEOUT - 5, + trace_lsp_communication: bool = False, +) -> SyncLanguageServer: + """ + Create a language server for a project. Note that you will have to start it + before performing any LS operations. + + :param project: either a path to the project root or a ProjectConfig instance. + If no project.yml is found, the default project configuration will be used. + :param log_level: the log level for the language server + :param ls_timeout: the timeout for the language server + :param trace_lsp_communication: whether to trace LSP communication + :return: the language server + """ + if isinstance(project, str): + project_instance = Project.load(project, autogenerate=True) + else: + project_instance = project + + project_config = project_instance.project_config + ignored_paths = project_config.ignored_paths + if len(ignored_paths) > 0: + log.info(f"Using {len(ignored_paths)} ignored paths from the explicit project configuration.") + log.debug(f"Ignored paths: {ignored_paths}") + if project_config.ignore_all_files_in_gitignore: + log.info(f"Parsing all gitignore files in {project_instance.project_root}") + gitignore_parser = GitignoreParser(project_instance.project_root) + log.info(f"Found {len(gitignore_parser.get_ignore_specs())} gitignore files.") + for spec in gitignore_parser.get_ignore_specs(): + log.debug(f"Adding {len(spec.patterns)} patterns from {spec.file_path} to the ignored paths.") + ignored_paths.extend(spec.patterns) + log.debug(f"Using {len(ignored_paths)} ignored paths in total.") + multilspy_config = MultilspyConfig( + code_language=project_instance.language, + ignored_paths=ignored_paths, + trace_lsp_communication=trace_lsp_communication, + ) + ls_logger = MultilspyLogger(log_level=log_level) + log.info(f"Creating language server instance for {project_instance.project_root}.") + return SyncLanguageServer.create( + multilspy_config, + ls_logger, + project_instance.project_root, + timeout=ls_timeout, + ) + + class SerenaAgent: def __init__( self, @@ -669,8 +697,6 @@ class SerenaAgent: # obtain serena configuration using the decoupled factory function self.serena_config = create_serena_config( serena_config=serena_config, - context=context, - modes=modes, enable_web_dashboard=enable_web_dashboard, enable_gui_log_window=enable_gui_log_window, log_level=log_level, @@ -1005,31 +1031,12 @@ class SerenaAgent: # instantiate and start the language server assert self._active_project is not None - ignored_paths = self._active_project.project_config.ignored_paths - if len(ignored_paths) > 0: - log.info(f"Using {len(ignored_paths)} ignored paths from the explicit project configuration.") - log.debug(f"Ignored paths: {ignored_paths}") - if self._active_project.project_config.ignore_all_files_in_gitignore: - log.info(f"Parsing all gitignore files in {self._active_project.project_root}") - gitignore_parser = GitignoreParser(self._active_project.project_root) - log.info(f"Found {len(gitignore_parser.get_ignore_specs())} gitignore files.") - for spec in gitignore_parser.get_ignore_specs(): - log.debug(f"Adding {len(spec.patterns)} patterns from {spec.file_path} to the ignored paths.") - ignored_paths.extend(spec.patterns) - log.debug(f"Using {len(ignored_paths)} ignored paths in total.") - multilspy_config = MultilspyConfig( - code_language=self._active_project.project_config.language, - ignored_paths=ignored_paths, + self.language_server = create_ls_for_project( + self._active_project, + log_level=self.serena_config.log_level, + ls_timeout=ls_timeout, trace_lsp_communication=self.serena_config.trace_lsp_communication, ) - ls_logger = MultilspyLogger(log_level=self.serena_config.log_level) - log.info(f"Starting language server for {self._active_project.project_root}.") - self.language_server = SyncLanguageServer.create( - multilspy_config, - ls_logger, - self._active_project.project_root, - timeout=ls_timeout, - ) self.language_server.start() if not self.language_server.is_running(): raise RuntimeError( diff --git a/src/serena/config.py b/src/serena/config.py index 0b9a85d..7f7fd34 100644 --- a/src/serena/config.py +++ b/src/serena/config.py @@ -3,7 +3,8 @@ Context and Mode configuration loader """ import os -from dataclasses import dataclass, field +from copy import copy +from dataclasses import asdict, dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Self @@ -30,6 +31,17 @@ class SerenaAgentMode: description: str = "" excluded_tools: set[str] = field(default_factory=set) + def to_json_dict(self) -> dict[str, str | list[str]]: + result = asdict(self) + result["excluded_tools"] = list(result["excluded_tools"]) + return result + + @classmethod + def from_json_dict(cls, data: dict) -> Self: + data = copy(data) + data["excluded_tools"] = set(data["excluded_tools"]) + return cls(**data) + def print_overview(self) -> None: """Print an overview of the mode.""" print(f"{self.name}:\n {self.description}") @@ -91,6 +103,17 @@ class SerenaAgentContext: description: str = "" excluded_tools: set[str] = field(default_factory=set) + def to_json_dict(self) -> dict[str, str | list[str]]: + result = asdict(self) + result["excluded_tools"] = list(result["excluded_tools"]) + return result + + @classmethod + def from_json_dict(cls, data: dict) -> Self: + data = copy(data) + data["excluded_tools"] = set(data["excluded_tools"]) + return cls(**data) + def get_excluded_tool_classes(self) -> list[type["Tool"]]: """Get the list of tool classes that are excluded from the context.""" from serena.agent import ToolRegistry diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 326ff9b..0f41ee1 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -99,6 +99,7 @@ def make_tool( fn_metadata=func_arg_metadata, is_async=is_async, context_kwarg=None, + annotations=None, ) @@ -141,15 +142,15 @@ def create_mcp_server_and_agent( try: serena_config = create_serena_config( - context=context_instance, - modes=modes_instances, enable_web_dashboard=enable_web_dashboard, enable_gui_log_window=enable_gui_log_window, log_level=log_level, trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, ) - serena_agent_process = ProcessIsolatedSerenaAgent(project=project, serena_config=serena_config) + serena_agent_process = ProcessIsolatedSerenaAgent( + project=project, serena_config=serena_config, modes=modes_instances, context=context_instance + ) # Start process-isolated dashboard if enabled serena_dashboard_process = None @@ -261,7 +262,7 @@ PROJECT_TYPE = ProjectType() @click.command() @click.option( "--project", - "project_file_opt", + "project", type=PROJECT_TYPE, default=None, help="Either an absolute path to the project directory or a name of an already registered project. " @@ -270,7 +271,7 @@ PROJECT_TYPE = ProjectType() # Keep --project-file for backwards compatibility @click.option( "--project-file", - "project_file_opt", # Use same destination variable to avoid conflicts + "project", # Use same destination variable to avoid conflicts type=PROJECT_TYPE, default=None, help="[DEPRECATED] Use --project instead.", @@ -357,7 +358,7 @@ PROJECT_TYPE = ProjectType() help="Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration.", ) def start_mcp_server( - project_file_opt: str | None, + project: str | None, project_file_arg: str | None, context: str = DEFAULT_CONTEXT, modes: tuple[str, ...] = DEFAULT_MODES, @@ -378,7 +379,7 @@ def start_mcp_server( """ # Prioritize the positional argument if provided # This is for backward compatibility with the old CLI, should be removed in the future! - project_file = project_file_arg if project_file_arg is not None else project_file_opt + project_file = project_file_arg if project_file_arg is not None else project # Use process isolation by default to prevent asyncio event loop contamination mcp_server, agent = create_mcp_server_and_agent( diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index f87856b..f4bc50c 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -5,7 +5,6 @@ import multiprocessing import os import traceback import webbrowser -from collections.abc import Callable from enum import StrEnum from logging.handlers import QueueHandler from multiprocessing.connection import Connection @@ -270,27 +269,19 @@ class SerenaAgentWorker: """Initialize the SerenaAgent.""" try: # Extract all possible initialization parameters + context_param = params.get("context") project = params.get("project") - project_activation_callback = params.get("project_activation_callback") - serena_config = params.get("serena_config") - context = params.get("context") - modes = params.get("modes") + serena_config = SerenaConfig.from_json_dict(params["serena_config"]) + context = SerenaAgentContext.from_json_dict(context_param) if context_param is not None else None + modes = [SerenaAgentMode.from_json_dict(m) for m in params["modes"]] enable_web_dashboard = params.get("enable_web_dashboard") enable_gui_log_window = params.get("enable_gui_log_window") log_level = params.get("log_level") trace_lsp_communication = params.get("trace_lsp_communication") tool_timeout = params.get("tool_timeout") - # Handle legacy config_dict parameter for backward compatibility - if "config" in params and serena_config is None: - config_dict = params["config"] - serena_config = SerenaConfig.from_dict(config_dict) - elif serena_config is not None and isinstance(serena_config, dict): - serena_config = SerenaConfig.from_dict(serena_config) - self.agent = SerenaAgent( project=project, - project_activation_callback=project_activation_callback, serena_config=serena_config, context=context, modes=modes, @@ -405,7 +396,6 @@ class ProcessIsolatedSerenaAgent: def __init__( self, project: str | None = None, - project_activation_callback: Callable[[], None] | None = None, serena_config: SerenaConfigBase | None = None, context: SerenaAgentContext | None = None, modes: list[SerenaAgentMode] | None = None, @@ -415,28 +405,17 @@ class ProcessIsolatedSerenaAgent: trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, ): - # Store all initialization parameters to pass to worker - self._init_params = { - "project": project, - "project_activation_callback": project_activation_callback, - "serena_config": serena_config, - "context": context, - "modes": modes, - "enable_web_dashboard": enable_web_dashboard, - "enable_gui_log_window": enable_gui_log_window, - "log_level": log_level, - "trace_lsp_communication": trace_lsp_communication, - "tool_timeout": tool_timeout, - } - - # Keep serena_config for compatibility - if serena_config is not None: - self.serena_config = serena_config - else: - # Use the default config loader - self.serena_config = SerenaConfig.from_config_file() - self.process: multiprocessing.Process | None = None self.project = project + self.serena_config = serena_config or SerenaConfig.from_config_file() + self.context = context + self.modes = modes or [] + self.enable_web_dashboard = enable_web_dashboard + self.enable_gui_log_window = enable_gui_log_window + self.log_level = log_level + self.trace_lsp_communication = trace_lsp_communication + self.tool_timeout = tool_timeout + + self.process: multiprocessing.Process | None = None self.conn: Connection | None = None def start(self) -> None: @@ -455,25 +434,20 @@ class ProcessIsolatedSerenaAgent: self.process = multiprocessing.Process(target=worker.run, args=[_global_log_queue]) self.process.start() + # Prepare initialization parameters, converting complex objects to dict if present + init_params = { + "project": self.project, + "serena_config": self.serena_config.to_json_dict(), + "context": self.context.to_json_dict() if self.context is not None else None, + "modes": [m.to_json_dict() for m in self.modes], + "enable_web_dashboard": self.enable_web_dashboard, + "enable_gui_log_window": self.enable_gui_log_window, + "log_level": self.log_level, + "trace_lsp_communication": self.trace_lsp_communication, + "tool_timeout": self.tool_timeout, + } # Initialize the agent in the worker process try: - # TODO: instead pass config and project to worker, use at startup of agent there - # Prepare initialization parameters, converting complex objects to dict if present - init_params = self._init_params.copy() - if init_params["serena_config"] is not None and hasattr(init_params["serena_config"], "to_dict"): - # Convert SerenaConfigBase to dict for serialization - serena_config_obj = init_params["serena_config"] - init_params["serena_config"] = serena_config_obj.to_dict() # type: ignore - # init_params["project"] = self.project - # Note: context and modes are not serializable across processes, - # so they will be None and SerenaAgent will use defaults - init_params["context"] = None - init_params["modes"] = None - # project_activation_callback cannot be serialized across processes - init_params["project_activation_callback"] = None - # Disable web dashboard in worker process - it will run in MCP process instead - init_params["enable_web_dashboard"] = False - self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, init_params) except Exception as e: self.stop() From 918ff32dcc9541e82e7d2377a97195d0497f4a0f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 15:59:40 +0200 Subject: [PATCH 063/174] Added an indexing command as uv script --- pyproject.toml | 1 + src/multilspy/language_server.py | 10 +++++++--- src/serena/agent.py | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a12c10c..7e18dec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ [project.scripts] serena-mcp-server = "serena.mcp:start_mcp_server" +index-project = "serena.agent:index_project" [project.license] text = "MIT" diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index f45c8f4..981769b 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -1607,7 +1607,7 @@ class LanguageServer: """ parsed_files = await self.request_parsed_files() files_processed = 0 - for relative_file_path in tqdm.tqdm(parsed_files, disable=not progress_bar): + for relative_file_path in tqdm.tqdm(parsed_files, disable=not progress_bar, desc="Indexing (files)"): await self.request_document_symbols(relative_file_path, include_body=False) await self.request_document_symbols(relative_file_path, include_body=True) files_processed += 1 @@ -2205,7 +2205,7 @@ class SyncLanguageServer: self.language_server.logger.log("Async shutdown tasks completed, returning to main thread", logging.DEBUG) # Note: NOT calling loop.stop() here - let main thread do it - def stop(self, shutdown_timeout: float = 5.0) -> None: + def stop(self, shutdown_timeout: float = 2.0) -> None: """ Stops the language server and robustly cleans up all associated resources, including the asyncio event loop, to prevent hangs on process exit. @@ -2230,26 +2230,30 @@ class SyncLanguageServer: if not is_windows: # 2. Graceful shutdown for Linux/Unix - try proper async cleanup first + self.language_server.logger.log("Starting graceful shutdown", logging.INFO) shutdown_future = None if self.loop and self.loop.is_running(): shutdown_future = asyncio.run_coroutine_threadsafe(self._shutdown_and_stop_loop(), self.loop) # 3. Wait for the background thread to exit if self.loop_thread: + self.language_server.logger.log("Waiting for event loop thread to exit", logging.INFO) self.loop_thread.join(timeout=shutdown_timeout) if self.loop_thread.is_alive(): self.language_server.logger.log("Event loop thread did not terminate within timeout", logging.WARNING) # 4. Wait for the shutdown coroutine to complete if it was scheduled if shutdown_future: + self.language_server.logger.log("Waiting for async shutdown to complete", logging.INFO) try: - shutdown_future.result(timeout=2.0) + shutdown_future.result(timeout=shutdown_timeout) self.language_server.logger.log("Graceful async shutdown completed", logging.DEBUG) except Exception as e: self.language_server.logger.log(f"Async shutdown failed: {e}", logging.WARNING) # 5. Close the loop properly after graceful shutdown if self.loop and not self.loop.is_closed(): + self.language_server.logger.log("Closing event loop", logging.INFO) try: self.loop.close() self.language_server.logger.log("Event loop closed successfully", logging.DEBUG) diff --git a/src/serena/agent.py b/src/serena/agent.py index 00760c1..b1df6da 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -23,6 +23,7 @@ from pathlib import Path from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast +import click import yaml from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata from overrides import override @@ -656,6 +657,7 @@ def create_ls_for_project( trace_lsp_communication=trace_lsp_communication, ) ls_logger = MultilspyLogger(log_level=log_level) + logging.basicConfig(format=LOG_DEFAULT_FORMAT, level=log_level, force=True) log.info(f"Creating language server instance for {project_instance.project_root}.") return SyncLanguageServer.create( multilspy_config, @@ -665,6 +667,23 @@ def create_ls_for_project( ) +@click.command() +@click.argument("project", type=click.Path(exists=True)) +@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="WARNING") +def index_project(project: str, log_level: str = "INFO") -> None: + """ + Index a project by saving the symbols of files to Serena's language server cache. + + :param project: the project to index + """ + log_level_int = logging.getLevelNamesMapping()[log_level.upper()] + project = os.path.abspath(project) + log.info(f"Indexing project {project}") + ls = create_ls_for_project(project, log_level=log_level_int) + with ls.start_server(): + ls.index_repository() + + class SerenaAgent: def __init__( self, From 859efe512dba70090c89fe66fe22f9b4cd872435 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 16:11:02 +0200 Subject: [PATCH 064/174] Improved prompt in ide-assistant mode (instruct to not use excluded tools) [ci skip] --- .../resources/config/contexts/ide-assistant.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/serena/resources/config/contexts/ide-assistant.yml b/src/serena/resources/config/contexts/ide-assistant.yml index 230e952..52f6ba0 100644 --- a/src/serena/resources/config/contexts/ide-assistant.yml +++ b/src/serena/resources/config/contexts/ide-assistant.yml @@ -1,8 +1,13 @@ description: Non-symbolic editing tools and general shell tool are excluded prompt: | - You are running in IDE assistant context where file operations and shell commands are handled by the IDE. - You should exclusively use symbolic tools for exploring and modifying the code, as the IDE handles - file-level operations. + 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. + However, if serena's tools can be used for achieving your task (see initial instructions), + you should prioritize them. excluded_tools: - create_text_file - read_file From 38f113aa09ed316230c33cc3ad581a5c0e9bdf3b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 16:29:35 +0200 Subject: [PATCH 065/174] Readme on indexing and higher visibility of Claude Code setup [ci skip] --- README.md | 54 +++++++++++++++++++++----------- src/multilspy/language_server.py | 4 ++- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 9b832ad..d46b676 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,12 @@ https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 Serena provides the necessary [tools](#full-list-of-tools) for coding workflows, but an LLM is required to do the actual work, orchestrating tool use. +For example, supercharge the performance of Claude Code with a [one-line shell command]((#claude-code). + Serena can be integrated with an LLM in several ways: * by using the **model context protocol (MCP)**. Serena provides an MCP server which integrates with - * Claude Desktop, + * Claude Code and Claude Desktop, * IDEs like VSCode, Cursor or IntelliJ, * Extensions like Cline or Roo Code * Goose (for a nice CLI experience) @@ -87,7 +89,7 @@ implementation. ## Table of Contents - + @@ -95,12 +97,14 @@ implementation. - [What Can I Use Serena For?](#what-can-i-use-serena-for) - [Free Coding Agents with Serena](#free-coding-agents-with-serena) - [Quick Start](#quick-start) - * [Setup](#setup) + * [Claude Code](#claude-code) + * [General Setup](#general-setup) * [Configuration](#configuration) * [Project Activation](#project-activation) * [MCP Server (Claude Desktop)](#mcp-server-claude-desktop) + + [Local Installation](#local-installation) + + [Docker Installation (Experimental)](#docker-installation-experimental) + [Troubleshooting](#troubleshooting) - * [Claude Code](#claude-code) * [Other MCP Clients (Cline, Roo-Code, Cursor, Windsurf, etc.)](#other-mcp-clients-cline-roo-code-cursor-windsurf-etc) * [Goose](#goose) * [Agno Agent](#agno-agent) @@ -171,11 +175,28 @@ Serena can be used in various ways, below you will find instructions for selecte - If you prefer using Serena through a CLI, you can use [goose](#goose). There again almost any model is possible. - If you want to use Serena integrated in your IDE, see the section on [other MCP clients](#other-mcp-clients---cline-roo-code-cursor-windsurf-etc). -### Setup - Serena is managed by `uv`, so you will need to [install it](https://docs.astral.sh/uv/getting-started/installation/)). -Then you can either +### Claude Code + +Serena is a great way to make Claude Code both cheaper and more powerful! We are collecting +several examples for that and have heard very positive feedback so far. Claude Code users can +add serena with (from the project dir) + +```shell +claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena-mcp-server --context ide-assistant --project $(pwd) +``` + +We recommend that you index your code once before starting (especially for larger projects), it will accelerate the symbolic operations. + +```shell +uvx --from git+https://github.com/oraios/serena index-project $(pwd) +``` + + +### General Setup + +You can either 1. Clone the repository and cd into it. 2. Optionally create a config file from the template and adjust it according to your preferences. @@ -193,7 +214,13 @@ uvx --from git+https://github.com/oraios/serena serena-mcp-server.exe in your MCP config (delete the `.exe` on macOS or Linux). You can now add Serena to your MCP client as described below for various clients and -[activate your first project](#project-activation). +[activate your first project](#project-activation). For larger projects, we recommend that you first index them +to accelerate Serena's tools on the first usage. + +```shell +uvx --from git+https://github.com/oraios/serena index-project /path/to/project +``` + > In the default configuration, Serena will start a small dashboard on localhost that will display logs and allow shutting down the > MCP server (since many clients fail to cleanup processes, leaving zombies behind). If you don't want that, simply set `web_dashboard` to `False` @@ -338,17 +365,6 @@ therefore needs to be configured with a launch command. In SSE transport you con For more information on MCP servers with Claude Desktop, see [the official quick start guide](https://modelcontextprotocol.io/quickstart/user). -### Claude Code - -Serena is a great way to make Claude Code both cheaper and more powerful! We are collecting -several examples for that and have heard very positive feedback so far. Claude Code users can -add serena with - -```shell -claude mcp add serena -- /path/to/uv "run" --directory /path/to/serena serena-mcp-server --context ide-assistant -``` - - ### Other MCP Clients (Cline, Roo-Code, Cursor, Windsurf, etc.) Being an MCP Server, Serena can be included in any MCP Client. The same configuration as above, diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 981769b..e1124cc 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -1607,7 +1607,9 @@ class LanguageServer: """ parsed_files = await self.request_parsed_files() files_processed = 0 - for relative_file_path in tqdm.tqdm(parsed_files, disable=not progress_bar, desc="Indexing (files)"): + pbar = tqdm.tqdm(parsed_files, disable=not progress_bar) + for relative_file_path in pbar: + pbar.set_description(f"Indexing ({os.path.basename(relative_file_path)})") await self.request_document_symbols(relative_file_path, include_body=False) await self.request_document_symbols(relative_file_path, include_body=True) files_processed += 1 From 3931aea514bf540fa2f7adf49cee65d0bc939fe7 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 16:32:53 +0200 Subject: [PATCH 066/174] Typo [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d46b676..de04e7b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 Serena provides the necessary [tools](#full-list-of-tools) for coding workflows, but an LLM is required to do the actual work, orchestrating tool use. -For example, supercharge the performance of Claude Code with a [one-line shell command]((#claude-code). +For example, **supercharge the performance of Claude Code** with a [one-line shell command](#claude-code). Serena can be integrated with an LLM in several ways: * by using the **model context protocol (MCP)**. From bbbbdc801e606d5b2083f89dcbe111bdeafc6db3 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 16:40:36 +0200 Subject: [PATCH 067/174] Docs [ci skip] --- CHANGELOG.md | 2 ++ lessons_learned.md | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aa710c..bfc0c7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,10 @@ Status of the main branch. Changes prior to the next official version change wil create `project.yaml` for each project. Project activation is now always available. Any project can now be activated by just asking the LLM to do so and passing the path to a repo. * Dashboard as web app and possibility to shut down Serena from it (or the old log GUI). +* Possibility to index your project beforehand, accelerating Serena's tools. * Initial prompt for project supported (has to be added manually for the moment) * Massive performance improvement of pattern search tool +* Fixed stability issues and deadlocks (see #170) # 2025-05-24 diff --git a/lessons_learned.md b/lessons_learned.md index 795c76d..f9fb78c 100644 --- a/lessons_learned.md +++ b/lessons_learned.md @@ -70,6 +70,14 @@ many clients, including Claude Desktop, fail to properly clean up, leaving zombi We mitigate this through the GUI window and the dashboard, so the user sees whether Serena is running and can terminate it there. +### Trusting Asyncio + +Running multiple asyncio apps led to non-deterministic +event loop contamination and deadlocks, which were very hard to debug +and understand. We solved this with a large hammer, by putting all asyncio apps into a separate +process. It made the code much more complex and slightly enhanced RAM requirements, but it seems +like that was the only way to reliably overcome asyncio deadlock issues. + ### Cross-OS Tkinter GUI Different OS have different limitations when it comes to starting a window or dealing with Tkinter From 9087f42389ebf03de0de68833772ef890f7115ab Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 16:45:04 +0200 Subject: [PATCH 068/174] Readme comment [ci skip] --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index de04e7b..8215234 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,9 @@ add serena with (from the project dir) claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena-mcp-server --context ide-assistant --project $(pwd) ``` -We recommend that you index your code once before starting (especially for larger projects), it will accelerate the symbolic operations. +You should ask claude to read the initial instructions as the first prompt, where it will receive information +on how to use Serena's tools. +We also recommend that you index your code once before starting (especially for larger projects), it will accelerate the symbolic operations. ```shell uvx --from git+https://github.com/oraios/serena index-project $(pwd) From 2b43dd62af0c9572f5253eafea2c8338138cfad5 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 17:13:12 +0200 Subject: [PATCH 069/174] Hotfix: don't start dashboard twice in process-isolated setting --- src/serena/process_isolated_agent.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index f4bc50c..48d5dc8 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -267,6 +267,8 @@ class SerenaAgentWorker: def _initialize(self, params: dict[str, Any]) -> dict[str, Any]: """Initialize the SerenaAgent.""" + if self.agent is not None: + return {"result": "SerenaAgent already initialized"} try: # Extract all possible initialization parameters context_param = params.get("context") @@ -274,8 +276,6 @@ class SerenaAgentWorker: serena_config = SerenaConfig.from_json_dict(params["serena_config"]) context = SerenaAgentContext.from_json_dict(context_param) if context_param is not None else None modes = [SerenaAgentMode.from_json_dict(m) for m in params["modes"]] - enable_web_dashboard = params.get("enable_web_dashboard") - enable_gui_log_window = params.get("enable_gui_log_window") log_level = params.get("log_level") trace_lsp_communication = params.get("trace_lsp_communication") tool_timeout = params.get("tool_timeout") @@ -285,8 +285,8 @@ class SerenaAgentWorker: serena_config=serena_config, context=context, modes=modes, - enable_web_dashboard=enable_web_dashboard, - enable_gui_log_window=enable_gui_log_window, + enable_web_dashboard=False, + enable_gui_log_window=False, log_level=log_level, trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, @@ -399,8 +399,6 @@ class ProcessIsolatedSerenaAgent: serena_config: SerenaConfigBase | None = None, context: SerenaAgentContext | None = None, modes: list[SerenaAgentMode] | 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, trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, @@ -409,8 +407,6 @@ class ProcessIsolatedSerenaAgent: self.serena_config = serena_config or SerenaConfig.from_config_file() self.context = context self.modes = modes or [] - self.enable_web_dashboard = enable_web_dashboard - self.enable_gui_log_window = enable_gui_log_window self.log_level = log_level self.trace_lsp_communication = trace_lsp_communication self.tool_timeout = tool_timeout @@ -440,8 +436,6 @@ class ProcessIsolatedSerenaAgent: "serena_config": self.serena_config.to_json_dict(), "context": self.context.to_json_dict() if self.context is not None else None, "modes": [m.to_json_dict() for m in self.modes], - "enable_web_dashboard": self.enable_web_dashboard, - "enable_gui_log_window": self.enable_gui_log_window, "log_level": self.log_level, "trace_lsp_communication": self.trace_lsp_communication, "tool_timeout": self.tool_timeout, From 91af75fc52002ee9a02fd5a8789598d9142dbb6b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 17:27:42 +0200 Subject: [PATCH 070/174] Try solving disappearing logs --- src/serena/process_isolated_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 48d5dc8..c28c7be 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -43,7 +43,7 @@ def _dashboard_worker( ) -> None: """Entry point for the dashboard process.""" # Route all logging to the shared queue - root = logging.getLogger() + root = logging.Logger.root root.handlers.clear() root.setLevel(logging.DEBUG) root.addHandler(QueueHandler(log_q)) From 35f2aaa5ff8e56c634eda30a73f14c834f940d6a Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 17:46:52 +0200 Subject: [PATCH 071/174] Fixes in glob matching, extended tests --- src/serena/text_utils.py | 53 ++++++++++++-- test/serena/test_text_utils.py | 125 +++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 6 deletions(-) diff --git a/src/serena/text_utils.py b/src/serena/text_utils.py index 75cc7dd..4f40aa0 100644 --- a/src/serena/text_utils.py +++ b/src/serena/text_utils.py @@ -1,3 +1,4 @@ +import fnmatch import logging import re from collections.abc import Callable @@ -6,8 +7,6 @@ from enum import StrEnum from typing import Any, Self from joblib import Parallel, delayed -from pathspec import PathSpec -from pathspec.patterns.gitwildmatch import GitWildMatchPattern log = logging.getLogger(__name__) @@ -258,6 +257,49 @@ def default_file_reader(file_path: str) -> str: return f.read() +def glob_match(pattern: str, path: str) -> bool: + """ + Match a file path against a glob pattern. + + Supports standard glob patterns: + - * matches any number of characters except / + - ** matches any number of directories (zero or more) + - ? matches a single character except / + - [seq] matches any character in seq + + :param pattern: Glob pattern (e.g., 'src/**/*.py', '**agent.py') + :param path: File path to match against + :return: True if path matches pattern + """ + # Handle ** patterns that should match zero or more directories + if "**" in pattern: + # Method 1: Standard fnmatch (matches one or more directories) + regex1 = fnmatch.translate(pattern) + if re.match(regex1, path): + return True + + # Method 2: Handle zero-directory case by removing /** entirely + # Convert "src/**/test.py" to "src/test.py" + if "/**/" in pattern: + zero_dir_pattern = pattern.replace("/**/", "/") + regex2 = fnmatch.translate(zero_dir_pattern) + if re.match(regex2, path): + return True + + # Method 3: Handle leading ** case by removing **/ + # Convert "**/test.py" to "test.py" + if pattern.startswith("**/"): + zero_dir_pattern = pattern[3:] # Remove "**/" + regex3 = fnmatch.translate(zero_dir_pattern) + if re.match(regex3, path): + return True + + return False + else: + # Simple pattern without **, use fnmatch directly + return fnmatch.fnmatch(path, pattern) + + def search_files( file_paths: list[str], pattern: re.Pattern | str, @@ -281,15 +323,14 @@ def search_files( :return: List of MatchedConsecutiveLines objects """ # Pre-filter paths (done sequentially to avoid overhead) - include_spec = PathSpec.from_lines(GitWildMatchPattern, [paths_include_glob]) if paths_include_glob else None - exclude_spec = PathSpec.from_lines(GitWildMatchPattern, [paths_exclude_glob]) if paths_exclude_glob else None + # Use proper glob matching instead of gitignore patterns filtered_paths = [] for path in file_paths: - if include_spec and not include_spec.match_file(path): + if paths_include_glob and not glob_match(paths_include_glob, path): log.debug(f"Skipping {path}: does not match include pattern {paths_include_glob}") continue - if exclude_spec and exclude_spec.match_file(path): + if paths_exclude_glob and glob_match(paths_exclude_glob, path): log.debug(f"Skipping {path}: matches exclude pattern {paths_exclude_glob}") continue filtered_paths.append(path) diff --git a/test/serena/test_text_utils.py b/test/serena/test_text_utils.py index 860ce6a..86ff975 100644 --- a/test/serena/test_text_utils.py +++ b/test/serena/test_text_utils.py @@ -285,6 +285,87 @@ class TestSearchFiles: assert result.matched_lines[0].line_content == "This line contains a match." assert result.matched_lines[0].match_type == LineType.MATCH + @pytest.mark.parametrize( + "file_paths, pattern, paths_include_glob, paths_exclude_glob, expected_matched_files, description", + [ + # Glob patterns that were problematic with gitignore syntax + ( + ["src/serena/agent.py", "src/serena/process_isolated_agent.py", "test/agent.py"], + "match", + "src/**agent.py", + None, + ["src/serena/agent.py", "src/serena/process_isolated_agent.py"], + "Glob: src/**agent.py should match files ending with agent.py under src/", + ), + ( + ["src/serena/agent.py", "src/serena/process_isolated_agent.py", "other/agent.py"], + "match", + "**agent.py", + None, + ["src/serena/agent.py", "src/serena/process_isolated_agent.py", "other/agent.py"], + "Glob: **agent.py should match files ending with agent.py anywhere", + ), + ( + ["dir/subdir/file.py", "dir/other/file.py", "elsewhere/file.py"], + "match", + "dir/**file.py", + None, + ["dir/subdir/file.py", "dir/other/file.py"], + "Glob: dir/**file.py should match files ending with file.py under dir/", + ), + ( + ["src/a/b/c/test.py", "src/x/test.py", "other/test.py"], + "match", + "src/**/test.py", + None, + ["src/a/b/c/test.py", "src/x/test.py"], + "Glob: src/**/test.py should match test.py files under src/ at any depth", + ), + # Edge cases for ** patterns + ( + ["agent.py", "src/agent.py", "src/serena/agent.py"], + "match", + "**agent.py", + None, + ["agent.py", "src/agent.py", "src/serena/agent.py"], + "Glob: **agent.py should match at root and any depth", + ), + (["file.txt", "src/file.txt"], "match", "src/**", None, ["src/file.txt"], "Glob: src/** should match everything under src/"), + ], + ids=lambda x: x if isinstance(x, str) else "", # Use description as test ID + ) + def test_search_files_glob_patterns( + self, file_paths, pattern, paths_include_glob, paths_exclude_glob, expected_matched_files, description + ): + """ + Test glob patterns that were problematic with the previous gitignore-based implementation. + """ + results = search_files( + file_paths=file_paths, + pattern=pattern, + file_reader=mock_reader_always_match, + paths_include_glob=paths_include_glob, + paths_exclude_glob=paths_exclude_glob, + context_lines_before=0, + context_lines_after=0, + ) + + # Extract the source file paths from the results + actual_matched_files = sorted([result.source_file_path for result in results if result.source_file_path]) + + # Assert that the matched files are exactly the ones expected + assert actual_matched_files == sorted( + expected_matched_files + ), f"Pattern '{paths_include_glob}' failed: expected {sorted(expected_matched_files)}, got {actual_matched_files}" + + # Basic check on results structure if files were expected + if expected_matched_files: + assert len(results) == len(expected_matched_files) + for result in results: + assert len(result.matched_lines) == 1 # Mock reader returns one matching line + assert result.matched_lines[0].line_content == "This line contains a match." + assert result.matched_lines[0].match_type == LineType.MATCH + def test_search_files_no_pattern_match_in_content(self): """Test that no results are returned if the pattern doesn't match the file content, even if files pass filters.""" file_paths = ["a.py", "b.txt"] @@ -365,3 +446,47 @@ class TestSearchFiles: assert result.lines[1].match_type == LineType.MATCH assert result.lines[2].line_content == "Line after 1", "Incorrect 'after' context line" assert result.lines[2].match_type == LineType.AFTER_MATCH + + +class TestGlobMatch: + """Test the glob_match function directly.""" + + @pytest.mark.parametrize( + "pattern, path, expected", + [ + # Basic wildcard patterns + ("*.py", "file.py", True), + ("*.py", "file.txt", False), + ("*agent.py", "agent.py", True), + ("*agent.py", "process_isolated_agent.py", True), + ("*agent.py", "agent_test.py", False), + # Double asterisk patterns + ("**agent.py", "agent.py", True), + ("**agent.py", "src/agent.py", True), + ("**agent.py", "src/serena/agent.py", True), + ("**agent.py", "src/serena/process_isolated_agent.py", True), + ("**agent.py", "agent_test.py", False), + # Prefix with double asterisk + ("src/**agent.py", "src/agent.py", True), + ("src/**agent.py", "src/serena/agent.py", True), + ("src/**agent.py", "src/serena/process_isolated_agent.py", True), + ("src/**agent.py", "other/agent.py", False), + ("src/**agent.py", "src/agent_test.py", False), + # Directory patterns + ("src/**", "src/file.py", True), + ("src/**", "src/dir/file.py", True), + ("src/**", "other/file.py", False), + # Exact matches with double asterisk + ("src/**/test.py", "src/test.py", True), + ("src/**/test.py", "src/a/b/test.py", True), + ("src/**/test.py", "src/test_file.py", False), + # Simple patterns without asterisks + ("src/file.py", "src/file.py", True), + ("src/file.py", "src/other.py", False), + ], + ) + def test_glob_match(self, pattern, path, expected): + """Test glob_match function with various patterns.""" + from src.serena.text_utils import glob_match + + assert glob_match(pattern, path) == expected From ec50e1a9c982c91cfd0a5627da3a789831d85119 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 18:13:01 +0200 Subject: [PATCH 072/174] Fix disappeared logs --- src/serena/agent.py | 1 - src/serena/process_isolated_agent.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index b1df6da..580b447 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -657,7 +657,6 @@ def create_ls_for_project( trace_lsp_communication=trace_lsp_communication, ) ls_logger = MultilspyLogger(log_level=log_level) - logging.basicConfig(format=LOG_DEFAULT_FORMAT, level=log_level, force=True) log.info(f"Creating language server instance for {project_instance.project_root}.") return SyncLanguageServer.create( multilspy_config, diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index c28c7be..48d5dc8 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -43,7 +43,7 @@ def _dashboard_worker( ) -> None: """Entry point for the dashboard process.""" # Route all logging to the shared queue - root = logging.Logger.root + root = logging.getLogger() root.handlers.clear() root.setLevel(logging.DEBUG) root.addHandler(QueueHandler(log_q)) From 47b50198ec2fbe646dd202ade127715d3beac643 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 19:28:14 +0200 Subject: [PATCH 073/174] Fix in path listing (were not relative). Minor other changes --- README.md | 3 ++- src/multilspy/language_server.py | 37 ++++++++++++++++++-------------- src/serena/agent.py | 21 +++++++++--------- src/serena/text_utils.py | 2 ++ 4 files changed, 35 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 8215234..5cc2ecc 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,8 @@ on how to use Serena's tools. We also recommend that you index your code once before starting (especially for larger projects), it will accelerate the symbolic operations. ```shell -uvx --from git+https://github.com/oraios/serena index-project $(pwd) +# from the project directory, or pass the path to the project as argument +uvx --from git+https://github.com/oraios/serena index-project ``` diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index e1124cc..dd123c9 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -1210,7 +1210,6 @@ class LanguageServer: symbol_body = symbol_body[symbol_start_column:] return symbol_body - async def request_parsed_files(self) -> list[str]: """Retrieves relative paths of all files analyzed by the Language Server.""" if not self.server_started: @@ -1221,13 +1220,9 @@ class LanguageServer: raise MultilspyException("Language Server not started") rel_file_paths = [] for root, dirs, files in os.walk(self.repository_root_path): - # Don't go into directories that are ignored by modifying dirs inplace - # Explanation for the + "/" part: - # pathspec can't handle the matching of directories if they don't end with a slash! - # see https://github.com/cpburnz/python-pathspec/issues/89 - dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d) + "/")] + dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d))] for file in files: - rel_file_path = os.path.join(root, file) + rel_file_path = os.path.relpath(os.path.join(root, file), start=self.repository_root_path) if not self.is_ignored_path(rel_file_path): rel_file_paths.append(rel_file_path) return rel_file_paths @@ -1596,7 +1591,10 @@ class LanguageServer: return defining_symbol @property - def _cache_path(self) -> Path: + def cache_path(self) -> Path: + """ + The path to the cache file for the document symbols. + """ return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v20-05-25.pkl" async def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None: @@ -1623,33 +1621,33 @@ class LanguageServer: self.logger.log("No changes to document symbols cache, skipping save", logging.DEBUG) return - self.logger.log(f"Saving updated document symbols cache to {self._cache_path}", logging.INFO) - self._cache_path.parent.mkdir(parents=True, exist_ok=True) + self.logger.log(f"Saving updated document symbols cache to {self.cache_path}", logging.INFO) + self.cache_path.parent.mkdir(parents=True, exist_ok=True) try: - with open(self._cache_path, "wb") as f: + with open(self.cache_path, "wb") as f: pickle.dump(self._document_symbols_cache, f) self._cache_has_changed = False except Exception as e: self.logger.log( - f"Failed to save document symbols cache to {self._cache_path}: {e}. " + f"Failed to save document symbols cache to {self.cache_path}: {e}. " "Note: this may have resulted in a corrupted cache file.", logging.ERROR, ) def load_cache(self): - if not self._cache_path.exists(): + if not self.cache_path.exists(): return with self._cache_lock: - self.logger.log(f"Loading document symbols cache from {self._cache_path}", logging.INFO) + self.logger.log(f"Loading document symbols cache from {self.cache_path}", logging.INFO) try: - with open(self._cache_path, "rb") as f: + with open(self.cache_path, "rb") as f: self._document_symbols_cache = pickle.load(f) self.logger.log(f"Loaded {len(self._document_symbols_cache)} document symbols from cache.", logging.INFO) except Exception as e: # cache often becomes corrupt, so just skip loading it self.logger.log( - f"Failed to load document symbols cache from {self._cache_path}: {e}. Possible cause: the cache file is corrupted. " + f"Failed to load document symbols cache from {self.cache_path}: {e}. Possible cause: the cache file is corrupted. " "Check for any errors related to saving the cache in the logs.", logging.ERROR, ) @@ -1704,6 +1702,13 @@ class SyncLanguageServer: self._shutdown_lock = threading.Lock() self._is_shutting_down = False + @property + def cache_path(self) -> Path: + """ + The path to the cache file for the document symbols. + """ + return self.language_server.cache_path + @classmethod def create( cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, diff --git a/src/serena/agent.py b/src/serena/agent.py index 580b447..6b16a9a 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -667,20 +667,21 @@ def create_ls_for_project( @click.command() -@click.argument("project", type=click.Path(exists=True)) +@click.argument("project", type=click.Path(exists=True), required=False, default=os.getcwd()) @click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="WARNING") def index_project(project: str, log_level: str = "INFO") -> None: """ Index a project by saving the symbols of files to Serena's language server cache. - :param project: the project to index + :param project: the project to index. By default, the current working directory is used. """ log_level_int = logging.getLevelNamesMapping()[log_level.upper()] project = os.path.abspath(project) - log.info(f"Indexing project {project}") + print(f"Indexing symbols in project {project}") ls = create_ls_for_project(project, log_level=log_level_int) with ls.start_server(): ls.index_repository() + print(f"Symbols saved to {ls.cache_path}") class SerenaAgent: @@ -2138,21 +2139,19 @@ class SearchForPatternTool(Tool): ) else: # we walk through all files in the project starting from the root - files_to_search = [] - for root, dirs, files in os.walk(self.get_project_root()): - # Don't go into directories that are ignored by modifying dirs inplace - # Explanation for the + "/" part: - # pathspec can't handle the matching of directories if they don't end with a slash! - # see https://github.com/cpburnz/python-pathspec/issues/89 + project_root = self.get_project_root() + rel_paths_to_search = [] + for root, dirs, files in os.walk(project_root): dirs[:] = [d for d in dirs if not self.agent.path_is_gitignored(os.path.join(root, d))] for file in files: file_path = os.path.join(root, file) if not self.agent.path_is_gitignored(file_path): - files_to_search.append(file_path) + relative_path = os.path.relpath(file_path, project_root) + rel_paths_to_search.append(relative_path) # TODO (maybe): not super efficient to walk through the files again and filter if glob patterns are provided # but it probably never matters and this version required no further refactoring matches = search_files( - files_to_search, + rel_paths_to_search, pattern, paths_include_glob=paths_include_glob, paths_exclude_glob=paths_exclude_glob, diff --git a/src/serena/text_utils.py b/src/serena/text_utils.py index 4f40aa0..729f170 100644 --- a/src/serena/text_utils.py +++ b/src/serena/text_utils.py @@ -271,6 +271,8 @@ def glob_match(pattern: str, path: str) -> bool: :param path: File path to match against :return: True if path matches pattern """ + pattern = pattern.replace("\\", "/") # Normalize backslashes to forward slashes + # Handle ** patterns that should match zero or more directories if "**" in pattern: # Method 1: Standard fnmatch (matches one or more directories) From 6d540249668e800cf63b9411d633c995c9b15a85 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 20:37:32 +0200 Subject: [PATCH 074/174] Bugfix: reset of lang server also replaces the reference in SymbolManager --- src/serena/agent.py | 4 ++++ src/serena/symbol.py | 37 ++++++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 6b16a9a..32070cf 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -1056,11 +1056,15 @@ class SerenaAgent: ls_timeout=ls_timeout, trace_lsp_communication=self.serena_config.trace_lsp_communication, ) + log.info(f"Starting the language server for {self._active_project.project_name}") self.language_server.start() if not self.language_server.is_running(): raise RuntimeError( f"Failed to start the language server for {self._active_project.project_name} at {self._active_project.project_root}" ) + assert self.symbol_manager is not None, "Should never be None with an active project" + log.debug("Setting the language server in the agent's symbol manager") + self.symbol_manager.set_language_server(self.language_server) def get_tool(self, tool_class: type[TTool]) -> TTool: return self._all_tools[tool_class] # type: ignore diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 7f75098..e4c4997 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -494,9 +494,16 @@ class SymbolManager: :param agent: the agent to use (only needed for marking files as modified). You can pass None if you don't need an agent to be avare of file modifications performed by the symbol manager. """ - self.lang_server = lang_server + self._lang_server = lang_server self.agent = agent + def set_language_server(self, lang_server: SyncLanguageServer) -> None: + """ + Set the language server to use for symbol retrieval and editing operations. + This is useful if you want to change the language server after initializing the SymbolManager. + """ + self._lang_server = lang_server + def find_by_name( self, name_path: str, @@ -512,7 +519,7 @@ class SymbolManager: to symbols within a specific file or directory. """ symbols: list[Symbol] = [] - symbol_roots = self.lang_server.request_full_symbol_tree(within_relative_path=within_relative_path, include_body=include_body) + symbol_roots = self._lang_server.request_full_symbol_tree(within_relative_path=within_relative_path, include_body=include_body) for root in symbol_roots: symbols.extend( Symbol(root).find( @@ -522,14 +529,14 @@ class SymbolManager: return symbols def get_document_symbols(self, relative_path: str) -> list[Symbol]: - symbol_dicts, roots = self.lang_server.request_document_symbols(relative_path, include_body=False) + symbol_dicts, roots = self._lang_server.request_document_symbols(relative_path, include_body=False) symbols = [Symbol(s) for s in symbol_dicts] return symbols def find_by_location(self, location: SymbolLocation) -> Symbol | None: if location.relative_path is None: return None - symbol_dicts, roots = self.lang_server.request_document_symbols(location.relative_path, include_body=False) + symbol_dicts, roots = self._lang_server.request_document_symbols(location.relative_path, include_body=False) for symbol_dict in symbol_dicts: symbol = Symbol(symbol_dict) if symbol.location == location: @@ -598,7 +605,7 @@ class SymbolManager: assert symbol_location.relative_path is not None assert symbol_location.line is not None assert symbol_location.column is not None - references = self.lang_server.request_referencing_symbols( + references = self._lang_server.request_referencing_symbols( relative_file_path=symbol_location.relative_path, line=symbol_location.line, column=symbol_location.column, @@ -618,9 +625,9 @@ class SymbolManager: @contextmanager def _edited_file(self, relative_path: str) -> Iterator[None]: - with self.lang_server.open_file(relative_path) as file_buffer: + with self._lang_server.open_file(relative_path) as file_buffer: yield - root_path = self.lang_server.language_server.repository_root_path + root_path = self._lang_server.language_server.repository_root_path abs_path = os.path.join(root_path, relative_path) with open(abs_path, "w", encoding="utf-8") as f: f.write(file_buffer.contents) @@ -641,7 +648,7 @@ class SymbolManager: def _get_code_file_content(self, relative_path: str) -> str: """Get the content of a file using the language server.""" - return self.lang_server.language_server.retrieve_full_file_content(relative_path) + return self._lang_server.language_server.retrieve_full_file_content(relative_path) def replace_body(self, name_path: str, relative_file_path: str, body: str, *, use_same_indentation: bool = True) -> None: """ @@ -690,8 +697,8 @@ class SymbolManager: # make sure body always ends with at least one newline if not body.endswith("\n"): body += "\n" - self.lang_server.delete_text_between_positions(location.relative_path, start_pos, end_pos) - self.lang_server.insert_text_at_position(location.relative_path, start_line, start_col, body) + self._lang_server.delete_text_between_positions(location.relative_path, start_pos, end_pos) + self._lang_server.insert_text_at_position(location.relative_path, start_line, start_col, body) def insert_after_symbol( self, @@ -774,7 +781,7 @@ class SymbolManager: col = 0 with self._edited_symbol_location(location): - self.lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) + self._lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) def insert_before_symbol( self, @@ -827,7 +834,7 @@ class SymbolManager: body += "\n" assert location.relative_path is not None - self.lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) + self._lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) def insert_at_line(self, relative_path: str, line: int, content: str) -> None: """ @@ -837,7 +844,7 @@ class SymbolManager: :param content: the content to insert """ with self._edited_file(relative_path): - self.lang_server.insert_text_at_position(relative_path, line, 0, content) + self._lang_server.insert_text_at_position(relative_path, line, 0, content) def delete_lines(self, relative_path: str, start_line: int, end_line: int) -> None: """ @@ -852,7 +859,7 @@ class SymbolManager: with self._edited_file(relative_path): start_pos = Position(line=start_line, character=start_col) end_pos = Position(line=end_line_for_delete, character=end_col) - self.lang_server.delete_text_between_positions(relative_path, start_pos, end_pos) + self._lang_server.delete_text_between_positions(relative_path, start_pos, end_pos) def delete_symbol_at_location(self, location: SymbolLocation) -> None: """ @@ -862,7 +869,7 @@ class SymbolManager: assert location.relative_path is not None assert symbol.body_start_position is not None assert symbol.body_end_position is not None - self.lang_server.delete_text_between_positions(location.relative_path, symbol.body_start_position, symbol.body_end_position) + self._lang_server.delete_text_between_positions(location.relative_path, symbol.body_start_position, symbol.body_end_position) def delete_symbol(self, name_path: str, relative_file_path: str) -> None: """ From c9e84b202229dce56039af3a1845e459e2fd3dc2 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 17 Jun 2025 00:20:47 +0200 Subject: [PATCH 075/174] Hotfix: symbol manager may be None --- src/serena/agent.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 32070cf..21ef632 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -922,6 +922,7 @@ class SerenaAgent: self.ignore_spec = self.language_server.get_ignore_spec() # initialize project-specific instances + log.debug(f"Initializing symbol and memories manager for {project.project_name} at {project.project_root}") self.symbol_manager = SymbolManager(self.language_server, self) self.memories_manager = MemoriesManagerMDFilesInProject(project.project_root) self.lines_read = LinesRead() @@ -1062,9 +1063,11 @@ class SerenaAgent: raise RuntimeError( f"Failed to start the language server for {self._active_project.project_name} at {self._active_project.project_root}" ) - assert self.symbol_manager is not None, "Should never be None with an active project" - log.debug("Setting the language server in the agent's symbol manager") - self.symbol_manager.set_language_server(self.language_server) + if self.symbol_manager is not None: + log.debug("Setting the language server in the agent's symbol manager") + self.symbol_manager.set_language_server(self.language_server) + else: + log.debug("No symbol manager available yet, skipping setting the language server") def get_tool(self, tool_class: type[TTool]) -> TTool: return self._all_tools[tool_class] # type: ignore From 3a20b076be63e11bfb8242846bc563135d917775 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 17 Jun 2025 02:20:02 +0200 Subject: [PATCH 076/174] Minor in logging --- src/multilspy/language_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index dd123c9..e910e43 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -227,7 +227,7 @@ class LanguageServer: self.completions_available = asyncio.Event() if config.trace_lsp_communication: def logging_fn(source: str, target: str, msg: StringDict | str): - self.logger.log(f"LSP: {source} -> {target}: {str(msg)}", logging.DEBUG) + self.logger.log(f"LSP: {source} -> {target}: {str(msg)}", self.logger.logger.level) else: logging_fn = None From d156d19046dce596be44953e02c67f9bf75939af Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 17 Jun 2025 02:46:49 +0200 Subject: [PATCH 077/174] Lock sending payloads in LSServerHandler --- src/multilspy/lsp_protocol_handler/server.py | 112 ++++++++++++++----- 1 file changed, 85 insertions(+), 27 deletions(-) diff --git a/src/multilspy/lsp_protocol_handler/server.py b/src/multilspy/lsp_protocol_handler/server.py index 3cba4d4..a224143 100644 --- a/src/multilspy/lsp_protocol_handler/server.py +++ b/src/multilspy/lsp_protocol_handler/server.py @@ -33,6 +33,8 @@ import dataclasses import json import logging import os +import threading + import psutil from typing import Any, Callable, Dict, List, Optional, Union @@ -209,6 +211,14 @@ class LanguageServerHandler: self.task_counter = 0 self.loop = None self.start_independent_lsp_process = start_independent_lsp_process + + # Add thread locks for shared resources to prevent race conditions + self._stdin_lock = threading.Lock() + self._request_id_lock = threading.Lock() + self._response_handlers_lock = threading.Lock() + self._tasks_lock = threading.Lock() + + def is_running(self) -> bool: """ @@ -244,10 +254,14 @@ class LanguageServerHandler: raise RuntimeError(f"Process terminated immediately with code {self.process.returncode}. Error: {error_message}") self.loop = asyncio.get_event_loop() - self.tasks[self.task_counter] = self.loop.create_task(self.run_forever()) - self.task_counter += 1 - self.tasks[self.task_counter] = self.loop.create_task(self.run_forever_stderr()) - self.task_counter += 1 + + # Use lock to prevent race conditions on tasks and task_counter during startup + with self._tasks_lock: + self.tasks[self.task_counter] = self.loop.create_task(self.run_forever()) + self.task_counter += 1 + self.tasks[self.task_counter] = self.loop.create_task(self.run_forever_stderr()) + self.task_counter += 1 + async def stop(self) -> None: @@ -269,18 +283,23 @@ class LanguageServerHandler: async def _cancel_pending_tasks(self): """Cancel all pending tasks and wait for them to complete or timeout.""" pending_tasks = [] - for task in self.tasks.values(): - if not task.done(): - task.cancel() - pending_tasks.append(task) + + # Use lock to safely access tasks dictionary + with self._tasks_lock: + for task in self.tasks.values(): + if not task.done(): + task.cancel() + pending_tasks.append(task) if pending_tasks: try: await asyncio.wait_for(asyncio.gather(*pending_tasks, return_exceptions=True), timeout=5.0) except (asyncio.TimeoutError, Exception): pass - - self.tasks = {} + + # Clear tasks dictionary under lock + with self._tasks_lock: + self.tasks = {} async def _cleanup_process(self, process): """Clean up a process: close stdin, terminate/kill process, close stdout/stderr.""" @@ -406,12 +425,15 @@ class LanguageServerHandler: continue body = await self.process.stdout.readexactly(num_bytes) - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._handle_body(body)) - self.task_counter += 1 + # Use lock to prevent race conditions on tasks and task_counter + with self._tasks_lock: + self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._handle_body(body)) + self.task_counter += 1 except (BrokenPipeError, ConnectionResetError, StopLoopException): pass return self._received_shutdown + async def run_forever_stderr(self) -> None: """ Continuously read from the language server process stderr and log the messages @@ -467,28 +489,40 @@ class LanguageServerHandler: """ Send response to the given request id to the server with the given parameters """ - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( - self._send_payload(make_response(request_id, params)) - ) - self.task_counter += 1 + # Use lock to prevent race conditions on tasks and task_counter + with self._tasks_lock: + self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( + self._send_payload(make_response(request_id, params)) + ) + self.task_counter += 1 + def send_error_response(self, request_id: Any, err: Error) -> None: """ Send error response to the given request id to the server with the given error """ - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( - self._send_payload(make_error_response(request_id, err)) - ) - self.task_counter += 1 + # Use lock to prevent race conditions on tasks and task_counter + with self._tasks_lock: + self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( + self._send_payload(make_error_response(request_id, err)) + ) + self.task_counter += 1 + async def send_request(self, method: str, params: Optional[dict] = None) -> PayloadLike: """ Send request to the server, register the request id, and wait for the response """ request = Request() - request_id = self.request_id - self.request_id += 1 - self._response_handlers[request_id] = request + + # Use lock to prevent race conditions on request_id and _response_handlers + with self._request_id_lock: + request_id = self.request_id + self.request_id += 1 + + with self._response_handlers_lock: + self._response_handlers[request_id] = request + async with request.cv: await self._send_payload(make_request(method, request_id, params)) self._log(f"Waiting for asyncio condition for request {method} with params:\n{params}") @@ -499,6 +533,7 @@ class LanguageServerHandler: self._log(f"Returning non-error result, which is:\n{request.result}") return request.result + def _send_payload_sync(self, payload: StringDict) -> None: """ Send the payload to the server by writing to its stdin synchronously @@ -508,7 +543,17 @@ class LanguageServerHandler: msg = create_message(payload) if self.logger: self.logger("client", "server", payload) - self.process.stdin.writelines(msg) + + # Use lock to prevent concurrent writes to stdin that cause buffer corruption + with self._stdin_lock: + try: + self.process.stdin.writelines(msg) + except (BrokenPipeError, ConnectionResetError, OSError) as e: + # Log the error but don't raise to prevent cascading failures + if self.logger: + self.logger("client", "logger", f"Failed to write to stdin: {e}") + return + async def _send_payload(self, payload: StringDict) -> None: """ @@ -518,8 +563,18 @@ class LanguageServerHandler: return self._log(payload) msg = create_message(payload) - self.process.stdin.writelines(msg) - await self.process.stdin.drain() + + # Use lock to prevent concurrent writes to stdin that cause buffer corruption + with self._stdin_lock: + try: + self.process.stdin.writelines(msg) + await self.process.stdin.drain() + except (BrokenPipeError, ConnectionResetError, OSError) as e: + # Log the error but don't raise to prevent cascading failures + if self.logger: + self.logger("client", "logger", f"Failed to write to stdin: {e}") + return + def on_request(self, method: str, cb) -> None: """ @@ -537,7 +592,9 @@ class LanguageServerHandler: """ Handle the response received from the server for a request, using the id to determine the request """ - request = self._response_handlers.pop(response["id"]) + with self._response_handlers_lock: + request = self._response_handlers.pop(response["id"]) + if "result" in response and "error" not in response: await request.on_result(response["result"]) elif "result" not in response and "error" in response: @@ -545,6 +602,7 @@ class LanguageServerHandler: else: await request.on_error(Error(ErrorCodes.InvalidRequest, "")) + async def _request_handler(self, response: StringDict) -> None: """ Handle the request received from the server: call the appropriate callback function and return the result From 206b6a1d5c50839e4360fe3f40d163c502ab1d65 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 17 Jun 2025 13:07:29 +0200 Subject: [PATCH 078/174] Multilspy: shortened lsp log messages by cutting them off at 90 chars --- src/multilspy/language_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index e910e43..db9c1cb 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -227,7 +227,7 @@ class LanguageServer: self.completions_available = asyncio.Event() if config.trace_lsp_communication: def logging_fn(source: str, target: str, msg: StringDict | str): - self.logger.log(f"LSP: {source} -> {target}: {str(msg)}", self.logger.logger.level) + self.logger.log(f"LSP: {source} -> {target}: {str(msg)[:90]}...", self.logger.logger.level) else: logging_fn = None From 3fddeb4620a9cf5619d261eaa796c9b5afbb9a5e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 18 Jun 2025 15:12:43 +0200 Subject: [PATCH 079/174] More logging about tool exclusion --- src/serena/agent.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 21ef632..6d273e4 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -894,12 +894,27 @@ class SerenaAgent: excluded_tool_classes: set[type[Tool]] = set() # modes for mode in self._modes: - excluded_tool_classes.update(mode.get_excluded_tool_classes()) + mode_excluded_tool_classes = mode.get_excluded_tool_classes() + if len(mode_excluded_tool_classes) > 0: + log.info( + f"Mode {mode.name} excluded {len(mode_excluded_tool_classes)} tools: {', '.join([tool.get_name_from_cls() for tool in mode_excluded_tool_classes])}" + ) + excluded_tool_classes.update(mode_excluded_tool_classes) # context - excluded_tool_classes.update(self._context.get_excluded_tool_classes()) + context_excluded_tool_classes = self._context.get_excluded_tool_classes() + if len(context_excluded_tool_classes) > 0: + log.info( + f"Context {self._context.name} excluded {len(context_excluded_tool_classes)} tools: {', '.join([tool.get_name_from_cls() for tool in context_excluded_tool_classes])}" + ) + excluded_tool_classes.update(context_excluded_tool_classes) # project config if self._active_project is not None: - excluded_tool_classes.update(self._active_project.project_config.get_excluded_tool_classes()) + project_excluded_tool_classes = self._active_project.project_config.get_excluded_tool_classes() + if len(project_excluded_tool_classes) > 0: + log.info( + f"Project {self._active_project.project_name} excluded {len(project_excluded_tool_classes)} tools: {', '.join([tool.get_name_from_cls() for tool in project_excluded_tool_classes])}" + ) + excluded_tool_classes.update(project_excluded_tool_classes) if self._active_project.project_config.read_only: for tool_class in self._all_tools: if tool_class.can_edit(): From f65a0c83505c29d35a651d8dbbc35d8e305997ba Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 18 Jun 2025 16:00:52 +0200 Subject: [PATCH 080/174] Better tool exclusion handling at mcp level Some tools will never be activated during a session, so it's better to not expose them --- src/serena/config.py | 33 +++++++++++++++++++++++++++ src/serena/mcp.py | 54 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/serena/config.py b/src/serena/config.py index 7f7fd34..2f2659d 100644 --- a/src/serena/config.py +++ b/src/serena/config.py @@ -5,6 +5,7 @@ Context and Mode configuration loader import os from copy import copy from dataclasses import asdict, dataclass, field +from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Self @@ -167,3 +168,35 @@ class SerenaAgentContext: 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) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 0f41ee1..aabdfbe 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -24,8 +24,16 @@ from mcp.server.fastmcp.tools.base import Tool as MCPTool from sensai.util import logging from sensai.util.helper import mark_used -from serena.agent import SerenaAgent, ToolInterface, create_serena_config, show_fatal_exception_safe -from serena.config import SerenaAgentContext, SerenaAgentMode +from serena.agent import ( + ActivateProjectTool, + Project, + SerenaAgent, + ToolInterface, + ToolRegistry, + create_serena_config, + show_fatal_exception_safe, +) +from serena.config import RegisteredContext, SerenaAgentContext, SerenaAgentMode from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES from serena.process_isolated_agent import ( ProcessIsolatedDashboard, @@ -137,10 +145,35 @@ def create_mcp_server_and_agent( context_instance = SerenaAgentContext.load(context) modes_instances = [SerenaAgentMode.load(mode) for mode in modes] + is_ide_assistant = context_instance.name == RegisteredContext.IDE_ASSISTANT.value + + project_instance: Project | None = None if project is not None: - log.info(f"Will use project at mcp server startup: {project}") + # Fail early if the project cannot be loaded + log.info(f"Will activate project {project} at mcp server startup") + try: + project_instance = Project.load(project) + except Exception as e: + log.error(f"Failed to load or generate project config for {project}: {e}") + raise + else: + log.warning("No project passed at startup, you will have to activate a project yourself (by asking the agent to do so).") + + tools_excluded_in_this_session = context_instance.get_excluded_tool_classes() + if is_ide_assistant and project_instance is not None: + tools_excluded_in_this_session.extend(project_instance.project_config.get_excluded_tool_classes()) + # if a project has been loaded, it will be activated at startup and in ide-assistant context, + # we assume that no other project will be activated in this session. + # Therefore, we exclude the activate project tool + tools_excluded_in_this_session.append(ActivateProjectTool) + tool_names_excluded_in_this_session = {tool.get_name_from_cls() for tool in tools_excluded_in_this_session} + + all_tool_names = set(ToolRegistry.get_tool_names()) + tool_names_included_in_this_session = all_tool_names - tool_names_excluded_in_this_session try: + # Start agent and dashboard processes (the latter only if enabled) + serena_dashboard_process = None serena_config = create_serena_config( enable_web_dashboard=enable_web_dashboard, enable_gui_log_window=enable_gui_log_window, @@ -148,14 +181,12 @@ def create_mcp_server_and_agent( trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, ) + if serena_config.web_dashboard: + serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(tool_names_included_in_this_session)) serena_agent_process = ProcessIsolatedSerenaAgent( project=project, serena_config=serena_config, modes=modes_instances, context=context_instance ) - # Start process-isolated dashboard if enabled - serena_dashboard_process = None - if serena_config.web_dashboard: - serena_dashboard_process = ProcessIsolatedDashboard(tool_names=[]) except Exception as e: show_fatal_exception_safe(e) raise @@ -164,15 +195,14 @@ def create_mcp_server_and_agent( """Update the tools in the MCP server - adapted for process isolation.""" nonlocal mcp, serena_agent_process - # Get tool names from process-isolated agent - # Tools may change as a result of project activation. + # Get tool names from agent + # Tools may change as a result of project or mode activation. # NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not, # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, - # so we need to register all tools at startup, unfortunately. - tool_names = serena_agent_process.get_exposed_tool_names() + # so we need to register all potentially active tools at startup, unfortunately. if mcp is not None: mcp._tool_manager._tools = {} - for tool_name in tool_names: + for tool_name in tool_names_included_in_this_session: process_isolated_tool = ProcessIsolatedTool(process_agent=serena_agent_process, tool_name=tool_name) mcp_tool = make_tool(process_isolated_tool) mcp._tool_manager._tools[tool_name] = mcp_tool From 71df71a3d74868d6369e1f4df06e4ae3ccc8c3ad Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 18 Jun 2025 16:29:06 +0200 Subject: [PATCH 081/174] Fix missing path normalization in glob_match --- src/serena/text_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/serena/text_utils.py b/src/serena/text_utils.py index 729f170..1b9deb2 100644 --- a/src/serena/text_utils.py +++ b/src/serena/text_utils.py @@ -272,6 +272,7 @@ def glob_match(pattern: str, path: str) -> bool: :return: True if path matches pattern """ pattern = pattern.replace("\\", "/") # Normalize backslashes to forward slashes + path = path.replace("\\", "/") # Normalize path backslashes to forward slashes # Handle ** patterns that should match zero or more directories if "**" in pattern: From 12475850b5aa744198b78c552cd129c9a294216c Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 19 Jun 2025 01:16:08 -0400 Subject: [PATCH 082/174] Add github action to codespell main on push and PRs --- .github/workflows/codespell.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/codespell.yml diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 0000000..b231667 --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,25 @@ +# Codespell configuration is within pyproject.toml +--- +name: Codespell + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + codespell: + name: Check for spelling errors + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Annotate locations with typos + uses: codespell-project/codespell-problem-matcher@v1 + - name: Codespell + uses: codespell-project/actions-codespell@v2 From 7c33eeaf4843d7f5d8fa88ac5feab5c7101b2fef Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 19 Jun 2025 01:16:08 -0400 Subject: [PATCH 083/174] Add rudimentary codespell config --- pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 7e18dec..467632a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -265,3 +265,10 @@ markers = [ "snapshot: snapshot tests for symbolic editing operations", "isolated_process: test runs with process isolated agent", ] + +[tool.codespell] +# Ref: https://github.com/codespell-project/codespell#using-a-config-file +skip = '.git*,*.svg,*.lock,*.min.*' +check-hidden = true +# ignore-regex = '' +# ignore-words-list = '' From 64ab987e1c8209a756c12905f73ed41567e670cf Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 19 Jun 2025 01:20:29 -0400 Subject: [PATCH 084/174] Ignore some lines in tests with "fo" --- test/serena/test_symbol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/serena/test_symbol.py b/test/serena/test_symbol.py index 7c21215..72fd8d4 100644 --- a/test/serena/test_symbol.py +++ b/test/serena/test_symbol.py @@ -107,7 +107,7 @@ class TestSymbolNameMatching: "bar/foo", ["mod", "bar", "foobar"], True, True, id="R: 'bar/foo' matches ['mod', 'bar', 'foobar'] as substring (suffix)" ), pytest.param("bar/foo", ["bar", "bazfoo"], True, True, id="R: 'bar/foo' matches ['bar', 'bazfoo'] as substring"), - pytest.param("bar/fo", ["bar", "foo"], True, True, id="R: 'bar/fo' matches ['bar', 'foo'] as substring"), + pytest.param("bar/fo", ["bar", "foo"], True, True, id="R: 'bar/fo' matches ['bar', 'foo'] as substring"), # codespell:ignore pytest.param("bar/foo", ["bar", "baz"], True, False, id="R: 'bar/foo' does not match ['bar', 'baz'] (last no substr)"), pytest.param( "bar/foo", ["baz", "foobar"], True, False, id="R: 'bar/foo' does not match ['baz', 'foobar'] (first part mismatch)" @@ -148,7 +148,7 @@ class TestSymbolNameMatching: # Substring matches (is_substring_match=True) pytest.param("/bar/foo", ["bar", "foobar"], True, True, id="A: '/bar/foo' matches ['bar', 'foobar'] as substring"), pytest.param("/bar/foo", ["bar", "bazfoo"], True, True, id="A: '/bar/foo' matches ['bar', 'bazfoo'] as substring"), - pytest.param("/bar/fo", ["bar", "foo"], True, True, id="A: '/bar/fo' matches ['bar', 'foo'] as substring"), + pytest.param("/bar/fo", ["bar", "foo"], True, True, id="A: '/bar/fo' matches ['bar', 'foo'] as substring"), # codespell:ignore pytest.param("/bar/foo", ["bar", "baz"], True, False, id="A: '/bar/foo' does not match ['bar', 'baz'] (last no substr)"), pytest.param( "/bar/foo", ["baz", "foobar"], True, False, id="A: '/bar/foo' does not match ['baz', 'foobar'] (first part mismatch)" From cef6614e5a85a8bd9b8297452647695920259102 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 19 Jun 2025 01:25:22 -0400 Subject: [PATCH 085/174] Fix typo interactively --- .serena/memories/suggested_commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md index 9ce3d46..296c37b 100644 --- a/.serena/memories/suggested_commands.md +++ b/.serena/memories/suggested_commands.md @@ -20,7 +20,7 @@ The following tasks should generally be executed using `uv run poe `. "isolated_process: test runs with process isolated agent", ] ``` - By default, `uv run poe test` uses the markes set in the env var `PYTEST_MARKERS`, or, if it unset, uses `-m "not java and not rust and not isolated process"`. + By default, `uv run poe test` uses the markers set in the env var `PYTEST_MARKERS`, or, if it unset, uses `-m "not java and not rust and not isolated process"`. You can override this behavior by simply passing the `-m` option to `uv run poe test`, e.g. `uv run poe test -m "python or go"`. For finishing a task, make sure format, type-check and test pass! Run them at the end of the task From fb16dcaedc96fce3fbad10fd7704755ecfd85527 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 19 Jun 2025 01:25:43 -0400 Subject: [PATCH 086/174] [DATALAD RUNCMD] chore: run codespell throughout fixing a few new typos automagically === Do not change lines below === { "chain": [], "cmd": "codespell -w", "exit": 0, "extra_inputs": [], "inputs": [], "outputs": [], "pwd": "." } ^^^ Do not change lines above ^^^ --- CHANGELOG.md | 2 +- lessons_learned.md | 2 +- src/multilspy/lsp_protocol_handler/lsp_constants.py | 2 +- src/multilspy/lsp_protocol_handler/lsp_types.py | 2 +- src/multilspy/multilspy_logger.py | 2 +- src/multilspy/multilspy_utils.py | 6 +++--- src/serena/agent.py | 8 ++++---- src/serena/resources/config/modes/editing.yml | 6 +++--- .../resources/config/prompt_templates/system_prompt.yml | 2 +- src/serena/symbol.py | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc0c7b..a52ff43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ Also some improvements to prompts. # 2025-05-21 -**Signficant improvement in symbol finding!** +**Significant improvement in symbol finding!** * Serena core: * `FindSymbolTool` now can look for symbols by specifying paths to them, not just the symbol name diff --git a/lessons_learned.md b/lessons_learned.md index f9fb78c..b6d15a7 100644 --- a/lessons_learned.md +++ b/lessons_learned.md @@ -56,7 +56,7 @@ When developing the `ReplaceRegexTool` we were initially not able to make Claude examples nor explicit instructions helped. It was only after adding ``` -IMPORTANT: REMEMBER TO USE WILDCARDS WEHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! +IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! ``` to the initial instructions and to the tool description that Claude finally started following the instructions. diff --git a/src/multilspy/lsp_protocol_handler/lsp_constants.py b/src/multilspy/lsp_protocol_handler/lsp_constants.py index 149af38..4ca5c3d 100644 --- a/src/multilspy/lsp_protocol_handler/lsp_constants.py +++ b/src/multilspy/lsp_protocol_handler/lsp_constants.py @@ -31,7 +31,7 @@ class LSPConstants: # key used to represent the language a document is in - "java", "csharp", etc. LANGUAGE_ID = "languageId" - # key used to represent the version of a document (a shared value betwen the client and server) + # key used to represent the version of a document (a shared value between the client and server) VERSION = "version" # key used to represent the text of a document being sent from the client to the server on open diff --git a/src/multilspy/lsp_protocol_handler/lsp_types.py b/src/multilspy/lsp_protocol_handler/lsp_types.py index 4ec467e..6d6a754 100644 --- a/src/multilspy/lsp_protocol_handler/lsp_types.py +++ b/src/multilspy/lsp_protocol_handler/lsp_types.py @@ -2243,7 +2243,7 @@ class SignatureHelp(TypedDict): range of `signatures` the value defaults to zero or is ignored if the `SignatureHelp` has no signatures. - Whenever possible implementors should make an active decision about + Whenever possible implementers should make an active decision about the active signature and shouldn't rely on a default value. In future version of the protocol this property might become diff --git a/src/multilspy/multilspy_logger.py b/src/multilspy/multilspy_logger.py index 495c8a7..c6135a5 100644 --- a/src/multilspy/multilspy_logger.py +++ b/src/multilspy/multilspy_logger.py @@ -32,7 +32,7 @@ class MultilspyLogger: def log(self, debug_message: str, level: int, sanitized_error_message: str = "", stacklevel: int = 2) -> None: """ - Log the debug and santized messages using the logger + Log the debug and sanitized messages using the logger """ debug_message = debug_message.replace("'", '"').replace("\n", " ") diff --git a/src/multilspy/multilspy_utils.py b/src/multilspy/multilspy_utils.py index 3655456..20959c4 100644 --- a/src/multilspy/multilspy_utils.py +++ b/src/multilspy/multilspy_utils.py @@ -109,7 +109,7 @@ class PathUtils: from urllib.parse import urlparse, unquote from urllib.request import url2pathname except ImportError: - # backwards compatability + # backwards compatibility from urlparse import urlparse from urllib import unquote, url2pathname parsed = urlparse(uri) @@ -168,12 +168,12 @@ class FileUtils: response = requests.get(url, stream=True, timeout=60) if response.status_code != 200: logger.log(f"Error downloading file '{url}': {response.status_code} {response.text}", logging.ERROR) - raise MultilspyException("Error downoading file.") + raise MultilspyException("Error downloading file.") with open(target_path, "wb") as f: shutil.copyfileobj(response.raw, f) except Exception as exc: logger.log(f"Error downloading file '{url}': {exc}", logging.ERROR) - raise MultilspyException("Error downoading file.") from None + raise MultilspyException("Error downloading file.") from None @staticmethod def download_and_extract_archive(logger: MultilspyLogger, url: str, target_path: str, archive_type: str) -> None: diff --git a/src/serena/agent.py b/src/serena/agent.py index 6d273e4..694bddf 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -1681,7 +1681,7 @@ class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit): as the tool will automatically add the indentation of the original symbol to each line. For example, for replacing a method in python, you can just write (using the standard python indentation): body="def my_method_replacement(self, ...):\n first_line\n second_line...". So each line after the first line only has - an indentation of 4 (the indentation relative to the first characted), + an indentation of 4 (the indentation relative to the first character), since the additional indentation will be added by the tool. Same for more deeply nested cases. You always only need to write the relative indentation to the first character of the first line, and that in turn should not have any indentation. @@ -1718,7 +1718,7 @@ class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit): :param name_path: for finding the symbol to insert after, same logic as in the `find_symbol` tool. :param relative_path: the relative path to the file containing the symbol - :param body: the body/content to be inserted. Important: the insterted code will automatically have the + :param body: the body/content to be inserted. Important: the inserted code will automatically have the same indentation as the symbol's body, so you do not need to provide any additional indentation. """ self.symbol_manager.insert_after_symbol( @@ -1748,7 +1748,7 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit): :param name_path: for finding the symbol to insert before, same logic as in the `find_symbol` tool. :param relative_path: the relative path to the file containing the symbol - :param body: the body/content to be inserted. Important: the insterted code will automatically have the + :param body: the body/content to be inserted. Important: the inserted code will automatically have the same indentation as the symbol's body, so you do not need to provide any additional indentation. """ self.symbol_manager.insert_before_symbol( @@ -1828,7 +1828,7 @@ class ReplaceRegexTool(Tool, ToolMarkerCanEdit): Always try to use wildcards to avoid specifying the exact content of the code to be replaced, especially if it spans several lines. - IMPORTANT: REMEMBER TO USE WILDCARDS WEHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! + IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! :param relative_path: the relative path to the file :param regex: a Python-style regular expression, matches of which will be replaced. diff --git a/src/serena/resources/config/modes/editing.yml b/src/serena/resources/config/modes/editing.yml index cbf1251..b50d3a5 100644 --- a/src/serena/resources/config/modes/editing.yml +++ b/src/serena/resources/config/modes/editing.yml @@ -27,7 +27,7 @@ prompt: | you are replacing or inserting above or below. 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 unterstand relationships between symbols by using the `find_referencing_symbols` tool. If not explicitly requested otherwise by a user, + 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. @@ -52,7 +52,7 @@ prompt: | 1. If the snippet to be replaced is likely to be unique within the file, you perform the replacement by directly using the escaped version of the original. 2. If the snippet is probably not unique, and you want to replace all occurrences, you use the `allow_multiple_occurrences` flag. - 3. If the snippet is not unique, and you want to replace a specific occurence, you make use of the code surrounding the snippet + 3. If the snippet is not unique, and you want to replace a specific occurrence, you make use of the code surrounding the snippet to extend the regex with content before/after such that the regex will have exactly one match. 4. You generally assume that a snippet is unique, knowing that the tool will return an error on multiple matches. You only read more file content (for crafvarting a more specific regex) if such a failure unexpectedly occurs. @@ -102,7 +102,7 @@ prompt: | Generally, I remind you that you rely on the regex tool with providing you the correct feedback, no need for more verification! - IMPORTANT: REMEMBER TO USE WILDCARDS WEHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! + IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! excluded_tools: - replace_lines - insert_at_line diff --git a/src/serena/resources/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml index c524579..8f3dd50 100644 --- a/src/serena/resources/config/prompt_templates/system_prompt.yml +++ b/src/serena/resources/config/prompt_templates/system_prompt.yml @@ -39,7 +39,7 @@ prompts: 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` - You can unterstand relationships between symbols by using the `find_referencing_symbols` tool. + You can understand relationships between symbols by using the `find_referencing_symbols` tool. 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 diff --git a/src/serena/symbol.py b/src/serena/symbol.py index e4c4997..f7cb194 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -492,7 +492,7 @@ class SymbolManager: """ :param lang_server: the language server to use for symbol retrieval as well as editing operations. :param agent: the agent to use (only needed for marking files as modified). You can pass None if you don't - need an agent to be avare of file modifications performed by the symbol manager. + need an agent to be aware of file modifications performed by the symbol manager. """ self._lang_server = lang_server self.agent = agent From 09d008fc91afb1159237085e1bbfef2e4779f0be Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 20 Jun 2025 14:01:25 +0200 Subject: [PATCH 087/174] Simplify and speed up GitignoreParser._find_gitignore_files --- src/serena/util/file_system.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index fa8ee32..4e88a9a 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -1,3 +1,4 @@ +import glob import os from collections.abc import Callable from dataclasses import dataclass, field @@ -134,18 +135,7 @@ class GitignoreParser: :return: List of absolute paths to .gitignore files """ - gitignore_files = [] - - for root, dirs, files in os.walk(self.repo_root): - # Skip .git directory - if ".git" in dirs: - dirs.remove(".git") - - if ".gitignore" in files: - gitignore_path = os.path.join(root, ".gitignore") - gitignore_files.append(gitignore_path) - - return gitignore_files + return glob.glob(self.repo_root + "/.gitignore") + glob.glob(self.repo_root + "/**/.gitignore") def _create_ignore_spec(self, gitignore_file_path: str) -> GitignoreSpec: """ From 5eb5930cbc36ba08378781e03e7547da0ed292d0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 20 Jun 2025 15:38:19 +0200 Subject: [PATCH 088/174] Improve README structure Remove obsolete section on Goose --- README.md | 275 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 141 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index 5cc2ecc..e5c6df2 100644 --- a/README.md +++ b/README.md @@ -97,16 +97,17 @@ implementation. - [What Can I Use Serena For?](#what-can-i-use-serena-for) - [Free Coding Agents with Serena](#free-coding-agents-with-serena) - [Quick Start](#quick-start) - * [Claude Code](#claude-code) - * [General Setup](#general-setup) - * [Configuration](#configuration) - * [Project Activation](#project-activation) - * [MCP Server (Claude Desktop)](#mcp-server-claude-desktop) + * [Running the Serena MCP Server](#running-the-serena-mcp-server) + [Local Installation](#local-installation) - + [Docker Installation (Experimental)](#docker-installation-experimental) + + [Using uvx](#using-uvx) + + [Using Docker (Experimental)](#using-docker-experimental) + + [Command-Line Arguments](#command-line-arguments) + * [Configuration](#configuration) + * [Project Activation & Indexing](#project-activation--indexing) + * [Claude Code](#claude-code) + * [Claude Desktop](#claude-desktop) + [Troubleshooting](#troubleshooting) * [Other MCP Clients (Cline, Roo-Code, Cursor, Windsurf, etc.)](#other-mcp-clients-cline-roo-code-cursor-windsurf-etc) - * [Goose](#goose) * [Agno Agent](#agno-agent) * [Other Agent Frameworks](#other-agent-frameworks) - [Detailed Usage and Recommendations](#detailed-usage-and-recommendations) @@ -170,68 +171,89 @@ We thus built Serena with the prospect of being able to cancel most other subscr Serena can be used in various ways, below you will find instructions for selected integrations. -- If you just want to turn Claude into a free-to-use coding agent, we recommend using Serena through Claude Desktop. -- If you want to use Gemini or any other model and you want a GUI experience, you should use [Agno](#agno-agent). On macOS you can also use the GUI of [goose](#goose). -- If you prefer using Serena through a CLI, you can use [goose](#goose). There again almost any model is possible. +- If you just want to turn Claude into a free-to-use coding agent, we recommend using Serena through [Claude Code](#claude-code) or [Claude Desktop](#claude-desktop). +- If you want to use Gemini or any other model, and you want a GUI experience, you can use [Agno](#agno-agent). On macOS, you can also use the GUI of [goose](#goose). - If you want to use Serena integrated in your IDE, see the section on [other MCP clients](#other-mcp-clients---cline-roo-code-cursor-windsurf-etc). Serena is managed by `uv`, so you will need to [install it](https://docs.astral.sh/uv/getting-started/installation/)). -### Claude Code +### Running the Serena MCP Server -Serena is a great way to make Claude Code both cheaper and more powerful! We are collecting -several examples for that and have heard very positive feedback so far. Claude Code users can -add serena with (from the project dir) +You have several options for running the MCP server, which are explained in the subsections below. -```shell -claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena-mcp-server --context ide-assistant --project $(pwd) -``` +The typical usage involves the client (Claude Code, Claude Desktop, etc.) running +the MCP server as a subprocess (using stdio communication), +so the client needs to be provided with the command to run the MCP server. +(Alternatively, you can run the MCP server in SSE mode and tell your client +how to connect to it.) -You should ask claude to read the initial instructions as the first prompt, where it will receive information -on how to use Serena's tools. -We also recommend that you index your code once before starting (especially for larger projects), it will accelerate the symbolic operations. +Note that no matter how you run the MCP server, Serena will, by default, start a small web-based dashboard on localhost that will display logs and allow shutting down the +MCP server (since many clients fail to clean up processes correctly). +This and other settings can be adjusted in the [configuration](#configuration) and/or by providing [command-line arguments](#command-line-arguments). -```shell -# from the project directory, or pass the path to the project as argument -uvx --from git+https://github.com/oraios/serena index-project -``` +#### Local Installation - -### General Setup - -You can either - -1. Clone the repository and cd into it. +1. Clone the repository and change into it. + ```shell + git clone https://github.com/oraios/serena + cd serena + ``` 2. Optionally create a config file from the template and adjust it according to your preferences. ```shell cp src/serena/resources/serena_config.template.yml serena_config.yml ``` If you just want the default config, you can skip this part, and a config file will be created when you first run Serena. +3. Run the server with `uv`: + ```shell + uv run serena-mcp-server + ``` + When running from outside the serena installation directory, be sure to pass it, i.e. use + ```shell + uv run --directory /abs/path/to/serena serena-mcp-server + ``` -or use `uvx` to run Serena directly by relying on +#### Using uvx + +`uvx` can be used to run the latest version of Serena directly from the repository, without an explicit local installation. + +* Windows: + ```shell + uvx --from git+https://github.com/oraios/serena serena-mcp-server.exe + ``` +* Other operating systems: + ```shell + uvx --from git+https://github.com/oraios/serena serena-mcp-server + ``` + +#### Using Docker (Experimental) + +âš ī¸ Docker support is currently experimental with several limitations. Please read the [Docker documentation](DOCKER.md) for important caveats before using it. + +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 -uvx --from git+https://github.com/oraios/serena serena-mcp-server.exe +docker run --rm -i --network host -v /path/to/your/projects:/workspaces/projects ghcr.io/oraios/serena:latest serena-mcp-server --transport stdio ``` -in your MCP config (delete the `.exe` on macOS or Linux). +Replace `/path/to/your/projects` with the absolute path to your projects directory. The Docker approach provides: +- Better security isolation for shell command execution +- No need to install language servers and dependencies locally +- Consistent environment across different systems -You can now add Serena to your MCP client as described below for various clients and -[activate your first project](#project-activation). For larger projects, we recommend that you first index them -to accelerate Serena's tools on the first usage. +See the [Docker documentation](DOCKER.md) for detailed setup instructions, configuration options, and known limitations. -```shell -uvx --from git+https://github.com/oraios/serena index-project /path/to/project -``` +#### Command-Line Arguments +The Serena MCP server supports a wide range of command-line options, including the option to run in SSE mode +and to adapt Serena to various [contexts and modes of operation](#modes-and-contexts). + +Run with parameter `--help` to get a list of available options. -> In the default configuration, Serena will start a small dashboard on localhost that will display logs and allow shutting down the -> MCP server (since many clients fail to cleanup processes, leaving zombies behind). If you don't want that, simply set `web_dashboard` to `False` -> in your `serena_config.yml` or pass ### Configuration -Serena's behavior (like available projects, active tools and prompts) is configured in four places: +Serena's behavior (active tools and prompts as well as logging configuration, etc.) is configured in four places: 1. The `serena_config.yml` for general settings that apply to all clients and projects 2. In the arguments passed to the `serena-mcp-server` in your client's config (see below), @@ -254,10 +276,13 @@ want to use Serena. You can just ask the LLM to show you the config of your session, Serena has a tool for it. -### Project Activation +### Project Activation & Indexing The recommended way is to just ask the LLM to activate a project by providing it an absolute path to, or, -in case the project was activated in the past, by it's name. The default project name is the directory name. +in case the project was activated in the past, by its name. The default project name is the directory name. + + * "Activate the project /path/to/my_project" + * "Activate the project my_project" All projects that have been activated will be automatically added to your `serena_config.yml`, and for each project, the file `.serena/project.yml` will be generated. You can adjust the latter, e.g., by changing the name @@ -267,72 +292,86 @@ same name. If you are mostly working with the same project, you can also configure to always activate a project at startup by passing `--project ` to the `serena-mcp-server` command in your client's MCP config. -### MCP Server (Claude Desktop) +â„šī¸ For larger projects, we recommend that you index your project to accelerate Serena's tools; otherwise the first +tool application may be very slow. +To do so, run one of these commands the project directory or pass the path to the project as an argument: + +* When using a local installation: + ```shell + uv run --directory /abs/path/to/serena index-project + ``` +* When using uvx: + ```shell + uvx --from git+https://github.com/oraios/serena index-project + ``` + +### Claude Code + +Serena is a great way to make Claude Code both cheaper and more powerful! + +From your project directory, add serena with a command like this, + +```shell +claude mcp add serena -- --context ide-assistant --project $(pwd) +``` + +where `` is your way of [running the Serena MCP server](#running-the-serena-mcp-server). +For example, when using `uvx`, you would run +```shell +claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena-mcp-server --context ide-assistant --project $(pwd) +``` + +â„šī¸ Once in Claude Code, you should ask Claude to "Read the initial instructions" as your first prompt, such that it will receive information +on how to use Serena's tools. + + +### Claude Desktop -Configure the MCP server in your client. For [Claude Desktop](https://claude.ai/download) (available for Windows and macOS), go to File / Settings / Developer / MCP Servers / Edit Config, -which will let you open the JSON file `claude_desktop_config.json`. Add the following (with adjusted paths) to enable Serena: +which will let you open the JSON file `claude_desktop_config.json`. +Add the `serena` MCP server configuration, using a [run command](#running-the-serena-mcp-server) depending on your setup. -#### Local Installation - -```json -{ - "mcpServers": { - "serena": { - "command": "/abs/path/to/uv", - "args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server"] - } - } -} -``` - -#### Docker Installation (Experimental) - -âš ī¸ **EXPERIMENTAL**: Docker support is currently experimental with several limitations. Please read the [Docker documentation](DOCKER.md) for important caveats before using. - -Alternatively, you can run Serena using Docker: - -```json -{ - "mcpServers": { - "serena": { - "command": "docker", - "args": ["run", "--rm", "-i", "--network", "host", "-v", "/path/to/your/projects:/workspaces/projects", "ghcr.io/oraios/serena:latest", "serena-mcp-server", "--transport", "stdio"] - } - } -} -``` - -Replace `/path/to/your/projects` with the absolute path to your projects directory. The Docker approach provides: -- Better security isolation for shell command execution -- No need to install language servers and dependencies locally -- Consistent environment across different systems - -See the [Docker documentation](DOCKER.md) for detailed setup instructions, configuration options, and known limitations. +* local installation: + ```json + { + "mcpServers": { + "serena": { + "command": "/abs/path/to/uv", + "args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server"] + } + } + } + ``` +* uvx: + ```json + { + "mcpServers": { + "serena": { + "command": "/abs/path/to/uvx", + "args": ["--from", "git+https://github.com/oraios/serena", "serena-mcp-server"] + } + } + } + ``` +* docker: + ```json + { + "mcpServers": { + "serena": { + "command": "docker", + "args": ["run", "--rm", "-i", "--network", "host", "-v", "/path/to/your/projects:/workspaces/projects", "ghcr.io/oraios/serena:latest", "serena-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 +That's it! Save the config and then restart Claude Desktop. You are ready for activating your first project. -â„šī¸ You can further customize the run command, see +â„šī¸ You can further customize the run command using additional arguments (see [above](#command-line-arguments)). -```shell -uv run serena-mcp-server --help -``` - -â„šī¸ You can use Serena without cloning or configuring it explicitly by using the Docker image above or: - -```json -{ - "mcpServers": { - "serena": { - "command": "uvx", - "args": ["--from", "git+https://github.com/oraios/serena", "serena-mcp-server"] - } - } -} -``` #### Troubleshooting @@ -356,11 +395,6 @@ community version](https://github.com/aaddrick/claude-desktop-debian). After restarting, you should see Serena's tools in your chat interface (notice the small hammer icon). -âš ī¸ Tool Names: Claude Desktop (and most MCP Clients) don't resolve the name of the server. So you shouldn't -say something like "use Serena's tools". Instead, you can instruct the LLM to use symbolic tools or to -use a particular tool by referring to its name. Moreover, in some clients, if you use multiple MCP Servers, you might get -**tool name collisions** which lead to undefined behavior. - â„šī¸ Note that MCP servers which use stdio as a protocol are somewhat unusual as far as client/server architectures go, as the server necessarily has to be started by the client in order for communication to take place via the server's standard input/output stream. In other words, you do not need to start the server yourself. The client application (e.g. Claude Desktop) takes care of this and @@ -384,33 +418,6 @@ e.g., for one of the following reasons: 2. You are on Linux and don't want to use the [community-created Claude Desktop](https://github.com/aaddrick/claude-desktop-debian) 3. You want tighter integration of Serena into your IDE and don't mind paying for that -### Goose - -[goose](https://github.com/block/goose) is a standalone coding agent which has an integration for MCP servers and offers a CLI (as well as a GUI on macOS). -Using goose is currently the simplest way of running Serena through a CLI-based UI with an LLM of your choice. - -Follow the instructions [here](https://block.github.io/goose/docs/getting-started/installation/) to install it. - -After that, use `goose configure` to add an extension. For adding Serena, choose the option `Command-line Extension`, name it `Serena` and add the following as command: - -``` -/abs/path/to/uv run --directory /abs/path/to/serena serena-mcp-server --project /optional/abs/path/to/project -``` - -Since Serena can do all necessary editing and command operations, you should disable the `developer` extension that goose enables by default. -For that execute - -```shell -goose configure -``` -again, choose the option `Toggle Extensions`, and make sure Serena is enabled selected while `developer` is not. - -That's it. Read through the configuration options of goose to see what you can do with it (which is a lot, like setting different levels of permissions for tool execution). - -> Goose does not seem to always properly terminate python processes for MCP servers when a session ends. -> You may want to disable the Serena GUI and/or to manually cleanup any running python processes after finishing your work -> with goose. - ### Agno Agent Agno is a model-agnostic agent framework that allows you to turn Serena into an agent From 018ad7ef66798b79863600af28a82bc36636533c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 20 Jun 2025 17:18:32 +0200 Subject: [PATCH 089/174] Fix .env files in user projects potentially affecting fastmcp Settings #200 --- src/serena/mcp.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index aabdfbe..9cbdc1f 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -21,6 +21,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 pydantic_settings import SettingsConfigDict from sensai.util import logging from sensai.util.helper import mark_used @@ -271,6 +272,11 @@ def create_mcp_server_and_agent( signal.signal(signal.SIGINT, sigint_singal) signal.signal(signal.SIGTERM, sigterm_signal) + # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain + # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; + # retain only FASTMCP_ prefix for already set environment variables. + Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") + mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) mcp = FastMCP(**mcp_settings.model_dump()) return mcp, serena_agent_process From 023727c8f3c1a727c69e1d865ce57bff5340fe3d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 20 Jun 2025 17:47:09 +0200 Subject: [PATCH 090/174] Restore backward compatibility with old project configurations, constructing ProjectConfig in a safe manner --- src/serena/agent.py | 18 +++++++++++++----- src/serena/constants.py | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 694bddf..0f7aefd 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -39,7 +39,7 @@ from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_types import SymbolKind from serena import serena_version from serena.config import SerenaAgentContext, SerenaAgentMode -from serena.constants import PROJECT_TEMPLATE_FILE, REPO_ROOT, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME +from serena.constants import DEFAULT_ENCODING, PROJECT_TEMPLATE_FILE, REPO_ROOT, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI from serena.prompt_factory import PromptFactory, SerenaPromptFactory from serena.symbol import SymbolManager @@ -127,7 +127,7 @@ class ProjectConfig(ToStringMixin): read_only: bool = False ignore_all_files_in_gitignore: bool = True initial_prompt: str = "" - encoding: str = "utf-8" + encoding: str = DEFAULT_ENCODING SERENA_DEFAULT_PROJECT_FILE = "project.yml" @@ -171,12 +171,20 @@ class ProjectConfig(ToStringMixin): """ Create a ProjectConfig instance from a configuration dictionary """ - data = copy(data) try: - data["language"] = Language(data["language"].lower()) + language = Language(data["language"].lower()) except ValueError as e: raise ValueError(f"Invalid language: {data['language']}.\nValid languages are: {[l.value for l in Language]}") from e - return cls(**data) + return cls( + project_name=data["project_name"], + language=language, + ignored_paths=data.get("ignored_paths", []), + excluded_tools=set(data.get("excluded_tools", [])), + read_only=data.get("read_only", False), + ignore_all_files_in_gitignore=data.get("ignore_all_files_in_gitignore", True), + initial_prompt=data.get("initial_prompt", ""), + encoding=data.get("encoding", DEFAULT_ENCODING), + ) def to_json_dict(self) -> dict[str, Any]: result = asdict(self) diff --git a/src/serena/constants.py b/src/serena/constants.py index 9e0980f..4cbf39a 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -12,6 +12,7 @@ SERENA_ICON_DIR = str(_serena_pkg_path / "resources" / "icons") SERENA_MANAGED_DIR_NAME = ".serena" +DEFAULT_ENCODING = "utf-8" DEFAULT_CONTEXT = "desktop-app" DEFAULT_MODES = ("interactive", "editing") From 162d3c81d3546a681a470ac93327b41918844a78 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 20 Jun 2025 22:36:34 +0200 Subject: [PATCH 091/174] Move instructions on SSE mode to the proper place --- README.md | 59 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index e5c6df2..1a5b7a5 100644 --- a/README.md +++ b/README.md @@ -98,15 +98,16 @@ implementation. - [Free Coding Agents with Serena](#free-coding-agents-with-serena) - [Quick Start](#quick-start) * [Running the Serena MCP Server](#running-the-serena-mcp-server) - + [Local Installation](#local-installation) - + [Using uvx](#using-uvx) - + [Using Docker (Experimental)](#using-docker-experimental) + + [Usage](#usage) + * [Local Installation](#local-installation) + - [Using uvx](#using-uvx) + - [Using Docker (Experimental)](#using-docker-experimental) + + [SSE Mode](#sse-mode) + [Command-Line Arguments](#command-line-arguments) * [Configuration](#configuration) * [Project Activation & Indexing](#project-activation--indexing) * [Claude Code](#claude-code) * [Claude Desktop](#claude-desktop) - + [Troubleshooting](#troubleshooting) * [Other MCP Clients (Cline, Roo-Code, Cursor, Windsurf, etc.)](#other-mcp-clients-cline-roo-code-cursor-windsurf-etc) * [Agno Agent](#agno-agent) * [Other Agent Frameworks](#other-agent-frameworks) @@ -127,7 +128,7 @@ implementation. * [Running Out of Context](#running-out-of-context) * [Combining Serena with Other MCP Servers](#combining-serena-with-other-mcp-servers) * [Serena's Logs: The Dashboard and GUI Tool](#serenas-logs-the-dashboard-and-gui-tool) - * [Troubleshooting](#troubleshooting-1) + * [Troubleshooting](#troubleshooting) - [Comparison with Other Coding Agents](#comparison-with-other-coding-agents) * [Subscription-Based Coding Agents](#subscription-based-coding-agents) * [API-Based Coding Agents](#api-based-coding-agents) @@ -181,6 +182,8 @@ Serena is managed by `uv`, so you will need to [install it](https://docs.astral. You have several options for running the MCP server, which are explained in the subsections below. +#### Usage + The typical usage involves the client (Claude Code, Claude Desktop, etc.) running the MCP server as a subprocess (using stdio communication), so the client needs to be provided with the command to run the MCP server. @@ -191,7 +194,7 @@ Note that no matter how you run the MCP server, Serena will, by default, start a MCP server (since many clients fail to clean up processes correctly). This and other settings can be adjusted in the [configuration](#configuration) and/or by providing [command-line arguments](#command-line-arguments). -#### Local Installation +###### Local Installation 1. Clone the repository and change into it. ```shell @@ -212,7 +215,7 @@ This and other settings can be adjusted in the [configuration](#configuration) a uv run --directory /abs/path/to/serena serena-mcp-server ``` -#### Using uvx +##### Using uvx `uvx` can be used to run the latest version of Serena directly from the repository, without an explicit local installation. @@ -225,7 +228,7 @@ This and other settings can be adjusted in the [configuration](#configuration) a uvx --from git+https://github.com/oraios/serena serena-mcp-server ``` -#### Using Docker (Experimental) +##### Using Docker (Experimental) âš ī¸ Docker support is currently experimental with several limitations. Please read the [Docker documentation](DOCKER.md) for important caveats before using it. @@ -243,9 +246,30 @@ Replace `/path/to/your/projects` with the absolute path to your projects directo See the [Docker documentation](DOCKER.md) for detailed setup instructions, configuration options, and known limitations. +#### SSE Mode + +â„šī¸ Note that MCP servers which use stdio as a protocol are somewhat unusual as far as client/server architectures go, as the server +necessarily has to be started by the client in order for communication to take place via the server's standard input/output stream. +In other words, you do not need to start the server yourself. The client application (e.g. Claude Desktop) takes care of this and +therefore needs to be configured with a launch command. + +When using instead the SSE mode, which uses HTTP-based communication, you control the server lifecycle yourself, +i.e. you start the server and provide the client with the URL to connect to it. + +Simply provide `serena-mcp-server` with the `--transport sse` option and optionally provide the port. +For example, to run the Serena MCP server in SSE mode on port 9121 using a local installation, +you would run this command from the Serena directory, + +```shell +uv run serena-mcp-server --transport sse --port 9121 +``` + +and then configure your client to connect to `http://localhost:9121`. + + #### Command-Line Arguments -The Serena MCP server supports a wide range of command-line options, including the option to run in SSE mode +The Serena MCP server supports a wide range of additional command-line options, including the option to run in SSE mode and to adapt Serena to various [contexts and modes of operation](#modes-and-contexts). Run with parameter `--help` to get a list of available options. @@ -372,18 +396,6 @@ That's it! Save the config and then restart Claude Desktop. You are ready for ac â„šī¸ You can further customize the run command using additional arguments (see [above](#command-line-arguments)). - -#### Troubleshooting - -Some client/OS/setup configurations were reported to cause issues when using Serena with the standard `stdio` protocol, where the MCP server is started by the client application. -If you experience such problems, you can start Serena in `sse` mode by running, e.g., - -```shell -uv run serena-mcp-server --transport sse --port 9121 -``` - -Then configure your client to connect to `http://localhost:9121`. - Note: on Windows and macOS there are official Claude Desktop applications by Anthropic, for Linux there is an [open-source community version](https://github.com/aaddrick/claude-desktop-debian). @@ -395,11 +407,6 @@ community version](https://github.com/aaddrick/claude-desktop-debian). After restarting, you should see Serena's tools in your chat interface (notice the small hammer icon). -â„šī¸ Note that MCP servers which use stdio as a protocol are somewhat unusual as far as client/server architectures go, as the server -necessarily has to be started by the client in order for communication to take place via the server's standard input/output stream. -In other words, you do not need to start the server yourself. The client application (e.g. Claude Desktop) takes care of this and -therefore needs to be configured with a launch command. In SSE transport you control the lifetime of the server yourself. - For more information on MCP servers with Claude Desktop, see [the official quick start guide](https://modelcontextprotocol.io/quickstart/user). ### Other MCP Clients (Cline, Roo-Code, Cursor, Windsurf, etc.) From 2f607ff2b0137827904ad93dbc5b5c4a7a743773 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 11:39:30 +0200 Subject: [PATCH 092/174] Add package solidlsp with copies of classes from multilspy (unmodified) --- pyproject.toml | 8 +- src/solidlsp/ls.py | 1851 +++++++++++++++++++++++++++++++++++ src/solidlsp/ls_handler.py | 531 ++++++++++ src/solidlsp/lsp_request.py | 435 ++++++++ 4 files changed, 2821 insertions(+), 4 deletions(-) create mode 100644 src/solidlsp/ls.py create mode 100644 src/solidlsp/ls_handler.py create mode 100644 src/solidlsp/lsp_request.py diff --git a/pyproject.toml b/pyproject.toml index 467632a..c7bc40a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,10 +112,10 @@ PYDEVD_DISABLE_FILE_VALIDATION = "1" # 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 java and not rust and not isolated_process}\"" -_black_check = "black --check --exclude src/multilspy/ src scripts test" -_ruff_check = "ruff check --exclude .venv/ --exclude src/multilspy/ src scripts test" -_black_format = "black --exclude .venv/|src/multilspy/ src scripts test" -_ruff_format = "ruff check --exclude src/multilspy/ --fix src scripts test" +_black_check = "black --check --exclude src/multilspy/ --exclude src/solidlsp/ src scripts test" +_ruff_check = "ruff check --exclude .venv/ --exclude src/multilspy/ --exclude src/solidlsp/ src scripts test" +_black_format = "black --exclude .venv/|src/multilspy/|src/solidlsp/ src scripts test" +_ruff_format = "ruff check --exclude src/multilspy/ --exclude src/solidlsp/ --fix src scripts test" lint = [ "_black_check", "_ruff_check", diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py new file mode 100644 index 0000000..6d84cad --- /dev/null +++ b/src/solidlsp/ls.py @@ -0,0 +1,1851 @@ +import asyncio +import json +import logging +import os +import pathlib +import pickle +import re +import threading +from collections import defaultdict +from contextlib import contextmanager +from copy import copy +from pathlib import Path, PurePath +from typing import Dict, Iterator, List, Optional, Tuple, Union, cast, Self + +import pathspec +import tqdm +from overrides import override + +from multilspy import multilspy_types +from multilspy.language_server import LSPFileBuffer, GenericDocumentSymbol, ReferenceInSymbol +from multilspy.lsp_protocol_handler import lsp_types +from multilspy.lsp_protocol_handler import lsp_types as LSPTypes +from multilspy.lsp_protocol_handler.lsp_constants import LSPConstants +from multilspy.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind, InitializeParams +from multilspy.lsp_protocol_handler.server import ( + Error, + LanguageServerHandler, + ProcessLaunchInfo, + StringDict, +) +from multilspy.multilspy_config import Language, MultilspyConfig +from multilspy.multilspy_exceptions import MultilspyException +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import FileUtils, PathUtils, TextUtils +from serena.text_utils import MatchedConsecutiveLines, search_files + + +class LanguageServer: + """ + The LanguageServer class provides a language agnostic interface to the Language Server Protocol. + It is used to communicate with Language Servers of different programming languages. + """ + + # To be overridden and extended by subclasses + def is_ignored_dirname(self, dirname: str) -> bool: + """ + A language-specific condition for directories that should always be ignored. For example, venv + in Python and node_modules in JS/TS should be ignored always. + """ + return dirname.startswith('.') + + @classmethod + def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str) -> "LanguageServer": + """ + Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. + + If language is Java, then ensure that jdk-17.0.6 or higher is installed, `java` is in PATH, and JAVA_HOME is set to the installation directory. + If language is JS/TS, then ensure that node (v18.16.0 or higher) is installed and in PATH. + + :param repository_root_path: The root path of the repository. + :param config: The Multilspy configuration. + :param logger: The logger to use. + :return LanguageServer: A language specific LanguageServer instance. + """ + if config.code_language == Language.PYTHON: + from multilspy.language_servers.pyright_language_server.pyright_server import ( + PyrightServer, + ) + + return PyrightServer(config, logger, repository_root_path) + # It used to be jedi, but pyright is a bit faster, and also more actively maintained + # Keeping the previous code for reference + # from multilspy.language_servers.jedi_language_server.jedi_server import ( + # JediServer, + # ) + + # return JediServer(config, logger, repository_root_path) + elif config.code_language == Language.JAVA: + from multilspy.language_servers.eclipse_jdtls.eclipse_jdtls import ( + EclipseJDTLS, + ) + + return EclipseJDTLS(config, logger, repository_root_path) + elif config.code_language == Language.KOTLIN: + from multilspy.language_servers.kotlin_language_server.kotlin_language_server import ( + KotlinLanguageServer, + ) + + return KotlinLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.RUST: + from multilspy.language_servers.rust_analyzer.rust_analyzer import ( + RustAnalyzer, + ) + + return RustAnalyzer(config, logger, repository_root_path) + elif config.code_language == Language.CSHARP: + from multilspy.language_servers.omnisharp.omnisharp import OmniSharp + + return OmniSharp(config, logger, repository_root_path) + elif config.code_language in [Language.TYPESCRIPT, Language.JAVASCRIPT]: + from multilspy.language_servers.typescript_language_server.typescript_language_server import ( + TypeScriptLanguageServer, + ) + return TypeScriptLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.GO: + from multilspy.language_servers.gopls.gopls import Gopls + + return Gopls(config, logger, repository_root_path) + elif config.code_language == Language.RUBY: + from multilspy.language_servers.solargraph.solargraph import Solargraph + + return Solargraph(config, logger, repository_root_path) + elif config.code_language == Language.DART: + from multilspy.language_servers.dart_language_server.dart_language_server import DartLanguageServer + + return DartLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.CPP: + from multilspy.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer + + return ClangdLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.PHP: + from multilspy.language_servers.intelephense.intelephense import Intelephense + + return Intelephense(config, logger, repository_root_path) + else: + logger.log(f"Language {config.code_language} is not supported", logging.ERROR) + raise MultilspyException(f"Language {config.code_language} is not supported") + + def __init__( + self, + config: MultilspyConfig, + logger: MultilspyLogger, + repository_root_path: str, + process_launch_info: ProcessLaunchInfo, + language_id: str, + ): + """ + Initializes a LanguageServer instance. + + Do not instantiate this class directly. Use `LanguageServer.create` method instead. + + :param config: The Multilspy configuration. + :param logger: The logger to use. + :param repository_root_path: The root path of the repository. + :param process_launch_info: Each language server has a specific command used to start the server. + This parameter is the command to launch the language server process. + The command must pass appropriate flags to the binary, so that it runs in the stdio mode, + as opposed to HTTP, TCP modes supported by some language servers. + """ + if type(self) == LanguageServer: + raise MultilspyException( + "LanguageServer is an abstract class and cannot be instantiated directly. Use LanguageServer.create method instead." + ) + + self.logger = logger + self.repository_root_path: str = repository_root_path + self.logger.log(f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) + + self.language_id = language_id + self.open_file_buffers: Dict[str, LSPFileBuffer] = {} + self.language = Language(language_id) + + # load cache first to prevent any racing conditions due to asyncio stuff + self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]]] = {} + """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" + self._cache_lock = threading.Lock() + self._cache_has_changed: bool = False + self.load_cache() + + self.server_started = False + self.completions_available = asyncio.Event() + if config.trace_lsp_communication: + def logging_fn(source: str, target: str, msg: StringDict | str): + self.logger.log(f"LSP: {source} -> {target}: {str(msg)[:90]}...", self.logger.logger.level) + else: + logging_fn = None + + + # cmd is obtained from the child classes, which provide the language specific command to start the language server + # LanguageServerHandler provides the functionality to start the language server and communicate with it + self.logger.log(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) + self.server = LanguageServerHandler( + process_launch_info, + logger=logging_fn, + start_independent_lsp_process=config.start_independent_lsp_process, + ) + + + # Set up the pathspec matcher for the ignored paths + # for all absolute paths in ignored_paths, convert them to relative paths + processed_patterns = [] + for pattern in set(config.ignored_paths): + # Normalize separators (pathspec expects forward slashes) + pattern = pattern.replace(os.path.sep, '/') + processed_patterns.append(pattern) + self.logger.log(f"Processing {len(processed_patterns)} ignored paths from the config", logging.DEBUG) + + # Create a pathspec matcher from the processed patterns + self._ignore_spec = pathspec.PathSpec.from_lines( + pathspec.patterns.GitWildMatchPattern, + processed_patterns + ) + + def get_ignore_spec(self) -> pathspec.PathSpec: + """Returns the pathspec matcher for the paths that were configured to be ignored through + the multilspy config. + + This is is a subset of the full language-specific ignore spec that determines + which files are relevant for the language server. + + This matcher is useful for operations outside of the language server, + such as when searching for relevant non-language files in the project. + """ + return self._ignore_spec + + def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool: + """ + Determine if a path should be ignored based on file type + and ignore patterns. + + :param relative_path: Relative path to check + :param ignore_unsupported_files: whether files that are not supported source files should be ignored + + :return: True if the path should be ignored, False otherwise + """ + abs_path = os.path.join(self.repository_root_path, relative_path) + if not os.path.exists(abs_path): + raise FileNotFoundError(f"File {abs_path} not found, the ignore check cannot be performed") + + # Check file extension if it's a file + is_file = os.path.isfile(abs_path) + if is_file and ignore_unsupported_files: + fn_matcher = self.language.get_source_fn_matcher() + if not fn_matcher.is_relevant_filename(abs_path): + return True + + # Create normalized path for consistent handling + rel_path = Path(relative_path) + + # Check each part of the path against always fulfilled ignore conditions + dir_parts = rel_path.parts + if is_file: + dir_parts = dir_parts[:-1] + for part in dir_parts: + if not part: # Skip empty parts (e.g., from leading '/') + continue + if self.is_ignored_dirname(part): + return True + + # Use pathspec for gitignore-style pattern matching + # Normalize path separators for pathspec (it expects forward slashes) + normalized_path = str(rel_path).replace(os.path.sep, '/') + + # pathspec can't handle the matching of directories if they don't end with a slash! + # see https://github.com/cpburnz/python-pathspec/issues/89 + if os.path.isdir(os.path.join(self.repository_root_path, normalized_path)) and not normalized_path.endswith('/'): + normalized_path = normalized_path + '/' + + # Use the pathspec matcher to check if the path matches any ignore pattern + if self.get_ignore_spec().match_file(normalized_path): + return True + + return False + + async def _shutdown(self, timeout: float = 5.0): + """ + A robust shutdown process designed to terminate cleanly on all platforms, including Windows, + by explicitly closing all I/O pipes. + """ + if not self.server.is_running(): + self.logger.log("Server process not running, skipping shutdown.", logging.DEBUG) + return + + self.logger.log(f"Initiating final robust shutdown with a {timeout}s timeout...", logging.INFO) + process = self.server.process + reader_tasks = list(self.server.tasks.values()) + + # --- Main Shutdown Logic --- + try: + # Stage 1: Graceful Termination Request + # Send LSP shutdown and close stdin to signal no more input. + try: + await asyncio.wait_for(self.server.shutdown(), timeout=2.0) + if process.stdin and not process.stdin.is_closing(): + process.stdin.close() + except Exception: + pass # Ignore errors here, we are proceeding to terminate anyway. + + # Stage 2: Terminate and Concurrently Drain stdout/stderr + process.terminate() + + # Wait for the process to exit AND for the output pipes to be drained. + # The reader tasks will exit when they hit EOF. + await asyncio.wait_for( + asyncio.gather(process.wait(), *reader_tasks), + timeout=timeout - 2.0 + ) + self.logger.log("Process terminated and output pipes drained.", logging.INFO) + + except asyncio.TimeoutError: + # Stage 3: Forceful Kill + self.logger.log("Graceful termination failed. Forcefully killing process...", logging.WARNING) + if self.server.is_running(): + try: + process.kill() + # Wait for the killed process to be reaped by the OS. + await process.wait() + except Exception as e: + self.logger.log(f"Error during forceful kill: {e}", logging.ERROR) + + except Exception as e: + self.logger.log(f"An unexpected error occurred during shutdown logic: {e}", logging.ERROR) + + finally: + # === STAGE 4: EXPLICIT TASK & PIPE CLEANUP === + self.logger.log("Performing final task cancellation and pipe handle cleanup...", logging.DEBUG) + + # 1. Cancel any lingering reader tasks. + for task in reader_tasks: + if not task.done(): + task.cancel() + # Wait for cancellations to complete. + await asyncio.gather(*reader_tasks, return_exceptions=True) + self.server.tasks = {} + + # 2. Explicitly close each pipe to release OS handles. + for pipe in [process.stdin, process.stdout, process.stderr]: + if pipe and hasattr(pipe, 'is_closing') and not pipe.is_closing(): + try: + pipe.close() + except Exception: + pass + + # 3. Null out the process object in the handler. + self.server.process = None + self.logger.log("Shutdown sequence fully finished.", logging.DEBUG) + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["LanguageServer"]: + """ + Starts the Language Server and yields the LanguageServer instance. + """ + self.server_started = True + try: + yield self + finally: + self.server_started = False + await self._shutdown() + + @contextmanager + def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]: + """ + Open a file in the Language Server. This is required before making any requests to the Language Server. + + :param relative_file_path: The relative path of the file to open. + """ + if not self.server_started: + self.logger.log( + "open_file called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) + uri = pathlib.Path(absolute_file_path).as_uri() + + if uri in self.open_file_buffers: + assert self.open_file_buffers[uri].uri == uri + assert self.open_file_buffers[uri].ref_count >= 1 + + self.open_file_buffers[uri].ref_count += 1 + yield self.open_file_buffers[uri] + self.open_file_buffers[uri].ref_count -= 1 + else: + contents = FileUtils.read_file(self.logger, absolute_file_path) + + version = 0 + self.open_file_buffers[uri] = LSPFileBuffer(uri, contents, version, self.language_id, 1) + + self.server.notify.did_open_text_document( + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.URI: uri, + LSPConstants.LANGUAGE_ID: self.language_id, + LSPConstants.VERSION: 0, + LSPConstants.TEXT: contents, + } + } + ) + yield self.open_file_buffers[uri] + self.open_file_buffers[uri].ref_count -= 1 + + if self.open_file_buffers[uri].ref_count == 0: + self.server.notify.did_close_text_document( + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.URI: uri, + } + } + ) + del self.open_file_buffers[uri] + + def insert_text_at_position( + self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str + ) -> multilspy_types.Position: + """ + Insert text at the given line and column in the given file and return + the updated cursor position after inserting the text. + + :param relative_file_path: The relative path of the file to open. + :param line: The line number at which text should be inserted. + :param column: The column number at which text should be inserted. + :param text_to_be_inserted: The text to insert. + """ + if not self.server_started: + self.logger.log( + "insert_text_at_position called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) + uri = pathlib.Path(absolute_file_path).as_uri() + + # Ensure the file is open + assert uri in self.open_file_buffers + + file_buffer = self.open_file_buffers[uri] + file_buffer.version += 1 + + new_contents, new_l, new_c = TextUtils.insert_text_at_position(file_buffer.contents, line, column, text_to_be_inserted) + file_buffer.contents = new_contents + self.server.notify.did_change_text_document( + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.VERSION: file_buffer.version, + LSPConstants.URI: file_buffer.uri, + }, + LSPConstants.CONTENT_CHANGES: [ + { + LSPConstants.RANGE: { + "start": {"line": line, "character": column}, + "end": {"line": line, "character": column}, + }, + "text": text_to_be_inserted, + } + ], + } + ) + return multilspy_types.Position(line=new_l, character=new_c) + + def delete_text_between_positions( + self, + relative_file_path: str, + start: multilspy_types.Position, + end: multilspy_types.Position, + ) -> str: + """ + Delete text between the given start and end positions in the given file and return the deleted text. + """ + if not self.server_started: + self.logger.log( + "insert_text_at_position called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) + uri = pathlib.Path(absolute_file_path).as_uri() + + # Ensure the file is open + assert uri in self.open_file_buffers + + file_buffer = self.open_file_buffers[uri] + file_buffer.version += 1 + new_contents, deleted_text = TextUtils.delete_text_between_positions(file_buffer.contents, start_line=start["line"], start_col=start["character"], end_line=end["line"], end_col=end["character"]) + file_buffer.contents = new_contents + self.server.notify.did_change_text_document( + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.VERSION: file_buffer.version, + LSPConstants.URI: file_buffer.uri, + }, + LSPConstants.CONTENT_CHANGES: [{LSPConstants.RANGE: {"start": start, "end": end}, "text": ""}], + } + ) + return deleted_text + + async def _send_definition_request(self, definition_params: DefinitionParams) -> Union[Definition, List[LocationLink], None]: + return await self.server.send.definition(definition_params) + + async def request_definition( + self, relative_file_path: str, line: int, column: int + ) -> List[multilspy_types.Location]: + """ + Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server + for the symbol at the given line and column in the given file. Wait for the response and return the result. + + :param relative_file_path: The relative path of the file that has the symbol for which definition should be looked up + :param line: The line number of the symbol + :param column: The column number of the symbol + + :return List[multilspy_types.Location]: A list of locations where the symbol is defined + """ + + if not self.server_started: + self.logger.log( + "find_function_definition called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + with self.open_file(relative_file_path): + # sending request to the language server and waiting for response + definition_params = cast(DefinitionParams, { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.URI: pathlib.Path( + str(PurePath(self.repository_root_path, relative_file_path)) + ).as_uri() + }, + LSPConstants.POSITION: { + LSPConstants.LINE: line, + LSPConstants.CHARACTER: column, + }, + }) + response = await self._send_definition_request(definition_params) + + ret: List[multilspy_types.Location] = [] + if isinstance(response, list): + # response is either of type Location[] or LocationLink[] + for item in response: + assert isinstance(item, dict) + if LSPConstants.URI in item and LSPConstants.RANGE in item: + new_item: multilspy_types.Location = {} + new_item.update(item) + new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) + new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) + ret.append(multilspy_types.Location(new_item)) + elif ( + LSPConstants.ORIGIN_SELECTION_RANGE in item + and LSPConstants.TARGET_URI in item + and LSPConstants.TARGET_RANGE in item + and LSPConstants.TARGET_SELECTION_RANGE in item + ): + new_item: multilspy_types.Location = {} + new_item["uri"] = item[LSPConstants.TARGET_URI] + new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) + new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) + new_item["range"] = item[LSPConstants.TARGET_SELECTION_RANGE] + ret.append(multilspy_types.Location(**new_item)) + else: + assert False, f"Unexpected response from Language Server: {item}" + elif isinstance(response, dict): + # response is of type Location + assert LSPConstants.URI in response + assert LSPConstants.RANGE in response + + new_item: multilspy_types.Location = {} + new_item.update(response) + new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) + new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) + ret.append(multilspy_types.Location(**new_item)) + elif response is None: + # Some language servers return None when they cannot find a definition + # This is expected for certain symbol types like generics or types with incomplete information + self.logger.log( + f"Language server returned None for definition request at {relative_file_path}:{line}:{column}", + logging.WARNING, + ) + else: + assert False, f"Unexpected response from Language Server: {response}" + + return ret + + # Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses + async def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[lsp_types.Location] | None: + return await self.server.send.references( + { + "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, + "position": {"line": line, "character": column}, + "context": {"includeDeclaration": False}, + } + ) + + async def request_references( + self, relative_file_path: str, line: int, column: int + ) -> List[multilspy_types.Location]: + """ + Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server + to find references to the symbol at the given line and column in the given file. Wait for the response and return the result. + Filters out references located in ignored directories. + + :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up + :param line: The line number of the symbol + :param column: The column number of the symbol + + :return: A list of locations where the symbol is referenced (excluding ignored directories) + """ + + if not self.server_started: + self.logger.log( + "request_references called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + + with self.open_file(relative_file_path): + try: + response = await self._send_references_request(relative_file_path, line=line, column=column) + except Exception as e: + # Catch LSP internal error (-32603) and raise a more informative exception + if isinstance(e, Error) and getattr(e, 'code', None) == -32603: + raise RuntimeError( + f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. " + "This often occurs when requesting references for a symbol not referenced in the expected way. " + ) from e + raise + if response is None: + return [] + + ret: List[multilspy_types.Location] = [] + assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}" + for item in response: + assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}" + assert LSPConstants.URI in item + assert LSPConstants.RANGE in item + + abs_path = PathUtils.uri_to_path(item[LSPConstants.URI]) + rel_path = Path(abs_path).relative_to(self.repository_root_path) + if self.is_ignored_path(str(rel_path)): + self.logger.log(f"Ignoring reference in {rel_path} since it should be ignored", logging.DEBUG) + continue + + new_item: multilspy_types.Location = {} + new_item.update(item) + new_item["absolutePath"] = str(abs_path) + new_item["relativePath"] = str(rel_path) + ret.append(multilspy_types.Location(**new_item)) + + return ret + + async def request_references_with_content( + self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0 + ) -> List[MatchedConsecutiveLines]: + """ + Like request_references, but returns the content of the lines containing the references, not just the locations. + + :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up + :param line: The line number of the symbol + :param column: The column number of the symbol + :param context_lines_before: The number of lines to include in the context before the line containing the reference + :param context_lines_after: The number of lines to include in the context after the line containing the reference + + :return: A list of MatchedConsecutiveLines objects, one for each reference. + """ + references = await self.request_references(relative_file_path, line, column) + return [self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) for ref in references] + + def retrieve_full_file_content(self, relative_file_path: str) -> str: + """ + Retrieve the full content of the given file. + """ + with self.open_file(relative_file_path) as file_data: + return file_data.contents + + def retrieve_content_around_line(self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0) -> MatchedConsecutiveLines: + """ + Retrieve the content of the given file around the given line. + + :param relative_file_path: The relative path of the file to retrieve the content from + :param line: The line number to retrieve the content around + :param context_lines_before: The number of lines to retrieve before the given line + :param context_lines_after: The number of lines to retrieve after the given line + + :return MatchedConsecutiveLines: A container with the desired lines. + """ + with self.open_file(relative_file_path) as file_data: + file_contents = file_data.contents + return MatchedConsecutiveLines.from_file_contents(file_contents, line=line, context_lines_before=context_lines_before, context_lines_after=context_lines_after, source_file_path=relative_file_path) + + + async def request_completions( + self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False + ) -> List[multilspy_types.CompletionItem]: + """ + Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server + to find completions at the given line and column in the given file. Wait for the response and return the result. + + :param relative_file_path: The relative path of the file that has the symbol for which completions should be looked up + :param line: The line number of the symbol + :param column: The column number of the symbol + + :return List[multilspy_types.CompletionItem]: A list of completions + """ + with self.open_file(relative_file_path): + open_file_buffer = self.open_file_buffers[ + pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() + ] + completion_params: LSPTypes.CompletionParams = { + "position": {"line": line, "character": column}, + "textDocument": {"uri": open_file_buffer.uri}, + "context": {"triggerKind": LSPTypes.CompletionTriggerKind.Invoked}, + } + response: Union[List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None] = None + + num_retries = 0 + while response is None or (response["isIncomplete"] and num_retries < 30): + await self.completions_available.wait() + response: Union[ + List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None + ] = await self.server.send.completion(completion_params) + if isinstance(response, list): + response = {"items": response, "isIncomplete": False} + num_retries += 1 + + # TODO: Understand how to appropriately handle `isIncomplete` + if response is None or (response["isIncomplete"] and not(allow_incomplete)): + return [] + + if "items" in response: + response = response["items"] + + response: List[LSPTypes.CompletionItem] = response + + # TODO: Handle the case when the completion is a keyword + items = [item for item in response if item["kind"] != LSPTypes.CompletionItemKind.Keyword] + + completions_list: List[multilspy_types.CompletionItem] = [] + + for item in items: + assert "insertText" in item or "textEdit" in item + assert "kind" in item + completion_item = {} + if "detail" in item: + completion_item["detail"] = item["detail"] + + if "label" in item: + completion_item["completionText"] = item["label"] + completion_item["kind"] = item["kind"] + elif "insertText" in item: + completion_item["completionText"] = item["insertText"] + completion_item["kind"] = item["kind"] + elif "textEdit" in item and "newText" in item["textEdit"]: + completion_item["completionText"] = item["textEdit"]["newText"] + completion_item["kind"] = item["kind"] + elif "textEdit" in item and "range" in item["textEdit"]: + new_dot_lineno, new_dot_colno = ( + completion_params["position"]["line"], + completion_params["position"]["character"], + ) + assert all( + ( + item["textEdit"]["range"]["start"]["line"] == new_dot_lineno, + item["textEdit"]["range"]["start"]["character"] == new_dot_colno, + item["textEdit"]["range"]["start"]["line"] == item["textEdit"]["range"]["end"]["line"], + item["textEdit"]["range"]["start"]["character"] + == item["textEdit"]["range"]["end"]["character"], + ) + ) + + completion_item["completionText"] = item["textEdit"]["newText"] + completion_item["kind"] = item["kind"] + elif "textEdit" in item and "insert" in item["textEdit"]: + assert False + else: + assert False + + completion_item = multilspy_types.CompletionItem(**completion_item) + completions_list.append(completion_item) + + return [ + json.loads(json_repr) + for json_repr in set([json.dumps(item, sort_keys=True) for item in completions_list]) + ] + + async def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: + """ + Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server + to find symbols in the given file. Wait for the response and return the result. + + :param relative_file_path: The relative path of the file that has the symbols + :param include_body: whether to include the body of the symbols in the result. + :return: A list of symbols in the file, and a list of root symbols that represent the tree structure of the symbols. + All symbols will have a location, a children, and a parent attribute, + where the parent attribute is None for root symbols. + Note that this is slightly different from the call to request_full_symbol_tree, + where the parent attribute will be the file symbol which in turn may have a package symbol as parent. + If you need a symbol tree that contains file symbols as well, you should use `request_full_symbol_tree` instead. + """ + # TODO: it's kinda dumb to not use the cache if include_body is False after include_body was True once + # Should be fixed in the future, it's a small performance optimization + cache_key = f"{relative_file_path}-{include_body}" + with self.open_file(relative_file_path) as file_data: + with self._cache_lock: + file_hash_and_result = self._document_symbols_cache.get(cache_key) + if file_hash_and_result is not None: + file_hash, result = file_hash_and_result + if file_hash == file_data.content_hash: + self.logger.log(f"Returning cached document symbols for {relative_file_path}", logging.DEBUG) + return result + else: + self.logger.log(f"Content for {relative_file_path} has changed. Will overwrite in-memory cache", logging.DEBUG) + else: + self.logger.log(f"No cache hit for symbols with {include_body=} in {relative_file_path}", logging.DEBUG) + + self.logger.log(f"Requesting document symbols for {relative_file_path} from the Language Server", logging.DEBUG) + response = await self.server.send.document_symbol( + { + "textDocument": { + "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() + } + } + ) + self.logger.log(f"Received {len(response) if response is not None else None} document symbols for {relative_file_path} from the Language Server", logging.DEBUG) + + def turn_item_into_symbol_with_children(item: GenericDocumentSymbol): + item = cast(multilspy_types.UnifiedSymbolInformation, item) + absolute_path = os.path.join(self.repository_root_path, relative_file_path) + + # handle missing entries in location + if "location" not in item: + uri = pathlib.Path(absolute_path).as_uri() + assert "range" in item + tree_location = multilspy_types.Location( + uri=uri, + range=item['range'], + absolutePath=absolute_path, + relativePath=relative_file_path, + ) + item['location'] = tree_location + location = item["location"] + if "absolutePath" not in location: + location["absolutePath"] = absolute_path + if "relativePath" not in location: + location["relativePath"] = relative_file_path + if include_body: + item['body'] = self.retrieve_symbol_body(item) + # handle missing selectionRange + if "selectionRange" not in item: + if "range" in item: + item["selectionRange"] = item["range"] + else: + item["selectionRange"] = item["location"]["range"] + children = item.get(LSPConstants.CHILDREN, []) + for child in children: + child["parent"] = item + item[LSPConstants.CHILDREN] = children + + flat_all_symbol_list: List[multilspy_types.UnifiedSymbolInformation] = [] + assert isinstance(response, list), f"Unexpected response from Language Server: {response}" + root_nodes: List[multilspy_types.UnifiedSymbolInformation] = [] + for root_item in response: + if "range" not in root_item and "location" not in root_item: + if root_item["kind"] in [SymbolKind.File, SymbolKind.Module]: + ... + + # mutation is more convenient than creating a new dict, + # so we cast and rename the var after the mutating call to turn_item_into_symbol_with_children + # which turned and item into a "symbol" + turn_item_into_symbol_with_children(root_item) + root_symbol = cast(multilspy_types.UnifiedSymbolInformation, root_item) + root_symbol["parent"] = None + + root_nodes.append(root_symbol) + assert isinstance(root_symbol, dict) + assert LSPConstants.NAME in root_symbol + assert LSPConstants.KIND in root_symbol + + if LSPConstants.CHILDREN in root_symbol: + # TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well + + def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> List[multilspy_types.UnifiedSymbolInformation]: + node = cast(multilspy_types.UnifiedSymbolInformation, node) + l: List[multilspy_types.UnifiedSymbolInformation] = [] + turn_item_into_symbol_with_children(node) + assert LSPConstants.CHILDREN in node + children = node[LSPConstants.CHILDREN] + l.append(node) + for child in children: + l.extend(visit_tree_nodes_and_build_tree_repr(child)) + return l + + flat_all_symbol_list.extend(visit_tree_nodes_and_build_tree_repr(root_symbol)) + else: + flat_all_symbol_list.append(multilspy_types.UnifiedSymbolInformation(**root_symbol)) + + result = flat_all_symbol_list, root_nodes + self.logger.log(f"Caching document symbols for {relative_file_path}", logging.DEBUG) + with self._cache_lock: + self._document_symbols_cache[cache_key] = (file_data.content_hash, result) + self._cache_has_changed = True + return result + + async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + """ + Will go through all files in the project or within a relative path and build a tree of symbols. + Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search. + + For each file, a symbol of kind File (2) will be created. For directories, a symbol of kind Package (4) will be created. + All symbols will have a children attribute, thereby representing the tree structure of all symbols in the project + that are within the repository. + All symbols except the root packages will have a parent attribute. + Will ignore directories starting with '.', language-specific defaults + and user-configured directories (e.g. from .gitignore). + + :param within_relative_path: pass a relative path to only consider symbols within this path. + If a file is passed, only the symbols within this file will be considered. + If a directory is passed, all files within this directory will be considered. + :param include_body: whether to include the body of the symbols in the result. + + :return: A list of root symbols representing the top-level packages/modules in the project. + """ + + if within_relative_path is not None: + within_abs_path = os.path.join(self.repository_root_path, within_relative_path) + if not os.path.exists(within_abs_path): + raise FileNotFoundError(f"File or directory not found: {within_abs_path}") + if os.path.isfile(within_abs_path): + if self.is_ignored_path(within_relative_path): + self.logger.log(f"You passed a file explicitly, but it is ignored. This is probably an error. File: {within_relative_path}", logging.ERROR) + return [] + else: + _, root_nodes = await self.request_document_symbols(within_relative_path, include_body=include_body) + return root_nodes + + # Helper function to recursively process directories + async def process_directory(rel_dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]: + abs_dir_path = self.repository_root_path if rel_dir_path == "." else os.path.join(self.repository_root_path, rel_dir_path) + abs_dir_path = os.path.realpath(abs_dir_path) + + if self.is_ignored_path(str(Path(abs_dir_path).relative_to(self.repository_root_path))): + self.logger.log(f"Skipping directory: {rel_dir_path}\n(because it should be ignored)", logging.DEBUG) + return [] + + result = [] + try: + contained_dir_or_file_names = os.listdir(abs_dir_path) + except OSError: + return [] + + # Create package symbol for directory + package_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore + name=os.path.basename(abs_dir_path), + kind=multilspy_types.SymbolKind.Package, + location=multilspy_types.Location( + uri=str(pathlib.Path(abs_dir_path).as_uri()), + range={"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}, + absolutePath=str(abs_dir_path), + relativePath=str(Path(abs_dir_path).resolve().relative_to(self.repository_root_path)), + ), + children=[] + ) + result.append(package_symbol) + + for contained_dir_or_file_name in contained_dir_or_file_names: + contained_dir_or_file_abs_path = os.path.join(abs_dir_path, contained_dir_or_file_name) + contained_dir_or_file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)) + if self.is_ignored_path(contained_dir_or_file_rel_path): + self.logger.log(f"Skipping item: {contained_dir_or_file_rel_path}\n(because it should be ignored)", logging.DEBUG) + continue + + if os.path.isdir(contained_dir_or_file_abs_path): + child_symbols = await process_directory(contained_dir_or_file_rel_path) + package_symbol["children"].extend(child_symbols) + for child in child_symbols: + child["parent"] = package_symbol + + elif os.path.isfile(contained_dir_or_file_abs_path): + _, file_root_nodes = await self.request_document_symbols(contained_dir_or_file_rel_path, include_body=include_body) + + # Create file symbol, link with children + file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)) + with self.open_file(file_rel_path) as file_data: + fileRange = self._get_range_from_file_content(file_data.contents) + file_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore + name=os.path.splitext(contained_dir_or_file_name)[0], + kind=multilspy_types.SymbolKind.File, + range=fileRange, + selectionRange=fileRange, + location=multilspy_types.Location( + uri=str(pathlib.Path(contained_dir_or_file_abs_path).as_uri()), + range=fileRange, + absolutePath=str(contained_dir_or_file_abs_path), + relativePath=str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)), + ), + children=file_root_nodes, + parent=package_symbol, + ) + for child in file_root_nodes: + child["parent"] = file_symbol + + # Link file symbol with package + package_symbol["children"].append(file_symbol) + + # TODO: Not sure if this is actually still needed given recent changes to relative path handling + def fix_relative_path(nodes: List[multilspy_types.UnifiedSymbolInformation]): + for node in nodes: + if "location" in node and "relativePath" in node["location"]: + path = Path(node["location"]["relativePath"]) + if path.is_absolute(): + try: + path = path.relative_to(self.repository_root_path) + node["location"]["relativePath"] = str(path) + except Exception: + pass + if "children" in node: + fix_relative_path(node["children"]) + + fix_relative_path(file_root_nodes) + + return result + + # Start from the root or the specified directory + start_rel_path = within_relative_path or "." + return await process_directory(start_rel_path) + + @staticmethod + def _get_range_from_file_content(file_content: str) -> multilspy_types.Range: + """ + Get the range for the given file. + """ + lines = file_content.split("\n") + end_line = len(lines) + end_column = len(lines[-1]) + return multilspy_types.Range( + start=multilspy_types.Position(line=0, character=0), + end=multilspy_types.Position(line=end_line, character=end_column) + ) + + async def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: + """ + An overview of the given directory. + + Maps relative paths of all contained files to info about top-level symbols in the file + (name, kind, line, column). + """ + symbol_tree = await self.request_full_symbol_tree(relative_dir_path) + # Initialize result dictionary + result: dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]] = defaultdict(list) + + # Helper function to process a symbol and its children + def process_symbol(symbol: multilspy_types.UnifiedSymbolInformation): + if symbol["kind"] == multilspy_types.SymbolKind.File: + # For file symbols, process their children (top-level symbols) + for child in symbol["children"]: + assert "location" in child + assert "selectionRange" in child + path = Path(child["location"]["absolutePath"]).resolve().relative_to(self.repository_root_path) + result[str(path)].append(( + child["name"], + child["kind"], + child["selectionRange"]["start"]["line"], + child["selectionRange"]["start"]["character"] + )) + # For package/directory symbols, process their children + for child in symbol["children"]: + process_symbol(child) + + # Process each root symbol + for root in symbol_tree: + process_symbol(root) + return result + + async def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: + """ + An overview of the given file. + Returns the list of tuples (name, kind, line, column) of all top-level symbols in the file. + """ + _, document_roots = await self.request_document_symbols(relative_file_path) + result = [] + for root in document_roots: + try: + result.append( + ( root["name"], + root["kind"], + root["selectionRange"]["start"]["line"], + root["selectionRange"]["start"]["character"],) + ) + except KeyError as e: + raise KeyError( + f"Could not process symbol of name {root.get('name', 'unknown')} in {relative_file_path=}" + ) from e + return result + + async def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: + """ + An overview of all symbols in the given file or directory. + + :param within_relative_path: the relative path to the file or directory to get the overview of. + :return: A mapping of all relative paths analyzed to lists of tuples (name, kind, line, column) of all top-level symbols in the corresponding file. + """ + abs_path = (Path(self.repository_root_path) / within_relative_path).resolve() + if not abs_path.exists(): + raise FileNotFoundError(f"File or directory not found: {abs_path}") + + if abs_path.is_file(): + symbols_overview = await self.request_document_overview(within_relative_path) + return {within_relative_path: symbols_overview} + else: + return await self.request_dir_overview(within_relative_path) + + async def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: + """ + Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server + to find the hover information at the given line and column in the given file. Wait for the response and return the result. + + :param relative_file_path: The relative path of the file that has the hover information + :param line: The line number of the symbol + :param column: The column number of the symbol + + :return None + """ + with self.open_file(relative_file_path): + response = await self.server.send.hover( + { + "textDocument": { + "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() + }, + "position": { + "line": line, + "character": column, + }, + } + ) + + if response is None: + return None + + assert isinstance(response, dict) + + return multilspy_types.Hover(**response) + + # ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA -------------------- + + def retrieve_symbol_body(self, symbol: multilspy_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation) -> str: + """ + Load the body of the given symbol. If the body is already contained in the symbol, just return it. + """ + existing_body = symbol.get("body", None) + if existing_body: + return existing_body + + assert "location" in symbol + symbol_start_line = symbol["location"]["range"]["start"]["line"] + symbol_end_line = symbol["location"]["range"]["end"]["line"] + assert "relativePath" in symbol["location"] + symbol_file = self.retrieve_full_file_content(symbol["location"]["relativePath"]) + symbol_lines = symbol_file.split("\n") + symbol_body = "\n".join(symbol_lines[symbol_start_line:symbol_end_line+1]) + + # remove leading indentation + symbol_start_column = symbol["location"]["range"]["start"]["character"] + symbol_body = symbol_body[symbol_start_column:] + return symbol_body + + async def request_parsed_files(self) -> list[str]: + """Retrieves relative paths of all files analyzed by the Language Server.""" + if not self.server_started: + self.logger.log( + "request_parsed_files called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + rel_file_paths = [] + for root, dirs, files in os.walk(self.repository_root_path): + dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d))] + for file in files: + rel_file_path = os.path.relpath(os.path.join(root, file), start=self.repository_root_path) + if not self.is_ignored_path(rel_file_path): + rel_file_paths.append(rel_file_path) + return rel_file_paths + + async def search_files_for_pattern( + self, + pattern: re.Pattern | str, + context_lines_before: int = 0, + context_lines_after: int = 0, + paths_include_glob: str | None = None, + paths_exclude_glob: str | None = None, + ) -> list[MatchedConsecutiveLines]: + """ + Search for a pattern across all files analyzed by the Language Server. + + :param pattern: Regular expression pattern to search for, either as a compiled Pattern or string + :param context_lines_before: Number of lines of context to include before each match + :param context_lines_after: Number of lines of context to include after each match + :param paths_include_glob: Glob pattern to filter which files to include in the search + :param paths_exclude_glob: Glob pattern to filter which files to exclude from the search. Takes precedence over paths_include_glob. + :return: List of matched consecutive lines with context + """ + if isinstance(pattern, str): + pattern = re.compile(pattern) + + relative_file_paths = await self.request_parsed_files() + return search_files( + relative_file_paths, + pattern, + file_reader=self.retrieve_full_file_content, + context_lines_before=context_lines_before, + context_lines_after=context_lines_after, + paths_include_glob=paths_include_glob, + paths_exclude_glob=paths_exclude_glob + ) + + async def request_referencing_symbols( + self, + relative_file_path: str, + line: int, + column: int, + include_imports: bool = True, + include_self: bool = False, + include_body: bool = False, + include_file_symbols: bool = False, + ) -> List[ReferenceInSymbol]: + """ + Finds all symbols that reference the symbol at the given location. + This is similar to request_references but filters to only include symbols + (functions, methods, classes, etc.) that reference the target symbol. + + :param relative_file_path: The relative path to the file. + :param line: The 0-indexed line number. + :param column: The 0-indexed column number. + :param include_imports: whether to also include imports as references. + Unfortunately, the LSP does not have an import type, so the references corresponding to imports + will not be easily distinguishable from definitions. + :param include_self: whether to include the references that is the "input symbol" itself. + Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition. + :param include_body: whether to include the body of the symbols in the result. + :param include_file_symbols: whether to include references that are file symbols. This + is often a fallback mechanism for when the reference cannot be resolved to a symbol. + :return: List of objects containing the symbol and the location of the reference. + """ + if not self.server_started: + self.logger.log( + "request_referencing_symbols called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + # First, get all references to the symbol + references = await self.request_references(relative_file_path, line, column) + if not references: + return [] + + # For each reference, find the containing symbol + result = [] + incoming_symbol = None + for ref in references: + ref_path = ref["relativePath"] + ref_line = ref["range"]["start"]["line"] + ref_col = ref["range"]["start"]["character"] + + with self.open_file(ref_path) as file_data: + # Get the containing symbol for this reference + containing_symbol = await self.request_containing_symbol( + ref_path, ref_line, ref_col, include_body=include_body + ) + if containing_symbol is None: + # TODO: HORRIBLE HACK! I don't know how to do it better for now... + # THIS IS BOUND TO BREAK IN MANY CASES! IT IS ALSO SPECIFIC TO PYTHON! + # Background: + # When a variable is used to change something, like + # + # instance = MyClass() + # instance.status = "new status" + # + # we can't find the containing symbol for the reference to `status` + # since there is no container on the line of the reference + # The hack is to try to find a variable symbol in the containing module + # by using the text of the reference to find the variable name (In a very heuristic way) + # and then look for a symbol with that name and kind Variable + ref_text = file_data.contents.split("\n")[ref_line] + if "." in ref_text: + containing_symbol_name = ref_text.split(".")[0] + all_symbols, _ = await self.request_document_symbols(ref_path) + for symbol in all_symbols: + if symbol["name"] == containing_symbol_name and symbol["kind"] == multilspy_types.SymbolKind.Variable: + containing_symbol = copy(symbol) + containing_symbol["location"] = ref + containing_symbol["range"] = ref["range"] + break + + # We failed retrieving the symbol, falling back to creating a file symbol + if containing_symbol is None and include_file_symbols: + self.logger.log( + f"Could not find containing symbol for {ref_path}:{ref_line}:{ref_col}. Returning file symbol instead", + logging.WARNING + ) + fileRange = self._get_range_from_file_content(file_data.contents) + location = multilspy_types.Location( + uri=str(pathlib.Path(os.path.join(self.repository_root_path, ref_path)).as_uri()), + range=fileRange, + absolutePath=str(os.path.join(self.repository_root_path, ref_path)), + relativePath=ref_path, + ) + name = os.path.splitext(os.path.basename(ref_path))[0] + + if include_body: + body = self.retrieve_full_file_content(ref_path) + else: + body = "" + + containing_symbol = multilspy_types.UnifiedSymbolInformation( + kind=multilspy_types.SymbolKind.File, + range=fileRange, + selectionRange=fileRange, + location=location, + name=name, + children=[], + body=body, + ) + if containing_symbol is None or not include_file_symbols and containing_symbol["kind"] == multilspy_types.SymbolKind.File: + continue + + assert "location" in containing_symbol + assert "selectionRange" in containing_symbol + + # Checking for self-reference + if ( + containing_symbol["location"]["relativePath"] == relative_file_path + and containing_symbol["selectionRange"]["start"]["line"] == ref_line + and containing_symbol["selectionRange"]["start"]["character"] == ref_col + ): + incoming_symbol = containing_symbol + if include_self: + result.append(ReferenceInSymbol(symbol=containing_symbol, line=ref_line, character=ref_col)) + continue + else: + self.logger.log(f"Found self-reference for {incoming_symbol['name']}, skipping it since {include_self=}", logging.DEBUG) + continue + + # checking whether reference is an import + # This is neither really safe nor elegant, but if we don't do it, + # there is no way to distinguish between definitions and imports as import is not a symbol-type + # and we get the type referenced symbol resulting from imports... + if (not include_imports \ + and incoming_symbol is not None \ + and containing_symbol["name"] == incoming_symbol["name"] \ + and containing_symbol["kind"] == incoming_symbol["kind"] \ + ): + self.logger.log( + f"Found import of referenced symbol {incoming_symbol['name']}" + f"in {containing_symbol['location']['relativePath']}, skipping", + logging.DEBUG + ) + continue + + result.append(ReferenceInSymbol(symbol=containing_symbol, line=ref_line, character=ref_col)) + + return result + + async def request_containing_symbol( + self, + relative_file_path: str, + line: int, + column: Optional[int] = None, + strict: bool = False, + include_body: bool = False, + ) -> multilspy_types.UnifiedSymbolInformation | None: + """ + Finds the first symbol containing the position for the given file. + For Python, container symbols are considered to be those with kinds corresponding to + functions, methods, or classes (typically: Function (12), Method (6), Class (5)). + + The method operates as follows: + - Request the document symbols for the file. + - Filter symbols to those that start at or before the given line. + - From these, first look for symbols whose range contains the (line, column). + - If one or more symbols contain the position, return the one with the greatest starting position + (i.e. the innermost container). + - If none (strictly) contain the position, return the symbol with the greatest starting position + among those above the given line. + - If no container candidates are found, return None. + + :param relative_file_path: The relative path to the Python file. + :param line: The 0-indexed line number. + :param column: The 0-indexed column (also called character). If not passed, the lookup will be based + only on the line. + :param strict: If True, the position must be strictly within the range of the symbol. + Setting to True is useful for example for finding the parent of a symbol, as with strict=False, + and the line pointing to a symbol itself, the containing symbol will be the symbol itself + (and not the parent). + :param include_body: Whether to include the body of the symbol in the result. + :return: The container symbol (if found) or None. + """ + # checking if the line is empty, unfortunately ugly and duplicating code, but I don't want to refactor + with self.open_file(relative_file_path): + absolute_file_path = str( + PurePath(self.repository_root_path, relative_file_path) + ) + content = FileUtils.read_file(self.logger, absolute_file_path) + if content.split("\n")[line].strip() == "": + self.logger.log( + f"Passing empty lines to request_container_symbol is currently not supported, {relative_file_path=}, {line=}", + logging.ERROR, + ) + return None + + symbols, _ = await self.request_document_symbols(relative_file_path) + + # make jedi and pyright api compatible + # the former has no location, the later has no range + # we will just always add location of the desired format to all symbols + for symbol in symbols: + if "location" not in symbol: + range = symbol["range"] + location = multilspy_types.Location( + uri=f"file:/{absolute_file_path}", + range=range, + absolutePath=absolute_file_path, + relativePath=relative_file_path, + ) + symbol["location"] = location + else: + location = symbol["location"] + assert "range" in location + location["absolutePath"] = absolute_file_path + location["relativePath"] = relative_file_path + location["uri"] = Path(absolute_file_path).as_uri() + + # Allowed container kinds, currently only for Python + container_symbol_kinds = { + multilspy_types.SymbolKind.Method, + multilspy_types.SymbolKind.Function, + multilspy_types.SymbolKind.Class + } + + def is_position_in_range(line: int, range_d: multilspy_types.Range) -> bool: + start = range_d["start"] + end = range_d["end"] + + column_condition = True + if strict: + line_condition = end["line"] >= line > start["line"] + if column is not None and line == start["line"]: + column_condition = column > start["character"] + else: + line_condition = end["line"] >= line >= start["line"] + if column is not None and line == start["line"]: + column_condition = column >= start["character"] + return line_condition and column_condition + + # Only consider containers that are not one-liners (otherwise we may get imports) + candidate_containers = [ + s for s in symbols if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"] + ] + var_containers = [ + s for s in symbols if s["kind"] == multilspy_types.SymbolKind.Variable + ] + candidate_containers.extend(var_containers) + + if not candidate_containers: + return None + + # From the candidates, find those whose range contains the given position. + containing_symbols = [] + for symbol in candidate_containers: + s_range = symbol["location"]["range"] + if not is_position_in_range(line, s_range): + continue + containing_symbols.append(symbol) + + if containing_symbols: + # Return the one with the greatest starting position (i.e. the innermost container). + containing_symbol = max(containing_symbols, key=lambda s: s["location"]["range"]["start"]["line"]) + if include_body: + containing_symbol["body"] = self.retrieve_symbol_body(containing_symbol) + return containing_symbol + else: + return None + + async def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None: + """ + Finds the container of the given symbol if there is one. If the parent attribute is present, the parent is returned + without further searching. + + :param symbol: The symbol to find the container of. + :param include_body: whether to include the body of the symbol in the result. + :return: The container of the given symbol or None if no container is found. + """ + if "parent" in symbol: + return symbol["parent"] + assert "location" in symbol, f"Symbol {symbol} has no location and no parent attribute" + return await self.request_containing_symbol( + symbol["location"]["relativePath"], + symbol["location"]["range"]["start"]["line"], + symbol["location"]["range"]["start"]["character"], + strict=True, + include_body=include_body, + ) + + async def request_defining_symbol( + self, + relative_file_path: str, + line: int, + column: int, + include_body: bool = False, + ) -> Optional[multilspy_types.UnifiedSymbolInformation]: + """ + Finds the symbol that defines the symbol at the given location. + + This method first finds the definition of the symbol at the given position, + then retrieves the full symbol information for that definition. + + :param relative_file_path: The relative path to the file. + :param line: The 0-indexed line number. + :param column: The 0-indexed column number. + :param include_body: whether to include the body of the symbol in the result. + :return: The symbol information for the definition, or None if not found. + """ + if not self.server_started: + self.logger.log( + "request_defining_symbol called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + # Get the definition location(s) + definitions = await self.request_definition(relative_file_path, line, column) + if not definitions: + return None + + # Use the first definition location + definition = definitions[0] + def_path = definition["relativePath"] + def_line = definition["range"]["start"]["line"] + def_col = definition["range"]["start"]["character"] + + # Find the symbol at or containing this location + defining_symbol = await self.request_containing_symbol( + def_path, def_line, def_col, strict=False, include_body=include_body + ) + + return defining_symbol + + @property + def cache_path(self) -> Path: + """ + The path to the cache file for the document symbols. + """ + return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v20-05-25.pkl" + + async def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None: + """Will go through the entire repository and "index" all files, meaning save their symbols to the cache. + + :param progress_bar: Whether to show a progress bar while indexing the repository. + :param save_after_n_files: How many files to process before saving a checkpoint of the cache. + """ + parsed_files = await self.request_parsed_files() + files_processed = 0 + pbar = tqdm.tqdm(parsed_files, disable=not progress_bar) + for relative_file_path in pbar: + pbar.set_description(f"Indexing ({os.path.basename(relative_file_path)})") + await self.request_document_symbols(relative_file_path, include_body=False) + await self.request_document_symbols(relative_file_path, include_body=True) + files_processed += 1 + if files_processed % save_after_n_files == 0: + self.save_cache() + self.save_cache() + + def save_cache(self): + with self._cache_lock: + if not self._cache_has_changed: + self.logger.log("No changes to document symbols cache, skipping save", logging.DEBUG) + return + + self.logger.log(f"Saving updated document symbols cache to {self.cache_path}", logging.INFO) + self.cache_path.parent.mkdir(parents=True, exist_ok=True) + try: + with open(self.cache_path, "wb") as f: + pickle.dump(self._document_symbols_cache, f) + self._cache_has_changed = False + except Exception as e: + self.logger.log( + f"Failed to save document symbols cache to {self.cache_path}: {e}. " + "Note: this may have resulted in a corrupted cache file.", + logging.ERROR, + ) + + def load_cache(self): + if not self.cache_path.exists(): + return + + with self._cache_lock: + self.logger.log(f"Loading document symbols cache from {self.cache_path}", logging.INFO) + try: + with open(self.cache_path, "rb") as f: + self._document_symbols_cache = pickle.load(f) + self.logger.log(f"Loaded {len(self._document_symbols_cache)} document symbols from cache.", logging.INFO) + except Exception as e: + # cache often becomes corrupt, so just skip loading it + self.logger.log( + f"Failed to load document symbols cache from {self.cache_path}: {e}. Possible cause: the cache file is corrupted. " + "Check for any errors related to saving the cache in the logs.", + logging.ERROR, + ) + + + async def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]: + """ + Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server + to find symbols across the whole workspace. Wait for the response and return the result. + + :param query: The query string to filter symbols by + + :return: A list of matching symbols + """ + response = await self.server.send.workspace_symbol({"query": query}) + if response is None: + return None + + assert isinstance(response, list) + + ret: List[multilspy_types.UnifiedSymbolInformation] = [] + for item in response: + assert isinstance(item, dict) + + assert LSPConstants.NAME in item + assert LSPConstants.KIND in item + assert LSPConstants.LOCATION in item + + ret.append(multilspy_types.UnifiedSymbolInformation(**item)) + + return ret + + +class PyrightServer(LanguageServer): + """ + Provides Python specific instantiation of the LanguageServer class using Pyright. + Contains various configurations and settings specific to Python. + """ + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + 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", + ) + + # Event to signal when initial workspace analysis is complete + self.analysis_complete = asyncio.Event() + self.found_source_files = False + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Pyright Language Server. + """ + # Create basic initialization parameters + initialize_params: InitializeParams = { # type: ignore + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": pathlib.Path(repository_absolute_path).as_uri(), + "initializationOptions": { + "exclude": [ + "**/__pycache__", + "**/.venv", + "**/.env", + "**/build", + "**/dist", + "**/.pixi", + ], + "reportMissingImports": "error", + }, + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": {"documentChanges": True}, + "didChangeConfiguration": {"dynamicRegistration": True}, + "didChangeWatchedFiles": {"dynamicRegistration": True}, + "symbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] + }, + }, + "executeCommand": {"dynamicRegistration": 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, + "preselectSupport": True, + }, + "completionItemKind": { + "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] + }, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": {"labelOffsetSupport": True}, + }, + }, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentHighlight": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] + }, + "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}, + "publishDiagnostics": {"relatedInformation": True}, + }, + }, + "workspaceFolders": [ + {"uri": pathlib.Path(repository_absolute_path).as_uri(), "name": os.path.basename(repository_absolute_path)} + ], + } + + return initialize_params + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["PyrightServer"]: + """ + Starts the Pyright Language Server and waits for initial workspace analysis to complete. + + This prevents zombie processes by ensuring Pyright has finished its initial background + tasks before we consider the server ready. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and workspace analysis is complete + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown cleanly + ``` + """ + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def window_log_message(msg): + """ + Monitor Pyright's log messages to detect when initial analysis is complete. + 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) + + # 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) + self.found_source_files = True + self.analysis_complete.set() + self.completions_available.set() + + async def check_experimental_status(params): + """ + 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) + if not self.found_source_files: + self.analysis_complete.set() + self.completions_available.set() + + # Set up notification handlers + 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) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + async with super().start_server(): + self.logger.log("Starting pyright-langserver server process", logging.INFO) + await 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, + ) + init_response = await self.server.send.initialize(initialize_params) + self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) + + # Verify that the server supports our required features + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + + # Complete the initialization handshake + self.server.notify.initialized({}) + + # 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) + try: + await asyncio.wait_for(self.analysis_complete.wait(), timeout=1.0) + self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) + except asyncio.TimeoutError: + self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) + # Fallback: assume analysis is complete after timeout + self.analysis_complete.set() + self.completions_available.set() + + yield self diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py new file mode 100644 index 0000000..91af017 --- /dev/null +++ b/src/solidlsp/ls_handler.py @@ -0,0 +1,531 @@ +import asyncio +import dataclasses +import json +import logging +import os +import subprocess +import threading + +import psutil +from typing import Any, Callable, Dict, List, Optional, Union + +from multilspy.lsp_protocol_handler.lsp_requests import LspNotification, LspRequest +from multilspy.lsp_protocol_handler.lsp_types import ErrorCodes +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo, StringDict, Request, content_length, ENCODING, Error, MessageType, \ + make_notification, PayloadLike, make_response, make_error_response, make_request, create_message +from multilspy.multilspy_exceptions import MultilspyException + + +log = logging.getLogger(__name__) + + +class LanguageServerHandler: + """ + This class provides the implementation of Python client for the Language Server Protocol. + A class that launches the language server and communicates with it + using the Language Server Protocol (LSP). + + It provides methods for sending requests, responses, and notifications to the server + and for registering handlers for requests and notifications from the server. + + Uses JSON-RPC 2.0 for communication with the server over stdin/stdout. + + Attributes: + send: A LspRequest object that can be used to send requests to the server and + await for the responses. + notify: A LspNotification object that can be used to send notifications to the server. + cmd: A string that represents the command to launch the language server process. + process: A subprocess.Popen object that represents the language server process. + _received_shutdown: A boolean flag that indicates whether the client has received + a shutdown request from the server. + request_id: An integer that represents the next available request id for the client. + _response_handlers: A dictionary that maps request ids to Request objects that + store the results or errors of the requests. + on_request_handlers: A dictionary that maps method names to callback functions + that handle requests from the server. + on_notification_handlers: A dictionary that maps method names to callback functions + that handle notifications from the server. + logger: An optional function that takes two strings (source and destination) and + a payload dictionary, and logs the communication between the client and the server. + tasks: A dictionary that maps task ids to asyncio.Task objects that represent + the asynchronous tasks created by the handler. + task_counter: An integer that represents the next available task id for the handler. + loop: An asyncio.AbstractEventLoop object that represents the event loop used by the handler. + start_independent_lsp_process: An optional boolean flag that indicates whether to start the + language server process in an independent process group. Default is `True`. Setting it to + `False` means that the language server process will be in the same process group as the + the current process, and any SIGINT and SIGTERM signals will be sent to both processes. + """ + + def __init__( + self, + process_launch_info: ProcessLaunchInfo, + logger: Optional[Callable[[str, str, StringDict | str], None]] = None, + start_independent_lsp_process=True, + ) -> None: + """ + Params: + cmd: A string that represents the command to launch the language server process. + logger: An optional function that takes two strings (source and destination) and + a payload dictionary, and logs the communication between the client and the server. + """ + self.send = LspRequest(self.send_request) + self.notify = LspNotification(self.send_notification) + + self.process_launch_info = process_launch_info + self.process = None + self._received_shutdown = False + + self.request_id = 1 + self._response_handlers: Dict[Any, Request] = {} + self.on_request_handlers = {} + self.on_notification_handlers = {} + self.logger = logger + self.tasks = {} + self.task_counter = 0 + self.loop = None + self.start_independent_lsp_process = start_independent_lsp_process + + # Add thread locks for shared resources to prevent race conditions + self._stdin_lock = threading.Lock() + self._request_id_lock = threading.Lock() + self._response_handlers_lock = threading.Lock() + self._tasks_lock = threading.Lock() + + + + def is_running(self) -> bool: + """ + Checks if the language server process is currently running. + """ + return self.process is not None and self.process.returncode is None + + async def start(self) -> None: + """ + Starts the language server process and creates a task to continuously read from its stdout to handle communications + from the server to the client + """ + child_proc_env = os.environ.copy() + child_proc_env.update(self.process_launch_info.env) + + log.info("Starting language server process via command: %s", self.process_launch_info.cmd) + self.process = await asyncio.create_subprocess_shell( + self.process_launch_info.cmd, + stdout=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=child_proc_env, + cwd=self.process_launch_info.cwd, + start_new_session=self.start_independent_lsp_process, + ) + + # Check if process terminated immediately + if self.process.returncode is not None: + log.error("Language server has already terminated/could not be started") + # Process has already terminated + stderr_data = await self.process.stderr.read() + error_message = stderr_data.decode('utf-8', errors='replace') + raise RuntimeError(f"Process terminated immediately with code {self.process.returncode}. Error: {error_message}") + + self.loop = asyncio.get_event_loop() + + # Use lock to prevent race conditions on tasks and task_counter during startup + with self._tasks_lock: + self.tasks[self.task_counter] = self.loop.create_task(self.run_forever()) + self.task_counter += 1 + self.tasks[self.task_counter] = self.loop.create_task(self.run_forever_stderr()) + self.task_counter += 1 + + + + async def stop(self) -> None: + """ + Sends the terminate signal to the language server process and waits for it to exit, with a timeout, killing it if necessary + """ + # First cancel all tasks + await self._cancel_pending_tasks() + + process = self.process + self.process = None + + if not process: + return + + # Clean up the process + await self._cleanup_process(process) + + async def _cancel_pending_tasks(self): + """Cancel all pending tasks and wait for them to complete or timeout.""" + pending_tasks = [] + + # Use lock to safely access tasks dictionary + with self._tasks_lock: + for task in self.tasks.values(): + if not task.done(): + task.cancel() + pending_tasks.append(task) + + if pending_tasks: + try: + await asyncio.wait_for(asyncio.gather(*pending_tasks, return_exceptions=True), timeout=5.0) + except (asyncio.TimeoutError, Exception): + pass + + # Clear tasks dictionary under lock + with self._tasks_lock: + self.tasks = {} + + async def _cleanup_process(self, process): + """Clean up a process: close stdin, terminate/kill process, close stdout/stderr.""" + # Close stdin first to prevent deadlocks + # See: https://bugs.python.org/issue35539 + self._safely_close_pipe(process.stdin) + + # Terminate/kill the process if it's still running + if process.returncode is None: + await self._terminate_or_kill_process(process) + + # Close stdout and stderr pipes after process has exited + # This is essential to prevent "I/O operation on closed pipe" errors and + # "Event loop is closed" errors during garbage collection + # See: https://bugs.python.org/issue41320 and https://github.com/python/cpython/issues/88050 + self._safely_close_pipe(process.stdout) + self._safely_close_pipe(process.stderr) + + # Small delay to ensure OS has released file handles + await asyncio.sleep(0.5) + + def _safely_close_pipe(self, pipe): + """Safely close a pipe, ignoring any exceptions.""" + if pipe: + try: + pipe.close() + except Exception: + pass + + async def _terminate_or_kill_process(self, process): + """Try to terminate the process gracefully, then forcefully if necessary.""" + # First try to terminate the process tree gracefully + self._signal_process_tree(process, terminate=True) + + # Wait for the process to exit (with timeout) + try: + await asyncio.wait_for(process.wait(), timeout=10) + except (asyncio.TimeoutError, Exception): + # If termination failed, forcefully kill the process tree + self._signal_process_tree(process, terminate=False) + try: + # Give it one more chance to exit + await asyncio.wait_for(process.wait(), timeout=2) + except Exception: + pass + + def _signal_process_tree(self, process, terminate=True): + """Send signal (terminate or kill) to the process and all its children.""" + signal_method = "terminate" if terminate else "kill" + + # Try to get the parent process + parent = None + try: + parent = psutil.Process(process.pid) + except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): + pass + + # If we have the parent process and it's running, signal the entire tree + if parent and parent.is_running(): + # Signal children first + for child in parent.children(recursive=True): + try: + getattr(child, signal_method)() + except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): + pass + + # Then signal the parent + try: + getattr(parent, signal_method)() + except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): + pass + else: + # Fall back to direct process signaling + try: + getattr(process, signal_method)() + except Exception: + pass + + + async def shutdown(self) -> None: + """ + Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit + """ + self._log("Sending shutdown request to server") + await self.send.shutdown() + self._log("Received shutdown response from server") + self._received_shutdown = True + self._log("Sending exit notification to server") + self.notify.exit() + self._log("Sent exit notification to server") + if self.process and self.process.stdout: + self.process.stdout.set_exception(StopLoopException()) + # This yields the control to the event loop to allow the exception to be handled + # in the run_forever and run_forever_stderr methods + await asyncio.sleep(0) + + def _log(self, message: str | StringDict) -> None: + """ + Create a log message + """ + if self.logger is not None: + self.logger("client", "logger", message) + + async def run_forever(self) -> bool: + """ + Continuously read from the language server process stdout and handle the messages + invoking the registered response and notification handlers + """ + try: + while self.process and self.process.stdout and not self.process.stdout.at_eof(): + line = await self.process.stdout.readline() + if not line: + continue + try: + num_bytes = content_length(line) + except ValueError: + continue + if num_bytes is None: + continue + while line and line.strip(): + line = await self.process.stdout.readline() + if not line: + continue + body = await self.process.stdout.readexactly(num_bytes) + + # Use lock to prevent race conditions on tasks and task_counter + with self._tasks_lock: + self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._handle_body(body)) + self.task_counter += 1 + except (BrokenPipeError, ConnectionResetError, StopLoopException): + pass + return self._received_shutdown + + + async def run_forever_stderr(self) -> None: + """ + Continuously read from the language server process stderr and log the messages + """ + try: + while self.process and self.process.stderr and not self.process.stderr.at_eof(): + line = await self.process.stderr.readline() + if not line: + continue + self._log("LSP stderr: " + line.decode(ENCODING, errors='replace')) + except (BrokenPipeError, ConnectionResetError, StopLoopException): + pass + + async def _handle_body(self, body: bytes) -> None: + """ + Parse the body text received from the language server process and invoke the appropriate handler + """ + try: + await self._receive_payload(json.loads(body)) + except IOError as ex: + self._log(f"malformed {ENCODING}: {ex}") + except UnicodeDecodeError as ex: + self._log(f"malformed {ENCODING}: {ex}") + except json.JSONDecodeError as ex: + self._log(f"malformed JSON: {ex}") + + async def _receive_payload(self, payload: StringDict) -> None: + """ + Determine if the payload received from server is for a request, response, or notification and invoke the appropriate handler + """ + if self.logger: + self.logger("server", "client", payload) + try: + if "method" in payload: + if "id" in payload: + await self._request_handler(payload) + else: + await self._notification_handler(payload) + elif "id" in payload: + await self._response_handler(payload) + else: + self._log(f"Unknown payload type: {payload}") + except Exception as err: + self._log(f"Error handling server payload: {err}") + + def send_notification(self, method: str, params: Optional[dict] = None) -> None: + """ + Send notification pertaining to the given method to the server with the given parameters + """ + self._send_payload_sync(make_notification(method, params)) + + def send_response(self, request_id: Any, params: PayloadLike) -> None: + """ + Send response to the given request id to the server with the given parameters + """ + # Use lock to prevent race conditions on tasks and task_counter + with self._tasks_lock: + self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( + self._send_payload(make_response(request_id, params)) + ) + self.task_counter += 1 + + + def send_error_response(self, request_id: Any, err: Error) -> None: + """ + Send error response to the given request id to the server with the given error + """ + # Use lock to prevent race conditions on tasks and task_counter + with self._tasks_lock: + self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( + self._send_payload(make_error_response(request_id, err)) + ) + self.task_counter += 1 + + + async def send_request(self, method: str, params: Optional[dict] = None) -> PayloadLike: + """ + Send request to the server, register the request id, and wait for the response + """ + request = Request() + + # Use lock to prevent race conditions on request_id and _response_handlers + with self._request_id_lock: + request_id = self.request_id + self.request_id += 1 + + with self._response_handlers_lock: + self._response_handlers[request_id] = request + + async with request.cv: + await self._send_payload(make_request(method, request_id, params)) + self._log(f"Waiting for asyncio condition for request {method} with params:\n{params}") + await request.cv.wait() + self._log(f"Finished waiting, processing result") + if isinstance(request.error, Error): + raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {request.error}") from request.error + self._log(f"Returning non-error result, which is:\n{request.result}") + return request.result + + + def _send_payload_sync(self, payload: StringDict) -> None: + """ + Send the payload to the server by writing to its stdin synchronously + """ + if not self.process or not self.process.stdin: + return + msg = create_message(payload) + if self.logger: + self.logger("client", "server", payload) + + # Use lock to prevent concurrent writes to stdin that cause buffer corruption + with self._stdin_lock: + try: + self.process.stdin.writelines(msg) + except (BrokenPipeError, ConnectionResetError, OSError) as e: + # Log the error but don't raise to prevent cascading failures + if self.logger: + self.logger("client", "logger", f"Failed to write to stdin: {e}") + return + + + async def _send_payload(self, payload: StringDict) -> None: + """ + Send the payload to the server by writing to its stdin asynchronously. + """ + if not self.process or not self.process.stdin: + return + self._log(payload) + msg = create_message(payload) + + # Use lock to prevent concurrent writes to stdin that cause buffer corruption + with self._stdin_lock: + try: + self.process.stdin.writelines(msg) + await self.process.stdin.drain() + except (BrokenPipeError, ConnectionResetError, OSError) as e: + # Log the error but don't raise to prevent cascading failures + if self.logger: + self.logger("client", "logger", f"Failed to write to stdin: {e}") + return + + + def on_request(self, method: str, cb) -> None: + """ + Register the callback function to handle requests from the server to the client for the given method + """ + self.on_request_handlers[method] = cb + + def on_notification(self, method: str, cb) -> None: + """ + Register the callback function to handle notifications from the server to the client for the given method + """ + self.on_notification_handlers[method] = cb + + async def _response_handler(self, response: StringDict) -> None: + """ + Handle the response received from the server for a request, using the id to determine the request + """ + with self._response_handlers_lock: + request = self._response_handlers.pop(response["id"]) + + if "result" in response and "error" not in response: + await request.on_result(response["result"]) + elif "result" not in response and "error" in response: + await request.on_error(Error.from_lsp(response["error"])) + else: + await request.on_error(Error(ErrorCodes.InvalidRequest, "")) + + + async def _request_handler(self, response: StringDict) -> None: + """ + Handle the request received from the server: call the appropriate callback function and return the result + """ + method = response.get("method", "") + params = response.get("params") + request_id = response.get("id") + handler = self.on_request_handlers.get(method) + if not handler: + self.send_error_response( + request_id, + Error( + ErrorCodes.MethodNotFound, + "method '{}' not handled on client.".format(method), + ), + ) + return + try: + self.send_response(request_id, await handler(params)) + except Error as ex: + self.send_error_response(request_id, ex) + except Exception as ex: + self.send_error_response(request_id, Error(ErrorCodes.InternalError, str(ex))) + + async def _notification_handler(self, response: StringDict) -> None: + """ + Handle the notification received from the server: call the appropriate callback function + """ + method = response.get("method", "") + params = response.get("params") + handler = self.on_notification_handlers.get(method) + if not handler: + self._log(f"unhandled {method}") + return + try: + await handler(params) + except asyncio.CancelledError: + return + except Exception as ex: + if (not self._received_shutdown) and self.logger: + self.logger( + "client", + "logger", + str( + { + "type": MessageType.error, + "message": str(ex), + "method": method, + "params": params, + } + ), + ) diff --git a/src/solidlsp/lsp_request.py b/src/solidlsp/lsp_request.py new file mode 100644 index 0000000..b46bf34 --- /dev/null +++ b/src/solidlsp/lsp_request.py @@ -0,0 +1,435 @@ +from typing import List, Union +from multilspy.lsp_protocol_handler import lsp_types + + +class LspRequest: + def __init__(self, send_request): + self.send_request = send_request + + async def implementation( + self, params: lsp_types.ImplementationParams + ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + """A request to resolve the implementation locations of a symbol at a given text + document position. The request's parameter is of type [TextDocumentPositionParams] + (#TextDocumentPositionParams) the response is of type {@link Definition} or a + Thenable that resolves to such.""" + return await self.send_request("textDocument/implementation", params) + + async def type_definition( + self, params: lsp_types.TypeDefinitionParams + ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + """A request to resolve the type definition locations of a symbol at a given text + document position. The request's parameter is of type [TextDocumentPositionParams] + (#TextDocumentPositionParams) the response is of type {@link Definition} or a + Thenable that resolves to such.""" + return await self.send_request("textDocument/typeDefinition", params) + + async def document_color( + self, params: lsp_types.DocumentColorParams + ) -> List["lsp_types.ColorInformation"]: + """A request to list all color symbols found in a given text document. The request's + parameter is of type {@link DocumentColorParams} the + response is of type {@link ColorInformation ColorInformation[]} or a Thenable + that resolves to such.""" + return await self.send_request("textDocument/documentColor", params) + + async def color_presentation( + self, params: lsp_types.ColorPresentationParams + ) -> List["lsp_types.ColorPresentation"]: + """A request to list all presentation for a color. The request's + parameter is of type {@link ColorPresentationParams} the + response is of type {@link ColorInformation ColorInformation[]} or a Thenable + that resolves to such.""" + return await self.send_request("textDocument/colorPresentation", params) + + async def folding_range( + self, params: lsp_types.FoldingRangeParams + ) -> Union[List["lsp_types.FoldingRange"], None]: + """A request to provide folding ranges in a document. The request's + parameter is of type {@link FoldingRangeParams}, the + response is of type {@link FoldingRangeList} or a Thenable + that resolves to such.""" + return await self.send_request("textDocument/foldingRange", params) + + async def declaration( + self, params: lsp_types.DeclarationParams + ) -> Union["lsp_types.Declaration", List["lsp_types.LocationLink"], None]: + """A request to resolve the type definition locations of a symbol at a given text + document position. The request's parameter is of type [TextDocumentPositionParams] + (#TextDocumentPositionParams) the response is of type {@link Declaration} + or a typed array of {@link DeclarationLink} or a Thenable that resolves + to such.""" + return await self.send_request("textDocument/declaration", params) + + async def selection_range( + self, params: lsp_types.SelectionRangeParams + ) -> Union[List["lsp_types.SelectionRange"], None]: + """A request to provide selection ranges in a document. The request's + parameter is of type {@link SelectionRangeParams}, the + response is of type {@link SelectionRange SelectionRange[]} or a Thenable + that resolves to such.""" + return await self.send_request("textDocument/selectionRange", params) + + async def prepare_call_hierarchy( + self, params: lsp_types.CallHierarchyPrepareParams + ) -> Union[List["lsp_types.CallHierarchyItem"], None]: + """A request to result a `CallHierarchyItem` in a document at a given position. + Can be used as an input to an incoming or outgoing call hierarchy. + + @since 3.16.0""" + return await self.send_request("textDocument/prepareCallHierarchy", params) + + async def incoming_calls( + self, params: lsp_types.CallHierarchyIncomingCallsParams + ) -> Union[List["lsp_types.CallHierarchyIncomingCall"], None]: + """A request to resolve the incoming calls for a given `CallHierarchyItem`. + + @since 3.16.0""" + return await self.send_request("callHierarchy/incomingCalls", params) + + async def outgoing_calls( + self, params: lsp_types.CallHierarchyOutgoingCallsParams + ) -> Union[List["lsp_types.CallHierarchyOutgoingCall"], None]: + """A request to resolve the outgoing calls for a given `CallHierarchyItem`. + + @since 3.16.0""" + return await self.send_request("callHierarchy/outgoingCalls", params) + + async def semantic_tokens_full( + self, params: lsp_types.SemanticTokensParams + ) -> Union["lsp_types.SemanticTokens", None]: + """@since 3.16.0""" + return await self.send_request("textDocument/semanticTokens/full", params) + + async def semantic_tokens_delta( + self, params: lsp_types.SemanticTokensDeltaParams + ) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]: + """@since 3.16.0""" + return await self.send_request("textDocument/semanticTokens/full/delta", params) + + async def semantic_tokens_range( + self, params: lsp_types.SemanticTokensRangeParams + ) -> Union["lsp_types.SemanticTokens", None]: + """@since 3.16.0""" + return await self.send_request("textDocument/semanticTokens/range", params) + + async def linked_editing_range( + self, params: lsp_types.LinkedEditingRangeParams + ) -> Union["lsp_types.LinkedEditingRanges", None]: + """A request to provide ranges that can be edited together. + + @since 3.16.0""" + return await self.send_request("textDocument/linkedEditingRange", params) + + async def will_create_files( + self, params: lsp_types.CreateFilesParams + ) -> Union["lsp_types.WorkspaceEdit", None]: + """The will create files request is sent from the client to the server before files are actually + created as long as the creation is triggered from within the client. + + @since 3.16.0""" + return await self.send_request("workspace/willCreateFiles", params) + + async def will_rename_files( + self, params: lsp_types.RenameFilesParams + ) -> Union["lsp_types.WorkspaceEdit", None]: + """The will rename files request is sent from the client to the server before files are actually + renamed as long as the rename is triggered from within the client. + + @since 3.16.0""" + return await self.send_request("workspace/willRenameFiles", params) + + async def will_delete_files( + self, params: lsp_types.DeleteFilesParams + ) -> Union["lsp_types.WorkspaceEdit", None]: + """The did delete files notification is sent from the client to the server when + files were deleted from within the client. + + @since 3.16.0""" + return await self.send_request("workspace/willDeleteFiles", params) + + async def moniker( + self, params: lsp_types.MonikerParams + ) -> Union[List["lsp_types.Moniker"], None]: + """A request to get the moniker of a symbol at a given text document position. + The request parameter is of type {@link TextDocumentPositionParams}. + The response is of type {@link Moniker Moniker[]} or `null`.""" + return await self.send_request("textDocument/moniker", params) + + async def prepare_type_hierarchy( + self, params: lsp_types.TypeHierarchyPrepareParams + ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + """A request to result a `TypeHierarchyItem` in a document at a given position. + Can be used as an input to a subtypes or supertypes type hierarchy. + + @since 3.17.0""" + return await self.send_request("textDocument/prepareTypeHierarchy", params) + + async def type_hierarchy_supertypes( + self, params: lsp_types.TypeHierarchySupertypesParams + ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + """A request to resolve the supertypes for a given `TypeHierarchyItem`. + + @since 3.17.0""" + return await self.send_request("typeHierarchy/supertypes", params) + + async def type_hierarchy_subtypes( + self, params: lsp_types.TypeHierarchySubtypesParams + ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + """A request to resolve the subtypes for a given `TypeHierarchyItem`. + + @since 3.17.0""" + return await self.send_request("typeHierarchy/subtypes", params) + + async def inline_value( + self, params: lsp_types.InlineValueParams + ) -> Union[List["lsp_types.InlineValue"], None]: + """A request to provide inline values in a document. The request's parameter is of + type {@link InlineValueParams}, the response is of type + {@link InlineValue InlineValue[]} or a Thenable that resolves to such. + + @since 3.17.0""" + return await self.send_request("textDocument/inlineValue", params) + + async def inlay_hint( + self, params: lsp_types.InlayHintParams + ) -> Union[List["lsp_types.InlayHint"], None]: + """A request to provide inlay hints in a document. The request's parameter is of + type {@link InlayHintsParams}, the response is of type + {@link InlayHint InlayHint[]} or a Thenable that resolves to such. + + @since 3.17.0""" + return await self.send_request("textDocument/inlayHint", params) + + async def resolve_inlay_hint( + self, params: lsp_types.InlayHint + ) -> "lsp_types.InlayHint": + """A request to resolve additional properties for an inlay hint. + The request's parameter is of type {@link InlayHint}, the response is + of type {@link InlayHint} or a Thenable that resolves to such. + + @since 3.17.0""" + return await self.send_request("inlayHint/resolve", params) + + async def text_document_diagnostic( + self, params: lsp_types.DocumentDiagnosticParams + ) -> "lsp_types.DocumentDiagnosticReport": + """The document diagnostic request definition. + + @since 3.17.0""" + return await self.send_request("textDocument/diagnostic", params) + + async def workspace_diagnostic( + self, params: lsp_types.WorkspaceDiagnosticParams + ) -> "lsp_types.WorkspaceDiagnosticReport": + """The workspace diagnostic request definition. + + @since 3.17.0""" + return await self.send_request("workspace/diagnostic", params) + + async def initialize( + self, params: lsp_types.InitializeParams + ) -> "lsp_types.InitializeResult": + """The initialize request is sent from the client to the server. + It is sent once as the request after starting up the server. + The requests parameter is of type {@link InitializeParams} + the response if of type {@link InitializeResult} of a Thenable that + resolves to such.""" + return await self.send_request("initialize", params) + + async def shutdown(self) -> None: + """A shutdown request is sent from the client to the server. + It is sent once when the client decides to shutdown the + server. The only notification that is sent after a shutdown request + is the exit event.""" + return await self.send_request("shutdown") + + async def will_save_wait_until( + self, params: lsp_types.WillSaveTextDocumentParams + ) -> Union[List["lsp_types.TextEdit"], None]: + """A document will save request is sent from the client to the server before + the document is actually saved. The request can return an array of TextEdits + which will be applied to the text document before it is saved. Please note that + clients might drop results if computing the text edits took too long or if a + server constantly fails on this request. This is done to keep the save fast and + reliable.""" + return await self.send_request("textDocument/willSaveWaitUntil", params) + + async def completion( + self, params: lsp_types.CompletionParams + ) -> Union[List["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: + """Request to request completion at a given text document position. The request's + parameter is of type {@link TextDocumentPosition} the response + is of type {@link CompletionItem CompletionItem[]} or {@link CompletionList} + or a Thenable that resolves to such. + + The request can delay the computation of the {@link CompletionItem.detail `detail`} + and {@link CompletionItem.documentation `documentation`} properties to the `completionItem/resolve` + request. However, properties that are needed for the initial sorting and filtering, like `sortText`, + `filterText`, `insertText`, and `textEdit`, must not be changed during resolve. + """ + return await self.send_request("textDocument/completion", params) + + async def resolve_completion_item( + self, params: lsp_types.CompletionItem + ) -> "lsp_types.CompletionItem": + """Request to resolve additional information for a given completion item.The request's + parameter is of type {@link CompletionItem} the response + is of type {@link CompletionItem} or a Thenable that resolves to such.""" + return await self.send_request("completionItem/resolve", params) + + async def hover( + self, params: lsp_types.HoverParams + ) -> Union["lsp_types.Hover", None]: + """Request to request hover information at a given text document position. The request's + parameter is of type {@link TextDocumentPosition} the response is of + type {@link Hover} or a Thenable that resolves to such.""" + return await self.send_request("textDocument/hover", params) + + async def signature_help( + self, params: lsp_types.SignatureHelpParams + ) -> Union["lsp_types.SignatureHelp", None]: + return await self.send_request("textDocument/signatureHelp", params) + + async def definition( + self, params: lsp_types.DefinitionParams + ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + """A request to resolve the definition location of a symbol at a given text + document position. The request's parameter is of type [TextDocumentPosition] + (#TextDocumentPosition) the response is of either type {@link Definition} + or a typed array of {@link DefinitionLink} or a Thenable that resolves + to such.""" + return await self.send_request("textDocument/definition", params) + + async def references( + self, params: lsp_types.ReferenceParams + ) -> Union[List["lsp_types.Location"], None]: + """A request to resolve project-wide references for the symbol denoted + by the given text document position. The request's parameter is of + type {@link ReferenceParams} the response is of type + {@link Location Location[]} or a Thenable that resolves to such.""" + return await self.send_request("textDocument/references", params) + + async def document_highlight( + self, params: lsp_types.DocumentHighlightParams + ) -> Union[List["lsp_types.DocumentHighlight"], None]: + """Request to resolve a {@link DocumentHighlight} for a given + text document position. The request's parameter is of type [TextDocumentPosition] + (#TextDocumentPosition) the request response is of type [DocumentHighlight[]] + (#DocumentHighlight) or a Thenable that resolves to such.""" + return await self.send_request("textDocument/documentHighlight", params) + + async def document_symbol( + self, params: lsp_types.DocumentSymbolParams + ) -> Union[ + List["lsp_types.SymbolInformation"], List["lsp_types.DocumentSymbol"], None + ]: + """A request to list all symbols found in a given text document. The request's + parameter is of type {@link TextDocumentIdentifier} the + response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable + that resolves to such.""" + return await self.send_request("textDocument/documentSymbol", params) + + async def code_action( + self, params: lsp_types.CodeActionParams + ) -> Union[List[Union["lsp_types.Command", "lsp_types.CodeAction"]], None]: + """A request to provide commands for the given text document and range.""" + return await self.send_request("textDocument/codeAction", params) + + async def resolve_code_action( + self, params: lsp_types.CodeAction + ) -> "lsp_types.CodeAction": + """Request to resolve additional information for a given code action.The request's + parameter is of type {@link CodeAction} the response + is of type {@link CodeAction} or a Thenable that resolves to such.""" + return await self.send_request("codeAction/resolve", params) + + async def workspace_symbol( + self, params: lsp_types.WorkspaceSymbolParams + ) -> Union[ + List["lsp_types.SymbolInformation"], List["lsp_types.WorkspaceSymbol"], None + ]: + """A request to list project-wide symbols matching the query string given + by the {@link WorkspaceSymbolParams}. The response is + of type {@link SymbolInformation SymbolInformation[]} or a Thenable that + resolves to such. + + @since 3.17.0 - support for WorkspaceSymbol in the returned data. Clients + need to advertise support for WorkspaceSymbols via the client capability + `workspace.symbol.resolveSupport`. + """ + return await self.send_request("workspace/symbol", params) + + async def resolve_workspace_symbol( + self, params: lsp_types.WorkspaceSymbol + ) -> "lsp_types.WorkspaceSymbol": + """A request to resolve the range inside the workspace + symbol's location. + + @since 3.17.0""" + return await self.send_request("workspaceSymbol/resolve", params) + + async def code_lens( + self, params: lsp_types.CodeLensParams + ) -> Union[List["lsp_types.CodeLens"], None]: + """A request to provide code lens for the given text document.""" + return await self.send_request("textDocument/codeLens", params) + + async def resolve_code_lens( + self, params: lsp_types.CodeLens + ) -> "lsp_types.CodeLens": + """A request to resolve a command for a given code lens.""" + return await self.send_request("codeLens/resolve", params) + + async def document_link( + self, params: lsp_types.DocumentLinkParams + ) -> Union[List["lsp_types.DocumentLink"], None]: + """A request to provide document links""" + return await self.send_request("textDocument/documentLink", params) + + async def resolve_document_link( + self, params: lsp_types.DocumentLink + ) -> "lsp_types.DocumentLink": + """Request to resolve additional information for a given document link. The request's + parameter is of type {@link DocumentLink} the response + is of type {@link DocumentLink} or a Thenable that resolves to such.""" + return await self.send_request("documentLink/resolve", params) + + async def formatting( + self, params: lsp_types.DocumentFormattingParams + ) -> Union[List["lsp_types.TextEdit"], None]: + """A request to to format a whole document.""" + return await self.send_request("textDocument/formatting", params) + + async def range_formatting( + self, params: lsp_types.DocumentRangeFormattingParams + ) -> Union[List["lsp_types.TextEdit"], None]: + """A request to to format a range in a document.""" + return await self.send_request("textDocument/rangeFormatting", params) + + async def on_type_formatting( + self, params: lsp_types.DocumentOnTypeFormattingParams + ) -> Union[List["lsp_types.TextEdit"], None]: + """A request to format a document on type.""" + return await self.send_request("textDocument/onTypeFormatting", params) + + async def rename( + self, params: lsp_types.RenameParams + ) -> Union["lsp_types.WorkspaceEdit", None]: + """A request to rename a symbol.""" + return await self.send_request("textDocument/rename", params) + + async def prepare_rename( + self, params: lsp_types.PrepareRenameParams + ) -> Union["lsp_types.PrepareRenameResult", None]: + """A request to test and perform the setup necessary for a rename. + + @since 3.16 - support for default behavior""" + return await self.send_request("textDocument/prepareRename", params) + + async def execute_command( + self, params: lsp_types.ExecuteCommandParams + ) -> Union["lsp_types.LSPAny", None]: + """A request send from the client to the server to execute a command. The request might return + a workspace edit which the client will apply to the workspace.""" + return await self.send_request("workspace/executeCommand", params) From de466ef6a93e5dfa17dc1538dfbaad465ae74bc3 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 13:05:46 +0200 Subject: [PATCH 093/174] Initial implementation of fully synchronous language server (SolidLanguageServer) with working PyRight implementation --- src/serena/agent.py | 22 ++- src/solidlsp/ls.py | 263 +++++++++++++++++------------------ src/solidlsp/ls_handler.py | 268 +++++++++++++++++------------------- src/solidlsp/lsp_request.py | 206 +++++++++++++-------------- 4 files changed, 374 insertions(+), 385 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 0f7aefd..6ced373 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -49,6 +49,7 @@ from serena.util.general import load_yaml, save_yaml from serena.util.inspection import determine_programming_language_composition, iter_subclasses from serena.util.shell import execute_shell_command from serena.util.thread import ExecutionResult, execute_with_timeout +from solidlsp.ls import SolidLanguageServer if TYPE_CHECKING: from serena.gui_log_viewer import GuiLogViewerHandler @@ -666,12 +667,21 @@ def create_ls_for_project( ) ls_logger = MultilspyLogger(log_level=log_level) log.info(f"Creating language server instance for {project_instance.project_root}.") - return SyncLanguageServer.create( - multilspy_config, - ls_logger, - project_instance.project_root, - timeout=ls_timeout, - ) + use_solid_ls = True + if use_solid_ls: + return SolidLanguageServer.create( + multilspy_config, + ls_logger, + project_instance.project_root, + timeout=ls_timeout, + ) + else: + return SyncLanguageServer.create( + multilspy_config, + ls_logger, + project_instance.project_root, + timeout=ls_timeout, + ) @click.command() diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 6d84cad..4d37ca7 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -24,7 +24,6 @@ from multilspy.lsp_protocol_handler.lsp_constants import LSPConstants from multilspy.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind, InitializeParams from multilspy.lsp_protocol_handler.server import ( Error, - LanguageServerHandler, ProcessLaunchInfo, StringDict, ) @@ -33,9 +32,10 @@ from multilspy.multilspy_exceptions import MultilspyException from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_utils import FileUtils, PathUtils, TextUtils from serena.text_utils import MatchedConsecutiveLines, search_files +from solidlsp.ls_handler import SolidLanguageServerHandler -class LanguageServer: +class SolidLanguageServer: """ The LanguageServer class provides a language agnostic interface to the Language Server Protocol. It is used to communicate with Language Servers of different programming languages. @@ -50,7 +50,7 @@ class LanguageServer: return dirname.startswith('.') @classmethod - def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str) -> "LanguageServer": + def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, timeout) -> "SolidLanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -67,7 +67,7 @@ class LanguageServer: PyrightServer, ) - return PyrightServer(config, logger, repository_root_path) + return SolidPyrightServer(config, logger, repository_root_path) # It used to be jedi, but pyright is a bit faster, and also more actively maintained # Keeping the previous code for reference # from multilspy.language_servers.jedi_language_server.jedi_server import ( @@ -147,11 +147,6 @@ class LanguageServer: The command must pass appropriate flags to the binary, so that it runs in the stdio mode, as opposed to HTTP, TCP modes supported by some language servers. """ - if type(self) == LanguageServer: - raise MultilspyException( - "LanguageServer is an abstract class and cannot be instantiated directly. Use LanguageServer.create method instead." - ) - self.logger = logger self.repository_root_path: str = repository_root_path self.logger.log(f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) @@ -168,7 +163,7 @@ class LanguageServer: self.load_cache() self.server_started = False - self.completions_available = asyncio.Event() + self.completions_available = threading.Event() if config.trace_lsp_communication: def logging_fn(source: str, target: str, msg: StringDict | str): self.logger.log(f"LSP: {source} -> {target}: {str(msg)[:90]}...", self.logger.logger.level) @@ -179,7 +174,7 @@ class LanguageServer: # cmd is obtained from the child classes, which provide the language specific command to start the language server # LanguageServerHandler provides the functionality to start the language server and communicate with it self.logger.log(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) - self.server = LanguageServerHandler( + self.server = SolidLanguageServerHandler( process_launch_info, logger=logging_fn, start_independent_lsp_process=config.start_independent_lsp_process, @@ -201,6 +196,8 @@ class LanguageServer: processed_patterns ) + self._server_context = None + def get_ignore_spec(self) -> pathspec.PathSpec: """Returns the pathspec matcher for the paths that were configured to be ignored through the multilspy config. @@ -262,7 +259,7 @@ class LanguageServer: return False - async def _shutdown(self, timeout: float = 5.0): + def _shutdown(self, timeout: float = 5.0): """ A robust shutdown process designed to terminate cleanly on all platforms, including Windows, by explicitly closing all I/O pipes. @@ -276,27 +273,19 @@ class LanguageServer: reader_tasks = list(self.server.tasks.values()) # --- Main Shutdown Logic --- + # Stage 1: Graceful Termination Request + # Send LSP shutdown and close stdin to signal no more input. try: - # Stage 1: Graceful Termination Request - # Send LSP shutdown and close stdin to signal no more input. - try: - await asyncio.wait_for(self.server.shutdown(), timeout=2.0) - if process.stdin and not process.stdin.is_closing(): - process.stdin.close() - except Exception: - pass # Ignore errors here, we are proceeding to terminate anyway. + self.server.shutdown() + if process.stdin and not process.stdin.is_closing(): + process.stdin.close() + except Exception: + pass # Ignore errors here, we are proceeding to terminate anyway. - # Stage 2: Terminate and Concurrently Drain stdout/stderr - process.terminate() - - # Wait for the process to exit AND for the output pipes to be drained. - # The reader tasks will exit when they hit EOF. - await asyncio.wait_for( - asyncio.gather(process.wait(), *reader_tasks), - timeout=timeout - 2.0 - ) - self.logger.log("Process terminated and output pipes drained.", logging.INFO) + # Stage 2: Terminate and Concurrently Drain stdout/stderr + process.terminate() + """ except asyncio.TimeoutError: # Stage 3: Forceful Kill self.logger.log("Graceful termination failed. Forcefully killing process...", logging.WARNING) @@ -307,7 +296,6 @@ class LanguageServer: await process.wait() except Exception as e: self.logger.log(f"Error during forceful kill: {e}", logging.ERROR) - except Exception as e: self.logger.log(f"An unexpected error occurred during shutdown logic: {e}", logging.ERROR) @@ -334,18 +322,16 @@ class LanguageServer: # 3. Null out the process object in the handler. self.server.process = None self.logger.log("Shutdown sequence fully finished.", logging.DEBUG) + """ - @asynccontextmanager - async def start_server(self) -> AsyncIterator["LanguageServer"]: - """ - Starts the Language Server and yields the LanguageServer instance. - """ + @contextmanager + def start_server(self) -> Iterator["SolidLanguageServer"]: + self.start() + yield self + self._shutdown() + + def _start_server(self) -> None: self.server_started = True - try: - yield self - finally: - self.server_started = False - await self._shutdown() @contextmanager def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]: @@ -404,7 +390,7 @@ class LanguageServer: self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str ) -> multilspy_types.Position: """ - Insert text at the given line and column in the given file and return + Insert text at the given line and column in the given file and return the updated cursor position after inserting the text. :param relative_file_path: The relative path of the file to open. @@ -486,10 +472,10 @@ class LanguageServer: ) return deleted_text - async def _send_definition_request(self, definition_params: DefinitionParams) -> Union[Definition, List[LocationLink], None]: - return await self.server.send.definition(definition_params) + def _send_definition_request(self, definition_params: DefinitionParams) -> Union[Definition, List[LocationLink], None]: + return self.server.send.definition(definition_params) - async def request_definition( + def request_definition( self, relative_file_path: str, line: int, column: int ) -> List[multilspy_types.Location]: """ @@ -523,7 +509,7 @@ class LanguageServer: LSPConstants.CHARACTER: column, }, }) - response = await self._send_definition_request(definition_params) + response = self._send_definition_request(definition_params) ret: List[multilspy_types.Location] = [] if isinstance(response, list): @@ -573,8 +559,8 @@ class LanguageServer: return ret # Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses - async def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[lsp_types.Location] | None: - return await self.server.send.references( + def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[lsp_types.Location] | None: + return self.server.send.references( { "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, "position": {"line": line, "character": column}, @@ -582,7 +568,7 @@ class LanguageServer: } ) - async def request_references( + def request_references( self, relative_file_path: str, line: int, column: int ) -> List[multilspy_types.Location]: """ @@ -607,7 +593,7 @@ class LanguageServer: with self.open_file(relative_file_path): try: - response = await self._send_references_request(relative_file_path, line=line, column=column) + response = self._send_references_request(relative_file_path, line=line, column=column) except Exception as e: # Catch LSP internal error (-32603) and raise a more informative exception if isinstance(e, Error) and getattr(e, 'code', None) == -32603: @@ -640,7 +626,7 @@ class LanguageServer: return ret - async def request_references_with_content( + def request_references_with_content( self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0 ) -> List[MatchedConsecutiveLines]: """ @@ -654,7 +640,7 @@ class LanguageServer: :return: A list of MatchedConsecutiveLines objects, one for each reference. """ - references = await self.request_references(relative_file_path, line, column) + references = self.request_references(relative_file_path, line, column) return [self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) for ref in references] def retrieve_full_file_content(self, relative_file_path: str) -> str: @@ -679,8 +665,7 @@ class LanguageServer: file_contents = file_data.contents return MatchedConsecutiveLines.from_file_contents(file_contents, line=line, context_lines_before=context_lines_before, context_lines_after=context_lines_after, source_file_path=relative_file_path) - - async def request_completions( + def request_completions( self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False ) -> List[multilspy_types.CompletionItem]: """ @@ -706,10 +691,10 @@ class LanguageServer: num_retries = 0 while response is None or (response["isIncomplete"] and num_retries < 30): - await self.completions_available.wait() + self.completions_available.wait() response: Union[ List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None - ] = await self.server.send.completion(completion_params) + ] = self.server.send.completion(completion_params) if isinstance(response, list): response = {"items": response, "isIncomplete": False} num_retries += 1 @@ -774,7 +759,7 @@ class LanguageServer: for json_repr in set([json.dumps(item, sort_keys=True) for item in completions_list]) ] - async def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: + def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: """ Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server to find symbols in the given file. Wait for the response and return the result. @@ -805,7 +790,7 @@ class LanguageServer: self.logger.log(f"No cache hit for symbols with {include_body=} in {relative_file_path}", logging.DEBUG) self.logger.log(f"Requesting document symbols for {relative_file_path} from the Language Server", logging.DEBUG) - response = await self.server.send.document_symbol( + response = self.server.send.document_symbol( { "textDocument": { "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() @@ -892,9 +877,9 @@ class LanguageServer: self._cache_has_changed = True return result - async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: """ - Will go through all files in the project or within a relative path and build a tree of symbols. + Will go through all files in the project or within a relative path and build a tree of symbols. Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search. For each file, a symbol of kind File (2) will be created. For directories, a symbol of kind Package (4) will be created. @@ -921,11 +906,11 @@ class LanguageServer: self.logger.log(f"You passed a file explicitly, but it is ignored. This is probably an error. File: {within_relative_path}", logging.ERROR) return [] else: - _, root_nodes = await self.request_document_symbols(within_relative_path, include_body=include_body) + _, root_nodes = self.request_document_symbols(within_relative_path, include_body=include_body) return root_nodes # Helper function to recursively process directories - async def process_directory(rel_dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]: + def process_directory(rel_dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]: abs_dir_path = self.repository_root_path if rel_dir_path == "." else os.path.join(self.repository_root_path, rel_dir_path) abs_dir_path = os.path.realpath(abs_dir_path) @@ -961,13 +946,13 @@ class LanguageServer: continue if os.path.isdir(contained_dir_or_file_abs_path): - child_symbols = await process_directory(contained_dir_or_file_rel_path) + child_symbols = process_directory(contained_dir_or_file_rel_path) package_symbol["children"].extend(child_symbols) for child in child_symbols: child["parent"] = package_symbol elif os.path.isfile(contained_dir_or_file_abs_path): - _, file_root_nodes = await self.request_document_symbols(contained_dir_or_file_rel_path, include_body=include_body) + _, file_root_nodes = self.request_document_symbols(contained_dir_or_file_rel_path, include_body=include_body) # Create file symbol, link with children file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)) @@ -1013,7 +998,7 @@ class LanguageServer: # Start from the root or the specified directory start_rel_path = within_relative_path or "." - return await process_directory(start_rel_path) + return process_directory(start_rel_path) @staticmethod def _get_range_from_file_content(file_content: str) -> multilspy_types.Range: @@ -1028,14 +1013,14 @@ class LanguageServer: end=multilspy_types.Position(line=end_line, character=end_column) ) - async def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: + def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: """ An overview of the given directory. Maps relative paths of all contained files to info about top-level symbols in the file (name, kind, line, column). """ - symbol_tree = await self.request_full_symbol_tree(relative_dir_path) + symbol_tree = self.request_full_symbol_tree(relative_dir_path) # Initialize result dictionary result: dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]] = defaultdict(list) @@ -1062,12 +1047,12 @@ class LanguageServer: process_symbol(root) return result - async def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: + def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: """ An overview of the given file. Returns the list of tuples (name, kind, line, column) of all top-level symbols in the file. """ - _, document_roots = await self.request_document_symbols(relative_file_path) + _, document_roots = self.request_document_symbols(relative_file_path) result = [] for root in document_roots: try: @@ -1083,7 +1068,7 @@ class LanguageServer: ) from e return result - async def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: + def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: """ An overview of all symbols in the given file or directory. @@ -1095,12 +1080,12 @@ class LanguageServer: raise FileNotFoundError(f"File or directory not found: {abs_path}") if abs_path.is_file(): - symbols_overview = await self.request_document_overview(within_relative_path) + symbols_overview = self.request_document_overview(within_relative_path) return {within_relative_path: symbols_overview} else: - return await self.request_dir_overview(within_relative_path) + return self.request_dir_overview(within_relative_path) - async def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: + def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: """ Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server to find the hover information at the given line and column in the given file. Wait for the response and return the result. @@ -1112,7 +1097,7 @@ class LanguageServer: :return None """ with self.open_file(relative_file_path): - response = await self.server.send.hover( + response = self.server.send.hover( { "textDocument": { "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() @@ -1154,7 +1139,7 @@ class LanguageServer: symbol_body = symbol_body[symbol_start_column:] return symbol_body - async def request_parsed_files(self) -> list[str]: + def request_parsed_files(self) -> list[str]: """Retrieves relative paths of all files analyzed by the Language Server.""" if not self.server_started: self.logger.log( @@ -1171,7 +1156,7 @@ class LanguageServer: rel_file_paths.append(rel_file_path) return rel_file_paths - async def search_files_for_pattern( + def search_files_for_pattern( self, pattern: re.Pattern | str, context_lines_before: int = 0, @@ -1192,7 +1177,7 @@ class LanguageServer: if isinstance(pattern, str): pattern = re.compile(pattern) - relative_file_paths = await self.request_parsed_files() + relative_file_paths = self.request_parsed_files() return search_files( relative_file_paths, pattern, @@ -1203,7 +1188,7 @@ class LanguageServer: paths_exclude_glob=paths_exclude_glob ) - async def request_referencing_symbols( + def request_referencing_symbols( self, relative_file_path: str, line: int, @@ -1239,7 +1224,7 @@ class LanguageServer: raise MultilspyException("Language Server not started") # First, get all references to the symbol - references = await self.request_references(relative_file_path, line, column) + references = self.request_references(relative_file_path, line, column) if not references: return [] @@ -1253,7 +1238,7 @@ class LanguageServer: with self.open_file(ref_path) as file_data: # Get the containing symbol for this reference - containing_symbol = await self.request_containing_symbol( + containing_symbol = self.request_containing_symbol( ref_path, ref_line, ref_col, include_body=include_body ) if containing_symbol is None: @@ -1273,7 +1258,7 @@ class LanguageServer: ref_text = file_data.contents.split("\n")[ref_line] if "." in ref_text: containing_symbol_name = ref_text.split(".")[0] - all_symbols, _ = await self.request_document_symbols(ref_path) + all_symbols, _ = self.request_document_symbols(ref_path) for symbol in all_symbols: if symbol["name"] == containing_symbol_name and symbol["kind"] == multilspy_types.SymbolKind.Variable: containing_symbol = copy(symbol) @@ -1350,7 +1335,7 @@ class LanguageServer: return result - async def request_containing_symbol( + def request_containing_symbol( self, relative_file_path: str, line: int, @@ -1397,7 +1382,7 @@ class LanguageServer: ) return None - symbols, _ = await self.request_document_symbols(relative_file_path) + symbols, _ = self.request_document_symbols(relative_file_path) # make jedi and pyright api compatible # the former has no location, the later has no range @@ -1470,7 +1455,7 @@ class LanguageServer: else: return None - async def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None: + def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None: """ Finds the container of the given symbol if there is one. If the parent attribute is present, the parent is returned without further searching. @@ -1482,7 +1467,7 @@ class LanguageServer: if "parent" in symbol: return symbol["parent"] assert "location" in symbol, f"Symbol {symbol} has no location and no parent attribute" - return await self.request_containing_symbol( + return self.request_containing_symbol( symbol["location"]["relativePath"], symbol["location"]["range"]["start"]["line"], symbol["location"]["range"]["start"]["character"], @@ -1490,7 +1475,7 @@ class LanguageServer: include_body=include_body, ) - async def request_defining_symbol( + def request_defining_symbol( self, relative_file_path: str, line: int, @@ -1517,7 +1502,7 @@ class LanguageServer: raise MultilspyException("Language Server not started") # Get the definition location(s) - definitions = await self.request_definition(relative_file_path, line, column) + definitions = self.request_definition(relative_file_path, line, column) if not definitions: return None @@ -1528,7 +1513,7 @@ class LanguageServer: def_col = definition["range"]["start"]["character"] # Find the symbol at or containing this location - defining_symbol = await self.request_containing_symbol( + defining_symbol = self.request_containing_symbol( def_path, def_line, def_col, strict=False, include_body=include_body ) @@ -1541,19 +1526,19 @@ class LanguageServer: """ return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v20-05-25.pkl" - async def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None: + def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None: """Will go through the entire repository and "index" all files, meaning save their symbols to the cache. :param progress_bar: Whether to show a progress bar while indexing the repository. :param save_after_n_files: How many files to process before saving a checkpoint of the cache. """ - parsed_files = await self.request_parsed_files() + parsed_files = self.request_parsed_files() files_processed = 0 pbar = tqdm.tqdm(parsed_files, disable=not progress_bar) for relative_file_path in pbar: pbar.set_description(f"Indexing ({os.path.basename(relative_file_path)})") - await self.request_document_symbols(relative_file_path, include_body=False) - await self.request_document_symbols(relative_file_path, include_body=True) + self.request_document_symbols(relative_file_path, include_body=False) + self.request_document_symbols(relative_file_path, include_body=True) files_processed += 1 if files_processed % save_after_n_files == 0: self.save_cache() @@ -1596,8 +1581,7 @@ class LanguageServer: logging.ERROR, ) - - async def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]: + def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]: """ Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server to find symbols across the whole workspace. Wait for the response and return the result. @@ -1606,7 +1590,7 @@ class LanguageServer: :return: A list of matching symbols """ - response = await self.server.send.workspace_symbol({"query": query}) + response = self.server.send.workspace_symbol({"query": query}) if response is None: return None @@ -1624,8 +1608,25 @@ class LanguageServer: return ret + def start(self) -> "SolidLanguageServer": + """ + Starts the language server process and connects to it. Call shutdown when ready. -class PyrightServer(LanguageServer): + :return: self for method chaining + """ + self.logger.log(f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}", logging.INFO) + self._server_context = self._start_server() + return self + + @property + def language_server(self) -> Self: + return self + + def is_running(self) -> bool: + return self.server.is_running() + + +class SolidPyrightServer(SolidLanguageServer): """ Provides Python specific instantiation of the LanguageServer class using Pyright. Contains various configurations and settings specific to Python. @@ -1646,7 +1647,7 @@ class PyrightServer(LanguageServer): ) # Event to signal when initial workspace analysis is complete - self.analysis_complete = asyncio.Event() + self.analysis_complete = threading.Event() self.found_source_files = False @override @@ -1753,8 +1754,7 @@ class PyrightServer(LanguageServer): return initialize_params - @asynccontextmanager - async def start_server(self) -> AsyncIterator["PyrightServer"]: + def _start_server(self): """ Starts the Pyright Language Server and waits for initial workspace analysis to complete. @@ -1772,13 +1772,13 @@ class PyrightServer(LanguageServer): ``` """ - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def window_log_message(msg): + def window_log_message(msg): """ Monitor Pyright's log messages to detect when initial analysis is complete. Pyright logs "Found X source files" when it finishes scanning the workspace. @@ -1794,7 +1794,7 @@ class PyrightServer(LanguageServer): self.analysis_complete.set() self.completions_available.set() - async def check_experimental_status(params): + def check_experimental_status(params): """ Also listen for experimental/serverStatus as a backup signal """ @@ -1814,38 +1814,35 @@ class PyrightServer(LanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting pyright-langserver server process", logging.INFO) - await self.server.start() + super()._start_server() + self.logger.log("Starting pyright-langserver server process", logging.INFO) + self.server.start() - # Send proper initialization parameters - initialize_params = self._get_initialize_params(self.repository_root_path) + # 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, - ) - init_response = await self.server.send.initialize(initialize_params) - self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) + self.logger.log( + "Sending initialize request from LSP client to pyright server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) - # Verify that the server supports our required features - assert "textDocumentSync" in init_response["capabilities"] - assert "completionProvider" in init_response["capabilities"] - assert "definitionProvider" in init_response["capabilities"] + # Verify that the server supports our required features + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] - # Complete the initialization handshake - self.server.notify.initialized({}) + # Complete the initialization handshake + self.server.notify.initialized({}) - # 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) - try: - await asyncio.wait_for(self.analysis_complete.wait(), timeout=1.0) - self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) - except asyncio.TimeoutError: - self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) - # Fallback: assume analysis is complete after timeout - self.analysis_complete.set() - self.completions_available.set() - - yield self + # 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) + if self.analysis_complete.wait(timeout=5.0): + self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) + else: + self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) + # Fallback: assume analysis is complete after timeout + self.analysis_complete.set() + self.completions_available.set() diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 91af017..b34571f 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -1,25 +1,51 @@ import asyncio -import dataclasses import json import logging import os import subprocess import threading +import time +from dataclasses import dataclass +from queue import Queue +from typing import Any, Callable, Dict, Optional import psutil -from typing import Any, Callable, Dict, List, Optional, Union -from multilspy.lsp_protocol_handler.lsp_requests import LspNotification, LspRequest +from multilspy.lsp_protocol_handler.lsp_requests import LspNotification from multilspy.lsp_protocol_handler.lsp_types import ErrorCodes -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo, StringDict, Request, content_length, ENCODING, Error, MessageType, \ +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo, StringDict, content_length, ENCODING, Error, MessageType, \ make_notification, PayloadLike, make_response, make_error_response, make_request, create_message from multilspy.multilspy_exceptions import MultilspyException - +from solidlsp.lsp_request import SolidLspRequest log = logging.getLogger(__name__) -class LanguageServerHandler: +class Request: + + @dataclass + class Result: + payload: Optional[PayloadLike] = None + error: Optional[Error] = None + + def is_error(self) -> bool: + return self.error is not None + + def __init__(self) -> None: + self._result_queue = Queue() + + def on_result(self, params: PayloadLike) -> None: + self._result_queue.put(Request.Result(payload=params)) + + def on_error(self, err: Error) -> None: + self._result_queue.put(Request.Result(error=err)) + + def get_result(self) -> Result: + # TODO could add timeout + return self._result_queue.get() + + +class SolidLanguageServerHandler: """ This class provides the implementation of Python client for the Language Server Protocol. A class that launches the language server and communicates with it @@ -69,7 +95,7 @@ class LanguageServerHandler: logger: An optional function that takes two strings (source and destination) and a payload dictionary, and logs the communication between the client and the server. """ - self.send = LspRequest(self.send_request) + self.send = SolidLspRequest(self.send_request) self.notify = LspNotification(self.send_notification) self.process_launch_info = process_launch_info @@ -92,15 +118,13 @@ class LanguageServerHandler: self._response_handlers_lock = threading.Lock() self._tasks_lock = threading.Lock() - - def is_running(self) -> bool: """ Checks if the language server process is currently running. """ return self.process is not None and self.process.returncode is None - async def start(self) -> None: + def start(self) -> None: """ Starts the language server process and creates a task to continuously read from its stdout to handle communications from the server to the client @@ -109,73 +133,47 @@ class LanguageServerHandler: child_proc_env.update(self.process_launch_info.env) log.info("Starting language server process via command: %s", self.process_launch_info.cmd) - self.process = await asyncio.create_subprocess_shell( + self.process = subprocess.Popen( self.process_launch_info.cmd, - stdout=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, env=child_proc_env, cwd=self.process_launch_info.cwd, start_new_session=self.start_independent_lsp_process, + shell=True ) # Check if process terminated immediately if self.process.returncode is not None: log.error("Language server has already terminated/could not be started") # Process has already terminated - stderr_data = await self.process.stderr.read() + stderr_data = self.process.stderr.read() error_message = stderr_data.decode('utf-8', errors='replace') raise RuntimeError(f"Process terminated immediately with code {self.process.returncode}. Error: {error_message}") - self.loop = asyncio.get_event_loop() + # start threads to read stdout and stderr of the process + threading.Thread( + target=self.run_forever, + name="LSP stdout reader", + daemon=True, + ).start() + threading.Thread( + target=self.run_forever_stderr, + name="LSP stderr reader", + daemon=True, + ).start() - # Use lock to prevent race conditions on tasks and task_counter during startup - with self._tasks_lock: - self.tasks[self.task_counter] = self.loop.create_task(self.run_forever()) - self.task_counter += 1 - self.tasks[self.task_counter] = self.loop.create_task(self.run_forever_stderr()) - self.task_counter += 1 - - - - async def stop(self) -> None: + def stop(self) -> None: """ Sends the terminate signal to the language server process and waits for it to exit, with a timeout, killing it if necessary """ - # First cancel all tasks - await self._cancel_pending_tasks() - process = self.process self.process = None + if process: + self._cleanup_process(process) - if not process: - return - - # Clean up the process - await self._cleanup_process(process) - - async def _cancel_pending_tasks(self): - """Cancel all pending tasks and wait for them to complete or timeout.""" - pending_tasks = [] - - # Use lock to safely access tasks dictionary - with self._tasks_lock: - for task in self.tasks.values(): - if not task.done(): - task.cancel() - pending_tasks.append(task) - - if pending_tasks: - try: - await asyncio.wait_for(asyncio.gather(*pending_tasks, return_exceptions=True), timeout=5.0) - except (asyncio.TimeoutError, Exception): - pass - - # Clear tasks dictionary under lock - with self._tasks_lock: - self.tasks = {} - - async def _cleanup_process(self, process): + def _cleanup_process(self, process): """Clean up a process: close stdin, terminate/kill process, close stdout/stderr.""" # Close stdin first to prevent deadlocks # See: https://bugs.python.org/issue35539 @@ -183,7 +181,7 @@ class LanguageServerHandler: # Terminate/kill the process if it's still running if process.returncode is None: - await self._terminate_or_kill_process(process) + self._terminate_or_kill_process(process) # Close stdout and stderr pipes after process has exited # This is essential to prevent "I/O operation on closed pipe" errors and @@ -192,9 +190,6 @@ class LanguageServerHandler: self._safely_close_pipe(process.stdout) self._safely_close_pipe(process.stderr) - # Small delay to ensure OS has released file handles - await asyncio.sleep(0.5) - def _safely_close_pipe(self, pipe): """Safely close a pipe, ignoring any exceptions.""" if pipe: @@ -203,14 +198,16 @@ class LanguageServerHandler: except Exception: pass - async def _terminate_or_kill_process(self, process): + def _terminate_or_kill_process(self, process): """Try to terminate the process gracefully, then forcefully if necessary.""" # First try to terminate the process tree gracefully self._signal_process_tree(process, terminate=True) + # TODO + """ # Wait for the process to exit (with timeout) try: - await asyncio.wait_for(process.wait(), timeout=10) + asyncio.wait_for(process.wait(), timeout=10) except (asyncio.TimeoutError, Exception): # If termination failed, forcefully kill the process tree self._signal_process_tree(process, terminate=False) @@ -219,6 +216,7 @@ class LanguageServerHandler: await asyncio.wait_for(process.wait(), timeout=2) except Exception: pass + """ def _signal_process_tree(self, process, terminate=True): """Send signal (terminate or kill) to the process and all its children.""" @@ -252,23 +250,25 @@ class LanguageServerHandler: except Exception: pass - - async def shutdown(self) -> None: + def shutdown(self) -> None: """ Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit """ self._log("Sending shutdown request to server") - await self.send.shutdown() + self.send.shutdown() self._log("Received shutdown response from server") self._received_shutdown = True self._log("Sending exit notification to server") self.notify.exit() self._log("Sent exit notification to server") + # TODO + """ if self.process and self.process.stdout: self.process.stdout.set_exception(StopLoopException()) # This yields the control to the event loop to allow the exception to be handled # in the run_forever and run_forever_stderr methods await asyncio.sleep(0) + """ def _log(self, message: str | StringDict) -> None: """ @@ -277,14 +277,33 @@ class LanguageServerHandler: if self.logger is not None: self.logger("client", "logger", message) - async def run_forever(self) -> bool: + @staticmethod + def _read_bytes_from_process(process, stream, num_bytes): + """Read exactly num_bytes from process stdout""" + if process.poll() is not None: + # Process has terminated, check if we can still read + pass + + data = b'' + while len(data) < num_bytes: + chunk = stream.read(num_bytes - len(data)) + if not chunk: + if process.poll() is not None: + raise EOFError(f"Process terminated. Expected {num_bytes} bytes, got {len(data)}") + # Process still running but no data available yet + time.sleep(0.01) # Small delay + continue + data += chunk + return data + + def run_forever(self) -> bool: """ Continuously read from the language server process stdout and handle the messages invoking the registered response and notification handlers """ try: - while self.process and self.process.stdout and not self.process.stdout.at_eof(): - line = await self.process.stdout.readline() + while self.process and self.process.stdout and self.process.stdout.readable(): + line = self.process.stdout.readline() if not line: continue try: @@ -294,39 +313,35 @@ class LanguageServerHandler: if num_bytes is None: continue while line and line.strip(): - line = await self.process.stdout.readline() + line = self.process.stdout.readline() if not line: continue - body = await self.process.stdout.readexactly(num_bytes) + body = self._read_bytes_from_process(self.process, self.process.stdout, num_bytes) - # Use lock to prevent race conditions on tasks and task_counter - with self._tasks_lock: - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._handle_body(body)) - self.task_counter += 1 - except (BrokenPipeError, ConnectionResetError, StopLoopException): + self._handle_body(body) + except (BrokenPipeError, ConnectionResetError): pass return self._received_shutdown - - async def run_forever_stderr(self) -> None: + def run_forever_stderr(self) -> None: """ Continuously read from the language server process stderr and log the messages """ try: - while self.process and self.process.stderr and not self.process.stderr.at_eof(): - line = await self.process.stderr.readline() + while self.process and self.process.stderr and self.process.stderr.readable(): + line = self.process.stderr.readline() if not line: continue self._log("LSP stderr: " + line.decode(ENCODING, errors='replace')) - except (BrokenPipeError, ConnectionResetError, StopLoopException): + except (BrokenPipeError, ConnectionResetError): pass - async def _handle_body(self, body: bytes) -> None: + def _handle_body(self, body: bytes) -> None: """ Parse the body text received from the language server process and invoke the appropriate handler """ try: - await self._receive_payload(json.loads(body)) + self._receive_payload(json.loads(body)) except IOError as ex: self._log(f"malformed {ENCODING}: {ex}") except UnicodeDecodeError as ex: @@ -334,7 +349,7 @@ class LanguageServerHandler: except json.JSONDecodeError as ex: self._log(f"malformed JSON: {ex}") - async def _receive_payload(self, payload: StringDict) -> None: + def _receive_payload(self, payload: StringDict) -> None: """ Determine if the payload received from server is for a request, response, or notification and invoke the appropriate handler """ @@ -343,11 +358,11 @@ class LanguageServerHandler: try: if "method" in payload: if "id" in payload: - await self._request_handler(payload) + self._request_handler(payload) else: - await self._notification_handler(payload) + self._notification_handler(payload) elif "id" in payload: - await self._response_handler(payload) + self._response_handler(payload) else: self._log(f"Unknown payload type: {payload}") except Exception as err: @@ -357,33 +372,22 @@ class LanguageServerHandler: """ Send notification pertaining to the given method to the server with the given parameters """ - self._send_payload_sync(make_notification(method, params)) + self._send_payload(make_notification(method, params)) def send_response(self, request_id: Any, params: PayloadLike) -> None: """ Send response to the given request id to the server with the given parameters """ - # Use lock to prevent race conditions on tasks and task_counter - with self._tasks_lock: - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( - self._send_payload(make_response(request_id, params)) - ) - self.task_counter += 1 - + self._send_payload(make_response(request_id, params)) def send_error_response(self, request_id: Any, err: Error) -> None: """ Send error response to the given request id to the server with the given error """ # Use lock to prevent race conditions on tasks and task_counter - with self._tasks_lock: - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( - self._send_payload(make_error_response(request_id, err)) - ) - self.task_counter += 1 + self._send_payload(make_error_response(request_id, err)) - - async def send_request(self, method: str, params: Optional[dict] = None) -> PayloadLike: + def send_request(self, method: str, params: Optional[dict] = None) -> PayloadLike: """ Send request to the server, register the request id, and wait for the response """ @@ -397,39 +401,19 @@ class LanguageServerHandler: with self._response_handlers_lock: self._response_handlers[request_id] = request - async with request.cv: - await self._send_payload(make_request(method, request_id, params)) - self._log(f"Waiting for asyncio condition for request {method} with params:\n{params}") - await request.cv.wait() - self._log(f"Finished waiting, processing result") - if isinstance(request.error, Error): - raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {request.error}") from request.error - self._log(f"Returning non-error result, which is:\n{request.result}") - return request.result + self._send_payload(make_request(method, request_id, params)) + self._log(f"Waiting for response to request {method} with params:\n{params}") + result = request.get_result() - def _send_payload_sync(self, payload: StringDict) -> None: - """ - Send the payload to the server by writing to its stdin synchronously - """ - if not self.process or not self.process.stdin: - return - msg = create_message(payload) - if self.logger: - self.logger("client", "server", payload) + self._log(f"Processing result") + if result.is_error(): + raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {result.error}") from result.error + + self._log(f"Returning non-error result, which is:\n{result.payload}") + return result.payload - # Use lock to prevent concurrent writes to stdin that cause buffer corruption - with self._stdin_lock: - try: - self.process.stdin.writelines(msg) - except (BrokenPipeError, ConnectionResetError, OSError) as e: - # Log the error but don't raise to prevent cascading failures - if self.logger: - self.logger("client", "logger", f"Failed to write to stdin: {e}") - return - - - async def _send_payload(self, payload: StringDict) -> None: + def _send_payload(self, payload: StringDict) -> None: """ Send the payload to the server by writing to its stdin asynchronously. """ @@ -442,14 +426,13 @@ class LanguageServerHandler: with self._stdin_lock: try: self.process.stdin.writelines(msg) - await self.process.stdin.drain() + self.process.stdin.flush() except (BrokenPipeError, ConnectionResetError, OSError) as e: # Log the error but don't raise to prevent cascading failures if self.logger: self.logger("client", "logger", f"Failed to write to stdin: {e}") return - def on_request(self, method: str, cb) -> None: """ Register the callback function to handle requests from the server to the client for the given method @@ -462,7 +445,7 @@ class LanguageServerHandler: """ self.on_notification_handlers[method] = cb - async def _response_handler(self, response: StringDict) -> None: + def _response_handler(self, response: StringDict) -> None: """ Handle the response received from the server for a request, using the id to determine the request """ @@ -470,14 +453,13 @@ class LanguageServerHandler: request = self._response_handlers.pop(response["id"]) if "result" in response and "error" not in response: - await request.on_result(response["result"]) + request.on_result(response["result"]) elif "result" not in response and "error" in response: - await request.on_error(Error.from_lsp(response["error"])) + request.on_error(Error.from_lsp(response["error"])) else: - await request.on_error(Error(ErrorCodes.InvalidRequest, "")) + request.on_error(Error(ErrorCodes.InvalidRequest, "")) - - async def _request_handler(self, response: StringDict) -> None: + def _request_handler(self, response: StringDict) -> None: """ Handle the request received from the server: call the appropriate callback function and return the result """ @@ -495,13 +477,13 @@ class LanguageServerHandler: ) return try: - self.send_response(request_id, await handler(params)) + self.send_response(request_id, handler(params)) except Error as ex: self.send_error_response(request_id, ex) except Exception as ex: self.send_error_response(request_id, Error(ErrorCodes.InternalError, str(ex))) - async def _notification_handler(self, response: StringDict) -> None: + def _notification_handler(self, response: StringDict) -> None: """ Handle the notification received from the server: call the appropriate callback function """ @@ -512,7 +494,7 @@ class LanguageServerHandler: self._log(f"unhandled {method}") return try: - await handler(params) + handler(params) except asyncio.CancelledError: return except Exception as ex: diff --git a/src/solidlsp/lsp_request.py b/src/solidlsp/lsp_request.py index b46bf34..984407b 100644 --- a/src/solidlsp/lsp_request.py +++ b/src/solidlsp/lsp_request.py @@ -2,56 +2,56 @@ from typing import List, Union from multilspy.lsp_protocol_handler import lsp_types -class LspRequest: +class SolidLspRequest: def __init__(self, send_request): self.send_request = send_request - async def implementation( + def implementation( self, params: lsp_types.ImplementationParams ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: """A request to resolve the implementation locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Definition} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/implementation", params) + return self.send_request("textDocument/implementation", params) - async def type_definition( + def type_definition( self, params: lsp_types.TypeDefinitionParams ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: """A request to resolve the type definition locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Definition} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/typeDefinition", params) + return self.send_request("textDocument/typeDefinition", params) - async def document_color( + def document_color( self, params: lsp_types.DocumentColorParams ) -> List["lsp_types.ColorInformation"]: """A request to list all color symbols found in a given text document. The request's parameter is of type {@link DocumentColorParams} the response is of type {@link ColorInformation ColorInformation[]} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/documentColor", params) + return self.send_request("textDocument/documentColor", params) - async def color_presentation( + def color_presentation( self, params: lsp_types.ColorPresentationParams ) -> List["lsp_types.ColorPresentation"]: """A request to list all presentation for a color. The request's parameter is of type {@link ColorPresentationParams} the response is of type {@link ColorInformation ColorInformation[]} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/colorPresentation", params) + return self.send_request("textDocument/colorPresentation", params) - async def folding_range( + def folding_range( self, params: lsp_types.FoldingRangeParams ) -> Union[List["lsp_types.FoldingRange"], None]: """A request to provide folding ranges in a document. The request's parameter is of type {@link FoldingRangeParams}, the response is of type {@link FoldingRangeList} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/foldingRange", params) + return self.send_request("textDocument/foldingRange", params) - async def declaration( + def declaration( self, params: lsp_types.DeclarationParams ) -> Union["lsp_types.Declaration", List["lsp_types.LocationLink"], None]: """A request to resolve the type definition locations of a symbol at a given text @@ -59,129 +59,129 @@ class LspRequest: (#TextDocumentPositionParams) the response is of type {@link Declaration} or a typed array of {@link DeclarationLink} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/declaration", params) + return self.send_request("textDocument/declaration", params) - async def selection_range( + def selection_range( self, params: lsp_types.SelectionRangeParams ) -> Union[List["lsp_types.SelectionRange"], None]: """A request to provide selection ranges in a document. The request's parameter is of type {@link SelectionRangeParams}, the response is of type {@link SelectionRange SelectionRange[]} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/selectionRange", params) + return self.send_request("textDocument/selectionRange", params) - async def prepare_call_hierarchy( + def prepare_call_hierarchy( self, params: lsp_types.CallHierarchyPrepareParams ) -> Union[List["lsp_types.CallHierarchyItem"], None]: """A request to result a `CallHierarchyItem` in a document at a given position. Can be used as an input to an incoming or outgoing call hierarchy. @since 3.16.0""" - return await self.send_request("textDocument/prepareCallHierarchy", params) + return self.send_request("textDocument/prepareCallHierarchy", params) - async def incoming_calls( + def incoming_calls( self, params: lsp_types.CallHierarchyIncomingCallsParams ) -> Union[List["lsp_types.CallHierarchyIncomingCall"], None]: """A request to resolve the incoming calls for a given `CallHierarchyItem`. @since 3.16.0""" - return await self.send_request("callHierarchy/incomingCalls", params) + return self.send_request("callHierarchy/incomingCalls", params) - async def outgoing_calls( + def outgoing_calls( self, params: lsp_types.CallHierarchyOutgoingCallsParams ) -> Union[List["lsp_types.CallHierarchyOutgoingCall"], None]: """A request to resolve the outgoing calls for a given `CallHierarchyItem`. @since 3.16.0""" - return await self.send_request("callHierarchy/outgoingCalls", params) + return self.send_request("callHierarchy/outgoingCalls", params) - async def semantic_tokens_full( + def semantic_tokens_full( self, params: lsp_types.SemanticTokensParams ) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" - return await self.send_request("textDocument/semanticTokens/full", params) + return self.send_request("textDocument/semanticTokens/full", params) - async def semantic_tokens_delta( + def semantic_tokens_delta( self, params: lsp_types.SemanticTokensDeltaParams ) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]: """@since 3.16.0""" - return await self.send_request("textDocument/semanticTokens/full/delta", params) + return self.send_request("textDocument/semanticTokens/full/delta", params) - async def semantic_tokens_range( + def semantic_tokens_range( self, params: lsp_types.SemanticTokensRangeParams ) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" - return await self.send_request("textDocument/semanticTokens/range", params) + return self.send_request("textDocument/semanticTokens/range", params) - async def linked_editing_range( + def linked_editing_range( self, params: lsp_types.LinkedEditingRangeParams ) -> Union["lsp_types.LinkedEditingRanges", None]: """A request to provide ranges that can be edited together. @since 3.16.0""" - return await self.send_request("textDocument/linkedEditingRange", params) + return self.send_request("textDocument/linkedEditingRange", params) - async def will_create_files( + def will_create_files( self, params: lsp_types.CreateFilesParams ) -> Union["lsp_types.WorkspaceEdit", None]: """The will create files request is sent from the client to the server before files are actually created as long as the creation is triggered from within the client. @since 3.16.0""" - return await self.send_request("workspace/willCreateFiles", params) + return self.send_request("workspace/willCreateFiles", params) - async def will_rename_files( + def will_rename_files( self, params: lsp_types.RenameFilesParams ) -> Union["lsp_types.WorkspaceEdit", None]: """The will rename files request is sent from the client to the server before files are actually renamed as long as the rename is triggered from within the client. @since 3.16.0""" - return await self.send_request("workspace/willRenameFiles", params) + return self.send_request("workspace/willRenameFiles", params) - async def will_delete_files( + def will_delete_files( self, params: lsp_types.DeleteFilesParams ) -> Union["lsp_types.WorkspaceEdit", None]: """The did delete files notification is sent from the client to the server when files were deleted from within the client. @since 3.16.0""" - return await self.send_request("workspace/willDeleteFiles", params) + return self.send_request("workspace/willDeleteFiles", params) - async def moniker( + def moniker( self, params: lsp_types.MonikerParams ) -> Union[List["lsp_types.Moniker"], None]: """A request to get the moniker of a symbol at a given text document position. The request parameter is of type {@link TextDocumentPositionParams}. The response is of type {@link Moniker Moniker[]} or `null`.""" - return await self.send_request("textDocument/moniker", params) + return self.send_request("textDocument/moniker", params) - async def prepare_type_hierarchy( + def prepare_type_hierarchy( self, params: lsp_types.TypeHierarchyPrepareParams ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: """A request to result a `TypeHierarchyItem` in a document at a given position. Can be used as an input to a subtypes or supertypes type hierarchy. @since 3.17.0""" - return await self.send_request("textDocument/prepareTypeHierarchy", params) + return self.send_request("textDocument/prepareTypeHierarchy", params) - async def type_hierarchy_supertypes( + def type_hierarchy_supertypes( self, params: lsp_types.TypeHierarchySupertypesParams ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: """A request to resolve the supertypes for a given `TypeHierarchyItem`. @since 3.17.0""" - return await self.send_request("typeHierarchy/supertypes", params) + return self.send_request("typeHierarchy/supertypes", params) - async def type_hierarchy_subtypes( + def type_hierarchy_subtypes( self, params: lsp_types.TypeHierarchySubtypesParams ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: """A request to resolve the subtypes for a given `TypeHierarchyItem`. @since 3.17.0""" - return await self.send_request("typeHierarchy/subtypes", params) + return self.send_request("typeHierarchy/subtypes", params) - async def inline_value( + def inline_value( self, params: lsp_types.InlineValueParams ) -> Union[List["lsp_types.InlineValue"], None]: """A request to provide inline values in a document. The request's parameter is of @@ -189,9 +189,9 @@ class LspRequest: {@link InlineValue InlineValue[]} or a Thenable that resolves to such. @since 3.17.0""" - return await self.send_request("textDocument/inlineValue", params) + return self.send_request("textDocument/inlineValue", params) - async def inlay_hint( + def inlay_hint( self, params: lsp_types.InlayHintParams ) -> Union[List["lsp_types.InlayHint"], None]: """A request to provide inlay hints in a document. The request's parameter is of @@ -199,9 +199,9 @@ class LspRequest: {@link InlayHint InlayHint[]} or a Thenable that resolves to such. @since 3.17.0""" - return await self.send_request("textDocument/inlayHint", params) + return self.send_request("textDocument/inlayHint", params) - async def resolve_inlay_hint( + def resolve_inlay_hint( self, params: lsp_types.InlayHint ) -> "lsp_types.InlayHint": """A request to resolve additional properties for an inlay hint. @@ -209,25 +209,25 @@ class LspRequest: of type {@link InlayHint} or a Thenable that resolves to such. @since 3.17.0""" - return await self.send_request("inlayHint/resolve", params) + return self.send_request("inlayHint/resolve", params) - async def text_document_diagnostic( + def text_document_diagnostic( self, params: lsp_types.DocumentDiagnosticParams ) -> "lsp_types.DocumentDiagnosticReport": """The document diagnostic request definition. @since 3.17.0""" - return await self.send_request("textDocument/diagnostic", params) + return self.send_request("textDocument/diagnostic", params) - async def workspace_diagnostic( + def workspace_diagnostic( self, params: lsp_types.WorkspaceDiagnosticParams ) -> "lsp_types.WorkspaceDiagnosticReport": """The workspace diagnostic request definition. @since 3.17.0""" - return await self.send_request("workspace/diagnostic", params) + return self.send_request("workspace/diagnostic", params) - async def initialize( + def initialize( self, params: lsp_types.InitializeParams ) -> "lsp_types.InitializeResult": """The initialize request is sent from the client to the server. @@ -235,16 +235,16 @@ class LspRequest: The requests parameter is of type {@link InitializeParams} the response if of type {@link InitializeResult} of a Thenable that resolves to such.""" - return await self.send_request("initialize", params) + return self.send_request("initialize", params) - async def shutdown(self) -> None: + def shutdown(self) -> None: """A shutdown request is sent from the client to the server. It is sent once when the client decides to shutdown the server. The only notification that is sent after a shutdown request is the exit event.""" - return await self.send_request("shutdown") + return self.send_request("shutdown") - async def will_save_wait_until( + def will_save_wait_until( self, params: lsp_types.WillSaveTextDocumentParams ) -> Union[List["lsp_types.TextEdit"], None]: """A document will save request is sent from the client to the server before @@ -253,9 +253,9 @@ class LspRequest: clients might drop results if computing the text edits took too long or if a server constantly fails on this request. This is done to keep the save fast and reliable.""" - return await self.send_request("textDocument/willSaveWaitUntil", params) + return self.send_request("textDocument/willSaveWaitUntil", params) - async def completion( + def completion( self, params: lsp_types.CompletionParams ) -> Union[List["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: """Request to request completion at a given text document position. The request's @@ -268,30 +268,30 @@ class LspRequest: request. However, properties that are needed for the initial sorting and filtering, like `sortText`, `filterText`, `insertText`, and `textEdit`, must not be changed during resolve. """ - return await self.send_request("textDocument/completion", params) + return self.send_request("textDocument/completion", params) - async def resolve_completion_item( + def resolve_completion_item( self, params: lsp_types.CompletionItem ) -> "lsp_types.CompletionItem": """Request to resolve additional information for a given completion item.The request's parameter is of type {@link CompletionItem} the response is of type {@link CompletionItem} or a Thenable that resolves to such.""" - return await self.send_request("completionItem/resolve", params) + return self.send_request("completionItem/resolve", params) - async def hover( + def hover( self, params: lsp_types.HoverParams ) -> Union["lsp_types.Hover", None]: """Request to request hover information at a given text document position. The request's parameter is of type {@link TextDocumentPosition} the response is of type {@link Hover} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/hover", params) + return self.send_request("textDocument/hover", params) - async def signature_help( + def signature_help( self, params: lsp_types.SignatureHelpParams ) -> Union["lsp_types.SignatureHelp", None]: - return await self.send_request("textDocument/signatureHelp", params) + return self.send_request("textDocument/signatureHelp", params) - async def definition( + def definition( self, params: lsp_types.DefinitionParams ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: """A request to resolve the definition location of a symbol at a given text @@ -299,27 +299,27 @@ class LspRequest: (#TextDocumentPosition) the response is of either type {@link Definition} or a typed array of {@link DefinitionLink} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/definition", params) + return self.send_request("textDocument/definition", params) - async def references( + def references( self, params: lsp_types.ReferenceParams ) -> Union[List["lsp_types.Location"], None]: """A request to resolve project-wide references for the symbol denoted by the given text document position. The request's parameter is of type {@link ReferenceParams} the response is of type {@link Location Location[]} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/references", params) + return self.send_request("textDocument/references", params) - async def document_highlight( + def document_highlight( self, params: lsp_types.DocumentHighlightParams ) -> Union[List["lsp_types.DocumentHighlight"], None]: """Request to resolve a {@link DocumentHighlight} for a given text document position. The request's parameter is of type [TextDocumentPosition] (#TextDocumentPosition) the request response is of type [DocumentHighlight[]] (#DocumentHighlight) or a Thenable that resolves to such.""" - return await self.send_request("textDocument/documentHighlight", params) + return self.send_request("textDocument/documentHighlight", params) - async def document_symbol( + def document_symbol( self, params: lsp_types.DocumentSymbolParams ) -> Union[ List["lsp_types.SymbolInformation"], List["lsp_types.DocumentSymbol"], None @@ -328,23 +328,23 @@ class LspRequest: parameter is of type {@link TextDocumentIdentifier} the response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable that resolves to such.""" - return await self.send_request("textDocument/documentSymbol", params) + return self.send_request("textDocument/documentSymbol", params) - async def code_action( + def code_action( self, params: lsp_types.CodeActionParams ) -> Union[List[Union["lsp_types.Command", "lsp_types.CodeAction"]], None]: """A request to provide commands for the given text document and range.""" - return await self.send_request("textDocument/codeAction", params) + return self.send_request("textDocument/codeAction", params) - async def resolve_code_action( + def resolve_code_action( self, params: lsp_types.CodeAction ) -> "lsp_types.CodeAction": """Request to resolve additional information for a given code action.The request's parameter is of type {@link CodeAction} the response is of type {@link CodeAction} or a Thenable that resolves to such.""" - return await self.send_request("codeAction/resolve", params) + return self.send_request("codeAction/resolve", params) - async def workspace_symbol( + def workspace_symbol( self, params: lsp_types.WorkspaceSymbolParams ) -> Union[ List["lsp_types.SymbolInformation"], List["lsp_types.WorkspaceSymbol"], None @@ -358,78 +358,78 @@ class LspRequest: need to advertise support for WorkspaceSymbols via the client capability `workspace.symbol.resolveSupport`. """ - return await self.send_request("workspace/symbol", params) + return self.send_request("workspace/symbol", params) - async def resolve_workspace_symbol( + def resolve_workspace_symbol( self, params: lsp_types.WorkspaceSymbol ) -> "lsp_types.WorkspaceSymbol": """A request to resolve the range inside the workspace symbol's location. @since 3.17.0""" - return await self.send_request("workspaceSymbol/resolve", params) + return self.send_request("workspaceSymbol/resolve", params) - async def code_lens( + def code_lens( self, params: lsp_types.CodeLensParams ) -> Union[List["lsp_types.CodeLens"], None]: """A request to provide code lens for the given text document.""" - return await self.send_request("textDocument/codeLens", params) + return self.send_request("textDocument/codeLens", params) - async def resolve_code_lens( + def resolve_code_lens( self, params: lsp_types.CodeLens ) -> "lsp_types.CodeLens": """A request to resolve a command for a given code lens.""" - return await self.send_request("codeLens/resolve", params) + return self.send_request("codeLens/resolve", params) - async def document_link( + def document_link( self, params: lsp_types.DocumentLinkParams ) -> Union[List["lsp_types.DocumentLink"], None]: """A request to provide document links""" - return await self.send_request("textDocument/documentLink", params) + return self.send_request("textDocument/documentLink", params) - async def resolve_document_link( + def resolve_document_link( self, params: lsp_types.DocumentLink ) -> "lsp_types.DocumentLink": """Request to resolve additional information for a given document link. The request's parameter is of type {@link DocumentLink} the response is of type {@link DocumentLink} or a Thenable that resolves to such.""" - return await self.send_request("documentLink/resolve", params) + return self.send_request("documentLink/resolve", params) - async def formatting( + def formatting( self, params: lsp_types.DocumentFormattingParams ) -> Union[List["lsp_types.TextEdit"], None]: """A request to to format a whole document.""" - return await self.send_request("textDocument/formatting", params) + return self.send_request("textDocument/formatting", params) - async def range_formatting( + def range_formatting( self, params: lsp_types.DocumentRangeFormattingParams ) -> Union[List["lsp_types.TextEdit"], None]: """A request to to format a range in a document.""" - return await self.send_request("textDocument/rangeFormatting", params) + return self.send_request("textDocument/rangeFormatting", params) - async def on_type_formatting( + def on_type_formatting( self, params: lsp_types.DocumentOnTypeFormattingParams ) -> Union[List["lsp_types.TextEdit"], None]: """A request to format a document on type.""" - return await self.send_request("textDocument/onTypeFormatting", params) + return self.send_request("textDocument/onTypeFormatting", params) - async def rename( + def rename( self, params: lsp_types.RenameParams ) -> Union["lsp_types.WorkspaceEdit", None]: """A request to rename a symbol.""" - return await self.send_request("textDocument/rename", params) + return self.send_request("textDocument/rename", params) - async def prepare_rename( + def prepare_rename( self, params: lsp_types.PrepareRenameParams ) -> Union["lsp_types.PrepareRenameResult", None]: """A request to test and perform the setup necessary for a rename. @since 3.16 - support for default behavior""" - return await self.send_request("textDocument/prepareRename", params) + return self.send_request("textDocument/prepareRename", params) - async def execute_command( + def execute_command( self, params: lsp_types.ExecuteCommandParams ) -> Union["lsp_types.LSPAny", None]: """A request send from the client to the server to execute a command. The request might return a workspace edit which the client will apply to the workspace.""" - return await self.send_request("workspace/executeCommand", params) + return self.send_request("workspace/executeCommand", params) From a43d0aa02146fa9e08c5cbfecc98f8d1efdb586d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 13:20:06 +0200 Subject: [PATCH 094/174] Add language server implementations from multilspy (unmodified) --- .gitignore | 1 + .../clangd_language_server.py | 190 ++++ .../initialize_params.json | 36 + .../runtime_dependencies.json | 29 + .../dart_language_server.py | 143 +++ .../initialize_params.json | 23 + .../runtime_dependencies.json | 45 + .../eclipse_jdtls/eclipse_jdtls.py | 422 ++++++++ .../eclipse_jdtls/initialize_params.json | 849 ++++++++++++++++ .../eclipse_jdtls/runtime_dependencies.json | 72 ++ src/solidlsp/language_servers/gopls/gopls.py | 154 +++ .../gopls/initialize_params.json | 46 + .../intelephense/initialize_params.json | 36 + .../intelephense/intelephense.py | 200 ++++ .../intelephense/runtime_dependencies.json | 10 + .../initialize_params.json | 905 +++++++++++++++++ .../jedi_language_server/jedi_server.py | 123 +++ .../initialize_params.json | 521 ++++++++++ .../kotlin_language_server.py | 227 +++++ .../runtime_dependencies.json | 41 + .../omnisharp/initialize_params.json | 631 ++++++++++++ .../language_servers/omnisharp/omnisharp.py | 406 ++++++++ .../omnisharp/runtime_dependencies.json | 441 +++++++++ .../workspace_did_change_configuration.json | 111 +++ .../pyright_language_server/pyright_server.py | 246 +++++ .../rust_analyzer/initialize_params.json | 917 ++++++++++++++++++ .../rust_analyzer/runtime_dependencies.json | 29 + .../rust_analyzer/rust_analyzer.py | 185 ++++ .../solargraph/initialize_params.json | 15 + .../solargraph/runtime_dependencies.json | 11 + .../language_servers/solargraph/solargraph.py | 189 ++++ .../initialize_params.json | 906 +++++++++++++++++ .../runtime_dependencies.json | 16 + .../typescript_language_server.py | 245 +++++ 34 files changed, 8421 insertions(+) create mode 100644 src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py create mode 100644 src/solidlsp/language_servers/clangd_language_server/initialize_params.json create mode 100644 src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json create mode 100644 src/solidlsp/language_servers/dart_language_server/dart_language_server.py create mode 100644 src/solidlsp/language_servers/dart_language_server/initialize_params.json create mode 100644 src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json create mode 100644 src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py create mode 100644 src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json create mode 100644 src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json create mode 100644 src/solidlsp/language_servers/gopls/gopls.py create mode 100644 src/solidlsp/language_servers/gopls/initialize_params.json create mode 100644 src/solidlsp/language_servers/intelephense/initialize_params.json create mode 100644 src/solidlsp/language_servers/intelephense/intelephense.py create mode 100644 src/solidlsp/language_servers/intelephense/runtime_dependencies.json create mode 100644 src/solidlsp/language_servers/jedi_language_server/initialize_params.json create mode 100644 src/solidlsp/language_servers/jedi_language_server/jedi_server.py create mode 100644 src/solidlsp/language_servers/kotlin_language_server/initialize_params.json create mode 100644 src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py create mode 100644 src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json create mode 100644 src/solidlsp/language_servers/omnisharp/initialize_params.json create mode 100644 src/solidlsp/language_servers/omnisharp/omnisharp.py create mode 100644 src/solidlsp/language_servers/omnisharp/runtime_dependencies.json create mode 100644 src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json create mode 100644 src/solidlsp/language_servers/pyright_language_server/pyright_server.py create mode 100644 src/solidlsp/language_servers/rust_analyzer/initialize_params.json create mode 100644 src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json create mode 100644 src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py create mode 100644 src/solidlsp/language_servers/solargraph/initialize_params.json create mode 100644 src/solidlsp/language_servers/solargraph/runtime_dependencies.json create mode 100644 src/solidlsp/language_servers/solargraph/solargraph.py create mode 100644 src/solidlsp/language_servers/typescript_language_server/initialize_params.json create mode 100644 src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json create mode 100644 src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py diff --git a/.gitignore b/.gitignore index ce9c54f..bee98b9 100644 --- a/.gitignore +++ b/.gitignore @@ -202,6 +202,7 @@ pylint.html /agent-ui # dynamic LS installations /src/multilspy/language_servers/*/static +/src/solidlsp/language_servers/*/static # Cache is in .multilspy **/.multilspy/** diff --git a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py new file mode 100644 index 0000000..e28aa2c --- /dev/null +++ b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py @@ -0,0 +1,190 @@ +""" +Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. +""" + +import asyncio +import json +import logging +import os +import stat +import pathlib +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_utils import FileUtils +from multilspy.multilspy_utils import PlatformUtils + + +class ClangdLanguageServer(LanguageServer): + """ + Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. + As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly. + Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + 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) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), + "cpp", + ) + self.server_ready = asyncio.Event() + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + """ + Setup runtime dependencies for ClangdLanguageServer. + """ + platform_id = PlatformUtils.get_platform_id() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: + d = json.load(f) + del d["_description"] + + assert platform_id.value in [ + "linux-x64", + "win-x64", + "osx-arm64", + ], "Unsupported platform: " + platform_id.value + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [ + dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value + ] + assert len(runtime_dependencies) == 1 + # Select dependency matching the current platform + dependency = next((dep for dep in runtime_dependencies if dep["platformId"] == platform_id.value), None) + if dependency is None: + raise RuntimeError(f"No runtime dependency found for platform {platform_id.value}") + + clangd_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clangd") + clangd_executable_path = os.path.join(clangd_ls_dir, "clangd_19.1.2", "bin", dependency["binaryName"]) + if not os.path.exists(clangd_executable_path): + clangd_url = dependency["url"] + logger.log(f"Clangd executable not found at {clangd_executable_path}. Downloading from {clangd_url}", logging.INFO) + os.makedirs(clangd_ls_dir, exist_ok=True) + if dependency["archiveType"] == "zip": + FileUtils.download_and_extract_archive( + logger, clangd_url, clangd_ls_dir, dependency["archiveType"] + ) + else: + raise RuntimeError(f"Unsupported archive type: {dependency['archiveType']}") + if not os.path.exists(clangd_executable_path): + raise FileNotFoundError( + f"Clangd executable not found at {clangd_executable_path}.\n" + "Make sure you have installed clangd. See https://clangd.llvm.org/installation" + ) + os.chmod(clangd_executable_path, stat.S_IEXEC) + + return clangd_executable_path + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the clangd Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["ClangdLanguageServer"]: + """ + Starts the Clangd Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + """ + async def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "workspace/executeCommand": + self.initialize_searcher_command_available.set() + self.resolve_main_method_available.set() + return + + async def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + self.service_ready_event.set() + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def check_experimental_status(params): + if params["quiescent"] == True: + self.server_ready.set() + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + 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) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + async with super().start_server(): + self.logger.log("Starting Clangd server process", logging.INFO) + await 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, + ) + init_response = await self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": ['.', '<', '>', ':', '"', '/', '*'], + "resolveProvider": False, + } + + self.server.notify.initialized({}) + + self.completions_available.set() + # set ready flag + self.server_ready.set() + await self.server_ready.wait() + + yield self diff --git a/src/solidlsp/language_servers/clangd_language_server/initialize_params.json b/src/solidlsp/language_servers/clangd_language_server/initialize_params.json new file mode 100644 index 0000000..4330560 --- /dev/null +++ b/src/solidlsp/language_servers/clangd_language_server/initialize_params.json @@ -0,0 +1,36 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "textDocument": { + "synchronization": { + "didSave": true, + "dynamicRegistration": true + }, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true + } + }, + "definition": { + "dynamicRegistration": true + } + }, + "workspace": { + "workspaceFolders": true, + "didChangeConfiguration": { + "dynamicRegistration": true + } + } + }, + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json new file mode 100644 index 0000000..53f5b7f --- /dev/null +++ b/src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json @@ -0,0 +1,29 @@ +{ + "_description": "Used to download the runtime dependencies for running Clangd.", + "runtimeDependencies": [ + { + "id": "Clangd", + "description": "Clangd for Linux (x64)", + "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip", + "platformId": "linux-x64", + "archiveType": "zip", + "binaryName": "clangd" + }, + { + "id": "Clangd", + "description": "Clangd for Windows (x64)", + "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip", + "platformId": "windows-x64", + "archiveType": "zip", + "binaryName": "clangd.exe" + }, + { + "id": "Clangd", + "description": "Clangd for macOS (Arm64)", + "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", + "platformId": "osx-arm64", + "archiveType": "zip", + "binaryName": "clangd" + } + ] +} diff --git a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py new file mode 100644 index 0000000..cd8ea51 --- /dev/null +++ b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py @@ -0,0 +1,143 @@ +from contextlib import asynccontextmanager +import logging +import os +import pathlib +import shutil +import stat +from typing import AsyncIterator +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +import json +from multilspy.multilspy_utils import FileUtils, PlatformUtils + + +class DartLanguageServer(LanguageServer): + """ + Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. + """ + + def __init__(self, config, logger, repository_root_path): + """ + Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + + executable_path = self.setup_runtime_dependencies(logger) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), + "dart", + ) + + def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str: + platform_id = PlatformUtils.get_platform_id() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: + d = json.load(f) + del d["_description"] + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [ + dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value + ] + + assert len(runtime_dependencies) == 1 + dependency = runtime_dependencies[0] + + dart_ls_dir = os.path.join(os.path.dirname(__file__), "static", "dart-language-server") + dart_executable_path = os.path.join(dart_ls_dir, dependency["binaryName"]) + + if not os.path.exists(dart_ls_dir): + os.makedirs(dart_ls_dir) + FileUtils.download_and_extract_archive( + logger, dependency["url"], dart_ls_dir, dependency["archiveType"] + ) + + + assert os.path.exists(dart_executable_path) + os.chmod(dart_executable_path, stat.S_IEXEC) + + return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" + + + def _get_initialize_params(self, repository_absolute_path: str): + """ + Returns the initialize params for the Dart Language Server. + """ + with open( + os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r" + ) as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path( + repository_absolute_path + ).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["DartLanguageServer"]: + """ + Start the language server and yield when the server is ready. + """ + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def check_experimental_status(params): + pass + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + 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) + self.server.on_notification( + "experimental/serverStatus", check_experimental_status + ) + + async with super().start_server(): + self.logger.log( + "Starting dart-language-server server process", logging.INFO + ) + await 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, + ) + init_response = await self.server.send_request( + "initialize", initialize_params + ) + self.logger.log( + f"Received initialize response from dart-language-server: {init_response}", + logging.INFO, + ) + + self.server.notify.initialized({}) + + yield self diff --git a/src/solidlsp/language_servers/dart_language_server/initialize_params.json b/src/solidlsp/language_servers/dart_language_server/initialize_params.json new file mode 100644 index 0000000..e90e408 --- /dev/null +++ b/src/solidlsp/language_servers/dart_language_server/initialize_params.json @@ -0,0 +1,23 @@ +{ + "_description": "This file contains the initialization parameters for the Dart Language Server.", + "processId": "$processId", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": {}, + "initializationOptions": { + "onlyAnalyzeProjectsWithOpenFiles": false, + "suggestFromUnimportedLibraries": true, + "closingLabels": false, + "outline": false, + "flutterOutline": false, + "allowOpenUri": false + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] + +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json new file mode 100644 index 0000000..32e4929 --- /dev/null +++ b/src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json @@ -0,0 +1,45 @@ +{ + "_description": "Used to download the runtime dependencies for running Dart Language Server, downloaded from https://dart.dev/get-dart/archive", + "runtimeDependencies": [ + { + "id": "DartLanguageServer", + "description": "Dart Language Server for Linux (x64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip", + "platformId": "linux-x64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart" + }, + { + "id": "DartLanguageServer", + "description": "Dart Language Server for Windows (x64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip", + "platformId": "win-x64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart.exe" + }, + { + "id": "DartLanguageServer", + "description": "Dart Language Server for Windows (arm64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-arm64-release.zip", + "platformId": "win-arm64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart.exe" + }, + { + "id": "DartLanguageServer", + "description": "Dart Language Server for macOS (x64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip", + "platformId": "osx-x64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart" + }, + { + "id": "DartLanguageServer", + "description": "Dart Language Server for macOS (arm64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip", + "platformId": "osx-arm64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py new file mode 100644 index 0000000..a88dc1b --- /dev/null +++ b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -0,0 +1,422 @@ +""" +Provides Java specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Java. +""" + +import asyncio +import dataclasses +import json +import logging +import os +import pathlib +import shutil +import stat +import uuid +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from overrides import override + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_settings import MultilspySettings +from multilspy.multilspy_utils import FileUtils +from multilspy.multilspy_utils import PlatformUtils +from pathlib import PurePath + + +@dataclasses.dataclass +class RuntimeDependencyPaths: + """ + Stores the paths to the runtime dependencies of EclipseJDTLS + """ + + gradle_path: str + lombok_jar_path: str + jre_path: str + jre_home_path: str + jdtls_launcher_jar_path: str + jdtls_readonly_config_path: str + intellicode_jar_path: str + intellisense_members_path: str + + +class EclipseJDTLS(LanguageServer): + """ + The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + 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) + self.runtime_dependency_paths = runtime_dependency_paths + + # ws_dir is the workspace directory for the EclipseJDTLS server + ws_dir = str( + PurePath( + MultilspySettings.get_language_server_directory(), + "EclipseJDTLS", + "workspaces", + uuid.uuid4().hex, + ) + ) + + # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces + shared_cache_location = str( + PurePath(MultilspySettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex") + ) + + jre_path = self.runtime_dependency_paths.jre_path + lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path + + jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path + + os.makedirs(ws_dir, exist_ok=True) + + data_dir = str(PurePath(ws_dir, "data_dir")) + jdtls_config_path = str(PurePath(ws_dir, "config_path")) + + jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path + + if not os.path.exists(jdtls_config_path): + shutil.copytree(jdtls_readonly_config_path, jdtls_config_path) + + for static_path in [ + jre_path, + lombok_jar_path, + jdtls_launcher_jar, + jdtls_config_path, + jdtls_readonly_config_path, + ]: + assert os.path.exists(static_path), static_path + + # TODO: Add "self.runtime_dependency_paths.jre_home_path"/bin to $PATH as well + proc_env = {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path} + proc_cwd = repository_root_path + cmd = " ".join( + [ + jre_path, + "--add-modules=ALL-SYSTEM", + "--add-opens", + "java.base/java.util=ALL-UNNAMED", + "--add-opens", + "java.base/java.lang=ALL-UNNAMED", + "--add-opens", + "java.base/sun.nio.fs=ALL-UNNAMED", + "-Declipse.application=org.eclipse.jdt.ls.core.id1", + "-Dosgi.bundles.defaultStartLevel=4", + "-Declipse.product=org.eclipse.jdt.ls.core.product", + "-Djava.import.generatesMetadataFilesAtProjectRoot=false", + "-Dfile.encoding=utf8", + "-noverify", + "-XX:+UseParallelGC", + "-XX:GCTimeRatio=4", + "-XX:AdaptiveSizePolicyWeight=90", + "-Dsun.zip.disableMemoryMapping=true", + "-Djava.lsp.joinOnCompletion=true", + "-Xmx3G", + "-Xms100m", + "-Xlog:disable", + "-Dlog.level=ALL", + f'"-javaagent:{lombok_jar_path}"', + f'"-Djdt.core.sharedIndexLocation={shared_cache_location}"', + "-jar", + f'"{jdtls_launcher_jar}"', + "-configuration", + f'"{jdtls_config_path}"', + "-data", + f'"{data_dir}"', + ] + ) + + self.service_ready_event = asyncio.Event() + self.intellicode_enable_command_available = asyncio.Event() + self.initialize_searcher_command_available = asyncio.Event() + + super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java") + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # Ignore common Java build directories from different build tools: + # - Maven: target + # - Gradle: build, .gradle + # - Eclipse: bin, .settings + # - IntelliJ IDEA: out, .idea + # - General: classes, dist, lib + return super().is_ignored_dirname(dirname) or dirname in [ + "target", # Maven + "build", # Gradle + "bin", # Eclipse + "out", # IntelliJ IDEA + "classes", # General + "dist", # General + "lib" # General + ] + + def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> RuntimeDependencyPaths: + """ + Setup runtime dependencies for EclipseJDTLS. + """ + platformId = PlatformUtils.get_platform_id() + + with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), "r", encoding="utf-8") as f: + runtimeDependencies = json.load(f) + del runtimeDependencies["_description"] + + os.makedirs(str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static")), exist_ok=True) + + # assert platformId.value in [ + # "linux-x64", + # "win-x64", + # ], "Only linux-x64 platform is supported for in multilspy at the moment" + + gradle_path = str( + PurePath( + os.path.abspath(os.path.dirname(__file__)), + "static/gradle-7.3.3", + ) + ) + + if not os.path.exists(gradle_path): + FileUtils.download_and_extract_archive( + logger, + runtimeDependencies["gradle"]["platform-agnostic"]["url"], + str(PurePath(gradle_path).parent), + runtimeDependencies["gradle"]["platform-agnostic"]["archiveType"], + ) + + assert os.path.exists(gradle_path) + + dependency = runtimeDependencies["vscode-java"][platformId.value] + vscode_java_path = str( + PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]) + ) + os.makedirs(vscode_java_path, exist_ok=True) + jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"])) + jre_path = str(PurePath(vscode_java_path, dependency["jre_path"])) + lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"])) + jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"])) + jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"])) + if not all( + [ + os.path.exists(vscode_java_path), + os.path.exists(jre_home_path), + os.path.exists(jre_path), + os.path.exists(lombok_jar_path), + os.path.exists(jdtls_launcher_jar_path), + os.path.exists(jdtls_readonly_config_path), + ] + ): + FileUtils.download_and_extract_archive( + logger, dependency["url"], vscode_java_path, dependency["archiveType"] + ) + + os.chmod(jre_path, stat.S_IEXEC) + + assert os.path.exists(vscode_java_path) + assert os.path.exists(jre_home_path) + assert os.path.exists(jre_path) + assert os.path.exists(lombok_jar_path) + assert os.path.exists(jdtls_launcher_jar_path) + assert os.path.exists(jdtls_readonly_config_path) + + dependency = runtimeDependencies["intellicode"]["platform-agnostic"] + intellicode_directory_path = str( + PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]) + ) + os.makedirs(intellicode_directory_path, exist_ok=True) + intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"])) + intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"])) + if not all( + [ + os.path.exists(intellicode_directory_path), + os.path.exists(intellicode_jar_path), + os.path.exists(intellisense_members_path), + ] + ): + FileUtils.download_and_extract_archive( + logger, dependency["url"], intellicode_directory_path, dependency["archiveType"] + ) + + assert os.path.exists(intellicode_directory_path) + assert os.path.exists(intellicode_jar_path) + assert os.path.exists(intellisense_members_path) + + return RuntimeDependencyPaths( + gradle_path=gradle_path, + lombok_jar_path=lombok_jar_path, + jre_path=jre_path, + jre_home_path=jre_home_path, + jdtls_launcher_jar_path=jdtls_launcher_jar_path, + jdtls_readonly_config_path=jdtls_readonly_config_path, + intellicode_jar_path=intellicode_jar_path, + intellisense_members_path=intellisense_members_path, + ) + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize parameters for the EclipseJDTLS server. + """ + # Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available + with open(str(PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: + d: InitializeParams = json.load(f) + + del d["_description"] + + if not os.path.isabs(repository_absolute_path): + repository_absolute_path = os.path.abspath(repository_absolute_path) + + assert d["processId"] == "os.getpid()" + d["processId"] = os.getpid() + + assert d["rootPath"] == "repository_absolute_path" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "pathlib.Path(repository_absolute_path).as_uri()" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["initializationOptions"]["workspaceFolders"] == "[pathlib.Path(repository_absolute_path).as_uri()]" + d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()] + + assert ( + d["workspaceFolders"] + == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' + ) + d["workspaceFolders"] = [ + { + "uri": pathlib.Path(repository_absolute_path).as_uri(), + "name": os.path.basename(repository_absolute_path), + } + ] + + assert d["initializationOptions"]["bundles"] == ["intellicode-core.jar"] + bundles = [self.runtime_dependency_paths.intellicode_jar_path] + d["initializationOptions"]["bundles"] = bundles + + assert d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] == [ + {"name": "JavaSE-21", "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": True} + ] + d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [ + {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True} + ] + + for runtime in d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]: + assert "name" in runtime + assert "path" in runtime + assert os.path.exists( + runtime["path"] + ), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist" + + assert d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] == "abs(static/gradle-7.3.3)" + d["initializationOptions"]["settings"]["java"]["import"]["gradle"][ + "home" + ] = self.runtime_dependency_paths.gradle_path + + d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"][ + "home" + ] = self.runtime_dependency_paths.jre_path + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["EclipseJDTLS"]: + """ + Starts the Eclipse JDTLS Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + ``` + """ + + async def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "textDocument/completion": + assert registration["registerOptions"]["resolveProvider"] == True + assert registration["registerOptions"]["triggerCharacters"] == [ + ".", + "@", + "#", + "*", + " ", + ] + self.completions_available.set() + if registration["method"] == "workspace/executeCommand": + if "java.intellicode.enable" in registration["registerOptions"]["commands"]: + self.intellicode_enable_command_available.set() + return + + async def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + self.service_ready_event.set() + + async def execute_client_command_handler(params): + assert params["command"] == "_java.reloadBundles.command" + assert params["arguments"] == [] + return [] + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + async def do_nothing(params): + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + 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) + + async with super().start_server(): + self.logger.log("Starting EclipseJDTLS server process", logging.INFO) + await 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, + ) + init_response = await self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" not in init_response["capabilities"] + assert "executeCommandProvider" not in init_response["capabilities"] + + self.server.notify.initialized({}) + + self.server.notify.workspace_did_change_configuration( + {"settings": initialize_params["initializationOptions"]["settings"]} + ) + + await self.intellicode_enable_command_available.wait() + + java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path + assert os.path.exists(java_intellisense_members_path) + intellicode_enable_result = await self.server.send.execute_command( + { + "command": "java.intellicode.enable", + "arguments": [True, java_intellisense_members_path], + } + ) + assert intellicode_enable_result + + # TODO: Add comments about why we wait here, and how this can be optimized + await self.service_ready_event.wait() + + yield self diff --git a/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json b/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json new file mode 100644 index 0000000..b65b7b6 --- /dev/null +++ b/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json @@ -0,0 +1,849 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Visual Studio Code - Insiders", + "version": "1.77.0-insider" + }, + "locale": "en", + "rootPath": "repository_absolute_path", + "rootUri": "pathlib.Path(repository_absolute_path).as_uri()", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "configuration": true, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": false, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": false, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": true + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0" + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + } + }, + "initializationOptions": { + "bundles": [ + "intellicode-core.jar" + ], + "workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]", + "settings": { + "java": { + "home": null, + "jdt": { + "ls": { + "java": { + "home": null + }, + "vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -Xlog:disable", + "lombokSupport": { + "enabled": true + }, + "protobufSupport": { + "enabled": true + }, + "androidSupport": { + "enabled": true + } + } + }, + "errors": { + "incompleteClasspath": { + "severity": "error" + } + }, + "configuration": { + "checkProjectSettingsExclusions": false, + "updateBuildConfiguration": "interactive", + "maven": { + "userSettings": null, + "globalSettings": null, + "notCoveredPluginExecutionSeverity": "warning", + "defaultMojoExecutionAction": "ignore" + }, + "workspaceCacheLimit": 90, + "runtimes": [ + { + "name": "JavaSE-21", + "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", + "default": true + } + ] + }, + "trace": { + "server": "verbose" + }, + "import": { + "maven": { + "enabled": true, + "offline": { + "enabled": false + }, + "disableTestClasspathFlag": false + }, + "gradle": { + "enabled": true, + "wrapper": { + "enabled": true + }, + "version": null, + "home": "abs(static/gradle-7.3.3)", + "java": { + "home": "abs(static/launch_jres/21.0.7-linux-x86_64)" + }, + "offline": { + "enabled": false + }, + "arguments": null, + "jvmArguments": null, + "user": { + "home": null + }, + "annotationProcessing": { + "enabled": true + } + }, + "exclusions": [ + "**/node_modules/**", + "**/.metadata/**", + "**/archetype-resources/**", + "**/META-INF/maven/**" + ], + "generatesMetadataFilesAtProjectRoot": false + }, + "maven": { + "downloadSources": true, + "updateSnapshots": true + }, + "eclipse": { + "downloadSources": true + }, + "referencesCodeLens": { + "enabled": true + }, + "signatureHelp": { + "enabled": true, + "description": { + "enabled": true + } + }, + "implementationsCodeLens": { + "enabled": true + }, + "format": { + "enabled": true, + "settings": { + "url": null, + "profile": null + }, + "comments": { + "enabled": true + }, + "onType": { + "enabled": true + }, + "insertSpaces": true, + "tabSize": 4 + }, + "saveActions": { + "organizeImports": false + }, + "project": { + "referencedLibraries": [ + "lib/**/*.jar" + ], + "importOnFirstTimeStartup": "automatic", + "importHint": true, + "resourceFilters": [ + "node_modules", + "\\.git" + ], + "encoding": "ignore", + "exportJar": { + "targetPath": "${workspaceFolder}/${workspaceFolderBasename}.jar" + } + }, + "contentProvider": { + "preferred": null + }, + "autobuild": { + "enabled": true + }, + "maxConcurrentBuilds": 1, + "recommendations": { + "dependency": { + "analytics": { + "show": true + } + } + }, + "completion": { + "maxResults": 0, + "enabled": true, + "guessMethodArguments": true, + "favoriteStaticMembers": [ + "org.junit.Assert.*", + "org.junit.Assume.*", + "org.junit.jupiter.api.Assertions.*", + "org.junit.jupiter.api.Assumptions.*", + "org.junit.jupiter.api.DynamicContainer.*", + "org.junit.jupiter.api.DynamicTest.*", + "org.mockito.Mockito.*", + "org.mockito.ArgumentMatchers.*", + "org.mockito.Answers.*" + ], + "filteredTypes": [ + "java.awt.*", + "com.sun.*", + "sun.*", + "jdk.*", + "org.graalvm.*", + "io.micrometer.shaded.*" + ], + "importOrder": [ + "#", + "java", + "javax", + "org", + "com", + "" + ], + "postfix": { + "enabled": false + }, + "matchCase": "off" + }, + "foldingRange": { + "enabled": true + }, + "progressReports": { + "enabled": false + }, + "codeGeneration": { + "hashCodeEquals": { + "useJava7Objects": false, + "useInstanceof": false + }, + "useBlocks": false, + "generateComments": false, + "toString": { + "template": "${object.className} [${member.name()}=${member.value}, ${otherMembers}]", + "codeStyle": "STRING_CONCATENATION", + "skipNullValues": false, + "listArrayContents": true, + "limitElements": 0 + }, + "insertionLocation": "afterCursor" + }, + "selectionRange": { + "enabled": true + }, + "showBuildStatusOnStart": { + "enabled": "notification" + }, + "server": { + "launchMode": "Standard" + }, + "sources": { + "organizeImports": { + "starThreshold": 99, + "staticStarThreshold": 99 + } + }, + "imports": { + "gradle": { + "wrapper": { + "checksums": [] + } + } + }, + "templates": { + "fileHeader": [], + "typeComment": [] + }, + "references": { + "includeAccessors": true, + "includeDecompiledSources": true + }, + "typeHierarchy": { + "lazyLoad": false + }, + "settings": { + "url": null + }, + "symbols": { + "includeSourceMethodDeclarations": false + }, + "quickfix": { + "showAt": "line" + }, + "inlayHints": { + "parameterNames": { + "enabled": "literals", + "exclusions": [] + } + }, + "codeAction": { + "sortMembers": { + "avoidVolatileChanges": true + } + }, + "compile": { + "nullAnalysis": { + "nonnull": [ + "javax.annotation.Nonnull", + "org.eclipse.jdt.annotation.NonNull", + "org.springframework.lang.NonNull" + ], + "nullable": [ + "javax.annotation.Nullable", + "org.eclipse.jdt.annotation.Nullable", + "org.springframework.lang.Nullable" + ], + "mode": "automatic" + } + }, + "cleanup": { + "actionsOnSave": [] + }, + "sharedIndexes": { + "enabled": "auto", + "location": "" + }, + "refactoring": { + "extract": { + "interface": { + "replace": true + } + } + }, + "debug": { + "logLevel": "verbose", + "settings": { + "showHex": false, + "showStaticVariables": false, + "showQualifiedNames": false, + "showLogicalStructure": true, + "showToString": true, + "maxStringLength": 0, + "numericPrecision": 0, + "hotCodeReplace": "manual", + "enableRunDebugCodeLens": true, + "forceBuildBeforeLaunch": true, + "onBuildFailureProceed": false, + "console": "integratedTerminal", + "exceptionBreakpoint": { + "skipClasses": [] + }, + "stepping": { + "skipClasses": [], + "skipSynthetics": false, + "skipStaticInitializers": false, + "skipConstructors": false + }, + "jdwp": { + "limitOfVariablesPerJdwpRequest": 100, + "requestTimeout": 3000, + "async": "auto" + }, + "vmArgs": "" + } + }, + "silentNotification": false, + "dependency": { + "showMembers": false, + "syncWithFolderExplorer": true, + "autoRefresh": true, + "refreshDelay": 2000, + "packagePresentation": "flat" + }, + "help": { + "firstView": "auto", + "showReleaseNotes": true, + "collectErrorLog": false + }, + "test": { + "defaultConfig": "", + "config": {} + } + } + }, + "extendedClientCapabilities": { + "progressReportProvider": false, + "classFileContentsSupport": true, + "overrideMethodsPromptSupport": true, + "hashCodeEqualsPromptSupport": true, + "advancedOrganizeImportsSupport": true, + "generateToStringPromptSupport": true, + "advancedGenerateAccessorsSupport": true, + "generateConstructorsPromptSupport": true, + "generateDelegateMethodsPromptSupport": true, + "advancedExtractRefactoringSupport": true, + "inferSelectionSupport": [ + "extractMethod", + "extractVariable", + "extractField" + ], + "moveRefactoringSupport": true, + "clientHoverProvider": true, + "clientDocumentSymbolProvider": true, + "gradleChecksumWrapperPromptSupport": true, + "resolveAdditionalTextEditsSupport": true, + "advancedIntroduceParameterRefactoringSupport": true, + "actionableRuntimeNotificationSupport": true, + "shouldLanguageServerExitOnShutdown": true, + "onCompletionItemSelectedCommand": "editor.action.triggerParameterHints", + "extractInterfaceSupport": true, + "advancedUpgradeGradleSupport": true + }, + "triggerFiles": [] + }, + "trace": "verbose", + "workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]" +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json b/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json new file mode 100644 index 0000000..1b00040 --- /dev/null +++ b/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json @@ -0,0 +1,72 @@ +{ + "_description": "This file lists the runtime dependencies for the Java Language Server", + "gradle": { + "platform-agnostic": { + "url": "https://services.gradle.org/distributions/gradle-7.3.3-bin.zip", + "archiveType": "zip", + "relative_extraction_path": "." + } + }, + "vscode-java": { + "darwin-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-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", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-macosx-aarch64", + "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_mac_arm" + }, + "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", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-macosx-x86_64", + "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_mac" + }, + "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", + "relative_extraction_path": "vscode-java" + }, + "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", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-linux-x86_64", + "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_linux" + }, + "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", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-win32-x86_64", + "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_win" + } + }, + "intellicode": { + "platform-agnostic": { + "url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage", + "alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage", + "archiveType": "zip", + "relative_extraction_path": "intellicode", + "intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar", + "intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members" + } + } +} diff --git a/src/solidlsp/language_servers/gopls/gopls.py b/src/solidlsp/language_servers/gopls/gopls.py new file mode 100644 index 0000000..0e5574a --- /dev/null +++ b/src/solidlsp/language_servers/gopls/gopls.py @@ -0,0 +1,154 @@ +import asyncio +import json +import logging +import os +import pathlib +import subprocess +from contextlib import asynccontextmanager +from typing import AsyncIterator, List + +from overrides import override + +from multilspy import multilspy_types +from multilspy.lsp_protocol_handler import lsp_types +from multilspy.multilspy_exceptions import MultilspyException +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import Error, ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig + + +class Gopls(LanguageServer): + """ + Provides Go specific instantiation of the LanguageServer class using gopls. + """ + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # For Go projects, we should ignore: + # - vendor: third-party dependencies vendored into the project + # - node_modules: if the project has JavaScript components + # - dist/build: common output directories + return super().is_ignored_dirname(dirname) or dirname in ["vendor", "node_modules", "dist", "build"] + + @staticmethod + def _get_go_version(): + """Get the installed Go version or None if not found.""" + try: + result = subprocess.run(['go', 'version'], capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip() + except FileNotFoundError: + return None + return None + + @staticmethod + def _get_gopls_version(): + """Get the installed gopls version or None if not found.""" + try: + result = subprocess.run(['gopls', 'version'], capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip() + except FileNotFoundError: + return None + return None + + @classmethod + def setup_runtime_dependency(cls): + """ + Check if required Go runtime dependencies are available. + Raises RuntimeError with helpful message if dependencies are missing. + """ + go_version = cls._get_go_version() + if not go_version: + raise RuntimeError("Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH.") + + gopls_version = cls._get_gopls_version() + if not gopls_version: + raise RuntimeError( + "Found a Go version but gopls is not installed.\n" + "Please install gopls as described in https://pkg.go.dev/golang.org/x/tools/gopls#section-readme\n\n" + "After installation, make sure it is added to your PATH (it might be installed in a different location than Go)." + ) + + return True + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + self.setup_runtime_dependency() + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), + "go", + ) + self.server_ready = asyncio.Event() + self.request_id = 0 + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the TypeScript Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["Gopls"]: + """Start gopls server process""" + async def register_capability_handler(params): + return + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + async def do_nothing(params): + 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) + + async with super().start_server(): + self.logger.log("Starting gopls server process", logging.INFO) + await 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, + ) + init_response = await self.server.send.initialize(initialize_params) + + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + + self.server.notify.initialized({}) + self.completions_available.set() + + # gopls server is typically ready immediately after initialization + self.server_ready.set() + await self.server_ready.wait() + + yield self diff --git a/src/solidlsp/language_servers/gopls/initialize_params.json b/src/solidlsp/language_servers/gopls/initialize_params.json new file mode 100644 index 0000000..40592e1 --- /dev/null +++ b/src/solidlsp/language_servers/gopls/initialize_params.json @@ -0,0 +1,46 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "textDocument": { + "synchronization": { + "didSave": true, + "dynamicRegistration": true + }, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true + } + }, + "definition": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "hierarchicalDocumentSymbolSupport": true, + "symbolKind": { + "valueSet": [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 + ] + } + } + }, + "workspace": { + "workspaceFolders": true, + "didChangeConfiguration": { + "dynamicRegistration": true + } + } + }, + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/intelephense/initialize_params.json b/src/solidlsp/language_servers/intelephense/initialize_params.json new file mode 100644 index 0000000..4330560 --- /dev/null +++ b/src/solidlsp/language_servers/intelephense/initialize_params.json @@ -0,0 +1,36 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "textDocument": { + "synchronization": { + "didSave": true, + "dynamicRegistration": true + }, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true + } + }, + "definition": { + "dynamicRegistration": true + } + }, + "workspace": { + "workspaceFolders": true, + "didChangeConfiguration": { + "dynamicRegistration": true + } + } + }, + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense/intelephense.py new file mode 100644 index 0000000..d2ad911 --- /dev/null +++ b/src/solidlsp/language_servers/intelephense/intelephense.py @@ -0,0 +1,200 @@ +""" +Provides PHP specific instantiation of the LanguageServer class using Intelephense. +""" + +import asyncio +import json +import shutil +import logging +import os +import subprocess +import pathlib +from contextlib import asynccontextmanager +from time import sleep +from typing import AsyncIterator + +from overrides import override + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_utils import PlatformUtils, PlatformId + +class Intelephense(LanguageServer): + """ + Provides PHP specific instantiation of the LanguageServer class using Intelephense. + """ + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # For PHP projects, we should ignore: + # - vendor: third-party dependencies managed by Composer + # - node_modules: if the project has JavaScript components + # - cache: commonly used for caching + return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + """ + Setup runtime dependencies for Intelephense. + """ + platform_id = PlatformUtils.get_platform_id() + + valid_platforms = [ + PlatformId.LINUX_x64, + PlatformId.LINUX_arm64, + PlatformId.OSX, + 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 multilspy PHP at the moment" + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + runtime_dependencies = d.get("runtimeDependencies", []) + intelephense_ls_dir = os.path.join(os.path.dirname(__file__), "static", "php-lsp") + + # Verify both node and npm are installed + is_node_installed = shutil.which('node') is not None + assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." + is_npm_installed = shutil.which('npm') is not None + assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." + + # Install intelephense if not already installed + if not os.path.exists(intelephense_ls_dir): + os.makedirs(intelephense_ls_dir, exist_ok=True) + for dependency in runtime_dependencies: + # Windows doesn't support the 'user' parameter and doesn't have pwd module + if PlatformUtils.get_platform_id().value.startswith("win"): + subprocess.run( + dependency["command"], + shell=True, + check=True, + cwd=intelephense_ls_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + else: + # On Unix-like systems, run as non-root user + import pwd + user = pwd.getpwuid(os.getuid()).pw_name + subprocess.run( + dependency["command"], + shell=True, + check=True, + user=user, + cwd=intelephense_ls_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") + assert os.path.exists(intelephense_executable_path), "intelephense executable not found. Please install intelephense and try again." + + return f"{intelephense_executable_path} --stdio" + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + # Setup runtime dependencies before initializing + intelephense_cmd = self.setup_runtime_dependencies(logger, config) + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), + "php" + ) + self.server_ready = asyncio.Event() + self.request_id = 0 + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the TypeScript Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["Intelephense"]: + """Start Intelephense server process""" + async def register_capability_handler(params): + return + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + async def do_nothing(params): + 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) + + async with super().start_server(): + self.logger.log("Starting Intelephense server process", logging.INFO) + await 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, + ) + init_response = await self.server.send.initialize(initialize_params) + self.logger.log( + "After sent initialize params", + logging.INFO, + ) + + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + + self.server.notify.initialized({}) + self.completions_available.set() + + # Intelephense server is typically ready immediately after initialization + self.server_ready.set() + await self.server_ready.wait() + + yield self + + @override + # For some reason, the LS may need longer to process this, so we just retry + async def _send_references_request(self, relative_file_path: str, line: int, column: int): + # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is + # despite the LS having processed requests already. I don't know what causes this, but sleeping + # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. + # May be related to the time it takes to read the files or something like that. + # The sleeping doesn't seem to be needed on all systems + sleep(1) + return await super()._send_references_request(relative_file_path, line, column) + + @override + async def _send_definition_request(self, definition_params: DefinitionParams): + # TODO: same as above, also only a problem if the definition is in another file + sleep(1) + return await super()._send_definition_request(definition_params) diff --git a/src/solidlsp/language_servers/intelephense/runtime_dependencies.json b/src/solidlsp/language_servers/intelephense/runtime_dependencies.json new file mode 100644 index 0000000..e3a6d7e --- /dev/null +++ b/src/solidlsp/language_servers/intelephense/runtime_dependencies.json @@ -0,0 +1,10 @@ +{ + "_description": "Used to download the runtime dependencies for running intelephense. Obtained from https://www.npmjs.com/package/intelephense", + "runtimeDependencies": [ + { + "id": "intelephense", + "description": "Intelephense package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", + "command": "npm install --prefix ./ intelephense@1.14.4" + } + ] +} diff --git a/src/solidlsp/language_servers/jedi_language_server/initialize_params.json b/src/solidlsp/language_servers/jedi_language_server/initialize_params.json new file mode 100644 index 0000000..5a04872 --- /dev/null +++ b/src/solidlsp/language_servers/jedi_language_server/initialize_params.json @@ -0,0 +1,905 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Visual Studio Code - Insiders", + "version": "1.81.0-insider" + }, + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "configuration": true, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": false + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0", + "allowedTags": [ + "ul", + "li", + "p", + "code", + "blockquote", + "ol", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "em", + "pre", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "div", + "del", + "a", + "strong", + "br", + "img", + "span" + ] + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + }, + "experimental": { + "snippetTextEdit": true, + "codeActionGroup": true, + "hoverActions": true, + "serverStatusNotification": true, + "colorDiagnosticOutput": true, + "openServerLogs": true, + "commands": { + "commands": [ + "rust-analyzer.runSingle", + "rust-analyzer.debugSingle", + "rust-analyzer.showReferences", + "rust-analyzer.gotoLocation", + "editor.action.triggerParameterHints" + ] + } + } + }, + "initializationOptions": { + "cargoRunner": null, + "runnables": { + "extraEnv": null, + "problemMatcher": [ + "$rustc" + ], + "command": null, + "extraArgs": [] + }, + "server": { + "path": null, + "extraEnv": null + }, + "trace": { + "server": "verbose", + "extension": true + }, + "debug": { + "engine": "auto", + "sourceFileMap": { + "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" + }, + "openDebugPane": false, + "engineSettings": {} + }, + "restartServerOnConfigChange": false, + "typing": { + "continueCommentsOnNewline": true, + "autoClosingAngleBrackets": { + "enable": false + } + }, + "diagnostics": { + "previewRustcOutput": false, + "useRustcErrorCode": false, + "disabled": [], + "enable": true, + "experimental": { + "enable": false + }, + "remapPrefix": {}, + "warningsAsHint": [], + "warningsAsInfo": [] + }, + "discoverProjectCommand": null, + "showUnlinkedFileNotification": true, + "showDependenciesExplorer": true, + "assist": { + "emitMustUse": false, + "expressionFillDefault": "todo" + }, + "cachePriming": { + "enable": true, + "numThreads": 0 + }, + "cargo": { + "autoreload": true, + "buildScripts": { + "enable": true, + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "overrideCommand": null, + "useRustcWrapper": true + }, + "cfgs": {}, + "extraArgs": [], + "extraEnv": {}, + "features": [], + "noDefaultFeatures": false, + "sysroot": "discover", + "sysrootSrc": null, + "target": null, + "unsetTest": [ + "core" + ] + }, + "checkOnSave": true, + "check": { + "allTargets": true, + "command": "check", + "extraArgs": [], + "extraEnv": {}, + "features": null, + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "noDefaultFeatures": null, + "overrideCommand": null, + "targets": null + }, + "completion": { + "autoimport": { + "enable": true + }, + "autoself": { + "enable": true + }, + "callable": { + "snippets": "fill_arguments" + }, + "limit": null, + "postfix": { + "enable": true + }, + "privateEditable": { + "enable": false + }, + "snippets": { + "custom": { + "Arc::new": { + "postfix": "arc", + "body": "Arc::new(${receiver})", + "requires": "std::sync::Arc", + "description": "Put the expression into an `Arc`", + "scope": "expr" + }, + "Rc::new": { + "postfix": "rc", + "body": "Rc::new(${receiver})", + "requires": "std::rc::Rc", + "description": "Put the expression into an `Rc`", + "scope": "expr" + }, + "Box::pin": { + "postfix": "pinbox", + "body": "Box::pin(${receiver})", + "requires": "std::boxed::Box", + "description": "Put the expression into a pinned `Box`", + "scope": "expr" + }, + "Ok": { + "postfix": "ok", + "body": "Ok(${receiver})", + "description": "Wrap the expression in a `Result::Ok`", + "scope": "expr" + }, + "Err": { + "postfix": "err", + "body": "Err(${receiver})", + "description": "Wrap the expression in a `Result::Err`", + "scope": "expr" + }, + "Some": { + "postfix": "some", + "body": "Some(${receiver})", + "description": "Wrap the expression in an `Option::Some`", + "scope": "expr" + } + } + } + }, + "files": { + "excludeDirs": [], + "watcher": "client" + }, + "highlightRelated": { + "breakPoints": { + "enable": true + }, + "closureCaptures": { + "enable": true + }, + "exitPoints": { + "enable": true + }, + "references": { + "enable": true + }, + "yieldPoints": { + "enable": true + } + }, + "hover": { + "actions": { + "debug": { + "enable": true + }, + "enable": true, + "gotoTypeDef": { + "enable": true + }, + "implementations": { + "enable": true + }, + "references": { + "enable": false + }, + "run": { + "enable": true + } + }, + "documentation": { + "enable": true, + "keywords": { + "enable": true + } + }, + "links": { + "enable": true + }, + "memoryLayout": { + "alignment": "hexadecimal", + "enable": true, + "niches": false, + "offset": "hexadecimal", + "size": "both" + } + }, + "imports": { + "granularity": { + "enforce": false, + "group": "crate" + }, + "group": { + "enable": true + }, + "merge": { + "glob": true + }, + "prefer": { + "no": { + "std": false + } + }, + "prefix": "plain" + }, + "inlayHints": { + "bindingModeHints": { + "enable": false + }, + "chainingHints": { + "enable": true + }, + "closingBraceHints": { + "enable": true, + "minLines": 25 + }, + "closureCaptureHints": { + "enable": false + }, + "closureReturnTypeHints": { + "enable": "never" + }, + "closureStyle": "impl_fn", + "discriminantHints": { + "enable": "never" + }, + "expressionAdjustmentHints": { + "enable": "never", + "hideOutsideUnsafe": false, + "mode": "prefix" + }, + "lifetimeElisionHints": { + "enable": "never", + "useParameterNames": false + }, + "maxLength": 25, + "parameterHints": { + "enable": true + }, + "reborrowHints": { + "enable": "never" + }, + "renderColons": true, + "typeHints": { + "enable": true, + "hideClosureInitialization": false, + "hideNamedConstructor": false + } + }, + "interpret": { + "tests": false + }, + "joinLines": { + "joinAssignments": true, + "joinElseIf": true, + "removeTrailingComma": true, + "unwrapTrivialBlock": true + }, + "lens": { + "debug": { + "enable": true + }, + "enable": true, + "forceCustomCommands": true, + "implementations": { + "enable": true + }, + "location": "above_name", + "references": { + "adt": { + "enable": false + }, + "enumVariant": { + "enable": false + }, + "method": { + "enable": false + }, + "trait": { + "enable": false + } + }, + "run": { + "enable": true + } + }, + "linkedProjects": [], + "lru": { + "capacity": null, + "query": { + "capacities": {} + } + }, + "notifications": { + "cargoTomlNotFound": true + }, + "numThreads": null, + "procMacro": { + "attributes": { + "enable": true + }, + "enable": true, + "ignored": {}, + "server": null + }, + "references": { + "excludeImports": false + }, + "rustc": { + "source": null + }, + "rustfmt": { + "extraArgs": [], + "overrideCommand": null, + "rangeFormatting": { + "enable": false + } + }, + "semanticHighlighting": { + "doc": { + "comment": { + "inject": { + "enable": true + } + } + }, + "nonStandardTokens": true, + "operator": { + "enable": true, + "specialization": { + "enable": false + } + }, + "punctuation": { + "enable": false, + "separate": { + "macro": { + "bang": false + } + }, + "specialization": { + "enable": false + } + }, + "strings": { + "enable": true + } + }, + "signatureInfo": { + "detail": "full", + "documentation": { + "enable": true + } + }, + "workspace": { + "symbols": { + "maxSymbols": 0 + } + } + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py new file mode 100644 index 0000000..28c9d91 --- /dev/null +++ b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py @@ -0,0 +1,123 @@ +""" +Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. +""" + +import json +import logging +import os +import pathlib +from contextlib import asynccontextmanager +from typing import AsyncIterator, Tuple + +from overrides import override + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig + + +class JediServer(LanguageServer): + """ + Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + 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", + ) + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Jedi Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["JediServer"]: + """ + Starts the JEDI Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + ``` + """ + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def check_experimental_status(params): + if params["quiescent"] == True: + self.completions_available.set() + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + 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) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + async with super().start_server(): + self.logger.log("Starting jedi-language-server server process", logging.INFO) + await 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, + ) + init_response = await self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": [".", "'", '"'], + "resolveProvider": True, + } + + self.server.notify.initialized({}) + + yield self diff --git a/src/solidlsp/language_servers/kotlin_language_server/initialize_params.json b/src/solidlsp/language_servers/kotlin_language_server/initialize_params.json new file mode 100644 index 0000000..a7b2ec0 --- /dev/null +++ b/src/solidlsp/language_servers/kotlin_language_server/initialize_params.json @@ -0,0 +1,521 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Multilspy Kotlin Client", + "version": "1.0.0" + }, + "locale": "en", + "rootPath": "repository_absolute_path", + "rootUri": "pathlib.Path(repository_absolute_path).as_uri()", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "configuration": true, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": false, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": false, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": true + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0" + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + } + }, + "initializationOptions": { + "workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]", + "storagePath": null, + "codegen": { + "enabled": false + }, + "compiler": { + "jvm": { + "target": "default" + } + }, + "completion": { + "snippets": { + "enabled": true + } + }, + "diagnostics": { + "enabled": true, + "level": 4, + "debounceTime": 250 + }, + "scripts": { + "enabled": true, + "buildScriptsEnabled": true + }, + "indexing": { + "enabled": true + }, + "externalSources": { + "useKlsScheme": false, + "autoConvertToKotlin": false + }, + "inlayHints": { + "typeHints": false, + "parameterHints": false, + "chainedHints": false + }, + "formatting": { + "formatter": "ktfmt", + "ktfmt": { + "style": "google", + "indent": 4, + "maxWidth": 100, + "continuationIndent": 8, + "removeUnusedImports": true + } + } + }, + "trace": "verbose", + "workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]" +} diff --git a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py new file mode 100644 index 0000000..ca4e866 --- /dev/null +++ b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py @@ -0,0 +1,227 @@ +""" +Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. +""" + +import asyncio +import dataclasses +import json +import logging +import os +import stat +import pathlib +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_utils import FileUtils +from multilspy.multilspy_utils import PlatformUtils + + +@dataclasses.dataclass +class KotlinRuntimeDependencyPaths: + """ + Stores the paths to the runtime dependencies of Kotlin Language Server + """ + java_path: str + java_home_path: str + kotlin_executable_path: str + + +class KotlinLanguageServer(LanguageServer): + """ + Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + 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) + self.runtime_dependency_paths = runtime_dependency_paths + + # Create command to execute the Kotlin Language Server script + cmd = f'"{self.runtime_dependency_paths.kotlin_executable_path}"' + + # Set environment variables including JAVA_HOME + proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), + "kotlin", + ) + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> KotlinRuntimeDependencyPaths: + """ + Setup runtime dependencies for Kotlin Language Server. + """ + 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 Kotlin in multilspy at the moment" + + # Load dependency information + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + kotlin_dependency = d["runtimeDependency"] + java_dependency = d["java"][platform_id.value] + + # Setup paths for dependencies + static_dir = os.path.join(os.path.dirname(__file__), "static") + os.makedirs(static_dir, exist_ok=True) + + # Setup Java paths + 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"]) + + # 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"] + ) + # Make Java executable + if not platform_id.value.startswith("win-"): + os.chmod(java_path, 0o755) + + assert os.path.exists(java_path), f"Java executable not found at {java_path}" + + # Setup Kotlin Language Server paths + kotlin_ls_dir = os.path.join(static_dir, "server") + + # Get platform-specific executable script path + if platform_id.value.startswith("win-"): + kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server.bat") + else: + kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server") + + # 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"] + ) + + # Make script executable on Unix platforms + if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"): + os.chmod(kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) + + # 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) + else: + raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}") + + return KotlinRuntimeDependencyPaths( + java_path=java_path, + java_home_path=java_home_path, + kotlin_executable_path=kotlin_executable_path + ) + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Kotlin Language Server. + """ + with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: + d: InitializeParams = json.load(f) + + del d["_description"] + + if not os.path.isabs(repository_absolute_path): + repository_absolute_path = os.path.abspath(repository_absolute_path) + + assert d["processId"] == "os.getpid()" + d["processId"] = os.getpid() + + assert d["rootPath"] == "repository_absolute_path" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "pathlib.Path(repository_absolute_path).as_uri()" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["initializationOptions"]["workspaceFolders"] == "[pathlib.Path(repository_absolute_path).as_uri()]" + d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()] + + assert ( + d["workspaceFolders"] + == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' + ) + d["workspaceFolders"] = [ + { + "uri": pathlib.Path(repository_absolute_path).as_uri(), + "name": os.path.basename(repository_absolute_path), + } + ] + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["KotlinLanguageServer"]: + """ + Starts the Kotlin Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + ``` + """ + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + 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) + + async with super().start_server(): + self.logger.log("Starting Kotlin server process", logging.INFO) + await 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, + ) + init_response = await 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" + assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens" + + self.server.notify.initialized({}) + self.completions_available.set() + + yield self diff --git a/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json new file mode 100644 index 0000000..3568c9d --- /dev/null +++ b/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json @@ -0,0 +1,41 @@ +{ + "_description": "Used to download the runtime dependencies for Kotlin Language Server from https://github.com/fwcd/kotlin-language-server", + "runtimeDependency": { + "id": "KotlinLsp", + "description": "Kotlin Language Server", + "url": "https://github.com/fwcd/kotlin-language-server/releases/download/1.3.13/server.zip", + "archiveType": "zip" + }, + "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" + } + } +} diff --git a/src/solidlsp/language_servers/omnisharp/initialize_params.json b/src/solidlsp/language_servers/omnisharp/initialize_params.json new file mode 100644 index 0000000..f9ee86c --- /dev/null +++ b/src/solidlsp/language_servers/omnisharp/initialize_params.json @@ -0,0 +1,631 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Visual Studio Code - Insiders", + "version": "1.82.0-insider" + }, + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "configuration": false, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": false + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0", + "allowedTags": [ + "ul", + "li", + "p", + "code", + "blockquote", + "ol", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "em", + "pre", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "div", + "del", + "a", + "strong", + "br", + "img", + "span" + ] + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + }, + "experimental": { + "snippetTextEdit": true, + "codeActionGroup": true, + "hoverActions": true, + "serverStatusNotification": true, + "colorDiagnosticOutput": true, + "openServerLogs": true, + "commands": { + "commands": [ + "editor.action.triggerParameterHints" + ] + } + } + }, + "initializationOptions": { + "RoslynExtensionsOptions": { + "EnableDecompilationSupport": false, + "EnableAnalyzersSupport": true, + "EnableImportCompletion": true, + "EnableAsyncCompletion": false, + "DocumentAnalysisTimeoutMs": 30000, + "DiagnosticWorkersThreadCount": 18, + "AnalyzeOpenDocumentsOnly": true, + "InlayHintsOptions": { + "EnableForParameters": false, + "ForLiteralParameters": false, + "ForIndexerParameters": false, + "ForObjectCreationParameters": false, + "ForOtherParameters": false, + "SuppressForParametersThatDifferOnlyBySuffix": false, + "SuppressForParametersThatMatchMethodIntent": false, + "SuppressForParametersThatMatchArgumentName": false, + "EnableForTypes": false, + "ForImplicitVariableTypes": false, + "ForLambdaParameterTypes": false, + "ForImplicitObjectCreation": false + }, + "LocationPaths": null + }, + "FormattingOptions": { + "OrganizeImports": false, + "EnableEditorConfigSupport": true, + "NewLine": "\n", + "UseTabs": false, + "TabSize": 4, + "IndentationSize": 4, + "SpacingAfterMethodDeclarationName": false, + "SeparateImportDirectiveGroups": false, + "SpaceWithinMethodDeclarationParenthesis": false, + "SpaceBetweenEmptyMethodDeclarationParentheses": false, + "SpaceAfterMethodCallName": false, + "SpaceWithinMethodCallParentheses": false, + "SpaceBetweenEmptyMethodCallParentheses": false, + "SpaceAfterControlFlowStatementKeyword": true, + "SpaceWithinExpressionParentheses": false, + "SpaceWithinCastParentheses": false, + "SpaceWithinOtherParentheses": false, + "SpaceAfterCast": false, + "SpaceBeforeOpenSquareBracket": false, + "SpaceBetweenEmptySquareBrackets": false, + "SpaceWithinSquareBrackets": false, + "SpaceAfterColonInBaseTypeDeclaration": true, + "SpaceAfterComma": true, + "SpaceAfterDot": false, + "SpaceAfterSemicolonsInForStatement": true, + "SpaceBeforeColonInBaseTypeDeclaration": true, + "SpaceBeforeComma": false, + "SpaceBeforeDot": false, + "SpaceBeforeSemicolonsInForStatement": false, + "SpacingAroundBinaryOperator": "single", + "IndentBraces": false, + "IndentBlock": true, + "IndentSwitchSection": true, + "IndentSwitchCaseSection": true, + "IndentSwitchCaseSectionWhenBlock": true, + "LabelPositioning": "oneLess", + "WrappingPreserveSingleLine": true, + "WrappingKeepStatementsOnSingleLine": true, + "NewLinesForBracesInTypes": true, + "NewLinesForBracesInMethods": true, + "NewLinesForBracesInProperties": true, + "NewLinesForBracesInAccessors": true, + "NewLinesForBracesInAnonymousMethods": true, + "NewLinesForBracesInControlBlocks": true, + "NewLinesForBracesInAnonymousTypes": true, + "NewLinesForBracesInObjectCollectionArrayInitializers": true, + "NewLinesForBracesInLambdaExpressionBody": true, + "NewLineForElse": true, + "NewLineForCatch": true, + "NewLineForFinally": true, + "NewLineForMembersInObjectInit": true, + "NewLineForMembersInAnonymousTypes": true, + "NewLineForClausesInQuery": true + }, + "FileOptions": { + "SystemExcludeSearchPatterns": [ + "**/node_modules/**/*", + "**/bin/**/*", + "**/obj/**/*", + "**/.git/**/*", + "**/.git", + "**/.svn", + "**/.hg", + "**/CVS", + "**/.DS_Store", + "**/Thumbs.db" + ], + "ExcludeSearchPatterns": [] + }, + "RenameOptions": { + "RenameOverloads": false, + "RenameInStrings": false, + "RenameInComments": false + }, + "ImplementTypeOptions": { + "InsertionBehavior": 0, + "PropertyGenerationBehavior": 0 + }, + "DotNetCliOptions": { + "LocationPaths": null + }, + "Plugins": { + "LocationPaths": null + } + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/omnisharp/omnisharp.py b/src/solidlsp/language_servers/omnisharp/omnisharp.py new file mode 100644 index 0000000..df3cf0c --- /dev/null +++ b/src/solidlsp/language_servers/omnisharp/omnisharp.py @@ -0,0 +1,406 @@ +""" +Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. +""" + +import asyncio +import json +import logging +import os +import pathlib +import stat +from contextlib import asynccontextmanager +from typing import AsyncIterator, Iterable + +from overrides import override + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_exceptions import MultilspyException +from multilspy.multilspy_utils import FileUtils, PlatformUtils, PlatformId, DotnetVersion + + +def breadth_first_file_scan(root) -> Iterable[str]: + """ + This function was obtained from https://stackoverflow.com/questions/49654234/is-there-a-breadth-first-search-option-available-in-os-walk-or-equivalent-py + It traverses the directory tree in breadth first order. + """ + dirs = [root] + # while we has dirs to scan + while len(dirs): + next_dirs = [] + for parent in dirs: + # scan each dir + for f in os.listdir(parent): + # if there is a dir, then save for next ittr + # if it is a file then yield it (we'll return later) + ff = os.path.join(parent, f) + if os.path.isdir(ff): + next_dirs.append(ff) + else: + yield ff + + # once we've done all the current dirs then + # we set up the next itter as the child dirs + # from the current itter. + dirs = next_dirs + + +def find_least_depth_sln_file(root_dir) -> str | None: + for filename in breadth_first_file_scan(root_dir): + if filename.endswith(".sln"): + return filename + return None + + +class OmniSharp(LanguageServer): + """ + Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + omnisharp_executable_path, dll_path = self.setupRuntimeDependencies(logger, config) + + slnfilename = find_least_depth_sln_file(repository_root_path) + if slnfilename is None: + logger.log("No *.sln file found in repository", logging.ERROR) + raise MultilspyException("No SLN file found in repository") + + cmd = " ".join( + [ + omnisharp_executable_path, + "-lsp", + "--encoding", + "ascii", + "-z", + "-s", + f'"{slnfilename}"', + "--hostPID", + str(os.getpid()), + "DotNet:enablePackageRestore=false", + "--loglevel", + "trace", + "--plugin", + dll_path, + "FileOptions:SystemExcludeSearchPatterns:0=**/.git", + "FileOptions:SystemExcludeSearchPatterns:1=**/.svn", + "FileOptions:SystemExcludeSearchPatterns:2=**/.hg", + "FileOptions:SystemExcludeSearchPatterns:3=**/CVS", + "FileOptions:SystemExcludeSearchPatterns:4=**/.DS_Store", + "FileOptions:SystemExcludeSearchPatterns:5=**/Thumbs.db", + "RoslynExtensionsOptions:EnableAnalyzersSupport=true", + "FormattingOptions:EnableEditorConfigSupport=true", + "RoslynExtensionsOptions:EnableImportCompletion=true", + "Sdk:IncludePrereleases=true", + "RoslynExtensionsOptions:AnalyzeOpenDocumentsOnly=true", + "formattingOptions:useTabs=false", + "formattingOptions:tabSize=4", + "formattingOptions:indentationSize=4", + ] + ) + super().__init__( + config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp" + ) + + self.definition_available = asyncio.Event() + self.references_available = asyncio.Event() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj"] + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Omnisharp Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> tuple[str, str]: + """ + Setup runtime dependencies for OmniSharp. + """ + platform_id = PlatformUtils.get_platform_id() + dotnet_version = PlatformUtils.get_dotnet_version() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + assert platform_id in [ + PlatformId.LINUX_x64, + PlatformId.WIN_x64, + ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" + assert dotnet_version in [ + DotnetVersion.V6, + DotnetVersion.V7, + DotnetVersion.V8 + ], "Only dotnet version 6 and 7 are supported in multilspy at the moment" + + # TODO: Do away with this assumption + # Currently, runtime binaries are not available for .Net 7 and .Net 8. Hence, we assume .Net 6 runtime binaries to be compatible with .Net 7, .Net 8 + if dotnet_version in [DotnetVersion.V7, DotnetVersion.V8]: + dotnet_version = DotnetVersion.V6 + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [ + dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value + ] + runtime_dependencies = [ + dependency + for dependency in runtime_dependencies + if not ("dotnet_version" in dependency) or dependency["dotnet_version"] == dotnet_version.value + ] + assert len(runtime_dependencies) == 2 + runtime_dependencies = { + runtime_dependencies[0]["id"]: runtime_dependencies[0], + runtime_dependencies[1]["id"]: runtime_dependencies[1], + } + + assert "OmniSharp" in runtime_dependencies + assert "RazorOmnisharp" in runtime_dependencies + + omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "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" + ) + 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, stat.S_IEXEC) + + razor_omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "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" + ) + razor_omnisharp_dll_path = os.path.join( + razor_omnisharp_ls_dir, runtime_dependencies["RazorOmnisharp"]["dll_path"] + ) + assert os.path.exists(razor_omnisharp_dll_path) + + return omnisharp_executable_path, razor_omnisharp_dll_path + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["OmniSharp"]: + """ + Starts the Omnisharp Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + """ + + async def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "textDocument/definition": + self.definition_available.set() + if registration["method"] == "textDocument/references": + self.references_available.set() + if registration["method"] == "textDocument/completion": + self.completions_available.set() + + async def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + # if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + # self.service_ready_event.set() + pass + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def check_experimental_status(params): + if params["quiescent"] == True: + self.server_ready.set() + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + async def workspace_configuration_handler(params): + # TODO: We do not know the appropriate way to handle this request. Should ideally contact the OmniSharp dev team + return [ + { + "RoslynExtensionsOptions": { + "EnableDecompilationSupport": False, + "EnableAnalyzersSupport": True, + "EnableImportCompletion": True, + "EnableAsyncCompletion": False, + "DocumentAnalysisTimeoutMs": 30000, + "DiagnosticWorkersThreadCount": 18, + "AnalyzeOpenDocumentsOnly": True, + "InlayHintsOptions": { + "EnableForParameters": False, + "ForLiteralParameters": False, + "ForIndexerParameters": False, + "ForObjectCreationParameters": False, + "ForOtherParameters": False, + "SuppressForParametersThatDifferOnlyBySuffix": False, + "SuppressForParametersThatMatchMethodIntent": False, + "SuppressForParametersThatMatchArgumentName": False, + "EnableForTypes": False, + "ForImplicitVariableTypes": False, + "ForLambdaParameterTypes": False, + "ForImplicitObjectCreation": False, + }, + "LocationPaths": None, + }, + "FormattingOptions": { + "OrganizeImports": False, + "EnableEditorConfigSupport": True, + "NewLine": "\n", + "UseTabs": False, + "TabSize": 4, + "IndentationSize": 4, + "SpacingAfterMethodDeclarationName": False, + "SeparateImportDirectiveGroups": False, + "SpaceWithinMethodDeclarationParenthesis": False, + "SpaceBetweenEmptyMethodDeclarationParentheses": False, + "SpaceAfterMethodCallName": False, + "SpaceWithinMethodCallParentheses": False, + "SpaceBetweenEmptyMethodCallParentheses": False, + "SpaceAfterControlFlowStatementKeyword": True, + "SpaceWithinExpressionParentheses": False, + "SpaceWithinCastParentheses": False, + "SpaceWithinOtherParentheses": False, + "SpaceAfterCast": False, + "SpaceBeforeOpenSquareBracket": False, + "SpaceBetweenEmptySquareBrackets": False, + "SpaceWithinSquareBrackets": False, + "SpaceAfterColonInBaseTypeDeclaration": True, + "SpaceAfterComma": True, + "SpaceAfterDot": False, + "SpaceAfterSemicolonsInForStatement": True, + "SpaceBeforeColonInBaseTypeDeclaration": True, + "SpaceBeforeComma": False, + "SpaceBeforeDot": False, + "SpaceBeforeSemicolonsInForStatement": False, + "SpacingAroundBinaryOperator": "single", + "IndentBraces": False, + "IndentBlock": True, + "IndentSwitchSection": True, + "IndentSwitchCaseSection": True, + "IndentSwitchCaseSectionWhenBlock": True, + "LabelPositioning": "oneLess", + "WrappingPreserveSingleLine": True, + "WrappingKeepStatementsOnSingleLine": True, + "NewLinesForBracesInTypes": True, + "NewLinesForBracesInMethods": True, + "NewLinesForBracesInProperties": True, + "NewLinesForBracesInAccessors": True, + "NewLinesForBracesInAnonymousMethods": True, + "NewLinesForBracesInControlBlocks": True, + "NewLinesForBracesInAnonymousTypes": True, + "NewLinesForBracesInObjectCollectionArrayInitializers": True, + "NewLinesForBracesInLambdaExpressionBody": True, + "NewLineForElse": True, + "NewLineForCatch": True, + "NewLineForFinally": True, + "NewLineForMembersInObjectInit": True, + "NewLineForMembersInAnonymousTypes": True, + "NewLineForClausesInQuery": True, + }, + "FileOptions": { + "SystemExcludeSearchPatterns": [ + "**/node_modules/**/*", + "**/bin/**/*", + "**/obj/**/*", + "**/.git/**/*", + "**/.git", + "**/.svn", + "**/.hg", + "**/CVS", + "**/.DS_Store", + "**/Thumbs.db", + ], + "ExcludeSearchPatterns": [], + }, + "RenameOptions": { + "RenameOverloads": False, + "RenameInStrings": False, + "RenameInComments": False, + }, + "ImplementTypeOptions": { + "InsertionBehavior": 0, + "PropertyGenerationBehavior": 0, + }, + "DotNetCliOptions": {"LocationPaths": None}, + "Plugins": {"LocationPaths": None}, + } + ] + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + 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) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + self.server.on_request("workspace/configuration", workspace_configuration_handler) + + async with super().start_server(): + self.logger.log("Starting OmniSharp server process", logging.INFO) + await 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, + ) + init_response = await self.server.send.initialize(initialize_params) + self.server.notify.initialized({}) + with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), "r", encoding="utf-8") as f: + self.server.notify.workspace_did_change_configuration({ + "settings": json.load(f) + }) + assert "capabilities" in init_response + if ( + "definitionProvider" in init_response["capabilities"] + and init_response["capabilities"]["definitionProvider"] + ): + self.definition_available.set() + if ( + "referencesProvider" in init_response["capabilities"] + and init_response["capabilities"]["referencesProvider"] + ): + self.references_available.set() + + await self.definition_available.wait() + await self.references_available.wait() + + yield self diff --git a/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json b/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json new file mode 100644 index 0000000..7dfa1fd --- /dev/null +++ b/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json @@ -0,0 +1,441 @@ +{ + "_description": "Used to download the runtime dependencies for running OmniSharp. Obtained from https://github.com/dotnet/vscode-csharp/blob/main/package.json", + "runtimeDependencies": [ + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 4 / x86)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x86-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "win32" + ], + "architectures": [ + "x86" + ], + "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe", + "platformId": "win-x86", + "isFramework": true, + "integrity": "C81CE2099AD494EF63F9D88FAA70D55A68CF175810F944526FF94AAC7A5109F9", + "dotnet_version": "4", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 6 / x86)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x86-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "win32" + ], + "architectures": [ + "x86" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "win-x86", + "isFramework": false, + "integrity": "B7E62415CFC3DAC2154AC636C5BF0FB4B2C9BBF11B5A1FBF72381DDDED59791E", + "dotnet_version": "6", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 4 / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x64-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "win32" + ], + "architectures": [ + "x86_64" + ], + "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe", + "platformId": "win-x64", + "isFramework": true, + "integrity": "BE0ED10AACEA17E14B78BD0D887DE5935D4ECA3712192A701F3F2100CA3C8B6E", + "dotnet_version": "4", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 6 / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "win32" + ], + "architectures": [ + "x86_64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "win-x64", + "isFramework": false, + "integrity": "A73327395E7EF92C1D8E307055463DA412662C03F077ECC743462FD2760BB537", + "dotnet_version": "6", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 4 / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-arm64-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "win32" + ], + "architectures": [ + "arm64" + ], + "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe", + "platformId": "win-arm64", + "isFramework": true, + "integrity": "32FA0067B0639F87760CD1A769B16E6A53588C137C4D31661836CA4FB28D3DD6", + "dotnet_version": "4", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 6 / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-arm64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "win32" + ], + "architectures": [ + "arm64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "win-arm64", + "isFramework": false, + "integrity": "433F9B360CAA7B4DDD85C604D5C5542C1A718BCF2E71B2BCFC7526E6D41F4E8F", + "dotnet_version": "6", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for OSX (Mono / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "darwin" + ], + "architectures": [ + "x86_64", + "arm64" + ], + "binaries": [ + "./mono.osx", + "./run" + ], + "installTestPath": "./.omnisharp/1.39.10/run", + "platformId": "osx", + "isFramework": true, + "integrity": "2CC42F0EC7C30CFA8858501D12ECB6FB685A1FCFB8ECB35698A4B12406551968", + "dotnet_version": "mono" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for OSX (.NET 6 / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-x64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "darwin" + ], + "architectures": [ + "x86_64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "osx-x64", + "isFramework": false, + "integrity": "C9D6E9F2C839A66A7283AE6A9EC545EE049B48EB230D33E91A6322CB67FF9D97", + "dotnet_version": "6" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for OSX (.NET 6 / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-arm64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "darwin" + ], + "architectures": [ + "arm64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "osx-arm64", + "isFramework": false, + "integrity": "851350F52F83E3BAD5A92D113E4B9882FCD1DEB16AA84FF94B6F2CEE3C70051E", + "dotnet_version": "6" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux (Mono / x86)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x86-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "linux" + ], + "architectures": [ + "x86", + "i686" + ], + "binaries": [ + "./mono.linux-x86", + "./run" + ], + "installTestPath": "./.omnisharp/1.39.10/run", + "platformId": "linux-x86", + "isFramework": true, + "integrity": "474B1CDBAE64CFEC655FB6B0659BCE481023C48274441C72991E67B6E13E56A1", + "dotnet_version": "mono" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux (Mono / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x64-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "linux" + ], + "architectures": [ + "x86_64" + ], + "binaries": [ + "./mono.linux-x86_64", + "./run" + ], + "installTestPath": "./.omnisharp/1.39.10/run", + "platformId": "linux-x64", + "isFramework": true, + "integrity": "FB4CAA47343265100349375D79DBCCE1868950CED675CB07FCBE8462EDBCDD37", + "dotnet_version": "mono" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux (.NET 6 / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "linux" + ], + "architectures": [ + "x86_64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "linux-x64", + "isFramework": false, + "integrity": "0926D3BEA060BF4373356B2FC0A68C10D0DE1B1150100B551BA5932814CE51E2", + "dotnet_version": "6", + "binaryName": "OmniSharp" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux (Mono / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-arm64-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "linux" + ], + "architectures": [ + "arm64" + ], + "binaries": [ + "./mono.linux-arm64", + "./run" + ], + "installTestPath": "./.omnisharp/1.39.10/run", + "platformId": "linux-arm64", + "isFramework": true, + "integrity": "478F3594DFD0167E9A56E36F0364A86C73F8132A3E7EA916CA1419EFE141D2CC", + "dotnet_version": "mono" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux (.NET 6 / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-arm64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "linux" + ], + "architectures": [ + "arm64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "linux-arm64", + "isFramework": false, + "integrity": "6FB6A572043A74220A92F6C19C7BB0C3743321C7563A815FD2702EF4FA7D688E", + "dotnet_version": "6" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux musl (.NET 6 / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-musl-x64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "linux-musl" + ], + "architectures": [ + "x86_64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "linux-musl-x64", + "isFramework": false, + "integrity": "6BFDA3AD11DBB0C6514B86ECC3E1597CC41C6E309B7575F7C599E07D9E2AE610", + "dotnet_version": "6" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux musl (.NET 6 / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-musl-arm64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "linux-musl" + ], + "architectures": [ + "arm64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "linux-musl-arm64", + "isFramework": false, + "integrity": "DA63619EA024EB9BBF6DB5A85C6150CAB5C0BD554544A3596ED1B17F926D6875", + "dotnet_version": "6" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Windows / x64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/8d42e62ea4051381c219b3e31bc4eced/razorlanguageserver-win-x64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "win32" + ], + "architectures": [ + "x86_64" + ], + "platformId": "win-x64", + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Windows / x86)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/e440c4f3a4a96334fe177513935fa010/razorlanguageserver-win-x86-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "win32" + ], + "architectures": [ + "x86" + ], + "platformId": "win-x86", + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Windows / ARM64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/4ef26e45cf32fe8d51c0e7dd21f1fef6/razorlanguageserver-win-arm64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "win32" + ], + "architectures": [ + "arm64" + ], + "platformId": "win-arm64", + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Linux / x64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/6d4e23a3c7cf0465743950a39515a716/razorlanguageserver-linux-x64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "linux" + ], + "architectures": [ + "x86_64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "linux-x64", + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Linux ARM64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/85deebd44647ebf65724cc291d722283/razorlanguageserver-linux-arm64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "linux" + ], + "architectures": [ + "arm64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "linux-arm64" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Linux musl / x64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/4f0caa94ae182785655efb15eafcef23/razorlanguageserver-linux-musl-x64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "linux-musl" + ], + "architectures": [ + "x86_64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "linux-musl-x64" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Linux musl ARM64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/0a24828206a6f3b4bc743d058ef88ce7/razorlanguageserver-linux-musl-arm64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "linux-musl" + ], + "architectures": [ + "arm64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "linux-musl-arm64" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (macOS / x64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/2afcafaf41082989efcc10405abb9314/razorlanguageserver-osx-x64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "darwin" + ], + "architectures": [ + "x86_64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "osx-x64" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (macOS ARM64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/8bf2ed2f00d481a5987e3eb5165afddd/razorlanguageserver-osx-arm64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "darwin" + ], + "architectures": [ + "arm64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "osx-arm64" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json b/src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json new file mode 100644 index 0000000..148841c --- /dev/null +++ b/src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json @@ -0,0 +1,111 @@ +{ + "RoslynExtensionsOptions": { + "EnableDecompilationSupport": false, + "EnableAnalyzersSupport": true, + "EnableImportCompletion": true, + "EnableAsyncCompletion": false, + "DocumentAnalysisTimeoutMs": 30000, + "DiagnosticWorkersThreadCount": 18, + "AnalyzeOpenDocumentsOnly": true, + "InlayHintsOptions": { + "EnableForParameters": false, + "ForLiteralParameters": false, + "ForIndexerParameters": false, + "ForObjectCreationParameters": false, + "ForOtherParameters": false, + "SuppressForParametersThatDifferOnlyBySuffix": false, + "SuppressForParametersThatMatchMethodIntent": false, + "SuppressForParametersThatMatchArgumentName": false, + "EnableForTypes": false, + "ForImplicitVariableTypes": false, + "ForLambdaParameterTypes": false, + "ForImplicitObjectCreation": false + }, + "LocationPaths": null + }, + "FormattingOptions": { + "OrganizeImports": false, + "EnableEditorConfigSupport": true, + "NewLine": "\n", + "UseTabs": false, + "TabSize": 4, + "IndentationSize": 4, + "SpacingAfterMethodDeclarationName": false, + "SeparateImportDirectiveGroups": false, + "SpaceWithinMethodDeclarationParenthesis": false, + "SpaceBetweenEmptyMethodDeclarationParentheses": false, + "SpaceAfterMethodCallName": false, + "SpaceWithinMethodCallParentheses": false, + "SpaceBetweenEmptyMethodCallParentheses": false, + "SpaceAfterControlFlowStatementKeyword": true, + "SpaceWithinExpressionParentheses": false, + "SpaceWithinCastParentheses": false, + "SpaceWithinOtherParentheses": false, + "SpaceAfterCast": false, + "SpaceBeforeOpenSquareBracket": false, + "SpaceBetweenEmptySquareBrackets": false, + "SpaceWithinSquareBrackets": false, + "SpaceAfterColonInBaseTypeDeclaration": true, + "SpaceAfterComma": true, + "SpaceAfterDot": false, + "SpaceAfterSemicolonsInForStatement": true, + "SpaceBeforeColonInBaseTypeDeclaration": true, + "SpaceBeforeComma": false, + "SpaceBeforeDot": false, + "SpaceBeforeSemicolonsInForStatement": false, + "SpacingAroundBinaryOperator": "single", + "IndentBraces": false, + "IndentBlock": true, + "IndentSwitchSection": true, + "IndentSwitchCaseSection": true, + "IndentSwitchCaseSectionWhenBlock": true, + "LabelPositioning": "oneLess", + "WrappingPreserveSingleLine": true, + "WrappingKeepStatementsOnSingleLine": true, + "NewLinesForBracesInTypes": true, + "NewLinesForBracesInMethods": true, + "NewLinesForBracesInProperties": true, + "NewLinesForBracesInAccessors": true, + "NewLinesForBracesInAnonymousMethods": true, + "NewLinesForBracesInControlBlocks": true, + "NewLinesForBracesInAnonymousTypes": true, + "NewLinesForBracesInObjectCollectionArrayInitializers": true, + "NewLinesForBracesInLambdaExpressionBody": true, + "NewLineForElse": true, + "NewLineForCatch": true, + "NewLineForFinally": true, + "NewLineForMembersInObjectInit": true, + "NewLineForMembersInAnonymousTypes": true, + "NewLineForClausesInQuery": true + }, + "FileOptions": { + "SystemExcludeSearchPatterns": [ + "**/node_modules/**/*", + "**/bin/**/*", + "**/obj/**/*", + "**/.git/**/*", + "**/.git", + "**/.svn", + "**/.hg", + "**/CVS", + "**/.DS_Store", + "**/Thumbs.db" + ], + "ExcludeSearchPatterns": [] + }, + "RenameOptions": { + "RenameOverloads": false, + "RenameInStrings": false, + "RenameInComments": false + }, + "ImplementTypeOptions": { + "InsertionBehavior": 0, + "PropertyGenerationBehavior": 0 + }, + "DotNetCliOptions": { + "LocationPaths": null + }, + "Plugins": { + "LocationPaths": null + } +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py new file mode 100644 index 0000000..4885583 --- /dev/null +++ b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py @@ -0,0 +1,246 @@ +""" +Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. +""" + +import asyncio +import json +import logging +import os +import pathlib +import re +from contextlib import asynccontextmanager +from typing import AsyncIterator, Tuple + +from overrides import override + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig + + +class PyrightServer(LanguageServer): + """ + Provides Python specific instantiation of the LanguageServer class using Pyright. + Contains various configurations and settings specific to Python. + """ + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + 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", + ) + + # Event to signal when initial workspace analysis is complete + self.analysis_complete = asyncio.Event() + self.found_source_files = False + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Pyright Language Server. + """ + # Create basic initialization parameters + initialize_params: InitializeParams = { # type: ignore + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": pathlib.Path(repository_absolute_path).as_uri(), + "initializationOptions": { + "exclude": [ + "**/__pycache__", + "**/.venv", + "**/.env", + "**/build", + "**/dist", + "**/.pixi", + ], + "reportMissingImports": "error", + }, + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": {"documentChanges": True}, + "didChangeConfiguration": {"dynamicRegistration": True}, + "didChangeWatchedFiles": {"dynamicRegistration": True}, + "symbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] + }, + }, + "executeCommand": {"dynamicRegistration": 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, + "preselectSupport": True, + }, + "completionItemKind": { + "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] + }, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": {"labelOffsetSupport": True}, + }, + }, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentHighlight": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] + }, + "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}, + "publishDiagnostics": {"relatedInformation": True}, + }, + }, + "workspaceFolders": [ + {"uri": pathlib.Path(repository_absolute_path).as_uri(), "name": os.path.basename(repository_absolute_path)} + ], + } + + return initialize_params + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["PyrightServer"]: + """ + Starts the Pyright Language Server and waits for initial workspace analysis to complete. + + This prevents zombie processes by ensuring Pyright has finished its initial background + tasks before we consider the server ready. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and workspace analysis is complete + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown cleanly + ``` + """ + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def window_log_message(msg): + """ + Monitor Pyright's log messages to detect when initial analysis is complete. + 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) + + # 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) + self.found_source_files = True + self.analysis_complete.set() + self.completions_available.set() + + async def check_experimental_status(params): + """ + 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) + if not self.found_source_files: + self.analysis_complete.set() + self.completions_available.set() + + # Set up notification handlers + 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) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + async with super().start_server(): + self.logger.log("Starting pyright-langserver server process", logging.INFO) + await 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, + ) + init_response = await self.server.send.initialize(initialize_params) + self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) + + # Verify that the server supports our required features + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + + # Complete the initialization handshake + self.server.notify.initialized({}) + + # 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) + try: + await asyncio.wait_for(self.analysis_complete.wait(), timeout=1.0) + self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) + except asyncio.TimeoutError: + self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) + # Fallback: assume analysis is complete after timeout + self.analysis_complete.set() + self.completions_available.set() + + yield self \ No newline at end of file diff --git a/src/solidlsp/language_servers/rust_analyzer/initialize_params.json b/src/solidlsp/language_servers/rust_analyzer/initialize_params.json new file mode 100644 index 0000000..00368ee --- /dev/null +++ b/src/solidlsp/language_servers/rust_analyzer/initialize_params.json @@ -0,0 +1,917 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Visual Studio Code - Insiders", + "version": "1.82.0-insider" + }, + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "configuration": true, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": false + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0", + "allowedTags": [ + "ul", + "li", + "p", + "code", + "blockquote", + "ol", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "em", + "pre", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "div", + "del", + "a", + "strong", + "br", + "img", + "span" + ] + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + }, + "experimental": { + "snippetTextEdit": true, + "codeActionGroup": true, + "hoverActions": true, + "serverStatusNotification": true, + "colorDiagnosticOutput": true, + "openServerLogs": true, + "localDocs": true, + "commands": { + "commands": [ + "rust-analyzer.runSingle", + "rust-analyzer.debugSingle", + "rust-analyzer.showReferences", + "rust-analyzer.gotoLocation", + "editor.action.triggerParameterHints" + ] + } + } + }, + "initializationOptions": { + "cargoRunner": null, + "runnables": { + "extraEnv": null, + "problemMatcher": [ + "$rustc" + ], + "command": null, + "extraArgs": [] + }, + "statusBar": { + "clickAction": "openLogs" + }, + "server": { + "path": null, + "extraEnv": null + }, + "trace": { + "server": "verbose", + "extension": false + }, + "debug": { + "engine": "auto", + "sourceFileMap": { + "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" + }, + "openDebugPane": false, + "engineSettings": {} + }, + "restartServerOnConfigChange": false, + "typing": { + "continueCommentsOnNewline": true, + "autoClosingAngleBrackets": { + "enable": false + } + }, + "diagnostics": { + "previewRustcOutput": false, + "useRustcErrorCode": false, + "disabled": [], + "enable": true, + "experimental": { + "enable": false + }, + "remapPrefix": {}, + "warningsAsHint": [], + "warningsAsInfo": [] + }, + "discoverProjectRunner": null, + "showUnlinkedFileNotification": true, + "showDependenciesExplorer": true, + "assist": { + "emitMustUse": false, + "expressionFillDefault": "todo" + }, + "cachePriming": { + "enable": true, + "numThreads": 0 + }, + "cargo": { + "autoreload": true, + "buildScripts": { + "enable": true, + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "overrideCommand": null, + "useRustcWrapper": true + }, + "cfgs": {}, + "extraArgs": [], + "extraEnv": {}, + "features": [], + "noDefaultFeatures": false, + "sysroot": "discover", + "sysrootSrc": null, + "target": null, + "unsetTest": [ + "core" + ] + }, + "checkOnSave": true, + "check": { + "allTargets": true, + "command": "check", + "extraArgs": [], + "extraEnv": {}, + "features": null, + "ignore": [], + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "noDefaultFeatures": null, + "overrideCommand": null, + "targets": null + }, + "completion": { + "autoimport": { + "enable": true + }, + "autoself": { + "enable": true + }, + "callable": { + "snippets": "fill_arguments" + }, + "fullFunctionSignatures": { + "enable": false + }, + "limit": null, + "postfix": { + "enable": true + }, + "privateEditable": { + "enable": false + }, + "snippets": { + "custom": { + "Arc::new": { + "postfix": "arc", + "body": "Arc::new(${receiver})", + "requires": "std::sync::Arc", + "description": "Put the expression into an `Arc`", + "scope": "expr" + }, + "Rc::new": { + "postfix": "rc", + "body": "Rc::new(${receiver})", + "requires": "std::rc::Rc", + "description": "Put the expression into an `Rc`", + "scope": "expr" + }, + "Box::pin": { + "postfix": "pinbox", + "body": "Box::pin(${receiver})", + "requires": "std::boxed::Box", + "description": "Put the expression into a pinned `Box`", + "scope": "expr" + }, + "Ok": { + "postfix": "ok", + "body": "Ok(${receiver})", + "description": "Wrap the expression in a `Result::Ok`", + "scope": "expr" + }, + "Err": { + "postfix": "err", + "body": "Err(${receiver})", + "description": "Wrap the expression in a `Result::Err`", + "scope": "expr" + }, + "Some": { + "postfix": "some", + "body": "Some(${receiver})", + "description": "Wrap the expression in an `Option::Some`", + "scope": "expr" + } + } + } + }, + "files": { + "excludeDirs": [], + "watcher": "client" + }, + "highlightRelated": { + "breakPoints": { + "enable": true + }, + "closureCaptures": { + "enable": true + }, + "exitPoints": { + "enable": true + }, + "references": { + "enable": true + }, + "yieldPoints": { + "enable": true + } + }, + "hover": { + "actions": { + "debug": { + "enable": true + }, + "enable": true, + "gotoTypeDef": { + "enable": true + }, + "implementations": { + "enable": true + }, + "references": { + "enable": false + }, + "run": { + "enable": true + } + }, + "documentation": { + "enable": true, + "keywords": { + "enable": true + } + }, + "links": { + "enable": true + }, + "memoryLayout": { + "alignment": "hexadecimal", + "enable": true, + "niches": false, + "offset": "hexadecimal", + "size": "both" + } + }, + "imports": { + "granularity": { + "enforce": false, + "group": "crate" + }, + "group": { + "enable": true + }, + "merge": { + "glob": true + }, + "preferNoStd": false, + "preferPrelude": false, + "prefix": "plain" + }, + "inlayHints": { + "bindingModeHints": { + "enable": false + }, + "chainingHints": { + "enable": true + }, + "closingBraceHints": { + "enable": true, + "minLines": 25 + }, + "closureCaptureHints": { + "enable": false + }, + "closureReturnTypeHints": { + "enable": "never" + }, + "closureStyle": "impl_fn", + "discriminantHints": { + "enable": "never" + }, + "expressionAdjustmentHints": { + "enable": "never", + "hideOutsideUnsafe": false, + "mode": "prefix" + }, + "lifetimeElisionHints": { + "enable": "never", + "useParameterNames": false + }, + "maxLength": 25, + "parameterHints": { + "enable": true + }, + "reborrowHints": { + "enable": "never" + }, + "renderColons": true, + "typeHints": { + "enable": true, + "hideClosureInitialization": false, + "hideNamedConstructor": false + } + }, + "interpret": { + "tests": false + }, + "joinLines": { + "joinAssignments": true, + "joinElseIf": true, + "removeTrailingComma": true, + "unwrapTrivialBlock": true + }, + "lens": { + "debug": { + "enable": true + }, + "enable": true, + "forceCustomCommands": true, + "implementations": { + "enable": true + }, + "location": "above_name", + "references": { + "adt": { + "enable": false + }, + "enumVariant": { + "enable": false + }, + "method": { + "enable": false + }, + "trait": { + "enable": false + } + }, + "run": { + "enable": true + } + }, + "linkedProjects": [], + "lru": { + "capacity": null, + "query": { + "capacities": {} + } + }, + "notifications": { + "cargoTomlNotFound": true + }, + "numThreads": null, + "procMacro": { + "attributes": { + "enable": true + }, + "enable": true, + "ignored": {}, + "server": null + }, + "references": { + "excludeImports": false + }, + "rust": { + "analyzerTargetDir": null + }, + "rustc": { + "source": null + }, + "rustfmt": { + "extraArgs": [], + "overrideCommand": null, + "rangeFormatting": { + "enable": false + } + }, + "semanticHighlighting": { + "doc": { + "comment": { + "inject": { + "enable": true + } + } + }, + "nonStandardTokens": true, + "operator": { + "enable": true, + "specialization": { + "enable": false + } + }, + "punctuation": { + "enable": false, + "separate": { + "macro": { + "bang": false + } + }, + "specialization": { + "enable": false + } + }, + "strings": { + "enable": true + } + }, + "signatureInfo": { + "detail": "full", + "documentation": { + "enable": true + } + }, + "workspace": { + "symbol": { + "search": { + "kind": "only_types", + "limit": 128, + "scope": "workspace" + } + } + } + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json b/src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json new file mode 100644 index 0000000..517d8b8 --- /dev/null +++ b/src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json @@ -0,0 +1,29 @@ +{ + "_description": "Used to download the runtime dependencies for running RustAnalyzer. Obtained from https://github.com/rust-lang/rust-analyzer/releases", + "runtimeDependencies": [ + { + "id": "RustAnalyzer", + "description": "RustAnalyzer for Linux (x64)", + "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-aarch64-apple-darwin.gz", + "platformId": "osx-arm64", + "archiveType": "gz", + "binaryName": "rust_analyzer" + }, + { + "id": "RustAnalyzer", + "description": "RustAnalyzer for Linux (x64)", + "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-unknown-linux-gnu.gz", + "platformId": "linux-x64", + "archiveType": "gz", + "binaryName": "rust_analyzer" + }, + { + "id": "RustAnalyzer", + "description": "RustAnalyzer for Windows (x64)", + "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-pc-windows-msvc.zip", + "platformId": "win-x64", + "archiveType": "zip", + "binaryName": "rust-analyzer.exe" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py new file mode 100644 index 0000000..65f3598 --- /dev/null +++ b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py @@ -0,0 +1,185 @@ +""" +Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. +""" + +import asyncio +import json +import logging +import os +import stat +import pathlib +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from overrides import override + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_utils import FileUtils +from multilspy.multilspy_utils import PlatformUtils + + +class RustAnalyzer(LanguageServer): + """ + Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + rustanalyzer_executable_path = self.setup_runtime_dependencies(logger, config) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path), + "rust", + ) + self.server_ready = asyncio.Event() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["target"] + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + """ + Setup runtime dependencies for rust_analyzer. + """ + platform_id = PlatformUtils.get_platform_id() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + # assert platform_id.value in [ + # "linux-x64", + # "win-x64", + # ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [ + dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value + ] + assert len(runtime_dependencies) == 1 + dependency = runtime_dependencies[0] + + rustanalyzer_ls_dir = os.path.join(os.path.dirname(__file__), "static", "RustAnalyzer") + rustanalyzer_executable_path = os.path.join(rustanalyzer_ls_dir, dependency["binaryName"]) + if not os.path.exists(rustanalyzer_ls_dir): + os.makedirs(rustanalyzer_ls_dir) + if dependency["archiveType"] == "gz": + FileUtils.download_and_extract_archive( + logger, dependency["url"], rustanalyzer_executable_path, dependency["archiveType"] + ) + else: + FileUtils.download_and_extract_archive( + logger, dependency["url"], rustanalyzer_ls_dir, dependency["archiveType"] + ) + assert os.path.exists(rustanalyzer_executable_path) + os.chmod(rustanalyzer_executable_path, stat.S_IEXEC) + + return rustanalyzer_executable_path + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Rust Analyzer Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["RustAnalyzer"]: + """ + Starts the Rust Analyzer Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + """ + + async def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "workspace/executeCommand": + self.initialize_searcher_command_available.set() + self.resolve_main_method_available.set() + return + + async def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + self.service_ready_event.set() + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def check_experimental_status(params): + if params["quiescent"] == True: + self.server_ready.set() + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + 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) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + async with super().start_server(): + self.logger.log("Starting RustAnalyzer server process", logging.INFO) + await 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, + ) + init_response = await self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "resolveProvider": True, + "triggerCharacters": [":", ".", "'", "("], + "completionItem": {"labelDetailsSupport": True}, + } + self.server.notify.initialized({}) + self.completions_available.set() + + await self.server_ready.wait() + + yield self diff --git a/src/solidlsp/language_servers/solargraph/initialize_params.json b/src/solidlsp/language_servers/solargraph/initialize_params.json new file mode 100644 index 0000000..2cbb41f --- /dev/null +++ b/src/solidlsp/language_servers/solargraph/initialize_params.json @@ -0,0 +1,15 @@ +{ + "_description": "This file contains the initialization parameters for the Solargraph Language Server.", + "processId": "$processId", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} diff --git a/src/solidlsp/language_servers/solargraph/runtime_dependencies.json b/src/solidlsp/language_servers/solargraph/runtime_dependencies.json new file mode 100644 index 0000000..38cc237 --- /dev/null +++ b/src/solidlsp/language_servers/solargraph/runtime_dependencies.json @@ -0,0 +1,11 @@ +{ + "_description": "This file contains URLs and other metadata required for downloading and installing the Solargraph language server.", + "runtimeDependencies": [ + { + "url": "https://rubygems.org/downloads/solargraph-0.51.1.gem", + "installCommand": "gem install solargraph -v 0.51.1", + "binaryName": "solargraph", + "archiveType": "gem" + } + ] +} diff --git a/src/solidlsp/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph/solargraph.py new file mode 100644 index 0000000..099ff9d --- /dev/null +++ b/src/solidlsp/language_servers/solargraph/solargraph.py @@ -0,0 +1,189 @@ +""" +Provides Ruby specific instantiation of the LanguageServer class using Solargraph. +Contains various configurations and settings specific to Ruby. +""" + +import asyncio +import json +import logging +import os +import stat +import subprocess +import pathlib +from contextlib import asynccontextmanager +from typing import AsyncIterator, override + +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_utils import FileUtils +from multilspy.multilspy_utils import PlatformUtils, PlatformId + + +class Solargraph(LanguageServer): + """ + Provides Ruby specific instantiation of the LanguageServer class using Solargraph. + Contains various configurations and settings specific to Ruby. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a Solargraph instance. This class is not meant to be instantiated directly. + Use LanguageServer.create() instead. + """ + solargraph_executable_path = self.setup_runtime_dependencies(logger, config, repository_root_path) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=f"{solargraph_executable_path} stdio", cwd=repository_root_path), + "ruby", + ) + self.server_ready = asyncio.Event() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["vendor"] + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig, repository_root_path: str) -> str: + """ + Setup runtime dependencies for Solargraph. + """ + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + dependency = d["runtimeDependencies"][0] + + # Check if Ruby is installed + try: + result = subprocess.run(["ruby", "--version"], check=True, capture_output=True, cwd=repository_root_path) + ruby_version = result.stdout.strip() + logger.log(f"Ruby version: {ruby_version}", logging.INFO) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Error checking for Ruby installation: {e.stderr}") + except FileNotFoundError: + raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") + + # Check if solargraph is installed + try: + result = subprocess.run(["gem", "list", "^solargraph$", "-i"], check=False, capture_output=True, text=True, cwd=repository_root_path) + if result.stdout.strip() == "false": + logger.log("Installing Solargraph...", logging.INFO) + subprocess.run(dependency["installCommand"].split(), check=True, capture_output=True, cwd=repository_root_path) + + # Get the gem executable path directly + result = subprocess.run(["gem", "which", "solargraph"], check=True, capture_output=True, text=True, cwd=repository_root_path) + gem_path = result.stdout.strip() + bin_dir = os.path.join(os.path.dirname(os.path.dirname(gem_path)), "bin") + executable_path = os.path.join(bin_dir, "solargraph") + + if not os.path.exists(executable_path): + raise RuntimeError(f"Solargraph executable not found at {executable_path}") + + # Ensure the executable has the right permissions + os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IEXEC) + + return executable_path + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Solargraph Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["Solargraph"]: + """ + Starts the Solargraph Language Server for Ruby, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + """ + + async def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "workspace/executeCommand": + self.initialize_searcher_command_available.set() + self.resolve_main_method_available.set() + return + + async def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + self.service_ready_event.set() + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + 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) + + async with super().start_server(): + self.logger.log("Starting solargraph server process", logging.INFO) + await 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, + ) + self.logger.log(f"Sending init params: {json.dumps(initialize_params, indent=4)}", logging.INFO) + init_response = await self.server.send.initialize(initialize_params) + self.logger.log(f"Received init response: {init_response}", logging.INFO) + assert init_response["capabilities"]["textDocumentSync"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "resolveProvider": True, + "triggerCharacters": [".", ":", "@"], + } + self.server.notify.initialized({}) + self.completions_available.set() + + self.server_ready.set() + await self.server_ready.wait() + + yield self diff --git a/src/solidlsp/language_servers/typescript_language_server/initialize_params.json b/src/solidlsp/language_servers/typescript_language_server/initialize_params.json new file mode 100644 index 0000000..6209ba4 --- /dev/null +++ b/src/solidlsp/language_servers/typescript_language_server/initialize_params.json @@ -0,0 +1,906 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Visual Studio Code - Insiders", + "version": "1.82.0-insider" + }, + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "configuration": true, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": false + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0", + "allowedTags": [ + "ul", + "li", + "p", + "code", + "blockquote", + "ol", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "em", + "pre", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "div", + "del", + "a", + "strong", + "br", + "img", + "span" + ] + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + }, + "experimental": { + "snippetTextEdit": true, + "codeActionGroup": true, + "hoverActions": true, + "serverStatusNotification": true, + "colorDiagnosticOutput": true, + "openServerLogs": true, + "localDocs": true, + "commands": { + "commands": [ + "rust-analyzer.runSingle", + "rust-analyzer.debugSingle", + "rust-analyzer.showReferences", + "rust-analyzer.gotoLocation", + "editor.action.triggerParameterHints" + ] + } + } + }, + "initializationOptions": { + "cargoRunner": null, + "runnables": { + "extraEnv": null, + "problemMatcher": [ + "$rustc" + ], + "command": null, + "extraArgs": [] + }, + "statusBar": { + "clickAction": "openLogs" + }, + "server": { + "path": null, + "extraEnv": null + }, + "trace": { + "server": "verbose", + "extension": false + }, + "debug": { + "engine": "auto", + "sourceFileMap": { + "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" + }, + "openDebugPane": false, + "engineSettings": {} + }, + "restartServerOnConfigChange": false, + "typing": { + "continueCommentsOnNewline": true, + "autoClosingAngleBrackets": { + "enable": false + } + }, + "diagnostics": { + "previewRustcOutput": false, + "useRustcErrorCode": false, + "disabled": [], + "enable": true, + "experimental": { + "enable": false + }, + "remapPrefix": {}, + "warningsAsHint": [], + "warningsAsInfo": [] + }, + "discoverProjectRunner": null, + "showUnlinkedFileNotification": true, + "showDependenciesExplorer": true, + "assist": { + "emitMustUse": false, + "expressionFillDefault": "todo" + }, + "cachePriming": { + "enable": true, + "numThreads": 0 + }, + "cargo": { + "autoreload": true, + "buildScripts": { + "enable": true, + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "overrideCommand": null, + "useRustcWrapper": true + }, + "cfgs": {}, + "extraArgs": [], + "extraEnv": {}, + "features": [], + "noDefaultFeatures": false, + "sysroot": "discover", + "sysrootSrc": null, + "target": null, + "unsetTest": [ + "core" + ] + }, + "checkOnSave": true, + "check": { + "allTargets": true, + "command": "check", + "extraArgs": [], + "extraEnv": {}, + "features": null, + "ignore": [], + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "noDefaultFeatures": null, + "overrideCommand": null, + "targets": null + }, + "completion": { + "autoimport": { + "enable": true + }, + "autoself": { + "enable": true + }, + "callable": { + "snippets": "fill_arguments" + }, + "fullFunctionSignatures": { + "enable": false + }, + "limit": null, + "postfix": { + "enable": true + }, + "privateEditable": { + "enable": false + }, + "snippets": { + "custom": { + "Arc::new": { + "postfix": "arc", + "body": "Arc::new(${receiver})", + "requires": "std::sync::Arc", + "description": "Put the expression into an `Arc`", + "scope": "expr" + }, + "Rc::new": { + "postfix": "rc", + "body": "Rc::new(${receiver})", + "requires": "std::rc::Rc", + "description": "Put the expression into an `Rc`", + "scope": "expr" + }, + "Box::pin": { + "postfix": "pinbox", + "body": "Box::pin(${receiver})", + "requires": "std::boxed::Box", + "description": "Put the expression into a pinned `Box`", + "scope": "expr" + }, + "Ok": { + "postfix": "ok", + "body": "Ok(${receiver})", + "description": "Wrap the expression in a `Result::Ok`", + "scope": "expr" + }, + "Err": { + "postfix": "err", + "body": "Err(${receiver})", + "description": "Wrap the expression in a `Result::Err`", + "scope": "expr" + }, + "Some": { + "postfix": "some", + "body": "Some(${receiver})", + "description": "Wrap the expression in an `Option::Some`", + "scope": "expr" + } + } + } + }, + "files": { + "excludeDirs": [], + "watcher": "client" + }, + "highlightRelated": { + "breakPoints": { + "enable": true + }, + "closureCaptures": { + "enable": true + }, + "exitPoints": { + "enable": true + }, + "references": { + "enable": true + }, + "yieldPoints": { + "enable": true + } + }, + "hover": { + "actions": { + "debug": { + "enable": true + }, + "enable": true, + "gotoTypeDef": { + "enable": true + }, + "implementations": { + "enable": true + }, + "references": { + "enable": false + }, + "run": { + "enable": true + } + }, + "documentation": { + "enable": true, + "keywords": { + "enable": true + } + }, + "links": { + "enable": true + }, + "memoryLayout": { + "alignment": "hexadecimal", + "enable": true, + "niches": false, + "offset": "hexadecimal", + "size": "both" + } + }, + "imports": { + "granularity": { + "enforce": false, + "group": "crate" + }, + "group": { + "enable": true + }, + "merge": { + "glob": true + }, + "preferNoStd": false, + "preferPrelude": false, + "prefix": "plain" + }, + "inlayHints": { + "bindingModeHints": { + "enable": false + }, + "chainingHints": { + "enable": true + }, + "closingBraceHints": { + "enable": true, + "minLines": 25 + }, + "closureCaptureHints": { + "enable": false + }, + "closureReturnTypeHints": { + "enable": "never" + }, + "closureStyle": "impl_fn", + "discriminantHints": { + "enable": "never" + }, + "expressionAdjustmentHints": { + "enable": "never", + "hideOutsideUnsafe": false, + "mode": "prefix" + }, + "lifetimeElisionHints": { + "enable": "never", + "useParameterNames": false + }, + "maxLength": 25, + "parameterHints": { + "enable": true + }, + "reborrowHints": { + "enable": "never" + }, + "renderColons": true, + "typeHints": { + "enable": true, + "hideClosureInitialization": false, + "hideNamedConstructor": false + } + }, + "interpret": { + "tests": false + }, + "joinLines": { + "joinAssignments": true, + "joinElseIf": true, + "removeTrailingComma": true, + "unwrapTrivialBlock": true + }, + "lens": { + "debug": { + "enable": true + }, + "enable": true, + "forceCustomCommands": true, + "implementations": { + "enable": true + }, + "location": "above_name", + "references": { + "adt": { + "enable": false + }, + "enumVariant": { + "enable": false + }, + "method": { + "enable": false + }, + "trait": { + "enable": false + } + }, + "run": { + "enable": true + } + }, + "linkedProjects": [], + "lru": { + "capacity": null, + "query": { + "capacities": {} + } + }, + "notifications": { + "cargoTomlNotFound": true + }, + "numThreads": null, + "procMacro": { + "attributes": { + "enable": true + }, + "enable": true, + "ignored": {}, + "server": null + }, + "references": { + "excludeImports": false + }, + "rust": { + "analyzerTargetDir": null + }, + "rustc": { + "source": null + }, + "rustfmt": { + "extraArgs": [], + "overrideCommand": null, + "rangeFormatting": { + "enable": false + } + }, + "semanticHighlighting": { + "doc": { + "comment": { + "inject": { + "enable": true + } + } + }, + "nonStandardTokens": true, + "operator": { + "enable": true, + "specialization": { + "enable": false + } + }, + "punctuation": { + "enable": false, + "separate": { + "macro": { + "bang": false + } + }, + "specialization": { + "enable": false + } + }, + "strings": { + "enable": true + } + }, + "signatureInfo": { + "detail": "full", + "documentation": { + "enable": true + } + }, + "workspace": { + "symbol": { + "search": { + "kind": "only_types", + "limit": 128, + "scope": "workspace" + } + } + } + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json new file mode 100644 index 0000000..90824a7 --- /dev/null +++ b/src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json @@ -0,0 +1,16 @@ +{ + "_description": "Used to download the runtime dependencies for running typescript-language-server. Obtained from https://github.com/typescript-language-server/typescript-language-server/releases", + "runtimeDependencies": [ + { + "id": "typescript", + "description": "typescript package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", + "command": "npm install --prefix ./ typescript@5.5.4" + }, + { + "id": "typescript-language-server", + "description": "typescript-language-server package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", + "command": "npm install --prefix ./ typescript-language-server@4.3.3" + } + ] +} + diff --git a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py new file mode 100644 index 0000000..479936c --- /dev/null +++ b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py @@ -0,0 +1,245 @@ +""" +Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. +""" + +import asyncio +import json +import logging +import os +import pathlib +import shutil +import subprocess +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from time import sleep + +from overrides import override + +from multilspy.language_server import LanguageServer +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import PlatformId, PlatformUtils + +# Platform-specific imports +if os.name != 'nt': # Unix-like systems + import pwd +else: + # Dummy pwd module for Windows + class pwd: + @staticmethod + def getpwuid(uid): + return type('obj', (), {'pw_name': os.environ.get('USERNAME', 'unknown')})() + + +# Conditionally import pwd module (Unix-only) +if not PlatformUtils.get_platform_id().value.startswith("win"): + import pwd + + +class TypeScriptLanguageServer(LanguageServer): + """ + Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + ts_lsp_executable_path = self.setup_runtime_dependencies(logger, config) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path), + "typescript", + ) + self.server_ready = asyncio.Event() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in [ + "node_modules", + "dist", + "build", + "coverage", + ] + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + """ + Setup runtime dependencies for TypeScript Language Server. + """ + platform_id = PlatformUtils.get_platform_id() + + valid_platforms = [ + PlatformId.LINUX_x64, + PlatformId.LINUX_arm64, + PlatformId.OSX, + 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 multilspy javascript/typescript at the moment" + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: + d = json.load(f) + del d["_description"] + + runtime_dependencies = d.get("runtimeDependencies", []) + tsserver_ls_dir = os.path.join(os.path.dirname(__file__), "static", "ts-lsp") + tsserver_executable_path = os.path.join(tsserver_ls_dir, "typescript-language-server") + + # Verify both node and npm are installed + is_node_installed = shutil.which('node') is not None + assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." + is_npm_installed = shutil.which('npm') is not None + assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." + + # Install typescript and typescript-language-server if not already installed + if not os.path.exists(tsserver_ls_dir): + os.makedirs(tsserver_ls_dir, exist_ok=True) + for dependency in runtime_dependencies: + # Windows doesn't support the 'user' parameter and doesn't have pwd module + if PlatformUtils.get_platform_id().value.startswith("win"): + subprocess.run( + dependency["command"], + shell=True, + check=True, + cwd=tsserver_ls_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + else: + # On Unix-like systems, run as non-root user + user = pwd.getpwuid(os.getuid()).pw_name + subprocess.run( + dependency["command"], + shell=True, + check=True, + user=user, + cwd=tsserver_ls_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") + + assert os.path.exists(tsserver_executable_path), "typescript-language-server executable not found. Please install typescript-language-server and try again." + return f"{tsserver_executable_path} --stdio" + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the TypeScript Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + @asynccontextmanager + async def start_server(self) -> AsyncIterator["TypeScriptLanguageServer"]: + """ + Starts the TypeScript Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + """ + + async def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "workspace/executeCommand": + self.initialize_searcher_command_available.set() + # TypeScript doesn't have a direct equivalent to resolve_main_method + # You might want to set a different flag or remove this line + # self.resolve_main_method_available.set() + return + + async def execute_client_command_handler(params): + return [] + + async def do_nothing(params): + return + + async def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + + async def check_experimental_status(params): + """ + Also listen for experimental/serverStatus as a backup signal + """ + if params.get("quiescent") == True: + self.server_ready.set() + self.completions_available.set() + + self.server.on_request("client/registerCapability", register_capability_handler) + 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("experimental/serverStatus", check_experimental_status) + + async with super().start_server(): + self.logger.log("Starting TypeScript server process", logging.INFO) + await 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, + ) + init_response = await self.server.send.initialize(initialize_params) + + # TypeScript-specific capability checks + assert init_response["capabilities"]["textDocumentSync"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": ['.', '"', "'", '/', '@', '<'], + "resolveProvider": True + } + + self.server.notify.initialized({}) + try: + await asyncio.wait_for(self.server_ready.wait(), timeout=1.0) + except asyncio.TimeoutError: + self.logger.log("Timeout waiting for TypeScript server to become ready, proceeding anyway", logging.INFO) + # Fallback: assume server is ready after timeout + self.server_ready.set() + self.completions_available.set() + + yield self + + @override + # For some reason, the LS may need longer to process this, so we just retry + async def _send_references_request(self, relative_file_path: str, line: int, column: int): + # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is + # despite the LS having processed requests already. I don't know what causes this, but sleeping + # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. + # It may be that even this 1sec is not enough in larger TS projects, at some point we should find what + # causes this and solve it. + sleep(1) + return await super()._send_references_request(relative_file_path, line, column) From 2a60b71047fff40dd15a0eece52f6bd2cdb04b19 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 13:38:40 +0200 Subject: [PATCH 095/174] Complete interface for server stop/start --- src/solidlsp/ls.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 4d37ca7..bd1eca2 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -6,6 +6,7 @@ import pathlib import pickle import re import threading +from abc import abstractmethod, ABC from collections import defaultdict from contextlib import contextmanager from copy import copy @@ -35,7 +36,7 @@ from serena.text_utils import MatchedConsecutiveLines, search_files from solidlsp.ls_handler import SolidLanguageServerHandler -class SolidLanguageServer: +class SolidLanguageServer(ABC): """ The LanguageServer class provides a language agnostic interface to the Language Server Protocol. It is used to communicate with Language Servers of different programming languages. @@ -63,7 +64,7 @@ class SolidLanguageServer: :return LanguageServer: A language specific LanguageServer instance. """ if config.code_language == Language.PYTHON: - from multilspy.language_servers.pyright_language_server.pyright_server import ( + from solidlsp.language_servers.pyright_language_server.pyright_server import ( PyrightServer, ) @@ -76,50 +77,50 @@ class SolidLanguageServer: # return JediServer(config, logger, repository_root_path) elif config.code_language == Language.JAVA: - from multilspy.language_servers.eclipse_jdtls.eclipse_jdtls import ( + from solidlsp.language_servers.eclipse_jdtls.eclipse_jdtls import ( EclipseJDTLS, ) return EclipseJDTLS(config, logger, repository_root_path) elif config.code_language == Language.KOTLIN: - from multilspy.language_servers.kotlin_language_server.kotlin_language_server import ( + from solidlsp.language_servers.kotlin_language_server.kotlin_language_server import ( KotlinLanguageServer, ) return KotlinLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.RUST: - from multilspy.language_servers.rust_analyzer.rust_analyzer import ( + from solidlsp.language_servers.rust_analyzer.rust_analyzer import ( RustAnalyzer, ) return RustAnalyzer(config, logger, repository_root_path) elif config.code_language == Language.CSHARP: - from multilspy.language_servers.omnisharp.omnisharp import OmniSharp + from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp return OmniSharp(config, logger, repository_root_path) elif config.code_language in [Language.TYPESCRIPT, Language.JAVASCRIPT]: - from multilspy.language_servers.typescript_language_server.typescript_language_server import ( + from solidlsp.language_servers.typescript_language_server.typescript_language_server import ( TypeScriptLanguageServer, ) return TypeScriptLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.GO: - from multilspy.language_servers.gopls.gopls import Gopls + from solidlsp.language_servers.gopls.gopls import Gopls return Gopls(config, logger, repository_root_path) elif config.code_language == Language.RUBY: - from multilspy.language_servers.solargraph.solargraph import Solargraph + from solidlsp.language_servers.solargraph.solargraph import Solargraph return Solargraph(config, logger, repository_root_path) elif config.code_language == Language.DART: - from multilspy.language_servers.dart_language_server.dart_language_server import DartLanguageServer + from solidlsp.language_servers.dart_language_server.dart_language_server import DartLanguageServer return DartLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.CPP: - from multilspy.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer + from solidlsp.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer return ClangdLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.PHP: - from multilspy.language_servers.intelephense.intelephense import Intelephense + from solidlsp.language_servers.intelephense.intelephense import Intelephense return Intelephense(config, logger, repository_root_path) else: @@ -328,10 +329,15 @@ class SolidLanguageServer: def start_server(self) -> Iterator["SolidLanguageServer"]: self.start() yield self - self._shutdown() + self.stop() - def _start_server(self) -> None: + def _start_server_process(self) -> None: self.server_started = True + self._start_server() + + @abstractmethod + def _start_server(self): + pass @contextmanager def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]: @@ -1615,9 +1621,12 @@ class SolidLanguageServer: :return: self for method chaining """ self.logger.log(f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}", logging.INFO) - self._server_context = self._start_server() + self._server_context = self._start_server_process() return self + def stop(self, shutdown_timeout: float = 2.0) -> None: + self._shutdown(timeout=shutdown_timeout) + @property def language_server(self) -> Self: return self @@ -1814,7 +1823,6 @@ class SolidPyrightServer(SolidLanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - super()._start_server() self.logger.log("Starting pyright-langserver server process", logging.INFO) self.server.start() From 0a800d0606f813a76272effdff48d90cf74651dc Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 14:29:15 +0200 Subject: [PATCH 096/174] Adapt Pyright and Intelephense servers to solidlsp --- .../intelephense/intelephense.py | 80 +++---- .../pyright_language_server/pyright_server.py | 88 ++++--- src/solidlsp/ls.py | 223 +----------------- 3 files changed, 77 insertions(+), 314 deletions(-) diff --git a/src/solidlsp/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense/intelephense.py index d2ad911..9b6eb95 100644 --- a/src/solidlsp/language_servers/intelephense/intelephense.py +++ b/src/solidlsp/language_servers/intelephense/intelephense.py @@ -2,27 +2,25 @@ Provides PHP specific instantiation of the LanguageServer class using Intelephense. """ -import asyncio import json -import shutil import logging import os -import subprocess import pathlib -from contextlib import asynccontextmanager +import shutil +import subprocess from time import sleep -from typing import AsyncIterator from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_utils import PlatformUtils, PlatformId +from solidlsp.ls import SolidLanguageServer -class Intelephense(LanguageServer): + +class Intelephense(SolidLanguageServer): """ Provides PHP specific instantiation of the LanguageServer class using Intelephense. """ @@ -109,7 +107,6 @@ class Intelephense(LanguageServer): ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php" ) - self.server_ready = asyncio.Event() self.request_id = 0 def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: @@ -136,16 +133,15 @@ class Intelephense(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["Intelephense"]: + def _start_server(self): """Start Intelephense server process""" - async def register_capability_handler(params): + def register_capability_handler(params): return - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - async def do_nothing(params): + def do_nothing(params): return self.server.on_request("client/registerCapability", register_capability_handler) @@ -153,48 +149,44 @@ class Intelephense(LanguageServer): self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) - async with super().start_server(): - self.logger.log("Starting Intelephense server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting Intelephense server process", logging.INFO) + 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, - ) - init_response = await self.server.send.initialize(initialize_params) - self.logger.log( - "After sent initialize params", - logging.INFO, - ) - - # Verify server capabilities - assert "textDocumentSync" in init_response["capabilities"] - assert "completionProvider" in init_response["capabilities"] - assert "definitionProvider" in init_response["capabilities"] + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + self.logger.log( + "After sent initialize params", + logging.INFO, + ) - self.server.notify.initialized({}) - self.completions_available.set() + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] - # Intelephense server is typically ready immediately after initialization - self.server_ready.set() - await self.server_ready.wait() + self.server.notify.initialized({}) + self.completions_available.set() + + # Intelephense server is typically ready immediately after initialization + # TODO: This is probably incorrect; the server does send an initialized notification, which we could wait for! - yield self - @override # For some reason, the LS may need longer to process this, so we just retry - async def _send_references_request(self, relative_file_path: str, line: int, column: int): + def _send_references_request(self, relative_file_path: str, line: int, column: int): # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is # despite the LS having processed requests already. I don't know what causes this, but sleeping # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. # May be related to the time it takes to read the files or something like that. # The sleeping doesn't seem to be needed on all systems sleep(1) - return await super()._send_references_request(relative_file_path, line, column) + return super()._send_references_request(relative_file_path, line, column) @override - async def _send_definition_request(self, definition_params: DefinitionParams): + def _send_definition_request(self, definition_params: DefinitionParams): # TODO: same as above, also only a problem if the definition is in another file sleep(1) - return await super()._send_definition_request(definition_params) + return super()._send_definition_request(definition_params) diff --git a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py index 4885583..b349050 100644 --- a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py @@ -2,25 +2,22 @@ Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ -import asyncio -import json import logging import os import pathlib import re -from contextlib import asynccontextmanager -from typing import AsyncIterator, Tuple +import threading from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.ls import SolidLanguageServer -class PyrightServer(LanguageServer): +class PyrightServer(SolidLanguageServer): """ Provides Python specific instantiation of the LanguageServer class using Pyright. Contains various configurations and settings specific to Python. @@ -39,9 +36,9 @@ class PyrightServer(LanguageServer): ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path), "python", ) - + # Event to signal when initial workspace analysis is complete - self.analysis_complete = asyncio.Event() + self.analysis_complete = threading.Event() self.found_source_files = False @override @@ -148,11 +145,10 @@ class PyrightServer(LanguageServer): return initialize_params - @asynccontextmanager - async def start_server(self) -> AsyncIterator["PyrightServer"]: + def _start_server(self): """ Starts the Pyright Language Server and waits for initial workspace analysis to complete. - + This prevents zombie processes by ensuring Pyright has finished its initial background tasks before we consider the server ready. @@ -167,20 +163,20 @@ class PyrightServer(LanguageServer): ``` """ - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def window_log_message(msg): + def window_log_message(msg): """ Monitor Pyright's log messages to detect when initial analysis is complete. 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) - + # 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): @@ -189,7 +185,7 @@ class PyrightServer(LanguageServer): self.analysis_complete.set() self.completions_available.set() - async def check_experimental_status(params): + def check_experimental_status(params): """ Also listen for experimental/serverStatus as a backup signal """ @@ -209,38 +205,34 @@ class PyrightServer(LanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting pyright-langserver server process", logging.INFO) - await self.server.start() + self.logger.log("Starting pyright-langserver server process", logging.INFO) + self.server.start() - # Send proper initialization parameters - initialize_params = self._get_initialize_params(self.repository_root_path) + # 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, - ) - init_response = await self.server.send.initialize(initialize_params) - self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) + self.logger.log( + "Sending initialize request from LSP client to pyright server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) - # Verify that the server supports our required features - assert "textDocumentSync" in init_response["capabilities"] - assert "completionProvider" in init_response["capabilities"] - assert "definitionProvider" in init_response["capabilities"] + # Verify that the server supports our required features + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] - # Complete the initialization handshake - self.server.notify.initialized({}) - - # 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) - try: - await asyncio.wait_for(self.analysis_complete.wait(), timeout=1.0) - self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) - except asyncio.TimeoutError: - self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) - # Fallback: assume analysis is complete after timeout - self.analysis_complete.set() - self.completions_available.set() + # Complete the initialization handshake + self.server.notify.initialized({}) - yield self \ No newline at end of file + # 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) + if self.analysis_complete.wait(timeout=5.0): + self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) + else: + self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) + # Fallback: assume analysis is complete after timeout + self.analysis_complete.set() + self.completions_available.set() diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index bd1eca2..bafd33c 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -68,7 +68,7 @@ class SolidLanguageServer(ABC): PyrightServer, ) - return SolidPyrightServer(config, logger, repository_root_path) + return PyrightServer(config, logger, repository_root_path) # It used to be jedi, but pyright is a bit faster, and also more actively maintained # Keeping the previous code for reference # from multilspy.language_servers.jedi_language_server.jedi_server import ( @@ -1633,224 +1633,3 @@ class SolidLanguageServer(ABC): def is_running(self) -> bool: return self.server.is_running() - - -class SolidPyrightServer(SolidLanguageServer): - """ - Provides Python specific instantiation of the LanguageServer class using Pyright. - Contains various configurations and settings specific to Python. - """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - """ - 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", - ) - - # Event to signal when initial workspace analysis is complete - self.analysis_complete = threading.Event() - self.found_source_files = False - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the Pyright Language Server. - """ - # Create basic initialization parameters - initialize_params: InitializeParams = { # type: ignore - "processId": os.getpid(), - "rootPath": repository_absolute_path, - "rootUri": pathlib.Path(repository_absolute_path).as_uri(), - "initializationOptions": { - "exclude": [ - "**/__pycache__", - "**/.venv", - "**/.env", - "**/build", - "**/dist", - "**/.pixi", - ], - "reportMissingImports": "error", - }, - "capabilities": { - "workspace": { - "applyEdit": True, - "workspaceEdit": {"documentChanges": True}, - "didChangeConfiguration": {"dynamicRegistration": True}, - "didChangeWatchedFiles": {"dynamicRegistration": True}, - "symbol": { - "dynamicRegistration": True, - "symbolKind": { - "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] - }, - }, - "executeCommand": {"dynamicRegistration": 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, - "preselectSupport": True, - }, - "completionItemKind": { - "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] - }, - }, - "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, - "signatureHelp": { - "dynamicRegistration": True, - "signatureInformation": { - "documentationFormat": ["markdown", "plaintext"], - "parameterInformation": {"labelOffsetSupport": True}, - }, - }, - "definition": {"dynamicRegistration": True}, - "references": {"dynamicRegistration": True}, - "documentHighlight": {"dynamicRegistration": True}, - "documentSymbol": { - "dynamicRegistration": True, - "symbolKind": { - "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] - }, - "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}, - "publishDiagnostics": {"relatedInformation": True}, - }, - }, - "workspaceFolders": [ - {"uri": pathlib.Path(repository_absolute_path).as_uri(), "name": os.path.basename(repository_absolute_path)} - ], - } - - return initialize_params - - def _start_server(self): - """ - Starts the Pyright Language Server and waits for initial workspace analysis to complete. - - This prevents zombie processes by ensuring Pyright has finished its initial background - tasks before we consider the server ready. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and workspace analysis is complete - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown cleanly - ``` - """ - - def execute_client_command_handler(params): - return [] - - def do_nothing(params): - return - - def window_log_message(msg): - """ - Monitor Pyright's log messages to detect when initial analysis is complete. - 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) - - # 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) - self.found_source_files = True - self.analysis_complete.set() - self.completions_available.set() - - def check_experimental_status(params): - """ - 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) - if not self.found_source_files: - self.analysis_complete.set() - self.completions_available.set() - - # Set up notification handlers - 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) - self.server.on_notification("experimental/serverStatus", check_experimental_status) - - self.logger.log("Starting pyright-langserver server process", logging.INFO) - 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, - ) - init_response = self.server.send.initialize(initialize_params) - self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) - - # Verify that the server supports our required features - assert "textDocumentSync" in init_response["capabilities"] - assert "completionProvider" in init_response["capabilities"] - assert "definitionProvider" in init_response["capabilities"] - - # Complete the initialization handshake - self.server.notify.initialized({}) - - # 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) - if self.analysis_complete.wait(timeout=5.0): - self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) - else: - self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) - # Fallback: assume analysis is complete after timeout - self.analysis_complete.set() - self.completions_available.set() From ce6a68080a1705c55f9b7d4c1342a41f3cc1ca62 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 14:59:36 +0200 Subject: [PATCH 097/174] Adapt EclipseJDTLS to solidlsp --- .../eclipse_jdtls/eclipse_jdtls.py | 102 ++++++++---------- src/solidlsp/ls.py | 21 ++-- 2 files changed, 54 insertions(+), 69 deletions(-) diff --git a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py index a88dc1b..1b92fc4 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -2,7 +2,6 @@ Provides Java specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Java. """ -import asyncio import dataclasses import json import logging @@ -10,21 +9,20 @@ import os import pathlib import shutil import stat +import threading import uuid -from contextlib import asynccontextmanager -from typing import AsyncIterator +from pathlib import PurePath from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_settings import MultilspySettings from multilspy.multilspy_utils import FileUtils from multilspy.multilspy_utils import PlatformUtils -from pathlib import PurePath +from solidlsp.ls import SolidLanguageServer @dataclasses.dataclass @@ -43,7 +41,7 @@ class RuntimeDependencyPaths: intellisense_members_path: str -class EclipseJDTLS(LanguageServer): +class EclipseJDTLS(SolidLanguageServer): """ The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class """ @@ -135,9 +133,9 @@ class EclipseJDTLS(LanguageServer): ] ) - self.service_ready_event = asyncio.Event() - self.intellicode_enable_command_available = asyncio.Event() - self.initialize_searcher_command_available = asyncio.Event() + self.service_ready_event = threading.Event() + self.intellicode_enable_command_available = threading.Event() + self.initialize_searcher_command_available = threading.Event() super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java") @@ -324,23 +322,12 @@ class EclipseJDTLS(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["EclipseJDTLS"]: + def _start_server(self): """ - Starts the Eclipse JDTLS Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` + Starts the Eclipse JDTLS Language Server """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "textDocument/completion": @@ -358,22 +345,22 @@ class EclipseJDTLS(LanguageServer): self.intellicode_enable_command_available.set() return - async def lang_status_handler(params): + def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": self.service_ready_event.set() - async def execute_client_command_handler(params): + def execute_client_command_handler(params): assert params["command"] == "_java.reloadBundles.command" assert params["arguments"] == [] return [] - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - async def do_nothing(params): + def do_nothing(params): return self.server.on_request("client/registerCapability", register_capability_handler) @@ -384,39 +371,36 @@ class EclipseJDTLS(LanguageServer): self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) - async with super().start_server(): - self.logger.log("Starting EclipseJDTLS server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting EclipseJDTLS server process", logging.INFO) + 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, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" not in init_response["capabilities"] - assert "executeCommandProvider" not in init_response["capabilities"] + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" not in init_response["capabilities"] + assert "executeCommandProvider" not in init_response["capabilities"] - self.server.notify.initialized({}) + self.server.notify.initialized({}) - self.server.notify.workspace_did_change_configuration( - {"settings": initialize_params["initializationOptions"]["settings"]} - ) + self.server.notify.workspace_did_change_configuration( + {"settings": initialize_params["initializationOptions"]["settings"]} + ) - await self.intellicode_enable_command_available.wait() + self.intellicode_enable_command_available.wait() - java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path - assert os.path.exists(java_intellisense_members_path) - intellicode_enable_result = await self.server.send.execute_command( - { - "command": "java.intellicode.enable", - "arguments": [True, java_intellisense_members_path], - } - ) - assert intellicode_enable_result + java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path + assert os.path.exists(java_intellisense_members_path) + intellicode_enable_result = self.server.send.execute_command( + { + "command": "java.intellicode.enable", + "arguments": [True, java_intellisense_members_path], + } + ) + assert intellicode_enable_result - # TODO: Add comments about why we wait here, and how this can be optimized - await self.service_ready_event.wait() - - yield self + # TODO: Add comments about why we wait here, and how this can be optimized + self.service_ready_event.wait() diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index bafd33c..eeb96ad 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -67,7 +67,6 @@ class SolidLanguageServer(ABC): from solidlsp.language_servers.pyright_language_server.pyright_server import ( PyrightServer, ) - return PyrightServer(config, logger, repository_root_path) # It used to be jedi, but pyright is a bit faster, and also more actively maintained # Keeping the previous code for reference @@ -76,53 +75,55 @@ class SolidLanguageServer(ABC): # ) # return JediServer(config, logger, repository_root_path) + elif config.code_language == Language.JAVA: from solidlsp.language_servers.eclipse_jdtls.eclipse_jdtls import ( EclipseJDTLS, ) - return EclipseJDTLS(config, logger, repository_root_path) + elif config.code_language == Language.KOTLIN: from solidlsp.language_servers.kotlin_language_server.kotlin_language_server import ( KotlinLanguageServer, ) - return KotlinLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.RUST: from solidlsp.language_servers.rust_analyzer.rust_analyzer import ( RustAnalyzer, ) - return RustAnalyzer(config, logger, repository_root_path) + elif config.code_language == Language.CSHARP: from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp - return OmniSharp(config, logger, repository_root_path) + elif config.code_language in [Language.TYPESCRIPT, Language.JAVASCRIPT]: from solidlsp.language_servers.typescript_language_server.typescript_language_server import ( TypeScriptLanguageServer, ) return TypeScriptLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.GO: from solidlsp.language_servers.gopls.gopls import Gopls - return Gopls(config, logger, repository_root_path) + elif config.code_language == Language.RUBY: from solidlsp.language_servers.solargraph.solargraph import Solargraph - return Solargraph(config, logger, repository_root_path) + elif config.code_language == Language.DART: from solidlsp.language_servers.dart_language_server.dart_language_server import DartLanguageServer - return DartLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.CPP: from solidlsp.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer - return ClangdLanguageServer(config, logger, repository_root_path) + elif config.code_language == Language.PHP: from solidlsp.language_servers.intelephense.intelephense import Intelephense - return Intelephense(config, logger, repository_root_path) + else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) raise MultilspyException(f"Language {config.code_language} is not supported") From a271dbc818baa7e96740d59ca04c0efb9c7903f8 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:01:41 +0200 Subject: [PATCH 098/174] Adapt KotlinLanguageServer to solidlsp --- .../kotlin_language_server.py | 77 ++++++++----------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py index ca4e866..88e7151 100644 --- a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py @@ -2,23 +2,20 @@ Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. """ -import asyncio import dataclasses import json import logging import os -import stat import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator +import stat -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_utils import FileUtils from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls import SolidLanguageServer @dataclasses.dataclass @@ -31,7 +28,7 @@ class KotlinRuntimeDependencyPaths: kotlin_executable_path: str -class KotlinLanguageServer(LanguageServer): +class KotlinLanguageServer(SolidLanguageServer): """ Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. """ @@ -167,28 +164,17 @@ class KotlinLanguageServer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["KotlinLanguageServer"]: + def _start_server(self): """ - Starts the Kotlin Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` + Starts the Kotlin Language Server """ - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", do_nothing) @@ -199,29 +185,26 @@ class KotlinLanguageServer(LanguageServer): self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) - async with super().start_server(): - self.logger.log("Starting Kotlin server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting Kotlin server process", logging.INFO) + 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, - ) - init_response = await self.server.send.initialize(initialize_params) + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + 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" - assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens" - - self.server.notify.initialized({}) - self.completions_available.set() + 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" + assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens" - yield self + self.server.notify.initialized({}) + self.completions_available.set() From befd13b0ea0f8e1341fc1c9ad968ab291fa82ce4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:05:57 +0200 Subject: [PATCH 099/174] Adapt OmniSharp to solidlsp --- .../language_servers/omnisharp/omnisharp.py | 101 ++++++++---------- 1 file changed, 44 insertions(+), 57 deletions(-) diff --git a/src/solidlsp/language_servers/omnisharp/omnisharp.py b/src/solidlsp/language_servers/omnisharp/omnisharp.py index df3cf0c..a449a98 100644 --- a/src/solidlsp/language_servers/omnisharp/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp/omnisharp.py @@ -2,24 +2,23 @@ Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. """ -import asyncio import json import logging import os import pathlib import stat -from contextlib import asynccontextmanager -from typing import AsyncIterator, Iterable +import threading +from typing import Iterable from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_exceptions import MultilspyException +from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_utils import FileUtils, PlatformUtils, PlatformId, DotnetVersion +from solidlsp.ls import SolidLanguageServer def breadth_first_file_scan(root) -> Iterable[str]: @@ -55,7 +54,7 @@ def find_least_depth_sln_file(root_dir) -> str | None: return None -class OmniSharp(LanguageServer): +class OmniSharp(SolidLanguageServer): """ Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. """ @@ -107,8 +106,9 @@ class OmniSharp(LanguageServer): config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp" ) - self.definition_available = asyncio.Event() - self.references_available = asyncio.Event() + self.server_ready = threading.Event() + self.definition_available = threading.Event() + self.references_available = threading.Event() @override def is_ignored_dirname(self, dirname: str) -> bool: @@ -205,22 +205,12 @@ class OmniSharp(LanguageServer): return omnisharp_executable_path, razor_omnisharp_dll_path - @asynccontextmanager - async def start_server(self) -> AsyncIterator["OmniSharp"]: + def _start_server(self): """ - Starts the Omnisharp Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown + Starts the Omnisharp Language Server """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "textDocument/definition": @@ -230,7 +220,7 @@ class OmniSharp(LanguageServer): if registration["method"] == "textDocument/completion": self.completions_available.set() - async def lang_status_handler(params): + def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? @@ -238,20 +228,20 @@ class OmniSharp(LanguageServer): # self.service_ready_event.set() pass - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): if params["quiescent"] == True: self.server_ready.set() - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - async def workspace_configuration_handler(params): + def workspace_configuration_handler(params): # TODO: We do not know the appropriate way to handle this request. Should ideally contact the OmniSharp dev team return [ { @@ -373,34 +363,31 @@ class OmniSharp(LanguageServer): self.server.on_notification("experimental/serverStatus", check_experimental_status) self.server.on_request("workspace/configuration", workspace_configuration_handler) - async with super().start_server(): - self.logger.log("Starting OmniSharp server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting OmniSharp server process", logging.INFO) + 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, - ) - init_response = await self.server.send.initialize(initialize_params) - self.server.notify.initialized({}) - with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), "r", encoding="utf-8") as f: - self.server.notify.workspace_did_change_configuration({ - "settings": json.load(f) - }) - assert "capabilities" in init_response - if ( - "definitionProvider" in init_response["capabilities"] - and init_response["capabilities"]["definitionProvider"] - ): - self.definition_available.set() - if ( - "referencesProvider" in init_response["capabilities"] - and init_response["capabilities"]["referencesProvider"] - ): - self.references_available.set() + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + self.server.notify.initialized({}) + with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), "r", encoding="utf-8") as f: + self.server.notify.workspace_did_change_configuration({ + "settings": json.load(f) + }) + assert "capabilities" in init_response + if ( + "definitionProvider" in init_response["capabilities"] + and init_response["capabilities"]["definitionProvider"] + ): + self.definition_available.set() + if ( + "referencesProvider" in init_response["capabilities"] + and init_response["capabilities"]["referencesProvider"] + ): + self.references_available.set() - await self.definition_available.wait() - await self.references_available.wait() - - yield self + self.definition_available.wait() + self.references_available.wait() From cf7e4eadcca78bef3de19eebfb96431106940e99 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:09:14 +0200 Subject: [PATCH 100/174] Adapt RustAnalyzer to solidlsp --- .../rust_analyzer/rust_analyzer.py | 86 ++++++++----------- 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py index 65f3598..eb072c4 100644 --- a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py @@ -2,27 +2,25 @@ Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. """ -import asyncio import json import logging import os -import stat import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator +import stat +import threading from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_utils import FileUtils from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls import SolidLanguageServer -class RustAnalyzer(LanguageServer): +class RustAnalyzer(SolidLanguageServer): """ Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. """ @@ -39,8 +37,11 @@ class RustAnalyzer(LanguageServer): ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path), "rust", ) - self.server_ready = asyncio.Event() - + self.server_ready = threading.Event() + self.service_ready_event = threading.Event() + self.initialize_searcher_command_available = threading.Event() + self.resolve_main_method_available = threading.Event() + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["target"] @@ -108,22 +109,12 @@ class RustAnalyzer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["RustAnalyzer"]: + def _start_server(self): """ - Starts the Rust Analyzer Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown + Starts the Rust Analyzer Language Server """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "workspace/executeCommand": @@ -131,24 +122,24 @@ class RustAnalyzer(LanguageServer): self.resolve_main_method_available.set() return - async def lang_status_handler(params): + def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": self.service_ready_event.set() - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): if params["quiescent"] == True: self.server_ready.set() - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", register_capability_handler) @@ -160,26 +151,23 @@ class RustAnalyzer(LanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting RustAnalyzer server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting RustAnalyzer server process", logging.INFO) + 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, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "resolveProvider": True, - "triggerCharacters": [":", ".", "'", "("], - "completionItem": {"labelDetailsSupport": True}, - } - self.server.notify.initialized({}) - self.completions_available.set() + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "resolveProvider": True, + "triggerCharacters": [":", ".", "'", "("], + "completionItem": {"labelDetailsSupport": True}, + } + self.server.notify.initialized({}) + self.completions_available.set() - await self.server_ready.wait() - - yield self + self.server_ready.wait() From 2c1c26ae3f0eff9d64b88106a12dc9be04795834 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:15:08 +0200 Subject: [PATCH 101/174] Adapt TypeScriptLanguageServer to solidlsp --- .../typescript_language_server.py | 77 +++++++++---------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py index 479936c..e17a098 100644 --- a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py @@ -2,25 +2,23 @@ Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. """ -import asyncio import json import logging import os import pathlib import shutil import subprocess -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager +import threading from time import sleep from overrides import override -from multilspy.language_server import LanguageServer from multilspy.lsp_protocol_handler.lsp_types import InitializeParams from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_utils import PlatformId, PlatformUtils +from solidlsp.ls import SolidLanguageServer # Platform-specific imports if os.name != 'nt': # Unix-like systems @@ -38,7 +36,7 @@ if not PlatformUtils.get_platform_id().value.startswith("win"): import pwd -class TypeScriptLanguageServer(LanguageServer): +class TypeScriptLanguageServer(SolidLanguageServer): """ Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. """ @@ -55,7 +53,8 @@ class TypeScriptLanguageServer(LanguageServer): ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path), "typescript", ) - self.server_ready = asyncio.Event() + self.server_ready = threading.Event() + self.initialize_searcher_command_available = threading.Event() @override def is_ignored_dirname(self, dirname: str) -> bool: @@ -153,8 +152,7 @@ class TypeScriptLanguageServer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["TypeScriptLanguageServer"]: + def _start_server(self): """ Starts the TypeScript Language Server, waits for the server to be ready and yields the LanguageServer instance. @@ -168,7 +166,7 @@ class TypeScriptLanguageServer(LanguageServer): # LanguageServer has been shutdown """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "workspace/executeCommand": @@ -178,17 +176,17 @@ class TypeScriptLanguageServer(LanguageServer): # self.resolve_main_method_available.set() return - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - async def check_experimental_status(params): + def check_experimental_status(params): """ Also listen for experimental/serverStatus as a backup signal """ @@ -203,43 +201,40 @@ class TypeScriptLanguageServer(LanguageServer): self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting TypeScript server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting TypeScript server process", logging.INFO) + 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, - ) - init_response = await self.server.send.initialize(initialize_params) + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) - # TypeScript-specific capability checks - assert init_response["capabilities"]["textDocumentSync"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": ['.', '"', "'", '/', '@', '<'], - "resolveProvider": True - } + # TypeScript-specific capability checks + assert init_response["capabilities"]["textDocumentSync"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": ['.', '"', "'", '/', '@', '<'], + "resolveProvider": True + } - self.server.notify.initialized({}) - try: - await asyncio.wait_for(self.server_ready.wait(), timeout=1.0) - except asyncio.TimeoutError: - self.logger.log("Timeout waiting for TypeScript server to become ready, proceeding anyway", logging.INFO) - # Fallback: assume server is ready after timeout - self.server_ready.set() - self.completions_available.set() - - yield self + self.server.notify.initialized({}) + if self.server_ready.wait(timeout=1.0): + self.logger.log("TypeScript server is ready", logging.INFO) + else: + self.logger.log("Timeout waiting for TypeScript server to become ready, proceeding anyway", logging.INFO) + # Fallback: assume server is ready after timeout + self.server_ready.set() + self.completions_available.set() @override # For some reason, the LS may need longer to process this, so we just retry - async def _send_references_request(self, relative_file_path: str, line: int, column: int): + def _send_references_request(self, relative_file_path: str, line: int, column: int): # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is # despite the LS having processed requests already. I don't know what causes this, but sleeping # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. # It may be that even this 1sec is not enough in larger TS projects, at some point we should find what # causes this and solve it. sleep(1) - return await super()._send_references_request(relative_file_path, line, column) + return super()._send_references_request(relative_file_path, line, column) From 1bbd5b82ac93db7cdf14cd09d954ac779c263a3c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:18:13 +0200 Subject: [PATCH 102/174] Adapt Gopls to solidlsp --- src/solidlsp/language_servers/gopls/gopls.py | 63 +++++++++----------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/src/solidlsp/language_servers/gopls/gopls.py b/src/solidlsp/language_servers/gopls/gopls.py index 0e5574a..a2ec272 100644 --- a/src/solidlsp/language_servers/gopls/gopls.py +++ b/src/solidlsp/language_servers/gopls/gopls.py @@ -1,25 +1,20 @@ -import asyncio import json import logging import os import pathlib import subprocess -from contextlib import asynccontextmanager -from typing import AsyncIterator, List +import threading from overrides import override -from multilspy import multilspy_types -from multilspy.lsp_protocol_handler import lsp_types -from multilspy.multilspy_exceptions import MultilspyException -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import Error, ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.ls import SolidLanguageServer -class Gopls(LanguageServer): +class Gopls(SolidLanguageServer): """ Provides Go specific instantiation of the LanguageServer class using gopls. """ @@ -84,7 +79,7 @@ class Gopls(LanguageServer): ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), "go", ) - self.server_ready = asyncio.Event() + self.server_ready = threading.Event() self.request_id = 0 def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: @@ -111,16 +106,15 @@ class Gopls(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["Gopls"]: + def _start_server(self): """Start gopls server process""" - async def register_capability_handler(params): + def register_capability_handler(params): return - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - async def do_nothing(params): + def do_nothing(params): return self.server.on_request("client/registerCapability", register_capability_handler) @@ -128,27 +122,24 @@ class Gopls(LanguageServer): self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) - async with super().start_server(): - self.logger.log("Starting gopls server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting gopls server process", logging.INFO) + 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, - ) - init_response = await self.server.send.initialize(initialize_params) - - # Verify server capabilities - assert "textDocumentSync" in init_response["capabilities"] - assert "completionProvider" in init_response["capabilities"] - assert "definitionProvider" in init_response["capabilities"] + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) - self.server.notify.initialized({}) - self.completions_available.set() + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] - # gopls server is typically ready immediately after initialization - self.server_ready.set() - await self.server_ready.wait() + self.server.notify.initialized({}) + self.completions_available.set() - yield self + # gopls server is typically ready immediately after initialization + self.server_ready.set() + self.server_ready.wait() From ddfa68b4da0e61ec4e67a9d5027d23a3ab395a5d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:21:22 +0200 Subject: [PATCH 103/174] Adapt Solargraph to solidlsp --- .../language_servers/solargraph/solargraph.py | 91 ++++++++----------- 1 file changed, 39 insertions(+), 52 deletions(-) diff --git a/src/solidlsp/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph/solargraph.py index 099ff9d..d036ed1 100644 --- a/src/solidlsp/language_servers/solargraph/solargraph.py +++ b/src/solidlsp/language_servers/solargraph/solargraph.py @@ -3,26 +3,23 @@ Provides Ruby specific instantiation of the LanguageServer class using Solargrap Contains various configurations and settings specific to Ruby. """ -import asyncio import json import logging import os +import pathlib import stat import subprocess -import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator, override +import threading +from typing import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils, PlatformId +from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.ls import SolidLanguageServer -class Solargraph(LanguageServer): +class Solargraph(SolidLanguageServer): """ Provides Ruby specific instantiation of the LanguageServer class using Solargraph. Contains various configurations and settings specific to Ruby. @@ -41,8 +38,11 @@ class Solargraph(LanguageServer): ProcessLaunchInfo(cmd=f"{solargraph_executable_path} stdio", cwd=repository_root_path), "ruby", ) - self.server_ready = asyncio.Event() - + self.server_ready = threading.Event() + self.service_ready_event = threading.Event() + self.initialize_searcher_command_available = threading.Event() + self.resolve_main_method_available = threading.Event() + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["vendor"] @@ -115,22 +115,12 @@ class Solargraph(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["Solargraph"]: + def _start_server(self): """ - Starts the Solargraph Language Server for Ruby, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown + Starts the Solargraph Language Server for Ruby """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "workspace/executeCommand": @@ -138,20 +128,20 @@ class Solargraph(LanguageServer): self.resolve_main_method_available.set() return - async def lang_status_handler(params): + def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": self.service_ready_event.set() - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", register_capability_handler) @@ -162,28 +152,25 @@ class Solargraph(LanguageServer): self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) - async with super().start_server(): - self.logger.log("Starting solargraph server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting solargraph server process", logging.INFO) + 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, - ) - self.logger.log(f"Sending init params: {json.dumps(initialize_params, indent=4)}", logging.INFO) - init_response = await self.server.send.initialize(initialize_params) - self.logger.log(f"Received init response: {init_response}", logging.INFO) - assert init_response["capabilities"]["textDocumentSync"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "resolveProvider": True, - "triggerCharacters": [".", ":", "@"], - } - self.server.notify.initialized({}) - self.completions_available.set() + 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) + init_response = self.server.send.initialize(initialize_params) + self.logger.log(f"Received init response: {init_response}", logging.INFO) + assert init_response["capabilities"]["textDocumentSync"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "resolveProvider": True, + "triggerCharacters": [".", ":", "@"], + } + self.server.notify.initialized({}) + self.completions_available.set() - self.server_ready.set() - await self.server_ready.wait() - - yield self + self.server_ready.set() + self.server_ready.wait() From 50116962a0f831a3d854f24bb39262b1f88144e6 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:23:26 +0200 Subject: [PATCH 104/174] Adapt ClangdLanguageServer to solidlsp --- .../clangd_language_server.py | 74 +++++++++---------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py index e28aa2c..0ee5f47 100644 --- a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py @@ -2,25 +2,23 @@ Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. """ -import asyncio import json import logging import os -import stat import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator +import stat +import threading -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_utils import FileUtils from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls import SolidLanguageServer -class ClangdLanguageServer(LanguageServer): +class ClangdLanguageServer(SolidLanguageServer): """ Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly. @@ -39,7 +37,10 @@ class ClangdLanguageServer(LanguageServer): ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), "cpp", ) - self.server_ready = asyncio.Event() + self.server_ready = threading.Event() + self.service_ready_event = threading.Event() + self.initialize_searcher_command_available = threading.Event() + self.resolve_main_method_available = threading.Event() def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: """ @@ -112,8 +113,7 @@ class ClangdLanguageServer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["ClangdLanguageServer"]: + def _start_server(self): """ Starts the Clangd Language Server, waits for the server to be ready and yields the LanguageServer instance. @@ -126,7 +126,7 @@ class ClangdLanguageServer(LanguageServer): # Shutdown the LanguageServer on exit from scope # LanguageServer has been shutdown """ - async def register_capability_handler(params): + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "workspace/executeCommand": @@ -134,24 +134,24 @@ class ClangdLanguageServer(LanguageServer): self.resolve_main_method_available.set() return - async def lang_status_handler(params): + def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": self.service_ready_event.set() - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): if params["quiescent"] == True: self.server_ready.set() - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", register_capability_handler) @@ -163,28 +163,26 @@ class ClangdLanguageServer(LanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting Clangd server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting Clangd server process", logging.INFO) + 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, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": ['.', '<', '>', ':', '"', '/', '*'], - "resolveProvider": False, - } + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": ['.', '<', '>', ':', '"', '/', '*'], + "resolveProvider": False, + } - self.server.notify.initialized({}) + self.server.notify.initialized({}) - self.completions_available.set() - # set ready flag - self.server_ready.set() - await self.server_ready.wait() + self.completions_available.set() + # set ready flag + self.server_ready.set() + self.server_ready.wait() - yield self From 027edf5b4db628eb0f6cf92c7027ff4cbecf69a8 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:25:15 +0200 Subject: [PATCH 105/174] Adapt DartLanguageServer to solidlsp --- .../dart_language_server.py | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py index cd8ea51..2a0c4e6 100644 --- a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py @@ -1,17 +1,16 @@ -from contextlib import asynccontextmanager +import json import logging import os import pathlib -import shutil import stat -from typing import AsyncIterator -from multilspy.language_server import LanguageServer + from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -import json +from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.ls import SolidLanguageServer -class DartLanguageServer(LanguageServer): +class DartLanguageServer(SolidLanguageServer): """ Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. """ @@ -89,22 +88,21 @@ class DartLanguageServer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["DartLanguageServer"]: + def _start_server(self): """ Start the language server and yield when the server is ready. """ - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): pass - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", do_nothing) @@ -120,24 +118,21 @@ class DartLanguageServer(LanguageServer): "experimental/serverStatus", check_experimental_status ) - async with super().start_server(): - self.logger.log( - "Starting dart-language-server server process", logging.INFO - ) - await 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, - ) - init_response = await self.server.send_request( - "initialize", initialize_params - ) - self.logger.log( - f"Received initialize response from dart-language-server: {init_response}", - logging.INFO, - ) + self.logger.log( + "Starting dart-language-server server process", logging.INFO + ) + 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, + ) + init_response = self.server.send_request( + "initialize", initialize_params + ) + self.logger.log( + f"Received initialize response from dart-language-server: {init_response}", + logging.INFO, + ) - self.server.notify.initialized({}) - - yield self + self.server.notify.initialized({}) From b44424a54b93a440c118768a5773b665cbf8307c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:36:13 +0200 Subject: [PATCH 106/174] Make timeout parameter optional --- src/solidlsp/ls.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index eeb96ad..9981ed1 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1,4 +1,3 @@ -import asyncio import json import logging import os @@ -15,14 +14,13 @@ from typing import Dict, Iterator, List, Optional, Tuple, Union, cast, Self import pathspec import tqdm -from overrides import override from multilspy import multilspy_types from multilspy.language_server import LSPFileBuffer, GenericDocumentSymbol, ReferenceInSymbol from multilspy.lsp_protocol_handler import lsp_types from multilspy.lsp_protocol_handler import lsp_types as LSPTypes from multilspy.lsp_protocol_handler.lsp_constants import LSPConstants -from multilspy.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind, InitializeParams +from multilspy.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind from multilspy.lsp_protocol_handler.server import ( Error, ProcessLaunchInfo, @@ -51,7 +49,7 @@ class SolidLanguageServer(ABC): return dirname.startswith('.') @classmethod - def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, timeout) -> "SolidLanguageServer": + def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, timeout: Optional[float] = None) -> "SolidLanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -63,6 +61,7 @@ class SolidLanguageServer(ABC): :param logger: The logger to use. :return LanguageServer: A language specific LanguageServer instance. """ + # TODO: Apply timeout if config.code_language == Language.PYTHON: from solidlsp.language_servers.pyright_language_server.pyright_server import ( PyrightServer, From 241d3c2346748e219e58c9051c8f96a9b24d0bf6 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:40:25 +0200 Subject: [PATCH 107/174] Fix too general exception handler for error during project activation --- src/serena/agent.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 6ced373..31fc9b4 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -215,6 +215,10 @@ class ProjectConfig(ToStringMixin): return set(ToolRegistry.get_tool_class_by_name(tool_name) for tool_name in self.excluded_tools) +class ProjectNotFoundError(Exception): + pass + + @dataclass class Project: project_root: str @@ -812,7 +816,7 @@ class SerenaAgent: if project is not None: try: self.activate_project_from_path_or_name(project) - except Exception as e: + except ProjectNotFoundError as e: log.error( f"Error activating project '{project}': {e}; Note that out-of-project configurations were migrated. " "You should now pass either --project or --project ." @@ -979,7 +983,7 @@ class SerenaAgent: log.info(f"Found registered project {project_instance.project_name} at path {project_instance.project_root}.") else: if not os.path.isdir(project_root_or_name): - raise ValueError( + raise ProjectNotFoundError( f"Project '{project_root_or_name}' not found: Not a valid project name or directory. " f"Existing project names: {self.serena_config.project_names}" ) From ac172dcab6e6d1782454388ddcd88b55b96dfdc2 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 15:36:32 +0200 Subject: [PATCH 108/174] Add global flag USE_SOLID_LSP to switch from multilspy to solidlsp --- src/serena/agent.py | 15 +++++++++++---- src/serena/constants.py | 2 ++ test/conftest.py | 9 ++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 31fc9b4..6ef76ef 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -39,7 +39,14 @@ from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_types import SymbolKind from serena import serena_version from serena.config import SerenaAgentContext, SerenaAgentMode -from serena.constants import DEFAULT_ENCODING, PROJECT_TEMPLATE_FILE, REPO_ROOT, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME +from serena.constants import ( + DEFAULT_ENCODING, + PROJECT_TEMPLATE_FILE, + REPO_ROOT, + SELENA_CONFIG_TEMPLATE_FILE, + SERENA_MANAGED_DIR_NAME, + USE_SOLID_LSP, +) from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI from serena.prompt_factory import PromptFactory, SerenaPromptFactory from serena.symbol import SymbolManager @@ -671,14 +678,14 @@ def create_ls_for_project( ) ls_logger = MultilspyLogger(log_level=log_level) log.info(f"Creating language server instance for {project_instance.project_root}.") - use_solid_ls = True - if use_solid_ls: - return SolidLanguageServer.create( + if USE_SOLID_LSP: + ls = SolidLanguageServer.create( multilspy_config, ls_logger, project_instance.project_root, timeout=ls_timeout, ) + return cast(SyncLanguageServer, ls) # TODO: Fix type else: return SyncLanguageServer.create( multilspy_config, diff --git a/src/serena/constants.py b/src/serena/constants.py index 4cbf39a..86c1b31 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -18,3 +18,5 @@ DEFAULT_MODES = ("interactive", "editing") PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.yml") SELENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml") + +USE_SOLID_LSP = True diff --git a/test/conftest.py b/test/conftest.py index 994034e..75bc8c7 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +from typing import cast import pytest from sensai.util.logging import configure @@ -7,7 +8,9 @@ from sensai.util.logging import configure from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger +from serena.constants import USE_SOLID_LSP from serena.util.file_system import GitignoreParser +from solidlsp.ls import SolidLanguageServer configure(level=logging.DEBUG) @@ -43,7 +46,11 @@ def create_ls( config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) logger = MultilspyLogger(log_level=log_level) - return SyncLanguageServer.create(config, logger, repo_path) + if USE_SOLID_LSP: + ls = SolidLanguageServer.create(config, logger, repo_path) + return cast(SyncLanguageServer, ls) # TODO: Fix type + else: + return SyncLanguageServer.create(config, logger, repo_path) def create_default_ls(language: Language) -> SyncLanguageServer: From 75354a30ca62825748b00d8b9f064c91f3ae0217 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 20:10:26 +0200 Subject: [PATCH 109/174] Refactoring of MCP creation, introducing SerenaMCPFactory --- src/serena/mcp.py | 309 ++++++++++++++++++++++++---------------------- 1 file changed, 160 insertions(+), 149 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 9cbdc1f..5097eb7 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -23,12 +23,12 @@ from mcp.server.fastmcp.server import FastMCP, Settings from mcp.server.fastmcp.tools.base import Tool as MCPTool from pydantic_settings import SettingsConfigDict from sensai.util import logging -from sensai.util.helper import mark_used from serena.agent import ( ActivateProjectTool, Project, SerenaAgent, + SerenaConfig, ToolInterface, ToolRegistry, create_serena_config, @@ -66,152 +66,169 @@ class SerenaMCPRequestContext: agent: SerenaAgent -def make_tool( - tool: ToolInterface, -) -> MCPTool: - func_name = tool.get_name() - func_doc = tool.get_apply_docstring() or "" - func_arg_metadata = tool.get_apply_fn_metadata() - is_async = False - parameters = func_arg_metadata.arg_model.model_json_schema() +class SerenaMCPFactory: + def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None): + """ + :param context: The context name or path to context file + :param project: Either an absolute path to the project directory or a name of an already registered project. + If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name + afterward. + """ + self.context = SerenaAgentContext.load(context) - docstring = docstring_parser.parse(func_doc) + # load project (if any) + project_instance: Project | None = None + if project is not None: + # Fail early if the project cannot be loaded + log.info(f"Will activate project {project} at mcp server startup") + try: + project_instance = Project.load(project) + except Exception as e: + log.error(f"Failed to load or generate project config for {project}: {e}") + raise + else: + log.warning("No project passed at startup, you will have to activate a project yourself (by asking the agent to do so).") + self.project_instance = project_instance + self.project = project - # Mount the tool description as a combination of the docstring description and - # the return value description, if it exists. - if docstring.description: - func_doc = f"{docstring.description.strip().strip('.')}." - else: - func_doc = "" - if docstring.returns and (docstring_returns_descr := docstring.returns.description): - # Only add a space before "Returns" if func_doc is not empty - prefix = " " if func_doc else "" - func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." + self.active_tool_names = self._determine_active_tool_names(self.context, self.project_instance) - # Parse the parameter descriptions from the docstring and add pass its description - # to the parameters schema. - docstring_params = {param.arg_name: param for param in docstring.params} - parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] - for parameter, properties in parameters_properties.items(): - if (param_doc := docstring_params.get(parameter)) and param_doc.description: - param_desc = f"{param_doc.description.strip().strip('.') + '.'}" - properties["description"] = param_desc[0].upper() + param_desc[1:] + self.serena_agent_process: ProcessIsolatedSerenaAgent | None = None + self.serena_dashboard_process: ProcessIsolatedDashboard | None = None - def execute_fn(**kwargs) -> str: # type: ignore - return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) + @staticmethod + def _determine_active_tool_names(context: SerenaAgentContext, project: Project) -> set[str]: + """ + Determine the names of tools that should be included in this session based on the context. + """ + tools_excluded_in_this_session = context.get_excluded_tool_classes() - return MCPTool( - fn=execute_fn, - name=func_name, - description=func_doc, - parameters=parameters, - fn_metadata=func_arg_metadata, - is_async=is_async, - context_kwarg=None, - annotations=None, - ) - - -def create_mcp_server_and_agent( - project: str | None, - host: str = "0.0.0.0", - port: int = 8000, - context: str = DEFAULT_CONTEXT, - modes: Sequence[str] = DEFAULT_MODES, - enable_web_dashboard: bool | None = None, - enable_gui_log_window: bool | None = None, - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, - trace_lsp_communication: bool | None = None, - tool_timeout: float | None = None, -) -> tuple[FastMCP, ProcessIsolatedSerenaAgent]: - """ - Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. - - :param project: "Either an absolute path to the project directory or a name of an already registered project. " - "If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name " - "afterwards. - :param host: The host to bind to - :param port: The port to bind to - :param context: The context name or path to context file - :param modes: List of mode names or paths to mode files - :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. - :param log_level: Log level. If not specified, will take the value from the serena configuration. - :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. - This is useful for debugging language server issues. - :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. - """ - mcp: FastMCP | None = None - context_instance = SerenaAgentContext.load(context) - modes_instances = [SerenaAgentMode.load(mode) for mode in modes] - - is_ide_assistant = context_instance.name == RegisteredContext.IDE_ASSISTANT.value - - project_instance: Project | None = None - if project is not None: - # Fail early if the project cannot be loaded - log.info(f"Will activate project {project} at mcp server startup") - try: - project_instance = Project.load(project) - except Exception as e: - log.error(f"Failed to load or generate project config for {project}: {e}") - raise - else: - log.warning("No project passed at startup, you will have to activate a project yourself (by asking the agent to do so).") - - tools_excluded_in_this_session = context_instance.get_excluded_tool_classes() - if is_ide_assistant and project_instance is not None: - tools_excluded_in_this_session.extend(project_instance.project_config.get_excluded_tool_classes()) # if a project has been loaded, it will be activated at startup and in ide-assistant context, # we assume that no other project will be activated in this session. # Therefore, we exclude the activate project tool - tools_excluded_in_this_session.append(ActivateProjectTool) - tool_names_excluded_in_this_session = {tool.get_name_from_cls() for tool in tools_excluded_in_this_session} + is_ide_assistant = context.name == RegisteredContext.IDE_ASSISTANT.value + if is_ide_assistant and project is not None: + tools_excluded_in_this_session.extend(project.project_config.get_excluded_tool_classes()) + tools_excluded_in_this_session.append(ActivateProjectTool) - all_tool_names = set(ToolRegistry.get_tool_names()) - tool_names_included_in_this_session = all_tool_names - tool_names_excluded_in_this_session + tool_names_excluded_in_this_session = {tool.get_name_from_cls() for tool in tools_excluded_in_this_session} - try: - # Start agent and dashboard processes (the latter only if enabled) - serena_dashboard_process = None - serena_config = create_serena_config( - enable_web_dashboard=enable_web_dashboard, - enable_gui_log_window=enable_gui_log_window, - log_level=log_level, - trace_lsp_communication=trace_lsp_communication, - tool_timeout=tool_timeout, - ) - if serena_config.web_dashboard: - serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(tool_names_included_in_this_session)) - serena_agent_process = ProcessIsolatedSerenaAgent( - project=project, serena_config=serena_config, modes=modes_instances, context=context_instance + all_tool_names = set(ToolRegistry.get_tool_names()) + tool_names_included_in_this_session = all_tool_names - tool_names_excluded_in_this_session + return tool_names_included_in_this_session + + def _make_tool(self, tool: ToolInterface) -> MCPTool: + func_name = tool.get_name() + func_doc = tool.get_apply_docstring() or "" + func_arg_metadata = tool.get_apply_fn_metadata() + is_async = False + parameters = func_arg_metadata.arg_model.model_json_schema() + + docstring = docstring_parser.parse(func_doc) + + # Mount the tool description as a combination of the docstring description and + # the return value description, if it exists. + if docstring.description: + func_doc = f"{docstring.description.strip().strip('.')}." + else: + func_doc = "" + if docstring.returns and (docstring_returns_descr := docstring.returns.description): + # Only add a space before "Returns" if func_doc is not empty + prefix = " " if func_doc else "" + func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." + + # Parse the parameter descriptions from the docstring and add pass its description + # to the parameter schema. + docstring_params = {param.arg_name: param for param in docstring.params} + parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] + for parameter, properties in parameters_properties.items(): + if (param_doc := docstring_params.get(parameter)) and param_doc.description: + param_desc = f"{param_doc.description.strip().strip('.') + '.'}" + properties["description"] = param_desc[0].upper() + param_desc[1:] + + def execute_fn(**kwargs) -> str: # type: ignore + return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) + + return MCPTool( + fn=execute_fn, + name=func_name, + description=func_doc, + parameters=parameters, + fn_metadata=func_arg_metadata, + is_async=is_async, + context_kwarg=None, + annotations=None, ) - except Exception as e: - show_fatal_exception_safe(e) - raise - - def update_tools() -> None: - """Update the tools in the MCP server - adapted for process isolation.""" - nonlocal mcp, serena_agent_process - - # Get tool names from agent - # Tools may change as a result of project or mode activation. - # NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not, - # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, - # so we need to register all potentially active tools at startup, unfortunately. + def _set_mcp_tools(self, mcp: FastMCP) -> None: + """Update the tools in the MCP server""" if mcp is not None: mcp._tool_manager._tools = {} - for tool_name in tool_names_included_in_this_session: - process_isolated_tool = ProcessIsolatedTool(process_agent=serena_agent_process, tool_name=tool_name) - mcp_tool = make_tool(process_isolated_tool) + for tool_name in self.active_tool_names: + process_isolated_tool = ProcessIsolatedTool(process_agent=self.serena_agent_process, tool_name=tool_name) + mcp_tool = self._make_tool(process_isolated_tool) mcp._tool_manager._tools[tool_name] = mcp_tool + def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: + if serena_config.web_dashboard: + self.serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(self.active_tool_names)) + self.serena_agent_process = ProcessIsolatedSerenaAgent( + project=self.project, serena_config=serena_config, modes=modes, context=self.context + ) + + def create_mcp_server( + self, + host: str = "0.0.0.0", + port: int = 8000, + modes: Sequence[str] = DEFAULT_MODES, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, + ) -> FastMCP: + """ + Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. + + :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 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. + :param log_level: Log level. If not specified, will take the value from the serena configuration. + :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. + This is useful for debugging language server issues. + :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + """ + try: + # Start agent and dashboard processes (the latter only if enabled) + serena_config = create_serena_config( + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) + modes_instances = [SerenaAgentMode.load(mode) for mode in modes] + self._instantiate_agent(serena_config, modes_instances) + + except Exception as e: + show_fatal_exception_safe(e) + raise + + # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain + # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; + # retain only FASTMCP_ prefix for already set environment variables. + Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") + + mcp_settings = Settings(lifespan=self.server_lifespan, host=host, port=port) + mcp = FastMCP(**mcp_settings.model_dump()) + return mcp + @asynccontextmanager - async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: + async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" - mark_used(mcp_server) def signal_handler(signum: int, frame: Any) -> None: log.info(f"Received signal {signum} in main process") @@ -220,6 +237,8 @@ def create_mcp_server_and_agent( def force_exit() -> None: time.sleep(2.0) # Wait 2 seconds for graceful shutdown log.warning("Forcing exit after timeout") + # noinspection PyProtectedMember + # noinspection PyUnresolvedReferences os._exit(1) threading.Thread(target=force_exit, daemon=True).start() @@ -228,12 +247,13 @@ def create_mcp_server_and_agent( sigint_singal = signal.signal(signal.SIGINT, signal_handler) sigterm_signal = signal.signal(signal.SIGTERM, signal_handler) - if serena_dashboard_process is not None: + if self.serena_dashboard_process is not None: log.info("Starting dashboard process") - serena_dashboard_process.start() + self.serena_dashboard_process.start() log.info("Starting serena agent process") - serena_agent_process.start() - update_tools() + self.serena_agent_process.start() + + self._set_mcp_tools(mcp_server) async def monitor_global_shutdown() -> None: """Monitor the global shutdown event and trigger local shutdown.""" @@ -264,23 +284,14 @@ def create_mcp_server_and_agent( with contextlib.suppress(asyncio.CancelledError): await monitor_task - serena_agent_process.stop() - if serena_dashboard_process is not None: - serena_dashboard_process.stop() + self.serena_agent_process.stop() + if self.serena_dashboard_process is not None: + self.serena_dashboard_process.stop() request_global_shutdown() log.info("Shutting down all processes") signal.signal(signal.SIGINT, sigint_singal) signal.signal(signal.SIGTERM, sigterm_signal) - # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain - # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; - # retain only FASTMCP_ prefix for already set environment variables. - Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") - - mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) - mcp = FastMCP(**mcp_settings.model_dump()) - return mcp, serena_agent_process - class ProjectType(click.ParamType): name = "[PROJECT_NAME|PROJECT_PATH]" @@ -417,12 +428,12 @@ def start_mcp_server( # This is for backward compatibility with the old CLI, should be removed in the future! project_file = project_file_arg if project_file_arg is not None else project + mcp_factory = SerenaMCPFactory(context=context, project=project_file) + # Use process isolation by default to prevent asyncio event loop contamination - mcp_server, agent = create_mcp_server_and_agent( - project=project_file, + mcp_server = mcp_factory.create_mcp_server( host=host, port=port, - context=context, modes=modes, enable_web_dashboard=enable_web_dashboard, enable_gui_log_window=enable_gui_log_window, @@ -439,6 +450,6 @@ def start_mcp_server( f"Used path: {project_file}" ) - log.info(f"Starting MCP server with config:\n{agent.serena_config}.\n Log level: {agent.serena_config.log_level}") + log.info("Starting MCP server ...") mcp_server.run(transport=transport) From aba7184e2762a67af84274c7b644d3a16c955bc4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 20:40:08 +0200 Subject: [PATCH 110/174] Fix project activation by name not working anymore #210 --- src/serena/mcp.py | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 5097eb7..c43a014 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -9,7 +9,7 @@ import signal import sys import threading import time -from collections.abc import AsyncIterator, Sequence +from collections.abc import AsyncIterator, Iterator, Sequence from contextlib import asynccontextmanager from dataclasses import dataclass from logging import Formatter, Logger, StreamHandler @@ -75,24 +75,9 @@ class SerenaMCPFactory: afterward. """ self.context = SerenaAgentContext.load(context) - - # load project (if any) - project_instance: Project | None = None - if project is not None: - # Fail early if the project cannot be loaded - log.info(f"Will activate project {project} at mcp server startup") - try: - project_instance = Project.load(project) - except Exception as e: - log.error(f"Failed to load or generate project config for {project}: {e}") - raise - else: - log.warning("No project passed at startup, you will have to activate a project yourself (by asking the agent to do so).") - self.project_instance = project_instance self.project = project - self.active_tool_names = self._determine_active_tool_names(self.context, self.project_instance) - + self.active_tool_names: list[str] | None = None self.serena_agent_process: ProcessIsolatedSerenaAgent | None = None self.serena_dashboard_process: ProcessIsolatedDashboard | None = None @@ -117,7 +102,8 @@ class SerenaMCPFactory: tool_names_included_in_this_session = all_tool_names - tool_names_excluded_in_this_session return tool_names_included_in_this_session - def _make_tool(self, tool: ToolInterface) -> MCPTool: + @staticmethod + def _make_mcp_tool(tool: ToolInterface) -> MCPTool: func_name = tool.get_name() func_doc = tool.get_apply_docstring() or "" func_arg_metadata = tool.get_apply_fn_metadata() @@ -160,21 +146,27 @@ class SerenaMCPFactory: annotations=None, ) + def _iter_tools(self) -> Iterator[ToolInterface]: + for tool_name in self.active_tool_names: + yield ProcessIsolatedTool(process_agent=self.serena_agent_process, tool_name=tool_name) + + # noinspection PyProtectedMember def _set_mcp_tools(self, mcp: FastMCP) -> None: """Update the tools in the MCP server""" if mcp is not None: mcp._tool_manager._tools = {} - for tool_name in self.active_tool_names: - process_isolated_tool = ProcessIsolatedTool(process_agent=self.serena_agent_process, tool_name=tool_name) - mcp_tool = self._make_tool(process_isolated_tool) - mcp._tool_manager._tools[tool_name] = mcp_tool + for tool in self._iter_tools(): + mcp_tool = self._make_mcp_tool(tool) + mcp._tool_manager._tools[tool.get_name()] = mcp_tool def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: - if serena_config.web_dashboard: - self.serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(self.active_tool_names)) + self.project_instance = serena_config.get_project(self.project) self.serena_agent_process = ProcessIsolatedSerenaAgent( project=self.project, serena_config=serena_config, modes=modes, context=self.context ) + self.active_tool_names = self._determine_active_tool_names(self.context, self.project_instance) + if serena_config.web_dashboard: + self.serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(self.active_tool_names)) def create_mcp_server( self, @@ -202,7 +194,6 @@ class SerenaMCPFactory: :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. """ try: - # Start agent and dashboard processes (the latter only if enabled) serena_config = create_serena_config( enable_web_dashboard=enable_web_dashboard, enable_gui_log_window=enable_gui_log_window, From d2c13fc081936f20d21e731d4852aaad896258bb Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 21:02:35 +0200 Subject: [PATCH 111/174] Add MCP factory which runs the agent and its LS in the same process again (isolation no longer required when using solidlsp) --- src/serena/agent.py | 9 ++- src/serena/mcp.py | 167 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 169 insertions(+), 7 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 6ef76ef..ef6e226 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -736,11 +736,10 @@ class SerenaAgent: :param modes: list of modes in which the agent is operating (they will be combined), None for default modes. The modes may adjust prompts, tool availability, and tool descriptions. :param serena_config: the Serena configuration or None to read the configuration from the default location. - :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. - :param log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. - :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + :param enable_web_dashboard: whether to enable the web dashboard; If None, will take the value from the Serena configuration. + :param enable_gui_log_window: whether to enable the GUI log window; If None, will take the value from the Serena configuration. + :param log_level: the log level for the GUI log window; If None, will take the value from the serena configuration. + :param tool_timeout: the timeout in seconds for tool execution. If None, will take the value from the serena configuration. """ # obtain serena configuration using the decoupled factory function self.serena_config = create_serena_config( diff --git a/src/serena/mcp.py b/src/serena/mcp.py index c43a014..2764f80 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -9,6 +9,7 @@ import signal import sys import threading import time +from abc import abstractmethod from collections.abc import AsyncIterator, Iterator, Sequence from contextlib import asynccontextmanager from dataclasses import dataclass @@ -35,7 +36,7 @@ from serena.agent import ( show_fatal_exception_safe, ) from serena.config import RegisteredContext, SerenaAgentContext, SerenaAgentMode -from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES +from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, USE_SOLID_LSP from serena.process_isolated_agent import ( ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent, @@ -77,6 +78,164 @@ class SerenaMCPFactory: self.context = SerenaAgentContext.load(context) self.project = project + @staticmethod + def _make_mcp_tool(tool: ToolInterface) -> MCPTool: + func_name = tool.get_name() + func_doc = tool.get_apply_docstring() or "" + func_arg_metadata = tool.get_apply_fn_metadata() + is_async = False + parameters = func_arg_metadata.arg_model.model_json_schema() + + docstring = docstring_parser.parse(func_doc) + + # Mount the tool description as a combination of the docstring description and + # the return value description, if it exists. + if docstring.description: + func_doc = f"{docstring.description.strip().strip('.')}." + else: + func_doc = "" + if docstring.returns and (docstring_returns_descr := docstring.returns.description): + # Only add a space before "Returns" if func_doc is not empty + prefix = " " if func_doc else "" + func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." + + # Parse the parameter descriptions from the docstring and add pass its description + # to the parameter schema. + docstring_params = {param.arg_name: param for param in docstring.params} + parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] + for parameter, properties in parameters_properties.items(): + if (param_doc := docstring_params.get(parameter)) and param_doc.description: + param_desc = f"{param_doc.description.strip().strip('.') + '.'}" + properties["description"] = param_desc[0].upper() + param_desc[1:] + + def execute_fn(**kwargs) -> str: # type: ignore + return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) + + return MCPTool( + fn=execute_fn, + name=func_name, + description=func_doc, + parameters=parameters, + fn_metadata=func_arg_metadata, + is_async=is_async, + context_kwarg=None, + annotations=None, + ) + + @abstractmethod + def _iter_tools(self) -> Iterator[ToolInterface]: + pass + + # noinspection PyProtectedMember + def _set_mcp_tools(self, mcp: FastMCP) -> None: + """Update the tools in the MCP server""" + if mcp is not None: + mcp._tool_manager._tools = {} + for tool in self._iter_tools(): + mcp_tool = self._make_mcp_tool(tool) + mcp._tool_manager._tools[tool.get_name()] = mcp_tool + + @abstractmethod + def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: + pass + + def create_mcp_server( + self, + host: str = "0.0.0.0", + port: int = 8000, + modes: Sequence[str] = DEFAULT_MODES, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, + ) -> FastMCP: + """ + Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. + + :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 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. + :param log_level: Log level. If not specified, will take the value from the serena configuration. + :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. + This is useful for debugging language server issues. + :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + """ + try: + serena_config = create_serena_config( + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) + modes_instances = [SerenaAgentMode.load(mode) for mode in modes] + self._instantiate_agent(serena_config, modes_instances) + + except Exception as e: + show_fatal_exception_safe(e) + raise + + # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain + # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; + # retain only FASTMCP_ prefix for already set environment variables. + Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") + + mcp_settings = Settings(lifespan=self.server_lifespan, host=host, port=port) + mcp = FastMCP(**mcp_settings.model_dump()) + return mcp + + @asynccontextmanager + @abstractmethod + async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: + """Manage server startup and shutdown lifecycle.""" + + +class SerenaMCPFactorySingleProcess(SerenaMCPFactory): + """ + MCP server factory where the SerenaAgent and its language server run in the same process as the MCP server + """ + + def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None): + """ + :param context: The context name or path to context file + :param project: Either an absolute path to the project directory or a name of an already registered project. + If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name + afterward. + """ + super().__init__(context=context, project=project) + self.agent: SerenaAgent | None = None + + def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: + self.agent = SerenaAgent(project=self.project, serena_config=serena_config, context=self.context, modes=modes) + + def _iter_tools(self) -> Iterator[ToolInterface]: + yield from self.agent.get_exposed_tool_instances() + + @asynccontextmanager + async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: + self._set_mcp_tools(mcp_server) + yield + + +class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): + """ + MCP server factory with process isolation for the SerenaAgent and its language server; they run in a separate process + from the MCP server. + """ + + def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None): + """ + :param context: The context name or path to context file + :param project: Either an absolute path to the project directory or a name of an already registered project. + If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name + afterward. + """ + super().__init__(context=context, project=project) + self.active_tool_names: list[str] | None = None self.serena_agent_process: ProcessIsolatedSerenaAgent | None = None self.serena_dashboard_process: ProcessIsolatedDashboard | None = None @@ -419,7 +578,11 @@ def start_mcp_server( # This is for backward compatibility with the old CLI, should be removed in the future! project_file = project_file_arg if project_file_arg is not None else project - mcp_factory = SerenaMCPFactory(context=context, project=project_file) + if USE_SOLID_LSP: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) + else: + # using multilspy requires process isolation to prevent asyncio contamination + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) # Use process isolation by default to prevent asyncio event loop contamination mcp_server = mcp_factory.create_mcp_server( From a2855bb8f995cd88e9dfed0d17848b560a432f05 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 21:09:14 +0200 Subject: [PATCH 112/174] Adapt JediServer to solidlsp --- .../jedi_language_server/jedi_server.py | 66 +++++++------------ 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py index 28c9d91..9a4dbc8 100644 --- a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py +++ b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py @@ -6,19 +6,17 @@ import json import logging import os import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator, Tuple from overrides import override -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.ls import SolidLanguageServer -class JediServer(LanguageServer): +class JediServer(SolidLanguageServer): """ Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ @@ -63,33 +61,22 @@ class JediServer(LanguageServer): return d - @asynccontextmanager - async def start_server(self) -> AsyncIterator["JediServer"]: + def _start_server(self): """ - Starts the JEDI Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` + Starts the JEDI Language Server """ - async def execute_client_command_handler(params): + def execute_client_command_handler(params): return [] - async def do_nothing(params): + def do_nothing(params): return - async def check_experimental_status(params): + def check_experimental_status(params): if params["quiescent"] == True: self.completions_available.set() - async def window_log_message(msg): + def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", do_nothing) @@ -101,23 +88,20 @@ class JediServer(LanguageServer): self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) - async with super().start_server(): - self.logger.log("Starting jedi-language-server server process", logging.INFO) - await self.server.start() - initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log("Starting jedi-language-server server process", logging.INFO) + 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, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": [".", "'", '"'], - "resolveProvider": True, - } + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": [".", "'", '"'], + "resolveProvider": True, + } - self.server.notify.initialized({}) - - yield self + self.server.notify.initialized({}) From c3bb2e3d772ad8daf167d8af525b207ab4612673 Mon Sep 17 00:00:00 2001 From: Serena Date: Sat, 21 Jun 2025 21:25:16 +0200 Subject: [PATCH 113/174] Change dashboard to use Flask instead of fastapi --- pyproject.toml | 2 +- src/serena/dashboard.py | 49 +++++++++++++++++++------- src/serena/process_isolated_agent.py | 25 +++++--------- uv.lock | 51 +++++++++++++++++++++++----- 4 files changed, 87 insertions(+), 40 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c7bc40a..9740f79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "overrides>=7.7.0,<8", "python-dotenv>=1.0.0, <2", "mcp>=1.5.0", - "fastapi>=0.115.12", + "flask>=3.0.0", "sensai-utils>=1.4.0", "pydantic>=2.10.6", "types-pyyaml>=6.0.12.20241230", diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 22e3495..fec3cda 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -4,9 +4,7 @@ import socket import threading from collections.abc import Callable -import uvicorn -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles +from flask import Flask, request, send_from_directory from pydantic import BaseModel from sensai.util import logging @@ -74,7 +72,7 @@ class SerenaDashboardAPI: self._memory_log_handler = memory_log_handler self._tool_names = tool_names self._shutdown_callback = shutdown_callback - self._app = FastAPI(title="Serena Dashboard") + self._app = Flask(__name__) self._setup_routes() @property @@ -82,21 +80,46 @@ class SerenaDashboardAPI: return self._memory_log_handler def _setup_routes(self) -> None: - self._app.mount("/dashboard", StaticFiles(directory=SERENA_DASHBOARD_DIR), name="dashboard") + # Static files + @self._app.route("/dashboard/") + def serve_dashboard(filename): + return send_from_directory(SERENA_DASHBOARD_DIR, filename) - self._app.add_api_route("/get_log_messages", self._get_log_messages, methods=["POST"], response_model=ResponseLog) - self._app.add_api_route("/get_tool_names", self._get_tool_names, methods=["GET"], response_model=ResponseToolNames) - self._app.add_api_route("/shutdown", self._shutdown, methods=["PUT"]) + @self._app.route("/dashboard/") + def serve_dashboard_index(): + return send_from_directory(SERENA_DASHBOARD_DIR, "index.html") - async def _get_log_messages(self, request: RequestLog) -> ResponseLog: + # API routes + @self._app.route("/get_log_messages", methods=["POST"]) + def get_log_messages(): + request_data = request.get_json() + if not request_data: + request_log = RequestLog() + else: + request_log = RequestLog.model_validate(request_data) + + result = self._get_log_messages(request_log) + return result.model_dump() + + @self._app.route("/get_tool_names", methods=["GET"]) + def get_tool_names(): + result = self._get_tool_names() + return result.model_dump() + + @self._app.route("/shutdown", methods=["PUT"]) + def shutdown(): + self._shutdown() + return {"status": "shutting down"} + + def _get_log_messages(self, request_log: RequestLog) -> ResponseLog: all_messages = self._memory_log_handler.get_log_messages() - requested_messages = all_messages[request.start_idx :] if request.start_idx <= len(all_messages) else [] + requested_messages = all_messages[request_log.start_idx :] if request_log.start_idx <= len(all_messages) else [] return ResponseLog(messages=requested_messages, max_idx=len(all_messages) - 1) - async def _get_tool_names(self) -> ResponseToolNames: + def _get_tool_names(self) -> ResponseToolNames: return ResponseToolNames(tool_names=self._tool_names) - async def _shutdown(self) -> None: + def _shutdown(self) -> None: log.info("Shutting down Serena") if self._shutdown_callback: self._shutdown_callback() @@ -125,7 +148,7 @@ class SerenaDashboardAPI: """ Runs the dashboard on the given host and port and returns the port number. """ - uvicorn.run(self._app, host=host, port=port, workers=1, log_config=None, log_level="critical") + 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]: diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 48d5dc8..b606c94 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -3,6 +3,7 @@ import contextlib import logging import multiprocessing import os +import threading import traceback import webbrowser from enum import StrEnum @@ -12,7 +13,6 @@ from multiprocessing.sharedctypes import Synchronized from multiprocessing.synchronize import Event as EventClass from typing import Any, Literal, Self -import uvicorn from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry @@ -79,30 +79,21 @@ def _dashboard_worker( port = api._find_first_free_port(0x5EDA) port_value.value = port - # Start server first, then log processing - config = uvicorn.Config( - app=api._app, - host="0.0.0.0", - port=port, - workers=1, - log_config=None, - log_level="critical", - ) - server = uvicorn.Server(config) + # Start Flask server in a thread + def run_flask_server(): + api._app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False, threaded=True) + + server_thread = threading.Thread(target=run_flask_server, daemon=True) + server_thread.start() - # Serve and await shutdown signal - server_task = asyncio.create_task(server.serve()) shutdown_task = asyncio.create_task(_monitor_shutdown()) logging_loop_task = asyncio.create_task(_process_logs(api)) dashboard_ready_event.set() done, pending = await asyncio.wait( - [server_task, shutdown_task, logging_loop_task], + [shutdown_task, logging_loop_task], return_when=asyncio.FIRST_COMPLETED, ) - if shutdown_task in done: - server.should_exit = True - await server_task # Cancel remaining tasks for task in pending: diff --git a/uv.lock b/uv.lock index 05834d9..c731811 100644 --- a/uv.lock +++ b/uv.lock @@ -101,6 +101,15 @@ jupyter = [ { name = "tokenize-rt" }, ] +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + [[package]] name = "cachetools" version = "5.5.2" @@ -210,17 +219,20 @@ wheels = [ ] [[package]] -name = "fastapi" -version = "0.115.12" +name = "flask" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, + { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305 }, ] [[package]] @@ -394,6 +406,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + [[package]] name = "jedi" version = "0.19.2" @@ -959,7 +980,7 @@ source = { editable = "." } dependencies = [ { name = "docstring-parser" }, { name = "dotenv" }, - { name = "fastapi" }, + { name = "flask" }, { name = "jinja2" }, { name = "joblib" }, { name = "mcp" }, @@ -1007,7 +1028,7 @@ requires-dist = [ { name = "black", extras = ["jupyter"], marker = "extra == 'dev'", specifier = ">=23.7.0" }, { name = "docstring-parser", specifier = ">=0.16" }, { name = "dotenv", specifier = ">=0.9.9" }, - { name = "fastapi", specifier = ">=0.115.12" }, + { name = "flask", specifier = ">=3.0.0" }, { name = "google-genai", marker = "extra == 'google'", specifier = ">=1.8.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", marker = "extra == 'dev'" }, @@ -1299,3 +1320,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, ] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, +] From e32caeb9d6b9297cfaf6a4de1574434535d8e97a Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 21:31:30 +0200 Subject: [PATCH 114/174] Disable werkzeug logs --- src/serena/dashboard.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index fec3cda..47bbe69 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -12,6 +12,9 @@ from serena.constants import SERENA_DASHBOARD_DIR log = logging.getLogger(__name__) +# disable Werkzeug's logging to avoid cluttering the output +logging.getLogger("werkzeug").setLevel(logging.WARNING) + class MemoryLogHandler(logging.Handler): def __init__(self, level: int = logging.NOTSET) -> None: From 5ae127722ff393b27b34d85fc7bbaa6756aaefd5 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 21:46:26 +0200 Subject: [PATCH 115/174] Prevent Flask from printing a startup message --- src/serena/dashboard.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 47bbe69..ee59aba 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -151,6 +151,11 @@ class SerenaDashboardAPI: """ Runs the dashboard on the given host and port and returns the port number. """ + # patch flask.cli.show_server to avoid printing the server info + from flask import cli + + cli.show_server_banner = lambda *args, **kwargs: None + self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True) return port From 772bc56fc37680d053d445b0f78a588c0b2502db Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 21 Jun 2025 21:52:00 +0200 Subject: [PATCH 116/174] Update references to make_tool function in tests --- src/serena/mcp.py | 8 ++++---- test/serena/test_make_tool_process_isolation.py | 4 +++- test/serena/test_mcp.py | 4 +++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 2764f80..856ad7b 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -79,7 +79,7 @@ class SerenaMCPFactory: self.project = project @staticmethod - def _make_mcp_tool(tool: ToolInterface) -> MCPTool: + def make_mcp_tool(tool: ToolInterface) -> MCPTool: func_name = tool.get_name() func_doc = tool.get_apply_docstring() or "" func_arg_metadata = tool.get_apply_fn_metadata() @@ -132,7 +132,7 @@ class SerenaMCPFactory: if mcp is not None: mcp._tool_manager._tools = {} for tool in self._iter_tools(): - mcp_tool = self._make_mcp_tool(tool) + mcp_tool = self.make_mcp_tool(tool) mcp._tool_manager._tools[tool.get_name()] = mcp_tool @abstractmethod @@ -262,7 +262,7 @@ class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): return tool_names_included_in_this_session @staticmethod - def _make_mcp_tool(tool: ToolInterface) -> MCPTool: + def make_mcp_tool(tool: ToolInterface) -> MCPTool: func_name = tool.get_name() func_doc = tool.get_apply_docstring() or "" func_arg_metadata = tool.get_apply_fn_metadata() @@ -315,7 +315,7 @@ class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): if mcp is not None: mcp._tool_manager._tools = {} for tool in self._iter_tools(): - mcp_tool = self._make_mcp_tool(tool) + mcp_tool = self.make_mcp_tool(tool) mcp._tool_manager._tools[tool.get_name()] = mcp_tool def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: diff --git a/test/serena/test_make_tool_process_isolation.py b/test/serena/test_make_tool_process_isolation.py index fb13f86..eb9b3f0 100644 --- a/test/serena/test_make_tool_process_isolation.py +++ b/test/serena/test_make_tool_process_isolation.py @@ -4,10 +4,12 @@ import pytest from mcp.server.fastmcp.tools.base import Tool as MCPTool from serena.agent import SerenaAgent, ToolRegistry -from serena.mcp import make_tool +from serena.mcp import SerenaMCPFactory from serena.process_isolated_agent import ProcessIsolatedSerenaAgent, ProcessIsolatedTool from test.serena.test_serena_agent import SerenaConfigForTests +make_tool = SerenaMCPFactory.make_mcp_tool + @pytest.fixture def in_memory_config(): diff --git a/test/serena/test_mcp.py b/test/serena/test_mcp.py index eed27a7..8ffb2c2 100644 --- a/test/serena/test_mcp.py +++ b/test/serena/test_mcp.py @@ -4,7 +4,9 @@ import pytest from mcp.server.fastmcp.tools.base import Tool as MCPTool from serena.agent import Tool, ToolRegistry -from serena.mcp import make_tool +from serena.mcp import SerenaMCPFactory + +make_tool = SerenaMCPFactory.make_mcp_tool class BaseMockTool(Tool): From ceae28d286b5d647d4e07b9e31407534c86e71eb Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 01:09:40 +0200 Subject: [PATCH 117/174] Fix hanging of tool executions that use shell execution #212 Affected ExecuteShellCommandTool and GetCurrentConfigTool (because of git status). Solution: Adjust parameters for subprocess creation --- src/serena/__init__.py | 12 +++++++----- src/serena/util/git.py | 21 +++++++++++++++++++++ src/serena/util/shell.py | 20 +++++++++++++++++++- 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 src/serena/util/git.py diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 1a6b238..9ee9fb1 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,17 +1,19 @@ __version__ = "2025-05-21" +import logging + +log = logging.getLogger(__name__) + def serena_version() -> str: """ :return: the version of the package, including git status if available. """ + from serena.util.git import get_git_status + version = __version__ try: - from sensai.util.git import git_status - from sensai.util.logging import LoggingDisabledContext - - with LoggingDisabledContext(): - git_status = git_status() + git_status = get_git_status() version += f"-{git_status.commit[:8]}" if not git_status.is_clean: version += "-dirty" diff --git a/src/serena/util/git.py b/src/serena/util/git.py new file mode 100644 index 0000000..b46d17e --- /dev/null +++ b/src/serena/util/git.py @@ -0,0 +1,21 @@ +import logging + +from sensai.util.git import GitStatus + +from .shell import subprocess_check_output + +log = logging.getLogger(__name__) + + +def get_git_status() -> GitStatus | None: + try: + commit_hash = subprocess_check_output(["git", "rev-parse", "HEAD"]) + unstaged = bool(subprocess_check_output(["git", "diff", "--name-only"])) + staged = bool(subprocess_check_output(["git", "diff", "--staged", "--name-only"])) + untracked = bool(subprocess_check_output(["git", "ls-files", "--others", "--exclude-standard"])) + return GitStatus( + commit=commit_hash, has_unstaged_changes=unstaged, has_staged_uncommitted_changes=staged, has_untracked_files=untracked + ) + except Exception as e: + log.error("Error determining Git status", exc_info=e) + return None diff --git a/src/serena/util/shell.py b/src/serena/util/shell.py index d940384..faef516 100644 --- a/src/serena/util/shell.py +++ b/src/serena/util/shell.py @@ -24,11 +24,14 @@ def execute_shell_command(command: str, cwd: str | None = None, capture_stderr: if cwd is None: cwd = os.getcwd() + is_windows = platform.system() == "Windows" process = subprocess.Popen( command, - shell=platform.system() != "Windows", + shell=not is_windows, + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE if capture_stderr else None, + creationflags=subprocess.CREATE_NO_WINDOW if is_windows else 0, text=True, encoding="utf-8", errors="replace", @@ -37,3 +40,18 @@ def execute_shell_command(command: str, cwd: str | None = None, capture_stderr: stdout, stderr = process.communicate() return ShellCommandResult(stdout=stdout, stderr=stderr, return_code=process.returncode, cwd=cwd) + + +def subprocess_check_output(args: list[str], encoding: str = "utf-8", strip: bool = True, timeout: float | None = None) -> str: + kwargs = { + "stdin": subprocess.DEVNULL, + "stderr": subprocess.PIPE, + "timeout": timeout, + "env": os.environ.copy(), + } + if platform.system() == "Windows": + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + output = subprocess.check_output(args, **kwargs).decode(encoding) + if strip: + output = output.strip() + return output From 383a5093700fec9153c28e8b227f5d0088185c58 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 10:23:18 +0200 Subject: [PATCH 118/174] Release 2025-06-20 --- CHANGELOG.md | 22 +++++++++------------- src/serena/__init__.py | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a52ff43..84b8f78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,8 @@ # Latest -Status of the main branch. Changes prior to the next official version change will appear here. -## Highlights +Status of the `main` branch. Changes prior to the next official version change will appear here. -### This version is a major change and improvement of Serena +# 2025-06-20 * **Overhaul and major improvement of editing tools!** This represents a very important change in Serena. Symbols can now be addressed by their `name_path` (including nested ones) @@ -17,19 +16,16 @@ Status of the main branch. Changes prior to the next official version change wil * Possibility to index your project beforehand, accelerating Serena's tools. * Initial prompt for project supported (has to be added manually for the moment) * Massive performance improvement of pattern search tool -* Fixed stability issues and deadlocks (see #170) +* Use **process isolation** to fix stability issues and deadlocks (see #170). + This uses separate process for the MCP server, the Serena agent and the dashboard in order to fix asyncio-related issues. # 2025-05-24 -## Highlights - -Important new feature: configurability of mode and context, allowing better integration in a variety of clients. -See corresponding section in readme - Serena can now be integrated in IDE assistants in a more productive way. - -You can now also do things like switching to one-shot planning mode, ask to plan something (which will create a memory), -then switch to interactive editing mode in the next conversation and work through the plan read from the memory. - -Also some improvements to prompts. +* Important new feature: **configurability of mode and context**, allowing better integration in a variety of clients. + See corresponding section in readme - Serena can now be integrated in IDE assistants in a more productive way. + You can now also do things like switching to one-shot planning mode, ask to plan something (which will create a memory), + then switch to interactive editing mode in the next conversation and work through the plan read from the memory. +* Some improvements to prompts. # 2025-05-21 diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 1a6b238..7b7f7b9 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2025-05-21" +__version__ = "2025-06-21" def serena_version() -> str: From 34d844506f2d6717a775ef1da8c90156e9ed02a7 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 10:40:26 +0200 Subject: [PATCH 119/174] Update change log --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b8f78..51a8fc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* **Reduce the use of asyncio to a minimum**, improving stability and reducing the need for workarounds + * Switch to newly developed fully synchronous LSP library `solidlsp` (derived from `multilspy`) + * Switch from fastapi (which uses asyncio) to Flask in the Serena dashboard + * The MCP server is the only asynchronous component now, which resolves cross-component loop contamination, + such that process isolation is no longer required. + Neither are non-graceful shutdowns on Windows. + +Fixes: +* Fix `ExecuteShellCommandTool` and `GetCurrentConfigTool` hanging on Windows +* Fix project activation by name via `--project` not working (was broken in previous release) + # 2025-06-20 * **Overhaul and major improvement of editing tools!** From 8958a2f6417eb47030e1d03459384dbf50eb2c34 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 22 Jun 2025 11:21:12 +0200 Subject: [PATCH 120/174] Add comprehensive solid-lsp architecture memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the solid-lsp implementation as a deadlock-resistant replacement for multilspy, designed to solve asyncio contamination issues in MCP environments. Key points covered: - Root cause analysis of multilspy deadlocks - Solid-lsp architectural improvements - Process isolation no longer needed - Language server compatibility - Migration strategy 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .serena/memories/solid-lsp-architecture.md | 161 +++++++++++++++++++++ src/serena/constants.py | 4 + 2 files changed, 165 insertions(+) create mode 100644 .serena/memories/solid-lsp-architecture.md diff --git a/.serena/memories/solid-lsp-architecture.md b/.serena/memories/solid-lsp-architecture.md new file mode 100644 index 0000000..4a334cf --- /dev/null +++ b/.serena/memories/solid-lsp-architecture.md @@ -0,0 +1,161 @@ +# Solid-LSP: Deadlock-Free Language Server Architecture + +## Overview + +Solid-LSP (`src/solidlsp/`) is a **simplified, deadlock-resistant** reimplementation of the language server communication layer, designed to replace the problematic `multilspy` architecture that suffered from asyncio contamination issues when used with MCP servers. + +## The Problem Solid-LSP Solves + +### Root Cause: Asyncio Context Contamination +The original `multilspy` implementation created **asyncio deadlocks** in MCP environments due to: + +1. **Dual Event Loop Interference**: MCP server runs its own asyncio event loop, while SerenaAgent creates separate asyncio loops for language servers and dashboard +2. **Coroutine Leakage**: When MCP server calls SerenaAgent tools, coroutines created by `multilspy.LanguageServer.request_full_symbol_tree()` were leaked into the MCP server's asyncio context but never awaited +3. **Resource Exhaustion**: Accumulated unawaited coroutines in the MCP server's event loop eventually caused blocking and apparent "hangs" +4. **MCP-Specific Issue**: Only occurred when using MCP clients, not in direct script execution + +### Evidence from Logs +``` +INFO mcp.server.lowlevel.server:_handle_message:524 - Warning: RuntimeWarning: coroutine 'LanguageServer.request_full_symbol_tree' was never awaited +``` + +## Solid-LSP Architecture + +### Core Design Principles + +1. **Simplified Protocol Handling**: Eliminates the complex `lsp_protocol_handler` layer from `multilspy` +2. **Direct Async Management**: More explicit control over async operations without complex threading abstractions +3. **Single Process Operation**: Designed to work directly in the MCP server process without requiring process isolation +4. **Clean Asyncio Boundaries**: Prevents coroutine leakage between async contexts + +### Key Architectural Differences from multilspy + +#### 1. Simplified Handler Architecture +- **multilspy**: Complex `LanguageServerHandler` with extensive async task management and event loops +- **solid-lsp**: Streamlined `SolidLanguageServerHandler` with direct process management + +#### 2. Protocol Simplification +- **No separate protocol handler package**: Direct LSP communication without abstraction layers +- **Reduced complexity**: Fewer components and dependencies +- **Same language server support**: Identical `language_servers/` implementations for all supported languages + +#### 3. Enhanced Process Control +**solid-lsp** provides additional methods for finer process control: +- `_start_server_process()`: Direct process control +- `_start_server()`: Server initialization +- `_server_context` attribute: Enhanced server state management + +## Process Isolation: No Longer Required + +### Key Benefit: Single Process Operation + +With solid-lsp, **process isolation is no longer needed** and is **disabled by default**: + +```python +# src/serena/mcp.py:581-585 +if USE_SOLID_LSP: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) +else: + # using multilspy requires process isolation to prevent asyncio contamination + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) +``` + +### Benefits of Single Process Operation +1. **Lower Resource Usage**: No separate processes for SerenaAgent and language servers +2. **Reduced Latency**: Direct method calls instead of inter-process communication +3. **Simplified Architecture**: No IPC overhead or process management complexity +4. **Better Performance**: Eliminated serialization/deserialization overhead +5. **Easier Debugging**: All components in same process, simplified stack traces + +### Why multilspy Required Process Isolation +- **Asyncio Contamination**: multilspy's async patterns leaked coroutines into MCP server context +- **Mandatory Isolation**: Process boundaries were the only way to prevent deadlocks +- **Performance Penalty**: IPC overhead was necessary for stability + +### How solid-lsp Enables Single Process +- **Clean Async Patterns**: No coroutine leakage between MCP server and language server contexts +- **Direct Integration**: Can safely run in same process as MCP server +- **Stable Operation**: No more timeout issues or hanging operations + +## Supported Language Servers + +Solid-LSP maintains compatibility with all language servers from `multilspy`: + +### Primary Languages +- **Python**: Pyright Language Server (`src/solidlsp/language_servers/pyright_language_server/`) +- **Java**: Eclipse JDTLS (`src/solidlsp/language_servers/eclipse_jdtls/`) +- **TypeScript/JavaScript**: TypeScript Language Server (`src/solidlsp/language_servers/typescript_language_server/`) + +### Additional Languages +- **C#**: OmniSharp (`src/solidlsp/language_servers/omnisharp/`) +- **Rust**: Rust-Analyzer (`src/solidlsp/language_servers/rust_analyzer/`) +- **Go**: Gopls (`src/solidlsp/language_servers/gopls/`) +- **Ruby**: Solargraph (`src/solidlsp/language_servers/solargraph/`) +- **C++**: Clangd (`src/solidlsp/language_servers/clangd_language_server/`) +- **Dart**: Dart Language Server (`src/solidlsp/language_servers/dart_language_server/`) +- **PHP**: Intelephense (`src/solidlsp/language_servers/intelephense/`) +- **Kotlin**: Kotlin Language Server (`src/solidlsp/language_servers/kotlin_language_server/`) + +## Implementation Details + +### Core Classes + +#### `SolidLanguageServer` (`src/solidlsp/ls.py`) +- **1,634 lines**: Main language server interface +- **Enhanced methods**: Same semantic capabilities as `multilspy.LanguageServer` but with improved async handling +- **Lifecycle control**: `start()`, `stop()`, `is_running()`, `language_server()` property (similar to multilspy's `SyncLanguageServer`) + +#### `SolidLanguageServerHandler` (`src/solidlsp/ls_handler.py`) +- **512 lines**: Simplified LSP protocol handler +- **Direct process management**: More straightforward than `multilspy`'s complex async task orchestration +- **Improved cleanup**: Better resource management and process termination + +#### `SolidLspRequest` (`src/solidlsp/lsp_request.py`) +- **Simplified request handling**: Direct LSP request/response without complex protocol abstractions + +### Configuration Integration + +Solid-LSP is controlled by the `USE_SOLID_LSP` constant in `src/serena/constants.py`: + +```python +USE_SOLID_LSP = True # Default: enabled +``` + +This allows: +- **Default Operation**: solid-lsp enabled by default for stability +- **Fallback Capability**: Can switch back to multilspy + process isolation if needed +- **A/B Testing**: Easy comparison between implementations + +## Deadlock Prevention + +### How Solid-LSP Prevents Deadlocks + +1. **Simplified Async Patterns**: Reduces complexity that led to coroutine leakage +2. **Clean Boundaries**: Proper async context management prevents MCP server contamination +3. **Direct Operations**: Eliminates abstraction layers where async context could be corrupted +4. **Single Process Safety**: Designed to work safely within MCP server process + +### Verification + +The solid-lsp approach provides: +- **No more timeout issues**: Clean async boundaries prevent MCP server contamination +- **Stable MCP operation**: Language server calls no longer hang after first timeout +- **Resource efficiency**: Lower memory usage without separate processes +- **Reliable semantic tools**: `find_symbol` and other tools work consistently +- **Better Performance**: Direct method calls instead of IPC + +## Migration Strategy + +### Current Status +- **Default**: Solid-LSP is the default implementation (`USE_SOLID_LSP = True`) +- **Stable**: Resolves known deadlock issues in MCP environments +- **Compatible**: Supports all existing language servers and semantic operations +- **Performant**: Single process operation with lower overhead + +### Rollback Path +If issues arise, can fallback to multilspy with process isolation: +1. **Disable solid-lsp**: Set `USE_SOLID_LSP = False` +2. **Automatic fallback**: System automatically uses multilspy + process isolation +3. **Performance trade-off**: Higher resource usage but guaranteed isolation + +Solid-LSP represents an **architectural evolution** that eliminates the need for process isolation while maintaining all semantic capabilities and providing **better performance and stability** for MCP server deployments. \ No newline at end of file diff --git a/src/serena/constants.py b/src/serena/constants.py index 86c1b31..8b9a4dd 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -20,3 +20,7 @@ PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.y SELENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml") USE_SOLID_LSP = True +"""Whether to use Solid LSP instead of multilspy for language server operations. +Multilspy uses asyncio, which creates many problems like deadlocks and requires full process +isolation (and even then some issues remain). +""" \ No newline at end of file From 38b138bb4700ae61d329cfbaaba0a0ad87a549de Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 11:49:47 +0200 Subject: [PATCH 121/174] Fix mypy issues --- src/serena/__init__.py | 7 ++++--- src/serena/dashboard.py | 13 +++++++------ src/serena/mcp.py | 18 +++++++++++++----- src/serena/process_isolated_agent.py | 6 +++--- src/serena/util/shell.py | 2 +- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 2353c4e..1643d83 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -14,9 +14,10 @@ def serena_version() -> str: version = __version__ try: git_status = get_git_status() - version += f"-{git_status.commit[:8]}" - if not git_status.is_clean: - version += "-dirty" + if git_status is not None: + version += f"-{git_status.commit[:8]}" + if not git_status.is_clean: + version += "-dirty" except: pass return version diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index ee59aba..eb86e74 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -3,8 +3,9 @@ import queue import socket import threading from collections.abc import Callable +from typing import Any -from flask import Flask, request, send_from_directory +from flask import Flask, Response, request, send_from_directory from pydantic import BaseModel from sensai.util import logging @@ -85,16 +86,16 @@ class SerenaDashboardAPI: def _setup_routes(self) -> None: # Static files @self._app.route("/dashboard/") - def serve_dashboard(filename): + def serve_dashboard(filename: str) -> Response: return send_from_directory(SERENA_DASHBOARD_DIR, filename) @self._app.route("/dashboard/") - def serve_dashboard_index(): + def serve_dashboard_index() -> Response: return send_from_directory(SERENA_DASHBOARD_DIR, "index.html") # API routes @self._app.route("/get_log_messages", methods=["POST"]) - def get_log_messages(): + def get_log_messages() -> dict[str, Any]: request_data = request.get_json() if not request_data: request_log = RequestLog() @@ -105,12 +106,12 @@ class SerenaDashboardAPI: return result.model_dump() @self._app.route("/get_tool_names", methods=["GET"]) - def get_tool_names(): + def get_tool_names() -> dict[str, Any]: result = self._get_tool_names() return result.model_dump() @self._app.route("/shutdown", methods=["PUT"]) - def shutdown(): + def shutdown() -> dict[str, str]: self._shutdown() return {"status": "shutting down"} diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 856ad7b..28150ba 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -184,11 +184,11 @@ class SerenaMCPFactory: # retain only FASTMCP_ prefix for already set environment variables. Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") - mcp_settings = Settings(lifespan=self.server_lifespan, host=host, port=port) + mcp_settings: Settings = Settings(lifespan=self.server_lifespan, host=host, port=port) mcp = FastMCP(**mcp_settings.model_dump()) return mcp - @asynccontextmanager + @asynccontextmanager # type: ignore @abstractmethod async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" @@ -213,6 +213,7 @@ class SerenaMCPFactorySingleProcess(SerenaMCPFactory): self.agent = SerenaAgent(project=self.project, serena_config=serena_config, context=self.context, modes=modes) def _iter_tools(self) -> Iterator[ToolInterface]: + assert self.agent is not None yield from self.agent.get_exposed_tool_instances() @asynccontextmanager @@ -236,12 +237,12 @@ class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): """ super().__init__(context=context, project=project) - self.active_tool_names: list[str] | None = None + self.active_tool_names: set[str] | None = None self.serena_agent_process: ProcessIsolatedSerenaAgent | None = None self.serena_dashboard_process: ProcessIsolatedDashboard | None = None @staticmethod - def _determine_active_tool_names(context: SerenaAgentContext, project: Project) -> set[str]: + def _determine_active_tool_names(context: SerenaAgentContext, project: Project | None) -> set[str]: """ Determine the names of tools that should be included in this session based on the context. """ @@ -306,6 +307,8 @@ class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): ) def _iter_tools(self) -> Iterator[ToolInterface]: + assert self.active_tool_names is not None + assert self.serena_agent_process is not None for tool_name in self.active_tool_names: yield ProcessIsolatedTool(process_agent=self.serena_agent_process, tool_name=tool_name) @@ -319,12 +322,14 @@ class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): mcp._tool_manager._tools[tool.get_name()] = mcp_tool def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: - self.project_instance = serena_config.get_project(self.project) + if self.project is not None: + self.project_instance = serena_config.get_project(self.project) self.serena_agent_process = ProcessIsolatedSerenaAgent( project=self.project, serena_config=serena_config, modes=modes, context=self.context ) self.active_tool_names = self._determine_active_tool_names(self.context, self.project_instance) if serena_config.web_dashboard: + assert self.active_tool_names is not None self.serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(self.active_tool_names)) def create_mcp_server( @@ -399,8 +404,10 @@ class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): if self.serena_dashboard_process is not None: log.info("Starting dashboard process") + assert self.serena_dashboard_process is not None self.serena_dashboard_process.start() log.info("Starting serena agent process") + assert self.serena_agent_process is not None self.serena_agent_process.start() self._set_mcp_tools(mcp_server) @@ -578,6 +585,7 @@ def start_mcp_server( # This is for backward compatibility with the old CLI, should be removed in the future! project_file = project_file_arg if project_file_arg is not None else project + mcp_factory: SerenaMCPFactory if USE_SOLID_LSP: mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) else: diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index b606c94..081dcc6 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -80,7 +80,7 @@ def _dashboard_worker( port_value.value = port # Start Flask server in a thread - def run_flask_server(): + def run_flask_server() -> None: api._app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False, threaded=True) server_thread = threading.Thread(target=run_flask_server, daemon=True) @@ -414,10 +414,10 @@ class ProcessIsolatedSerenaAgent: # Create communication pipe parent_conn, child_conn = multiprocessing.Pipe() - self.conn = parent_conn + self.conn = parent_conn # type: ignore # Create and start worker process, passing along the dashboard's queue if available - worker = SerenaAgentWorker(child_conn) + worker = SerenaAgentWorker(child_conn) # type: ignore self.process = multiprocessing.Process(target=worker.run, args=[_global_log_queue]) self.process.start() diff --git a/src/serena/util/shell.py b/src/serena/util/shell.py index faef516..05080e0 100644 --- a/src/serena/util/shell.py +++ b/src/serena/util/shell.py @@ -51,7 +51,7 @@ def subprocess_check_output(args: list[str], encoding: str = "utf-8", strip: boo } if platform.system() == "Windows": kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW - output = subprocess.check_output(args, **kwargs).decode(encoding) + output = subprocess.check_output(args, **kwargs).decode(encoding) # type: ignore if strip: output = output.strip() return output From d680c83f767e4da24fc270df1e7cf1bac481261b Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 22 Jun 2025 11:58:00 +0200 Subject: [PATCH 122/174] Fixing more minor typing issues --- src/serena/constants.py | 2 +- src/serena/mcp.py | 3 ++- src/serena/process_isolated_agent.py | 4 ++-- src/serena/util/shell.py | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/serena/constants.py b/src/serena/constants.py index 8b9a4dd..6a27759 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -23,4 +23,4 @@ USE_SOLID_LSP = True """Whether to use Solid LSP instead of multilspy for language server operations. Multilspy uses asyncio, which creates many problems like deadlocks and requires full process isolation (and even then some issues remain). -""" \ No newline at end of file +""" diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 28150ba..e2a0bc9 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -188,10 +188,11 @@ class SerenaMCPFactory: mcp = FastMCP(**mcp_settings.model_dump()) return mcp - @asynccontextmanager # type: ignore + @asynccontextmanager @abstractmethod async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" + yield None # ensures MyPy understands we yield None class SerenaMCPFactorySingleProcess(SerenaMCPFactory): diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 081dcc6..90699b0 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -414,10 +414,10 @@ class ProcessIsolatedSerenaAgent: # Create communication pipe parent_conn, child_conn = multiprocessing.Pipe() - self.conn = parent_conn # type: ignore + self.conn = parent_conn # Create and start worker process, passing along the dashboard's queue if available - worker = SerenaAgentWorker(child_conn) # type: ignore + worker = SerenaAgentWorker(child_conn) self.process = multiprocessing.Process(target=worker.run, args=[_global_log_queue]) self.process.start() diff --git a/src/serena/util/shell.py b/src/serena/util/shell.py index 05080e0..c899b21 100644 --- a/src/serena/util/shell.py +++ b/src/serena/util/shell.py @@ -31,7 +31,7 @@ def execute_shell_command(command: str, cwd: str | None = None, capture_stderr: stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE if capture_stderr else None, - creationflags=subprocess.CREATE_NO_WINDOW if is_windows else 0, + creationflags=subprocess.CREATE_NO_WINDOW if is_windows else 0, # type: ignore text=True, encoding="utf-8", errors="replace", @@ -50,7 +50,7 @@ def subprocess_check_output(args: list[str], encoding: str = "utf-8", strip: boo "env": os.environ.copy(), } if platform.system() == "Windows": - kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW # type: ignore output = subprocess.check_output(args, **kwargs).decode(encoding) # type: ignore if strip: output = output.strip() From 8a1b8606c7c0be7026129352b24037d7e67b451d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 12:13:04 +0200 Subject: [PATCH 123/174] Reintroducd 'type: ignore' annotation required on Windows only (and generally allow unused 'type: ignore') --- pyproject.toml | 2 +- src/serena/process_isolated_agent.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9740f79..8ce6dd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,7 +101,7 @@ warn_no_return = true warn_redundant_casts = true warn_unreachable = true warn_unused_configs = true -warn_unused_ignores = true +warn_unused_ignores = false exclude = "^build/|^docs/" [tool.poe.env] diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 90699b0..081dcc6 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -414,10 +414,10 @@ class ProcessIsolatedSerenaAgent: # Create communication pipe parent_conn, child_conn = multiprocessing.Pipe() - self.conn = parent_conn + self.conn = parent_conn # type: ignore # Create and start worker process, passing along the dashboard's queue if available - worker = SerenaAgentWorker(child_conn) + worker = SerenaAgentWorker(child_conn) # type: ignore self.process = multiprocessing.Process(target=worker.run, args=[_global_log_queue]) self.process.start() From 3d2b1e1449b4b61783c1ec761134259db0fbc572 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 12:20:38 +0200 Subject: [PATCH 124/174] Configure black/ruff exclusions in tool sections instead of in command lines --- pyproject.toml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8ce6dd6..a708b56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,12 @@ line-length = 140 target-version = [ "py311" ] +exclude = ''' +/( + src/multilspy + | src/solidlsp +)/ +''' [tool.doc8] max-line-length = 1000 @@ -112,10 +118,10 @@ PYDEVD_DISABLE_FILE_VALIDATION = "1" # 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 java and not rust and not isolated_process}\"" -_black_check = "black --check --exclude src/multilspy/ --exclude src/solidlsp/ src scripts test" -_ruff_check = "ruff check --exclude .venv/ --exclude src/multilspy/ --exclude src/solidlsp/ src scripts test" -_black_format = "black --exclude .venv/|src/multilspy/|src/solidlsp/ src scripts test" -_ruff_format = "ruff check --exclude src/multilspy/ --exclude src/solidlsp/ --fix src scripts test" +_black_check = "black --check src scripts test" +_ruff_check = "ruff check src scripts test" +_black_format = "black src scripts test" +_ruff_format = "ruff check --fix src scripts test" lint = [ "_black_check", "_ruff_check", @@ -132,6 +138,10 @@ type-check = [ [tool.ruff] target-version = "py311" line-length = 140 +exclude = [ + "src/multilspy", + "src/solidlsp" +] [tool.ruff.format] quote-style = "double" From f607b76a1f9132450f25e6799fec246aa729292f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 22 Jun 2025 12:57:39 +0200 Subject: [PATCH 125/174] Hotfix: missing src/solidlsp in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a708b56..a34dd30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ google = [ Homepage = "https://github.com/oraios/serena" [tool.hatch.build.targets.wheel] -packages = ["src/serena", "src/multilspy", "src/interprompt"] +packages = ["src/serena", "src/multilspy", "src/interprompt", "src/solidlsp"] [tool.black] line-length = 140 From 654b949bde7627acdf4a67f362af3daa1c32d954 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 19:36:04 +0200 Subject: [PATCH 126/174] Simplify insertion logic for InsertBefore|AfterSymbolTool (LLM provides full indentation; ensure suitable whitespace before/after) --- src/serena/agent.py | 15 +++++++-------- src/serena/resources/config/modes/editing.yml | 3 +-- src/serena/symbol.py | 19 ++++++++++++------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index ef6e226..f2c740d 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -1744,16 +1744,16 @@ class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit): Inserts the given body/content after the end of the definition of the given symbol (via the symbol's location). A typical use case is to insert a new class, function, method, field or variable assignment. - :param name_path: for finding the symbol to insert after, same logic as in the `find_symbol` tool. + :param name_path: name path of the symbol after which to insert content (definitions in the `find_symbol` tool apply) :param relative_path: the relative path to the file containing the symbol - :param body: the body/content to be inserted. Important: the inserted code will automatically have the - same indentation as the symbol's body, so you do not need to provide any additional indentation. + :param body: the body/content to be inserted. The inserted code shall begin with the next line after + the symbol. """ self.symbol_manager.insert_after_symbol( name_path, relative_file_path=relative_path, body=body, - use_same_indentation=True, + use_same_indentation=False, ) return SUCCESS_RESULT @@ -1774,16 +1774,15 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit): A typical use case is to insert a new class, function, method, field or variable assignment. It also can be used to insert a new import statement before the first symbol in the file. - :param name_path: for finding the symbol to insert before, same logic as in the `find_symbol` tool. + :param name_path: name path of the symbol before which to insert content (definitions in the `find_symbol` tool apply) :param relative_path: the relative path to the file containing the symbol - :param body: the body/content to be inserted. Important: the inserted code will automatically have the - same indentation as the symbol's body, so you do not need to provide any additional indentation. + :param body: the body/content to be inserted before the line in which the referenced symbol is defined """ self.symbol_manager.insert_before_symbol( name_path, relative_file_path=relative_path, body=body, - use_same_indentation=True, + use_same_indentation=False, ) return SUCCESS_RESULT diff --git a/src/serena/resources/config/modes/editing.yml b/src/serena/resources/config/modes/editing.yml index b50d3a5..a0ba5f7 100644 --- a/src/serena/resources/config/modes/editing.yml +++ b/src/serena/resources/config/modes/editing.yml @@ -23,8 +23,7 @@ prompt: | 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`. - Note that you never need to add additional indentation, as all symbol editing tools will automatically add the indentation of the symbol that - you are replacing or inserting above or below. 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 + 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, diff --git a/src/serena/symbol.py b/src/serena/symbol.py index f7cb194..66df7aa 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -751,11 +751,16 @@ class SymbolManager: raise ValueError(f"Symbol at {location} does not have a defined end position.") line, col = pos["line"], pos["character"] + if at_new_line: - line += 1 + # start at beginning of next line col = 0 - if not body.startswith("\n"): - body = "\n" + body + line += 1 + # make sure there is one empty line before the new symbol + body = "\n" + body.lstrip("\n") + # make sure the one line break succeeding the original symbol, which we repurposed as prefix, is replaced + body = body.rstrip("\n") + "\n" + if use_same_indentation: symbol_start_pos = symbol.body_start_position assert symbol_start_pos is not None, f"Symbol at {location=} does not have a defined start position." @@ -822,16 +827,16 @@ class SymbolManager: raise ValueError(f"Symbol at {location} does not have a defined start position.") line = symbol_start_pos["line"] col = symbol_start_pos["character"] + if use_same_indentation: indent = " " * (col) body = "\n".join(indent + line for line in body.splitlines()) - # similar problems as in insert_after_symbol_at_location, see comment there if at_new_line: col = 0 - line -= 1 - if not body.endswith("\n"): - body += "\n" + # ensure two newlines after inserted body (eol + empty line) + body = body.rstrip() + "\n\n" + assert location.relative_path is not None self._lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) From 6b8b103deda86cd9f6b5c993fc3e02fbf07a28fa Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 19:50:36 +0200 Subject: [PATCH 127/174] TextUtils: Allow insertion in new line after full text, adding missing newline at end of file --- src/multilspy/multilspy_utils.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/multilspy/multilspy_utils.py b/src/multilspy/multilspy_utils.py index 20959c4..8c54cd6 100644 --- a/src/multilspy/multilspy_utils.py +++ b/src/multilspy/multilspy_utils.py @@ -20,6 +20,10 @@ from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_types import UnifiedSymbolInformation +class InvalidTextLocationError(Exception): + pass + + class TextUtils: """ Utilities for text operations. @@ -49,7 +53,8 @@ class TextUtils: """ idx = 0 while line > 0: - assert idx < len(text), (idx, len(text), text) + if idx >= len(text): + raise InvalidTextLocationError() if text[idx] == "\n": line -= 1 idx += 1 @@ -88,7 +93,17 @@ class TextUtils: Inserts the given text at the given line and column. Returns the modified text and the new line and column. """ - change_index = TextUtils.get_index_from_line_col(text, line, col) + try: + change_index = TextUtils.get_index_from_line_col(text, line, col) + except InvalidTextLocationError as e: + num_lines_in_text = text.count("\n") + 1 + max_line = num_lines_in_text - 1 + if line == max_line + 1 and col == 0: # trying to insert at new line after full text + # insert at end, adding missing newline + change_index = len(text) + text_to_be_inserted = "\n" + text_to_be_inserted + else: + raise new_text = text[:change_index] + text_to_be_inserted + text[change_index:] new_l, new_c = TextUtils._get_updated_position_from_line_and_column_and_edit(line, col, text_to_be_inserted) return new_text, new_l, new_c From 122e99e8c336ffea0af06ce57f22f8686f001a54 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 20:08:36 +0200 Subject: [PATCH 128/174] Simplify ReplaceSymbolBodyTool, making the LLM provide the indentation, and fix the tool inserting additional line breaks (there should be no whitespace changes whatsoever before/after the inserted content) --- src/serena/agent.py | 17 +++-------------- src/serena/symbol.py | 8 +++++--- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index f2c740d..b51285e 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -1704,27 +1704,16 @@ class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit): r""" Replaces the body of the symbol with the given `name_path`. - Important: - You don't need to provide an adjusted indentation, - as the tool will automatically add the indentation of the original symbol to each line. For example, - for replacing a method in python, you can just write (using the standard python indentation): - body="def my_method_replacement(self, ...):\n first_line\n second_line...". So each line after the first line only has - an indentation of 4 (the indentation relative to the first character), - since the additional indentation will be added by the tool. Same for more deeply nested - cases. You always only need to write the relative indentation to the first character of the first line, and that - in turn should not have any indentation. - ALWAYS REMEMBER TO USE THE CORRECT INDENTATION IN THE BODY! - :param name_path: for finding the symbol to replace, same logic as in the `find_symbol` tool. :param relative_path: the relative path to the file containing the symbol - :param body: the new symbol body. - + :param body: the new symbol body. Important: Begin directly with the symbol definition and provide no + leading indentation for the first line (but do indent the rest of the body according to the context). """ self.symbol_manager.replace_body( name_path, relative_file_path=relative_path, body=body, - use_same_indentation=True, + use_same_indentation=False, ) return SUCCESS_RESULT diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 66df7aa..af1810d 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -689,14 +689,16 @@ class SymbolManager: if start_pos is None or end_pos is None: raise ValueError(f"Symbol at {location} does not have a defined body range.") start_line, start_col = start_pos["line"], start_pos["character"] + if use_same_indentation: indent = " " * start_col body_lines = body.splitlines() body = body_lines[0] + "\n" + "\n".join(indent + line for line in body_lines[1:]) - # make sure body always ends with at least one newline - if not body.endswith("\n"): - body += "\n" + # 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() + self._lang_server.delete_text_between_positions(location.relative_path, start_pos, end_pos) self._lang_server.insert_text_at_position(location.relative_path, start_line, start_col, body) From 8837635f55242a89132cddb2d01ac42382061edf Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 23:16:43 +0200 Subject: [PATCH 129/174] Make syrupy diffs render correctly in IntelliJ/PyCharm --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..22e867b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --snapshot-patch-pycharm-diff From 4a50c6ec61951228b03c0a60e9a18dea82c2d551 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 23:35:58 +0200 Subject: [PATCH 130/174] Further improve InsertBefore|AfterSymbolTool by handling empty lines depending on the symbol type --- src/serena/symbol.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index af1810d..35d73b8 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -256,6 +256,13 @@ class Symbol(ToStringMixin): def symbol_kind(self) -> SymbolKind: return self.symbol_root["kind"] + def is_neighbouring_definition_separated_by_empty_line(self) -> bool: + """ + :return: whether a symbol definition of this symbol's kind is usually separated from the + previous/next definition by at least one empty line. + """ + return self.symbol_kind in (SymbolKind.Function, SymbolKind.Method, SymbolKind.Class, SymbolKind.Interface, SymbolKind.Struct) + @property def relative_path(self) -> str | None: location = self.symbol_root.get("location") @@ -758,10 +765,12 @@ class SymbolManager: # start at beginning of next line col = 0 line += 1 - # make sure there is one empty line before the new symbol - body = "\n" + body.lstrip("\n") + # strip preceding newlines, re-adding an empty line before the new symbol if appropriate + body = body.lstrip("\r\n") + if symbol.is_neighbouring_definition_separated_by_empty_line(): + body = "\n" + body # make sure the one line break succeeding the original symbol, which we repurposed as prefix, is replaced - body = body.rstrip("\n") + "\n" + body = body.rstrip("\r\n") + "\n" if use_same_indentation: symbol_start_pos = symbol.body_start_position @@ -836,8 +845,11 @@ class SymbolManager: if at_new_line: col = 0 - # ensure two newlines after inserted body (eol + empty line) - body = body.rstrip() + "\n\n" + # ensure eol is present at end + body = body.rstrip() + "\n" + # add empty line if appropriate + if symbol.is_neighbouring_definition_separated_by_empty_line(): + body += "\n" assert location.relative_path is not None From 3726408a6f8a99260b9a26d8d623a26460f27431 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 22 Jun 2025 23:39:13 +0200 Subject: [PATCH 131/174] Update editing tests --- test/serena/test_symbol_editing.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index 12be047..b034d7c 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -159,9 +159,9 @@ class InsertInRelToSymbolTest(EditingTest): def _apply_edit(self, symbol_manager: SymbolManager) -> None: assert self.mode is not None if self.mode == "before": - symbol_manager.insert_before_symbol(self.symbol_name, self.rel_path, self.new_content) + symbol_manager.insert_before_symbol(self.symbol_name, self.rel_path, self.new_content, use_same_indentation=False) elif self.mode == "after": - symbol_manager.insert_after_symbol(self.symbol_name, self.rel_path, self.new_content) + symbol_manager.insert_after_symbol(self.symbol_name, self.rel_path, self.new_content, use_same_indentation=False) @pytest.mark.parametrize("mode", ["before", "after"]) @@ -211,18 +211,16 @@ def test_insert_in_rel_to_symbol(test_case: InsertInRelToSymbolTest, mode: Liter test_case.run_test(content_after_ground_truth=snapshot) -PYTHON_REPLACED_BODY = """ -def modify_instance_var(self): - # This body has been replaced - self.instance_var = "Replaced!" - self.reassignable_instance_var = 999 +PYTHON_REPLACED_BODY = """def modify_instance_var(self): + # This body has been replaced + self.instance_var = "Replaced!" + self.reassignable_instance_var = 999 """ -TYPESCRIPT_REPLACED_BODY = """ -function printValue() { - // This body has been replaced - console.warn("New value: " + this.value); -} +TYPESCRIPT_REPLACED_BODY = """function printValue() { + // This body has been replaced + console.warn("New value: " + this.value); + } """ @@ -233,7 +231,7 @@ class ReplaceBodyTest(EditingTest): self.new_body = new_body def _apply_edit(self, symbol_manager: SymbolManager) -> None: - symbol_manager.replace_body(self.symbol_name, self.rel_path, self.new_body) + symbol_manager.replace_body(self.symbol_name, self.rel_path, self.new_body, use_same_indentation=False) @pytest.mark.parametrize( From b972f2959de1f8c3279c25705826818b2b048b55 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 23 Jun 2025 02:07:07 -0400 Subject: [PATCH 132/174] Just minor formatting fixes - no space after ** for bold - consistent trailing periods in a list --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1a5b7a5..d67000a 100644 --- a/README.md +++ b/README.md @@ -414,7 +414,7 @@ For more information on MCP servers with Claude Desktop, see [the official quick Being an MCP Server, Serena can be included in any MCP Client. The same configuration as above, perhaps with small client-specific modifications, should work. Most of the popular existing coding assistants (IDE extensions or VSCode-like IDEs) support connections -to MCP Servers. It is ** recommended to use the `ide-assistant` context** for these integrations by adding `"--context", "ide-assistant"` to the `args` in your MCP client's configuration. Including Serena generally boosts their performance +to MCP Servers. It is **recommended to use the `ide-assistant` context** for these integrations by adding `"--context", "ide-assistant"` to the `args` in your MCP client's configuration. Including Serena generally boosts their performance by providing them tools for symbolic operations. In this case, the billing for the usage continues to be controlled by the client of your choice @@ -422,8 +422,8 @@ In this case, the billing for the usage continues to be controlled by the client e.g., for one of the following reasons: 1. You are already using a coding assistant (say Cline or Cursor) and just want to make it more powerful. -2. You are on Linux and don't want to use the [community-created Claude Desktop](https://github.com/aaddrick/claude-desktop-debian) -3. You want tighter integration of Serena into your IDE and don't mind paying for that +2. You are on Linux and don't want to use the [community-created Claude Desktop](https://github.com/aaddrick/claude-desktop-debian). +3. You want tighter integration of Serena into your IDE and don't mind paying for that. ### Agno Agent From 07ea1370dc5e4e3f965b6106487e4ed98d2aaa9c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 00:31:07 +0200 Subject: [PATCH 133/174] Add tests for adding a class with 'LLM-provided' leading/trailing newlines --- .../__snapshots__/test_symbol_editing.ambr | 208 ++++++++++++++++++ test/serena/test_symbol_editing.py | 40 +++- 2 files changed, 246 insertions(+), 2 deletions(-) diff --git a/test/serena/__snapshots__/test_symbol_editing.ambr b/test/serena/__snapshots__/test_symbol_editing.ambr index 2b698a2..b1f12e9 100644 --- a/test/serena/__snapshots__/test_symbol_editing.ambr +++ b/test/serena/__snapshots__/test_symbol_editing.ambr @@ -584,6 +584,214 @@ ''' # --- +# name: test_insert_python_class_after + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + class NewInsertedClass: + pass + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_python_class_before + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + class NewInsertedClass: + pass + + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- # name: test_replace_body[test_case0] ''' """ diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index b034d7c..29976a5 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -134,6 +134,18 @@ def test_delete_symbol(test_case, snapshot): NEW_PYTHON_FUNCTION = """def new_inserted_function(): print("This is a new function inserted before another.")""" +NEW_PYTHON_CLASS_WITH_LEADING_NEWLINES = """ + +class NewInsertedClass: + pass +""" + +NEW_PYTHON_CLASS_WITH_TRAILING_NEWLINES = """class NewInsertedClass: + pass + + +""" + NEW_TYPESCRIPT_FUNCTION = """function newInsertedFunction(): void { console.log("This is a new function inserted before another."); }""" @@ -147,11 +159,13 @@ NEW_TYPESCRIPT_FUNCTION_AFTER = """function newFunctionAfterClass(): void { class InsertInRelToSymbolTest(EditingTest): - def __init__(self, language: Language, rel_path: str, symbol_name: str, new_content: str): + def __init__( + self, language: Language, rel_path: str, symbol_name: str, new_content: str, mode: Literal["before", "after"] | None = None + ): super().__init__(language, rel_path) self.symbol_name = symbol_name self.new_content = new_content - self.mode: Literal["before", "after"] | None = None + self.mode: Literal["before", "after"] | None = mode def set_mode(self, mode: Literal["before", "after"]): self.mode = mode @@ -211,6 +225,28 @@ def test_insert_in_rel_to_symbol(test_case: InsertInRelToSymbolTest, mode: Liter test_case.run_test(content_after_ground_truth=snapshot) +@pytest.mark.python +def test_insert_python_class_before(snapshot): + InsertInRelToSymbolTest( + Language.PYTHON, + PYTHON_TEST_REL_FILE_PATH, + "VariableDataclass", + NEW_PYTHON_CLASS_WITH_TRAILING_NEWLINES, + mode="before", + ).run_test(snapshot) + + +@pytest.mark.python +def test_insert_python_class_after(snapshot): + InsertInRelToSymbolTest( + Language.PYTHON, + PYTHON_TEST_REL_FILE_PATH, + "VariableDataclass", + NEW_PYTHON_CLASS_WITH_LEADING_NEWLINES, + mode="after", + ).run_test(snapshot) + + PYTHON_REPLACED_BODY = """def modify_instance_var(self): # This body has been replaced self.instance_var = "Replaced!" From 0586f4cd62492c1429afb2374952beb9da0fdb71 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 00:42:47 +0200 Subject: [PATCH 134/174] InsertBefore|AfterSymbolTool: Retain leading/trailing empty lines that are provided by the caller (if empty lines are appropriate for the symbol type) --- src/serena/symbol.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 35d73b8..3e342fb 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -5,7 +5,7 @@ from collections.abc import Iterator, Sequence from contextlib import contextmanager from dataclasses import asdict, dataclass, field from difflib import SequenceMatcher -from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Self, Union +from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Self, Union, Iterable, Reversible from sensai.util.string import ToStringMixin @@ -709,6 +709,22 @@ class SymbolManager: self._lang_server.delete_text_between_positions(location.relative_path, start_pos, end_pos) self._lang_server.insert_text_at_position(location.relative_path, start_line, start_col, body) + @staticmethod + def _count_leading_newlines(text: Iterable) -> int: + cnt = 0 + for c in text: + if c == "\n": + cnt += 1 + elif c == "\r": + continue + else: + break + return cnt + + @classmethod + def _count_trailing_newlines(cls, text: Reversible) -> int: + return cls._count_leading_newlines(reversed(text)) + def insert_after_symbol( self, name_path: str, @@ -765,10 +781,12 @@ class SymbolManager: # start at beginning of next line col = 0 line += 1 - # strip preceding newlines, re-adding an empty line before the new symbol if appropriate + # strip preceding newlines, re-adding one or more empty lines before the new symbol if appropriate + original_leading_newlines = self._count_leading_newlines(body) body = body.lstrip("\r\n") if symbol.is_neighbouring_definition_separated_by_empty_line(): - body = "\n" + body + num_leading_newlines = max(1, original_leading_newlines) + body = ("\n" * num_leading_newlines) + body # make sure the one line break succeeding the original symbol, which we repurposed as prefix, is replaced body = body.rstrip("\r\n") + "\n" @@ -845,11 +863,13 @@ class SymbolManager: if at_new_line: col = 0 + original_trailing_empty_lines = self._count_trailing_newlines(body) - 1 # ensure eol is present at end body = body.rstrip() + "\n" - # add empty line if appropriate + # add one or more empty lines if appropriate if symbol.is_neighbouring_definition_separated_by_empty_line(): - body += "\n" + num_trailing_newlines = max(1, original_trailing_empty_lines) + body += "\n" * num_trailing_newlines assert location.relative_path is not None From 48296943e93aaaeae2a57b11f31e361d5fc647a5 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 14:03:35 +0200 Subject: [PATCH 135/174] Improve empty line handling in InsertBefore|AfterSymbolTool: Always respect the original caller's number of empty lines, but add a minimum number of empty lines depending on symbol kind --- src/serena/symbol.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 3e342fb..4466322 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -1,11 +1,11 @@ import json import logging import os -from collections.abc import Iterator, Sequence +from collections.abc import Iterable, Iterator, Reversible, Sequence from contextlib import contextmanager from dataclasses import asdict, dataclass, field from difflib import SequenceMatcher -from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Self, Union, Iterable, Reversible +from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Self, Union from sensai.util.string import ToStringMixin @@ -781,13 +781,18 @@ class SymbolManager: # start at beginning of next line col = 0 line += 1 - # strip preceding newlines, re-adding one or more empty lines before the new symbol if appropriate + # make sure a suitable number of leading empty lines is used (at least 0/1 depending on the symbol type, + # otherweise as many as the caller wanted to insert) original_leading_newlines = self._count_leading_newlines(body) body = body.lstrip("\r\n") + min_empty_lines = 0 if symbol.is_neighbouring_definition_separated_by_empty_line(): - num_leading_newlines = max(1, original_leading_newlines) - body = ("\n" * num_leading_newlines) + body - # make sure the one line break succeeding the original symbol, which we repurposed as prefix, is replaced + min_empty_lines = 1 + num_leading_empty_lines = max(min_empty_lines, original_leading_newlines) + if num_leading_empty_lines: + body = ("\n" * num_leading_empty_lines) + body + # make sure the one line break succeeding the original symbol, which we repurposed as prefix via + # `line += 1`, is replaced body = body.rstrip("\r\n") + "\n" if use_same_indentation: @@ -866,10 +871,13 @@ class SymbolManager: original_trailing_empty_lines = self._count_trailing_newlines(body) - 1 # ensure eol is present at end body = body.rstrip() + "\n" - # add one or more empty lines if appropriate + # add suitable number of trailing empty lines after the body (at least 0/1 depending on the symbol type, + # otherwise as many as the caller wanted to insert) + min_trailing_empty_lines = 0 if symbol.is_neighbouring_definition_separated_by_empty_line(): - num_trailing_newlines = max(1, original_trailing_empty_lines) - body += "\n" * num_trailing_newlines + min_trailing_empty_lines = 1 + num_trailing_newlines = max(min_trailing_empty_lines, original_trailing_empty_lines) + body += "\n" * num_trailing_newlines assert location.relative_path is not None From 0c11ebebcc63bca1afbb9263b2230438a32274ce Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 14:11:41 +0200 Subject: [PATCH 136/174] Remove obsolete flag at_new_line --- src/serena/agent.py | 14 +----- src/serena/symbol.py | 102 +++++++++++++++++-------------------------- 2 files changed, 41 insertions(+), 75 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index b51285e..b8cde47 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -1738,12 +1738,7 @@ class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit): :param body: the body/content to be inserted. The inserted code shall begin with the next line after the symbol. """ - self.symbol_manager.insert_after_symbol( - name_path, - relative_file_path=relative_path, - body=body, - use_same_indentation=False, - ) + self.symbol_manager.insert_after_symbol(name_path, relative_file_path=relative_path, body=body, use_same_indentation=False) return SUCCESS_RESULT @@ -1767,12 +1762,7 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit): :param relative_path: the relative path to the file containing the symbol :param body: the body/content to be inserted before the line in which the referenced symbol is defined """ - self.symbol_manager.insert_before_symbol( - name_path, - relative_file_path=relative_path, - body=body, - use_same_indentation=False, - ) + self.symbol_manager.insert_before_symbol(name_path, relative_file_path=relative_path, body=body, use_same_indentation=False) return SUCCESS_RESULT diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 4466322..e92ffdf 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -725,15 +725,7 @@ class SymbolManager: def _count_trailing_newlines(cls, text: Reversible) -> int: return cls._count_leading_newlines(reversed(text)) - def insert_after_symbol( - self, - name_path: str, - relative_file_path: str, - body: str, - *, - use_same_indentation: bool = True, - at_new_line: bool = True, - ) -> None: + def insert_after_symbol(self, name_path: str, relative_file_path: str, body: str, *, use_same_indentation: bool = True) -> None: """ Inserts content after the symbol with the given name in the given file. """ @@ -747,13 +739,9 @@ class SymbolManager: f"Found symbols at locations: \n" + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2) ) symbol = symbol_candidates[-1] - return self.insert_after_symbol_at_location( - symbol.location, body, at_new_line=at_new_line, use_same_indentation=use_same_indentation - ) + return self.insert_after_symbol_at_location(symbol.location, body, use_same_indentation=use_same_indentation) - def insert_after_symbol_at_location( - self, location: SymbolLocation, body: str, *, at_new_line: bool = True, use_same_indentation: bool = True - ) -> None: + def insert_after_symbol_at_location(self, location: SymbolLocation, body: str, *, use_same_indentation: bool = True) -> None: """ Appends content after the given symbol @@ -775,25 +763,22 @@ class SymbolManager: if pos is None: raise ValueError(f"Symbol at {location} does not have a defined end position.") - line, col = pos["line"], pos["character"] - - if at_new_line: - # start at beginning of next line - col = 0 - line += 1 - # make sure a suitable number of leading empty lines is used (at least 0/1 depending on the symbol type, - # otherweise as many as the caller wanted to insert) - original_leading_newlines = self._count_leading_newlines(body) - body = body.lstrip("\r\n") - min_empty_lines = 0 - if symbol.is_neighbouring_definition_separated_by_empty_line(): - min_empty_lines = 1 - num_leading_empty_lines = max(min_empty_lines, original_leading_newlines) - if num_leading_empty_lines: - body = ("\n" * num_leading_empty_lines) + body - # make sure the one line break succeeding the original symbol, which we repurposed as prefix via - # `line += 1`, is replaced - body = body.rstrip("\r\n") + "\n" + # start at the beginning of the next line + col = 0 + line = pos["line"] + 1 + # make sure a suitable number of leading empty lines is used (at least 0/1 depending on the symbol type, + # otherweise as many as the caller wanted to insert) + original_leading_newlines = self._count_leading_newlines(body) + body = body.lstrip("\r\n") + min_empty_lines = 0 + if symbol.is_neighbouring_definition_separated_by_empty_line(): + min_empty_lines = 1 + num_leading_empty_lines = max(min_empty_lines, original_leading_newlines) + if num_leading_empty_lines: + body = ("\n" * num_leading_empty_lines) + body + # make sure the one line break succeeding the original symbol, which we repurposed as prefix via + # `line += 1`, is replaced + body = body.rstrip("\r\n") + "\n" if use_same_indentation: symbol_start_pos = symbol.body_start_position @@ -817,20 +802,11 @@ class SymbolManager: # > test test # > second line # > dataclass_instance.status = "active" # Reassign dataclass field - col = 0 with self._edited_symbol_location(location): self._lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body) - def insert_before_symbol( - self, - name_path: str, - relative_file_path: str, - body: str, - *, - at_new_line: bool = True, - use_same_indentation: bool = True, - ) -> None: + def insert_before_symbol(self, name_path: str, relative_file_path: str, body: str, *, use_same_indentation: bool = True) -> None: """ Inserts content before the symbol with the given name in the given file. """ @@ -844,11 +820,9 @@ class SymbolManager: f"Found symbols at locations: \n" + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2) ) symbol = symbol_candidates[0] - self.insert_before_symbol_at_location(symbol.location, body, at_new_line=at_new_line, use_same_indentation=use_same_indentation) + self.insert_before_symbol_at_location(symbol.location, body, use_same_indentation=use_same_indentation) - def insert_before_symbol_at_location( - self, location: SymbolLocation, body: str, *, at_new_line: bool = True, use_same_indentation: bool = True - ) -> None: + def insert_before_symbol_at_location(self, location: SymbolLocation, body: str, *, use_same_indentation: bool = True) -> None: """ Inserts content before the given symbol @@ -859,25 +833,27 @@ class SymbolManager: symbol_start_pos = symbol.body_start_position if symbol_start_pos is None: raise ValueError(f"Symbol at {location} does not have a defined start position.") - line = symbol_start_pos["line"] - col = symbol_start_pos["character"] if use_same_indentation: - indent = " " * (col) + indent = " " * (symbol_start_pos["character"]) body = "\n".join(indent + line for line in body.splitlines()) - if at_new_line: - col = 0 - original_trailing_empty_lines = self._count_trailing_newlines(body) - 1 - # ensure eol is present at end - body = body.rstrip() + "\n" - # add suitable number of trailing empty lines after the body (at least 0/1 depending on the symbol type, - # otherwise as many as the caller wanted to insert) - min_trailing_empty_lines = 0 - if symbol.is_neighbouring_definition_separated_by_empty_line(): - min_trailing_empty_lines = 1 - num_trailing_newlines = max(min_trailing_empty_lines, original_trailing_empty_lines) - body += "\n" * num_trailing_newlines + # insert position is the start of line where the symbol is defined + line = symbol_start_pos["line"] + col = 0 + + original_trailing_empty_lines = self._count_trailing_newlines(body) - 1 + + # ensure eol is present at end + body = body.rstrip() + "\n" + + # add suitable number of trailing empty lines after the body (at least 0/1 depending on the symbol type, + # otherwise as many as the caller wanted to insert) + min_trailing_empty_lines = 0 + if symbol.is_neighbouring_definition_separated_by_empty_line(): + min_trailing_empty_lines = 1 + num_trailing_newlines = max(min_trailing_empty_lines, original_trailing_empty_lines) + body += "\n" * num_trailing_newlines assert location.relative_path is not None From b5d94888839d6fb3bf8282023ed3b7788f6647dc Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 14:43:43 +0200 Subject: [PATCH 137/174] Update test snapshots --- .../__snapshots__/test_symbol_editing.ambr | 1846 +++++++++-------- 1 file changed, 925 insertions(+), 921 deletions(-) diff --git a/test/serena/__snapshots__/test_symbol_editing.ambr b/test/serena/__snapshots__/test_symbol_editing.ambr index b1f12e9..327a5ef 100644 --- a/test/serena/__snapshots__/test_symbol_editing.ambr +++ b/test/serena/__snapshots__/test_symbol_editing.ambr @@ -1,921 +1,925 @@ -# serializer version: 1 -# name: test_delete_symbol[test_case0] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_delete_symbol[test_case1] - ''' - - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case0-after] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - new_module_var = "Inserted after typed_module_var" - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case0-before] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - new_module_var = "Inserted after typed_module_var" - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case1-after] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - def new_inserted_function(): - print("This is a new function inserted before another.") - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case1-before] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - def new_inserted_function(): - print("This is a new function inserted before another.") - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case2-after] - ''' - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - printValue() { - console.log(this.value); - } - } - - function newFunctionAfterClass(): void { - console.log("This function is after DemoClass."); - } - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case2-before] - ''' - function newFunctionAfterClass(): void { - console.log("This function is after DemoClass."); - } - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - printValue() { - console.log(this.value); - } - } - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case3-after] - ''' - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - printValue() { - console.log(this.value); - } - } - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - function newInsertedFunction(): void { - console.log("This is a new function inserted before another."); - } - helperFunction(); - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case3-before] - ''' - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - printValue() { - console.log(this.value); - } - } - function newInsertedFunction(): void { - console.log("This is a new function inserted before another."); - } - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - ''' -# --- -# name: test_insert_python_class_after - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - class NewInsertedClass: - pass - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_insert_python_class_before - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - class NewInsertedClass: - pass - - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_replace_body[test_case0] - ''' - """ - Test module for variable declarations and usage. - - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ - - from dataclasses import dataclass, field - - # Module-level variables - module_var = "Initial module value" - - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - - def modify_instance_var(self): - # This body has been replaced - self.instance_var = "Replaced!" - self.reassignable_instance_var = 999 - # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status - - ''' -# --- -# name: test_replace_body[test_case1] - ''' - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - - function printValue() { - // This body has been replaced - console.warn("New value: " + this.value); - } - - } - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - ''' -# --- +# serializer version: 1 +# name: test_delete_symbol[test_case0] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_delete_symbol[test_case1] + ''' + + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + helperFunction(); + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case0-after] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + new_module_var = "Inserted after typed_module_var" + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case0-before] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + new_module_var = "Inserted after typed_module_var" + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case1-after] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + def new_inserted_function(): + print("This is a new function inserted before another.") + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case1-before] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def new_inserted_function(): + print("This is a new function inserted before another.") + + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case2-after] + ''' + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } + } + + function newFunctionAfterClass(): void { + console.log("This function is after DemoClass."); + } + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + helperFunction(); + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case2-before] + ''' + function newFunctionAfterClass(): void { + console.log("This function is after DemoClass."); + } + + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } + } + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + helperFunction(); + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case3-after] + ''' + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } + } + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + function newInsertedFunction(): void { + console.log("This is a new function inserted before another."); + } + + helperFunction(); + + ''' +# --- +# name: test_insert_in_rel_to_symbol[test_case3-before] + ''' + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } + } + + function newInsertedFunction(): void { + console.log("This is a new function inserted before another."); + } + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + helperFunction(); + + ''' +# --- +# name: test_insert_python_class_after + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + class NewInsertedClass: + pass + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_insert_python_class_before + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # Reassign instance variable + self.instance_var = "Modified instance value" + self.reassignable_instance_var = 200 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + class NewInsertedClass: + pass + + + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_replace_body[test_case0] + ''' + """ + Test module for variable declarations and usage. + + This module tests various types of variable declarations and usages including: + - Module-level variables + - Class-level variables + - Instance variables + - Variable reassignments + """ + + from dataclasses import dataclass, field + + # Module-level variables + module_var = "Initial module value" + + reassignable_module_var = 10 + reassignable_module_var = 20 # Reassigned + + # Module-level variable with type annotation + typed_module_var: int = 42 + + + # Regular class with class and instance variables + class VariableContainer: + """Class that contains various variables.""" + + # Class-level variables + class_var = "Initial class value" + + reassignable_class_var = True + reassignable_class_var = False # Reassigned #noqa: PIE794 + + # Class-level variable with type annotation + typed_class_var: str = "typed value" + + def __init__(self): + # Instance variables + self.instance_var = "Initial instance value" + self.reassignable_instance_var = 100 + + # Instance variable with type annotation + self.typed_instance_var: list[str] = ["item1", "item2"] + + def modify_instance_var(self): + # This body has been replaced + self.instance_var = "Replaced!" + self.reassignable_instance_var = 999 # Reassigned + + def use_module_var(self): + # Use module-level variables + result = module_var + " used in method" + other_result = reassignable_module_var + 5 + return result, other_result + + def use_class_var(self): + # Use class-level variables + result = VariableContainer.class_var + " used in method" + other_result = VariableContainer.reassignable_class_var + return result, other_result + + + # Dataclass with variables + @dataclass + class VariableDataclass: + """Dataclass that contains various fields.""" + + # Field variables with type annotations + id: int + name: str + items: list[str] = field(default_factory=list) + metadata: dict[str, str] = field(default_factory=dict) + optional_value: float | None = None + + # This will be reassigned in various places + status: str = "pending" + + + # Function that uses the module variables + def use_module_variables(): + """Function that uses module-level variables.""" + result = module_var + " used in function" + other_result = reassignable_module_var * 2 + return result, other_result + + + # Create instances and use variables + dataclass_instance = VariableDataclass(id=1, name="Test") + dataclass_instance.status = "active" # Reassign dataclass field + + # Use variables at module level + module_result = module_var + " used at module level" + other_module_result = reassignable_module_var + 30 + + # Create a second dataclass instance with different status + second_dataclass = VariableDataclass(id=2, name="Another Test") + second_dataclass.status = "completed" # Another reassignment of status + + ''' +# --- +# name: test_replace_body[test_case1] + ''' + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + function printValue() { + // This body has been replaced + console.warn("New value: " + this.value); + } + } + + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } + + helperFunction(); + + ''' +# --- From f04bd7d65be2f8e0f1459efdc660ce9c40597ce7 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 15:29:48 +0200 Subject: [PATCH 138/174] Apply black & ruff to solidlsp --- pyproject.toml | 5 +- .../clangd_language_server.py | 23 +- .../dart_language_server.py | 37 +- .../eclipse_jdtls/eclipse_jdtls.py | 56 +-- src/solidlsp/language_servers/gopls/gopls.py | 19 +- .../intelephense/intelephense.py | 38 +- .../jedi_language_server/jedi_server.py | 4 +- .../kotlin_language_server.py | 61 ++- .../language_servers/omnisharp/omnisharp.py | 52 +-- .../pyright_language_server/pyright_server.py | 3 +- .../rust_analyzer/rust_analyzer.py | 19 +- .../language_servers/solargraph/solargraph.py | 23 +- .../typescript_language_server.py | 25 +- src/solidlsp/ls.py | 437 ++++++++---------- src/solidlsp/ls_handler.py | 58 ++- src/solidlsp/lsp_request.py | 316 ++++++------- 16 files changed, 522 insertions(+), 654 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a34dd30..0b0f9f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,6 @@ target-version = [ exclude = ''' /( src/multilspy - | src/solidlsp )/ ''' @@ -140,7 +139,6 @@ target-version = "py311" line-length = 140 exclude = [ "src/multilspy", - "src/solidlsp" ] [tool.ruff.format] @@ -240,6 +238,9 @@ ignore = [ "SIM110", # requires use of any(...) instead of for-loop "G001", # forbids str.format in log statements "E722", # forbids unspecific except clause + "SIM105", # forbids empty/general except clause + "SIM113", # wants to enforce use of enumerate + "E712", # forbids equality comparison with True/False ] unfixable = [ "F841", diff --git a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py index 0ee5f47..54d364e 100644 --- a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py @@ -13,8 +13,7 @@ from multilspy.lsp_protocol_handler.lsp_types import InitializeParams from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from multilspy.multilspy_utils import FileUtils, PlatformUtils from solidlsp.ls import SolidLanguageServer @@ -48,7 +47,7 @@ class ClangdLanguageServer(SolidLanguageServer): """ platform_id = PlatformUtils.get_platform_id() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: d = json.load(f) del d["_description"] @@ -56,12 +55,12 @@ class ClangdLanguageServer(SolidLanguageServer): "linux-x64", "win-x64", "osx-arm64", - ], "Unsupported platform: " + platform_id.value + ], ( + "Unsupported platform: " + platform_id.value + ) runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] assert len(runtime_dependencies) == 1 # Select dependency matching the current platform dependency = next((dep for dep in runtime_dependencies if dep["platformId"] == platform_id.value), None) @@ -75,9 +74,7 @@ class ClangdLanguageServer(SolidLanguageServer): logger.log(f"Clangd executable not found at {clangd_executable_path}. Downloading from {clangd_url}", logging.INFO) os.makedirs(clangd_ls_dir, exist_ok=True) if dependency["archiveType"] == "zip": - FileUtils.download_and_extract_archive( - logger, clangd_url, clangd_ls_dir, dependency["archiveType"] - ) + FileUtils.download_and_extract_archive(logger, clangd_url, clangd_ls_dir, dependency["archiveType"]) else: raise RuntimeError(f"Unsupported archive type: {dependency['archiveType']}") if not os.path.exists(clangd_executable_path): @@ -93,7 +90,7 @@ class ClangdLanguageServer(SolidLanguageServer): """ Returns the initialize params for the clangd Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) as f: d = json.load(f) del d["_description"] @@ -126,6 +123,7 @@ class ClangdLanguageServer(SolidLanguageServer): # Shutdown the LanguageServer on exit from scope # LanguageServer has been shutdown """ + def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: @@ -175,7 +173,7 @@ class ClangdLanguageServer(SolidLanguageServer): assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 assert "completionProvider" in init_response["capabilities"] assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": ['.', '<', '>', ':', '"', '/', '*'], + "triggerCharacters": [".", "<", ">", ":", '"', "/", "*"], "resolveProvider": False, } @@ -185,4 +183,3 @@ class ClangdLanguageServer(SolidLanguageServer): # set ready flag self.server_ready.set() self.server_ready.wait() - diff --git a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py index 2a0c4e6..ec81c8c 100644 --- a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py @@ -19,7 +19,6 @@ class DartLanguageServer(SolidLanguageServer): """ Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ - executable_path = self.setup_runtime_dependencies(logger) super().__init__( config, @@ -32,14 +31,12 @@ class DartLanguageServer(SolidLanguageServer): def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str: platform_id = PlatformUtils.get_platform_id() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: d = json.load(f) del d["_description"] runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] assert len(runtime_dependencies) == 1 dependency = runtime_dependencies[0] @@ -49,24 +46,18 @@ class DartLanguageServer(SolidLanguageServer): if not os.path.exists(dart_ls_dir): os.makedirs(dart_ls_dir) - FileUtils.download_and_extract_archive( - logger, dependency["url"], dart_ls_dir, dependency["archiveType"] - ) - + FileUtils.download_and_extract_archive(logger, dependency["url"], dart_ls_dir, dependency["archiveType"]) assert os.path.exists(dart_executable_path) os.chmod(dart_executable_path, stat.S_IEXEC) return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" - def _get_initialize_params(self, repository_absolute_path: str): """ Returns the initialize params for the Dart Language Server. """ - with open( - os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r" - ) as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) as f: d = json.load(f) del d["_description"] @@ -79,9 +70,7 @@ class DartLanguageServer(SolidLanguageServer): d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path( - repository_absolute_path - ).as_uri() + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() assert d["workspaceFolders"][0]["name"] == "$name" d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) @@ -108,28 +97,20 @@ class DartLanguageServer(SolidLanguageServer): 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_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) - self.server.on_notification( - "experimental/serverStatus", check_experimental_status - ) + self.server.on_notification("experimental/serverStatus", check_experimental_status) - self.logger.log( - "Starting dart-language-server server process", logging.INFO - ) + self.logger.log("Starting dart-language-server server process", logging.INFO) 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, ) - init_response = self.server.send_request( - "initialize", initialize_params - ) + init_response = self.server.send_request("initialize", initialize_params) self.logger.log( f"Received initialize response from dart-language-server: {init_response}", logging.INFO, diff --git a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py index 1b92fc4..8bce13b 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -20,8 +20,7 @@ from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_settings import MultilspySettings -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from multilspy.multilspy_utils import FileUtils, PlatformUtils from solidlsp.ls import SolidLanguageServer @@ -51,7 +50,6 @@ class EclipseJDTLS(SolidLanguageServer): 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) self.runtime_dependency_paths = runtime_dependency_paths @@ -66,9 +64,7 @@ class EclipseJDTLS(SolidLanguageServer): ) # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces - shared_cache_location = str( - PurePath(MultilspySettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex") - ) + shared_cache_location = str(PurePath(MultilspySettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex")) jre_path = self.runtime_dependency_paths.jre_path lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path @@ -138,7 +134,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") - + @override def is_ignored_dirname(self, dirname: str) -> bool: # Ignore common Java build directories from different build tools: @@ -148,13 +144,13 @@ class EclipseJDTLS(SolidLanguageServer): # - IntelliJ IDEA: out, .idea # - General: classes, dist, lib return super().is_ignored_dirname(dirname) or dirname in [ - "target", # Maven - "build", # Gradle - "bin", # Eclipse - "out", # IntelliJ IDEA - "classes", # General - "dist", # General - "lib" # General + "target", # Maven + "build", # Gradle + "bin", # Eclipse + "out", # IntelliJ IDEA + "classes", # General + "dist", # General + "lib", # General ] def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> RuntimeDependencyPaths: @@ -163,7 +159,7 @@ class EclipseJDTLS(SolidLanguageServer): """ platformId = PlatformUtils.get_platform_id() - with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), "r", encoding="utf-8") as f: + with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), encoding="utf-8") as f: runtimeDependencies = json.load(f) del runtimeDependencies["_description"] @@ -192,9 +188,7 @@ class EclipseJDTLS(SolidLanguageServer): assert os.path.exists(gradle_path) dependency = runtimeDependencies["vscode-java"][platformId.value] - vscode_java_path = str( - PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]) - ) + vscode_java_path = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"])) os.makedirs(vscode_java_path, exist_ok=True) jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"])) jre_path = str(PurePath(vscode_java_path, dependency["jre_path"])) @@ -211,9 +205,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(logger, dependency["url"], vscode_java_path, dependency["archiveType"]) os.chmod(jre_path, stat.S_IEXEC) @@ -238,9 +230,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(logger, dependency["url"], intellicode_directory_path, dependency["archiveType"]) assert os.path.exists(intellicode_directory_path) assert os.path.exists(intellicode_jar_path) @@ -262,7 +252,7 @@ class EclipseJDTLS(SolidLanguageServer): Returns the initialize parameters for the EclipseJDTLS server. """ # Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available - with open(str(PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: + with open(str(PurePath(os.path.dirname(__file__), "initialize_params.json")), encoding="utf-8") as f: d: InitializeParams = json.load(f) del d["_description"] @@ -307,18 +297,12 @@ class EclipseJDTLS(SolidLanguageServer): for runtime in d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]: assert "name" in runtime assert "path" in runtime - assert os.path.exists( - runtime["path"] - ), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist" + assert os.path.exists(runtime["path"]), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist" assert d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] == "abs(static/gradle-7.3.3)" - d["initializationOptions"]["settings"]["java"]["import"]["gradle"][ - "home" - ] = self.runtime_dependency_paths.gradle_path + d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] = self.runtime_dependency_paths.gradle_path - d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"][ - "home" - ] = self.runtime_dependency_paths.jre_path + d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"]["home"] = self.runtime_dependency_paths.jre_path return d @@ -386,9 +370,7 @@ class EclipseJDTLS(SolidLanguageServer): self.server.notify.initialized({}) - self.server.notify.workspace_did_change_configuration( - {"settings": initialize_params["initializationOptions"]["settings"]} - ) + self.server.notify.workspace_did_change_configuration({"settings": initialize_params["initializationOptions"]["settings"]}) self.intellicode_enable_command_available.wait() diff --git a/src/solidlsp/language_servers/gopls/gopls.py b/src/solidlsp/language_servers/gopls/gopls.py index a2ec272..78f15ca 100644 --- a/src/solidlsp/language_servers/gopls/gopls.py +++ b/src/solidlsp/language_servers/gopls/gopls.py @@ -18,7 +18,7 @@ class Gopls(SolidLanguageServer): """ Provides Go specific instantiation of the LanguageServer class using gopls. """ - + @override def is_ignored_dirname(self, dirname: str) -> bool: # For Go projects, we should ignore: @@ -31,7 +31,7 @@ class Gopls(SolidLanguageServer): def _get_go_version(): """Get the installed Go version or None if not found.""" try: - result = subprocess.run(['go', 'version'], capture_output=True, text=True) + result = subprocess.run(["go", "version"], capture_output=True, text=True, check=False) if result.returncode == 0: return result.stdout.strip() except FileNotFoundError: @@ -42,7 +42,7 @@ class Gopls(SolidLanguageServer): def _get_gopls_version(): """Get the installed gopls version or None if not found.""" try: - result = subprocess.run(['gopls', 'version'], capture_output=True, text=True) + result = subprocess.run(["gopls", "version"], capture_output=True, text=True, check=False) if result.returncode == 0: return result.stdout.strip() except FileNotFoundError: @@ -57,8 +57,10 @@ class Gopls(SolidLanguageServer): """ go_version = cls._get_go_version() if not go_version: - raise RuntimeError("Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH.") - + raise RuntimeError( + "Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH." + ) + gopls_version = cls._get_gopls_version() if not gopls_version: raise RuntimeError( @@ -66,12 +68,12 @@ class Gopls(SolidLanguageServer): "Please install gopls as described in https://pkg.go.dev/golang.org/x/tools/gopls#section-readme\n\n" "After installation, make sure it is added to your PATH (it might be installed in a different location than Go)." ) - + return True def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): self.setup_runtime_dependency() - + super().__init__( config, logger, @@ -86,7 +88,7 @@ class Gopls(SolidLanguageServer): """ Returns the initialize params for the TypeScript Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -108,6 +110,7 @@ class Gopls(SolidLanguageServer): def _start_server(self): """Start gopls server process""" + def register_capability_handler(params): return diff --git a/src/solidlsp/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense/intelephense.py index 9b6eb95..fa41573 100644 --- a/src/solidlsp/language_servers/intelephense/intelephense.py +++ b/src/solidlsp/language_servers/intelephense/intelephense.py @@ -16,7 +16,7 @@ from multilspy.lsp_protocol_handler.lsp_types import DefinitionParams, Initializ from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import PlatformUtils, PlatformId +from multilspy.multilspy_utils import PlatformId, PlatformUtils from solidlsp.ls import SolidLanguageServer @@ -24,14 +24,14 @@ class Intelephense(SolidLanguageServer): """ Provides PHP specific instantiation of the LanguageServer class using Intelephense. """ - + @override def is_ignored_dirname(self, dirname: str) -> bool: # For PHP projects, we should ignore: # - vendor: third-party dependencies managed by Composer # - node_modules: if the project has JavaScript components # - cache: commonly used for caching - return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] + return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: """ @@ -50,17 +50,17 @@ class Intelephense(SolidLanguageServer): ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy PHP at the moment" - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] runtime_dependencies = d.get("runtimeDependencies", []) intelephense_ls_dir = os.path.join(os.path.dirname(__file__), "static", "php-lsp") - + # Verify both node and npm are installed - is_node_installed = shutil.which('node') is not None + is_node_installed = shutil.which("node") is not None assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." - is_npm_installed = shutil.which('npm') is not None + is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." # Install intelephense if not already installed @@ -75,11 +75,12 @@ class Intelephense(SolidLanguageServer): check=True, cwd=intelephense_ls_dir, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) else: # On Unix-like systems, run as non-root user import pwd + user = pwd.getpwuid(os.getuid()).pw_name subprocess.run( dependency["command"], @@ -88,32 +89,26 @@ class Intelephense(SolidLanguageServer): user=user, cwd=intelephense_ls_dir, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) - + intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") assert os.path.exists(intelephense_executable_path), "intelephense executable not found. Please install intelephense and try again." - + return f"{intelephense_executable_path} --stdio" def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): # Setup runtime dependencies before initializing intelephense_cmd = self.setup_runtime_dependencies(logger, config) - - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), - "php" - ) + + super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php") self.request_id = 0 def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the TypeScript Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -135,6 +130,7 @@ class Intelephense(SolidLanguageServer): def _start_server(self): """Start Intelephense server process""" + def register_capability_handler(params): return @@ -184,7 +180,7 @@ class Intelephense(SolidLanguageServer): # The sleeping doesn't seem to be needed on all systems sleep(1) return super()._send_references_request(relative_file_path, line, column) - + @override def _send_definition_request(self, definition_params: DefinitionParams): # TODO: same as above, also only a problem if the definition is in another file diff --git a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py index 9a4dbc8..65fbe62 100644 --- a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py +++ b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py @@ -32,7 +32,7 @@ class JediServer(SolidLanguageServer): ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path), "python", ) - + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] @@ -41,7 +41,7 @@ class JediServer(SolidLanguageServer): """ Returns the initialize params for the Jedi Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] diff --git a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py index 88e7151..bdb1333 100644 --- a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py @@ -13,8 +13,7 @@ from multilspy.lsp_protocol_handler.lsp_types import InitializeParams from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from multilspy.multilspy_utils import FileUtils, PlatformUtils from solidlsp.ls import SolidLanguageServer @@ -23,6 +22,7 @@ class KotlinRuntimeDependencyPaths: """ Stores the paths to the runtime dependencies of Kotlin Language Server """ + java_path: str java_home_path: str kotlin_executable_path: str @@ -39,13 +39,13 @@ class KotlinLanguageServer(SolidLanguageServer): """ runtime_dependency_paths = self.setup_runtime_dependencies(logger, config) self.runtime_dependency_paths = runtime_dependency_paths - + # Create command to execute the Kotlin Language Server script cmd = f'"{self.runtime_dependency_paths.kotlin_executable_path}"' - + # Set environment variables including JAVA_HOME proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} - + super().__init__( config, logger, @@ -61,77 +61,75 @@ class KotlinLanguageServer(SolidLanguageServer): 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 Kotlin in multilspy at the moment" + 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 Kotlin in multilspy at the moment" # Load dependency information - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] - + kotlin_dependency = d["runtimeDependency"] java_dependency = d["java"][platform_id.value] # Setup paths for dependencies static_dir = os.path.join(os.path.dirname(__file__), "static") os.makedirs(static_dir, exist_ok=True) - + # Setup Java paths 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"]) - + # 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"] - ) + FileUtils.download_and_extract_archive(logger, java_dependency["url"], java_dir, java_dependency["archiveType"]) # Make Java executable if not platform_id.value.startswith("win-"): os.chmod(java_path, 0o755) - + assert os.path.exists(java_path), f"Java executable not found at {java_path}" - + # Setup Kotlin Language Server paths kotlin_ls_dir = os.path.join(static_dir, "server") - + # Get platform-specific executable script path if platform_id.value.startswith("win-"): kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server.bat") else: kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server") - + # 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"] - ) - + FileUtils.download_and_extract_archive(logger, kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"]) + # Make script executable on Unix platforms if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"): - os.chmod(kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) - + os.chmod( + kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH + ) + # 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) else: raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}") - + return KotlinRuntimeDependencyPaths( - java_path=java_path, - java_home_path=java_home_path, - kotlin_executable_path=kotlin_executable_path + java_path=java_path, java_home_path=java_home_path, kotlin_executable_path=kotlin_executable_path ) def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Kotlin Language Server. """ - with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: + with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), encoding="utf-8") as f: d: InitializeParams = json.load(f) del d["_description"] @@ -152,8 +150,8 @@ class KotlinLanguageServer(SolidLanguageServer): d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()] assert ( - d["workspaceFolders"] - == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' + d["workspaceFolders"] + == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' ) d["workspaceFolders"] = [ { @@ -168,6 +166,7 @@ class KotlinLanguageServer(SolidLanguageServer): """ Starts the Kotlin Language Server """ + def execute_client_command_handler(params): return [] diff --git a/src/solidlsp/language_servers/omnisharp/omnisharp.py b/src/solidlsp/language_servers/omnisharp/omnisharp.py index a449a98..cc10f58 100644 --- a/src/solidlsp/language_servers/omnisharp/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp/omnisharp.py @@ -8,7 +8,7 @@ import os import pathlib import stat import threading -from typing import Iterable +from collections.abc import Iterable from overrides import override @@ -17,7 +17,7 @@ from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_exceptions import MultilspyException from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import FileUtils, PlatformUtils, PlatformId, DotnetVersion +from multilspy.multilspy_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils from solidlsp.ls import SolidLanguageServer @@ -28,7 +28,7 @@ def breadth_first_file_scan(root) -> Iterable[str]: """ dirs = [root] # while we has dirs to scan - while len(dirs): + while dirs: next_dirs = [] for parent in dirs: # scan each dir @@ -102,14 +102,12 @@ class OmniSharp(SolidLanguageServer): "formattingOptions:indentationSize=4", ] ) - super().__init__( - config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp" - ) + super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp") self.server_ready = threading.Event() self.definition_available = threading.Event() self.references_available = threading.Event() - + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj"] @@ -118,7 +116,7 @@ class OmniSharp(SolidLanguageServer): """ Returns the initialize params for the Omnisharp Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -145,7 +143,7 @@ class OmniSharp(SolidLanguageServer): platform_id = PlatformUtils.get_platform_id() dotnet_version = PlatformUtils.get_dotnet_version() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -156,7 +154,7 @@ class OmniSharp(SolidLanguageServer): assert dotnet_version in [ DotnetVersion.V6, DotnetVersion.V7, - DotnetVersion.V8 + DotnetVersion.V8, ], "Only dotnet version 6 and 7 are supported in multilspy at the moment" # TODO: Do away with this assumption @@ -165,13 +163,11 @@ class OmniSharp(SolidLanguageServer): dotnet_version = DotnetVersion.V6 runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] runtime_dependencies = [ dependency for dependency in runtime_dependencies - if not ("dotnet_version" in dependency) or dependency["dotnet_version"] == dotnet_version.value + if "dotnet_version" not in dependency or dependency["dotnet_version"] == dotnet_version.value ] assert len(runtime_dependencies) == 2 runtime_dependencies = { @@ -185,9 +181,7 @@ class OmniSharp(SolidLanguageServer): omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "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(logger, 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, stat.S_IEXEC) @@ -195,12 +189,8 @@ class OmniSharp(SolidLanguageServer): razor_omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "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" - ) - razor_omnisharp_dll_path = os.path.join( - razor_omnisharp_ls_dir, runtime_dependencies["RazorOmnisharp"]["dll_path"] - ) + FileUtils.download_and_extract_archive(logger, 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) return omnisharp_executable_path, razor_omnisharp_dll_path @@ -373,20 +363,12 @@ class OmniSharp(SolidLanguageServer): ) init_response = self.server.send.initialize(initialize_params) self.server.notify.initialized({}) - with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), "r", encoding="utf-8") as f: - self.server.notify.workspace_did_change_configuration({ - "settings": json.load(f) - }) + with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), encoding="utf-8") as f: + self.server.notify.workspace_did_change_configuration({"settings": json.load(f)}) assert "capabilities" in init_response - if ( - "definitionProvider" in init_response["capabilities"] - and init_response["capabilities"]["definitionProvider"] - ): + if "definitionProvider" in init_response["capabilities"] and init_response["capabilities"]["definitionProvider"]: self.definition_available.set() - if ( - "referencesProvider" in init_response["capabilities"] - and init_response["capabilities"]["referencesProvider"] - ): + if "referencesProvider" in init_response["capabilities"] and init_response["capabilities"]["referencesProvider"]: self.references_available.set() self.definition_available.wait() diff --git a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py index b349050..1307e0e 100644 --- a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py @@ -22,6 +22,7 @@ class PyrightServer(SolidLanguageServer): Provides Python specific instantiation of the LanguageServer class using Pyright. Contains various configurations and settings specific to Python. """ + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): """ Creates a PyrightServer instance. This class is not meant to be instantiated directly. @@ -50,7 +51,7 @@ class PyrightServer(SolidLanguageServer): Returns the initialize params for the Pyright Language Server. """ # Create basic initialization parameters - initialize_params: InitializeParams = { # type: ignore + initialize_params: InitializeParams = { # type: ignore "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": pathlib.Path(repository_absolute_path).as_uri(), diff --git a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py index eb072c4..e0b1a8c 100644 --- a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py @@ -15,8 +15,7 @@ from multilspy.lsp_protocol_handler.lsp_types import InitializeParams from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo from multilspy.multilspy_config import MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from multilspy.multilspy_utils import FileUtils, PlatformUtils from solidlsp.ls import SolidLanguageServer @@ -52,7 +51,7 @@ class RustAnalyzer(SolidLanguageServer): """ platform_id = PlatformUtils.get_platform_id() - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -62,9 +61,7 @@ class RustAnalyzer(SolidLanguageServer): # ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] assert len(runtime_dependencies) == 1 dependency = runtime_dependencies[0] @@ -73,13 +70,9 @@ class RustAnalyzer(SolidLanguageServer): if not os.path.exists(rustanalyzer_ls_dir): os.makedirs(rustanalyzer_ls_dir) if dependency["archiveType"] == "gz": - FileUtils.download_and_extract_archive( - logger, dependency["url"], rustanalyzer_executable_path, dependency["archiveType"] - ) + FileUtils.download_and_extract_archive(logger, dependency["url"], rustanalyzer_executable_path, dependency["archiveType"]) else: - FileUtils.download_and_extract_archive( - logger, dependency["url"], rustanalyzer_ls_dir, dependency["archiveType"] - ) + FileUtils.download_and_extract_archive(logger, dependency["url"], rustanalyzer_ls_dir, dependency["archiveType"]) assert os.path.exists(rustanalyzer_executable_path) os.chmod(rustanalyzer_executable_path, stat.S_IEXEC) @@ -89,7 +82,7 @@ class RustAnalyzer(SolidLanguageServer): """ Returns the initialize params for the Rust Analyzer Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] diff --git a/src/solidlsp/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph/solargraph.py index d036ed1..96fc704 100644 --- a/src/solidlsp/language_servers/solargraph/solargraph.py +++ b/src/solidlsp/language_servers/solargraph/solargraph.py @@ -51,8 +51,7 @@ class Solargraph(SolidLanguageServer): """ Setup runtime dependencies for Solargraph. """ - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] @@ -64,38 +63,40 @@ class Solargraph(SolidLanguageServer): ruby_version = result.stdout.strip() logger.log(f"Ruby version: {ruby_version}", logging.INFO) except subprocess.CalledProcessError as e: - raise RuntimeError(f"Error checking for Ruby installation: {e.stderr}") - except FileNotFoundError: - raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") + raise RuntimeError(f"Error checking for Ruby installation: {e.stderr}") from e + except FileNotFoundError as e: + raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") from e # Check if solargraph is installed try: - result = subprocess.run(["gem", "list", "^solargraph$", "-i"], check=False, capture_output=True, text=True, cwd=repository_root_path) + result = subprocess.run( + ["gem", "list", "^solargraph$", "-i"], check=False, capture_output=True, text=True, cwd=repository_root_path + ) if result.stdout.strip() == "false": logger.log("Installing Solargraph...", logging.INFO) subprocess.run(dependency["installCommand"].split(), check=True, capture_output=True, cwd=repository_root_path) - + # Get the gem executable path directly result = subprocess.run(["gem", "which", "solargraph"], check=True, capture_output=True, text=True, cwd=repository_root_path) gem_path = result.stdout.strip() bin_dir = os.path.join(os.path.dirname(os.path.dirname(gem_path)), "bin") executable_path = os.path.join(bin_dir, "solargraph") - + if not os.path.exists(executable_path): raise RuntimeError(f"Solargraph executable not found at {executable_path}") - + # Ensure the executable has the right permissions os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IEXEC) return executable_path except subprocess.CalledProcessError as e: - raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") + raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") from e def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Solargraph Language Server. """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: d = json.load(f) del d["_description"] diff --git a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py index e17a098..7d5fae6 100644 --- a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py @@ -21,14 +21,14 @@ from multilspy.multilspy_utils import PlatformId, PlatformUtils from solidlsp.ls import SolidLanguageServer # Platform-specific imports -if os.name != 'nt': # Unix-like systems +if os.name != "nt": # Unix-like systems import pwd else: # Dummy pwd module for Windows class pwd: @staticmethod def getpwuid(uid): - return type('obj', (), {'pw_name': os.environ.get('USERNAME', 'unknown')})() + return type("obj", (), {"pw_name": os.environ.get("USERNAME", "unknown")})() # Conditionally import pwd module (Unix-only) @@ -91,9 +91,9 @@ class TypeScriptLanguageServer(SolidLanguageServer): tsserver_executable_path = os.path.join(tsserver_ls_dir, "typescript-language-server") # Verify both node and npm are installed - is_node_installed = shutil.which('node') is not None + is_node_installed = shutil.which("node") is not None assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." - is_npm_installed = shutil.which('npm') is not None + is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." # Install typescript and typescript-language-server if not already installed @@ -108,7 +108,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): check=True, cwd=tsserver_ls_dir, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) else: # On Unix-like systems, run as non-root user @@ -120,12 +120,14 @@ class TypeScriptLanguageServer(SolidLanguageServer): user=user, cwd=tsserver_ls_dir, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) - + tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") - assert os.path.exists(tsserver_executable_path), "typescript-language-server executable not found. Please install typescript-language-server and try again." + assert os.path.exists( + tsserver_executable_path + ), "typescript-language-server executable not found. Please install typescript-language-server and try again." return f"{tsserver_executable_path} --stdio" def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: @@ -184,8 +186,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - + def check_experimental_status(params): """ Also listen for experimental/serverStatus as a backup signal @@ -215,8 +216,8 @@ class TypeScriptLanguageServer(SolidLanguageServer): assert init_response["capabilities"]["textDocumentSync"] == 2 assert "completionProvider" in init_response["capabilities"] assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": ['.', '"', "'", '/', '@', '<'], - "resolveProvider": True + "triggerCharacters": [".", '"', "'", "/", "@", "<"], + "resolveProvider": True, } self.server.notify.initialized({}) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 9981ed1..11c9feb 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -5,18 +5,19 @@ import pathlib import pickle import re import threading -from abc import abstractmethod, ABC +from abc import ABC, abstractmethod from collections import defaultdict +from collections.abc import Iterator from contextlib import contextmanager from copy import copy from pathlib import Path, PurePath -from typing import Dict, Iterator, List, Optional, Tuple, Union, cast, Self +from typing import Self, cast import pathspec import tqdm from multilspy import multilspy_types -from multilspy.language_server import LSPFileBuffer, GenericDocumentSymbol, ReferenceInSymbol +from multilspy.language_server import GenericDocumentSymbol, LSPFileBuffer, ReferenceInSymbol from multilspy.lsp_protocol_handler import lsp_types from multilspy.lsp_protocol_handler import lsp_types as LSPTypes from multilspy.lsp_protocol_handler.lsp_constants import LSPConstants @@ -46,10 +47,12 @@ class SolidLanguageServer(ABC): A language-specific condition for directories that should always be ignored. For example, venv in Python and node_modules in JS/TS should be ignored always. """ - return dirname.startswith('.') + return dirname.startswith(".") @classmethod - def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, timeout: Optional[float] = None) -> "SolidLanguageServer": + def create( + cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, timeout: float | None = None + ) -> "SolidLanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -66,6 +69,7 @@ class SolidLanguageServer(ABC): from solidlsp.language_servers.pyright_language_server.pyright_server import ( PyrightServer, ) + return PyrightServer(config, logger, repository_root_path) # It used to be jedi, but pyright is a bit faster, and also more actively maintained # Keeping the previous code for reference @@ -79,48 +83,58 @@ class SolidLanguageServer(ABC): from solidlsp.language_servers.eclipse_jdtls.eclipse_jdtls import ( EclipseJDTLS, ) + return EclipseJDTLS(config, logger, repository_root_path) elif config.code_language == Language.KOTLIN: from solidlsp.language_servers.kotlin_language_server.kotlin_language_server import ( KotlinLanguageServer, ) + return KotlinLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.RUST: from solidlsp.language_servers.rust_analyzer.rust_analyzer import ( RustAnalyzer, ) + return RustAnalyzer(config, logger, repository_root_path) elif config.code_language == Language.CSHARP: from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp + return OmniSharp(config, logger, repository_root_path) elif config.code_language in [Language.TYPESCRIPT, Language.JAVASCRIPT]: from solidlsp.language_servers.typescript_language_server.typescript_language_server import ( TypeScriptLanguageServer, ) + return TypeScriptLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.GO: from solidlsp.language_servers.gopls.gopls import Gopls + return Gopls(config, logger, repository_root_path) elif config.code_language == Language.RUBY: from solidlsp.language_servers.solargraph.solargraph import Solargraph + return Solargraph(config, logger, repository_root_path) elif config.code_language == Language.DART: from solidlsp.language_servers.dart_language_server.dart_language_server import DartLanguageServer + return DartLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.CPP: from solidlsp.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer + return ClangdLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.PHP: from solidlsp.language_servers.intelephense.intelephense import Intelephense + return Intelephense(config, logger, repository_root_path) else: @@ -128,12 +142,12 @@ class SolidLanguageServer(ABC): raise MultilspyException(f"Language {config.code_language} is not supported") def __init__( - self, - config: MultilspyConfig, - logger: MultilspyLogger, - repository_root_path: str, - process_launch_info: ProcessLaunchInfo, - language_id: str, + self, + config: MultilspyConfig, + logger: MultilspyLogger, + repository_root_path: str, + process_launch_info: ProcessLaunchInfo, + language_id: str, ): """ Initializes a LanguageServer instance. @@ -150,14 +164,19 @@ class SolidLanguageServer(ABC): """ self.logger = logger self.repository_root_path: str = repository_root_path - self.logger.log(f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) + self.logger.log( + f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}", + logging.DEBUG, + ) self.language_id = language_id - self.open_file_buffers: Dict[str, LSPFileBuffer] = {} + self.open_file_buffers: dict[str, LSPFileBuffer] = {} self.language = Language(language_id) # load cache first to prevent any racing conditions due to asyncio stuff - self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]]] = {} + self._document_symbols_cache: dict[ + str, tuple[str, tuple[list[multilspy_types.UnifiedSymbolInformation], list[multilspy_types.UnifiedSymbolInformation]]] + ] = {} """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" self._cache_lock = threading.Lock() self._cache_has_changed: bool = False @@ -166,36 +185,35 @@ class SolidLanguageServer(ABC): self.server_started = False self.completions_available = threading.Event() if config.trace_lsp_communication: + def logging_fn(source: str, target: str, msg: StringDict | str): self.logger.log(f"LSP: {source} -> {target}: {str(msg)[:90]}...", self.logger.logger.level) + else: logging_fn = None - # cmd is obtained from the child classes, which provide the language specific command to start the language server # LanguageServerHandler provides the functionality to start the language server and communicate with it - self.logger.log(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) + self.logger.log( + f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG + ) self.server = SolidLanguageServerHandler( process_launch_info, logger=logging_fn, start_independent_lsp_process=config.start_independent_lsp_process, ) - # Set up the pathspec matcher for the ignored paths # for all absolute paths in ignored_paths, convert them to relative paths processed_patterns = [] for pattern in set(config.ignored_paths): # Normalize separators (pathspec expects forward slashes) - pattern = pattern.replace(os.path.sep, '/') + pattern = pattern.replace(os.path.sep, "/") processed_patterns.append(pattern) self.logger.log(f"Processing {len(processed_patterns)} ignored paths from the config", logging.DEBUG) # Create a pathspec matcher from the processed patterns - self._ignore_spec = pathspec.PathSpec.from_lines( - pathspec.patterns.GitWildMatchPattern, - processed_patterns - ) + self._ignore_spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, processed_patterns) self._server_context = None @@ -247,12 +265,12 @@ class SolidLanguageServer(ABC): # Use pathspec for gitignore-style pattern matching # Normalize path separators for pathspec (it expects forward slashes) - normalized_path = str(rel_path).replace(os.path.sep, '/') + normalized_path = str(rel_path).replace(os.path.sep, "/") # pathspec can't handle the matching of directories if they don't end with a slash! # see https://github.com/cpburnz/python-pathspec/issues/89 - if os.path.isdir(os.path.join(self.repository_root_path, normalized_path)) and not normalized_path.endswith('/'): - normalized_path = normalized_path + '/' + if os.path.isdir(os.path.join(self.repository_root_path, normalized_path)) and not normalized_path.endswith("/"): + normalized_path = normalized_path + "/" # Use the pathspec matcher to check if the path matches any ignore pattern if self.get_ignore_spec().match_file(normalized_path): @@ -271,7 +289,6 @@ class SolidLanguageServer(ABC): self.logger.log(f"Initiating final robust shutdown with a {timeout}s timeout...", logging.INFO) process = self.server.process - reader_tasks = list(self.server.tasks.values()) # --- Main Shutdown Logic --- # Stage 1: Graceful Termination Request @@ -281,50 +298,11 @@ class SolidLanguageServer(ABC): if process.stdin and not process.stdin.is_closing(): process.stdin.close() except Exception: - pass # Ignore errors here, we are proceeding to terminate anyway. + pass # Ignore errors here, we are proceeding to terminate anyway. # Stage 2: Terminate and Concurrently Drain stdout/stderr process.terminate() - """ - except asyncio.TimeoutError: - # Stage 3: Forceful Kill - self.logger.log("Graceful termination failed. Forcefully killing process...", logging.WARNING) - if self.server.is_running(): - try: - process.kill() - # Wait for the killed process to be reaped by the OS. - await process.wait() - except Exception as e: - self.logger.log(f"Error during forceful kill: {e}", logging.ERROR) - except Exception as e: - self.logger.log(f"An unexpected error occurred during shutdown logic: {e}", logging.ERROR) - - finally: - # === STAGE 4: EXPLICIT TASK & PIPE CLEANUP === - self.logger.log("Performing final task cancellation and pipe handle cleanup...", logging.DEBUG) - - # 1. Cancel any lingering reader tasks. - for task in reader_tasks: - if not task.done(): - task.cancel() - # Wait for cancellations to complete. - await asyncio.gather(*reader_tasks, return_exceptions=True) - self.server.tasks = {} - - # 2. Explicitly close each pipe to release OS handles. - for pipe in [process.stdin, process.stdout, process.stderr]: - if pipe and hasattr(pipe, 'is_closing') and not pipe.is_closing(): - try: - pipe.close() - except Exception: - pass - - # 3. Null out the process object in the handler. - self.server.process = None - self.logger.log("Shutdown sequence fully finished.", logging.DEBUG) - """ - @contextmanager def start_server(self) -> Iterator["SolidLanguageServer"]: self.start() @@ -393,10 +371,10 @@ class SolidLanguageServer(ABC): del self.open_file_buffers[uri] def insert_text_at_position( - self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str + self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str ) -> multilspy_types.Position: """ - Insert text at the given line and column in the given file and return + Insert text at the given line and column in the given file and return the updated cursor position after inserting the text. :param relative_file_path: The relative path of the file to open. @@ -442,10 +420,10 @@ class SolidLanguageServer(ABC): return multilspy_types.Position(line=new_l, character=new_c) def delete_text_between_positions( - self, - relative_file_path: str, - start: multilspy_types.Position, - end: multilspy_types.Position, + self, + relative_file_path: str, + start: multilspy_types.Position, + end: multilspy_types.Position, ) -> str: """ Delete text between the given start and end positions in the given file and return the deleted text. @@ -465,7 +443,9 @@ class SolidLanguageServer(ABC): file_buffer = self.open_file_buffers[uri] file_buffer.version += 1 - new_contents, deleted_text = TextUtils.delete_text_between_positions(file_buffer.contents, start_line=start["line"], start_col=start["character"], end_line=end["line"], end_col=end["character"]) + new_contents, deleted_text = TextUtils.delete_text_between_positions( + file_buffer.contents, start_line=start["line"], start_col=start["character"], end_line=end["line"], end_col=end["character"] + ) file_buffer.contents = new_contents self.server.notify.did_change_text_document( { @@ -478,12 +458,10 @@ class SolidLanguageServer(ABC): ) return deleted_text - def _send_definition_request(self, definition_params: DefinitionParams) -> Union[Definition, List[LocationLink], None]: + def _send_definition_request(self, definition_params: DefinitionParams) -> Definition | list[LocationLink] | None: return self.server.send.definition(definition_params) - def request_definition( - self, relative_file_path: str, line: int, column: int - ) -> List[multilspy_types.Location]: + def request_definition(self, relative_file_path: str, line: int, column: int) -> list[multilspy_types.Location]: """ Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server for the symbol at the given line and column in the given file. Wait for the response and return the result. @@ -494,7 +472,6 @@ class SolidLanguageServer(ABC): :return List[multilspy_types.Location]: A list of locations where the symbol is defined """ - if not self.server_started: self.logger.log( "find_function_definition called before Language Server started", @@ -504,20 +481,21 @@ class SolidLanguageServer(ABC): with self.open_file(relative_file_path): # sending request to the language server and waiting for response - definition_params = cast(DefinitionParams, { - LSPConstants.TEXT_DOCUMENT: { - LSPConstants.URI: pathlib.Path( - str(PurePath(self.repository_root_path, relative_file_path)) - ).as_uri() + definition_params = cast( + DefinitionParams, + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri() + }, + LSPConstants.POSITION: { + LSPConstants.LINE: line, + LSPConstants.CHARACTER: column, + }, }, - LSPConstants.POSITION: { - LSPConstants.LINE: line, - LSPConstants.CHARACTER: column, - }, - }) + ) response = self._send_definition_request(definition_params) - ret: List[multilspy_types.Location] = [] + ret: list[multilspy_types.Location] = [] if isinstance(response, list): # response is either of type Location[] or LocationLink[] for item in response: @@ -529,10 +507,10 @@ class SolidLanguageServer(ABC): new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) ret.append(multilspy_types.Location(new_item)) elif ( - LSPConstants.ORIGIN_SELECTION_RANGE in item - and LSPConstants.TARGET_URI in item - and LSPConstants.TARGET_RANGE in item - and LSPConstants.TARGET_SELECTION_RANGE in item + LSPConstants.ORIGIN_SELECTION_RANGE in item + and LSPConstants.TARGET_URI in item + and LSPConstants.TARGET_RANGE in item + and LSPConstants.TARGET_SELECTION_RANGE in item ): new_item: multilspy_types.Location = {} new_item["uri"] = item[LSPConstants.TARGET_URI] @@ -565,7 +543,7 @@ class SolidLanguageServer(ABC): return ret # Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses - def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[lsp_types.Location] | None: + def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None: return self.server.send.references( { "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, @@ -574,9 +552,7 @@ class SolidLanguageServer(ABC): } ) - def request_references( - self, relative_file_path: str, line: int, column: int - ) -> List[multilspy_types.Location]: + def request_references(self, relative_file_path: str, line: int, column: int) -> list[multilspy_types.Location]: """ Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server to find references to the symbol at the given line and column in the given file. Wait for the response and return the result. @@ -588,7 +564,6 @@ class SolidLanguageServer(ABC): :return: A list of locations where the symbol is referenced (excluding ignored directories) """ - if not self.server_started: self.logger.log( "request_references called before Language Server started", @@ -596,13 +571,12 @@ class SolidLanguageServer(ABC): ) raise MultilspyException("Language Server not started") - with self.open_file(relative_file_path): try: response = self._send_references_request(relative_file_path, line=line, column=column) except Exception as e: # Catch LSP internal error (-32603) and raise a more informative exception - if isinstance(e, Error) and getattr(e, 'code', None) == -32603: + if isinstance(e, Error) and getattr(e, "code", None) == -32603: raise RuntimeError( f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. " "This often occurs when requesting references for a symbol not referenced in the expected way. " @@ -611,7 +585,7 @@ class SolidLanguageServer(ABC): if response is None: return [] - ret: List[multilspy_types.Location] = [] + ret: list[multilspy_types.Location] = [] assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}" for item in response: assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}" @@ -633,8 +607,8 @@ class SolidLanguageServer(ABC): return ret def request_references_with_content( - self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0 - ) -> List[MatchedConsecutiveLines]: + self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0 + ) -> list[MatchedConsecutiveLines]: """ Like request_references, but returns the content of the lines containing the references, not just the locations. @@ -647,7 +621,10 @@ class SolidLanguageServer(ABC): :return: A list of MatchedConsecutiveLines objects, one for each reference. """ references = self.request_references(relative_file_path, line, column) - return [self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) for ref in references] + return [ + self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) + for ref in references + ] def retrieve_full_file_content(self, relative_file_path: str) -> str: """ @@ -656,7 +633,9 @@ class SolidLanguageServer(ABC): with self.open_file(relative_file_path) as file_data: return file_data.contents - def retrieve_content_around_line(self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0) -> MatchedConsecutiveLines: + def retrieve_content_around_line( + self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0 + ) -> MatchedConsecutiveLines: """ Retrieve the content of the given file around the given line. @@ -669,11 +648,17 @@ class SolidLanguageServer(ABC): """ with self.open_file(relative_file_path) as file_data: file_contents = file_data.contents - return MatchedConsecutiveLines.from_file_contents(file_contents, line=line, context_lines_before=context_lines_before, context_lines_after=context_lines_after, source_file_path=relative_file_path) + return MatchedConsecutiveLines.from_file_contents( + file_contents, + line=line, + context_lines_before=context_lines_before, + context_lines_after=context_lines_after, + source_file_path=relative_file_path, + ) def request_completions( - self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False - ) -> List[multilspy_types.CompletionItem]: + self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False + ) -> list[multilspy_types.CompletionItem]: """ Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server to find completions at the given line and column in the given file. Wait for the response and return the result. @@ -685,39 +670,35 @@ class SolidLanguageServer(ABC): :return List[multilspy_types.CompletionItem]: A list of completions """ with self.open_file(relative_file_path): - open_file_buffer = self.open_file_buffers[ - pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() - ] + open_file_buffer = self.open_file_buffers[pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()] completion_params: LSPTypes.CompletionParams = { "position": {"line": line, "character": column}, "textDocument": {"uri": open_file_buffer.uri}, "context": {"triggerKind": LSPTypes.CompletionTriggerKind.Invoked}, } - response: Union[List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None] = None + response: list[LSPTypes.CompletionItem] | LSPTypes.CompletionList | None = None num_retries = 0 while response is None or (response["isIncomplete"] and num_retries < 30): self.completions_available.wait() - response: Union[ - List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None - ] = self.server.send.completion(completion_params) + response: list[LSPTypes.CompletionItem] | LSPTypes.CompletionList | None = self.server.send.completion(completion_params) if isinstance(response, list): response = {"items": response, "isIncomplete": False} num_retries += 1 # TODO: Understand how to appropriately handle `isIncomplete` - if response is None or (response["isIncomplete"] and not(allow_incomplete)): + if response is None or (response["isIncomplete"] and not (allow_incomplete)): return [] if "items" in response: response = response["items"] - response: List[LSPTypes.CompletionItem] = response + response = cast(list[LSPTypes.CompletionItem], response) # TODO: Handle the case when the completion is a keyword items = [item for item in response if item["kind"] != LSPTypes.CompletionItemKind.Keyword] - completions_list: List[multilspy_types.CompletionItem] = [] + completions_list: list[multilspy_types.CompletionItem] = [] for item in items: assert "insertText" in item or "textEdit" in item @@ -745,8 +726,7 @@ class SolidLanguageServer(ABC): item["textEdit"]["range"]["start"]["line"] == new_dot_lineno, item["textEdit"]["range"]["start"]["character"] == new_dot_colno, item["textEdit"]["range"]["start"]["line"] == item["textEdit"]["range"]["end"]["line"], - item["textEdit"]["range"]["start"]["character"] - == item["textEdit"]["range"]["end"]["character"], + item["textEdit"]["range"]["start"]["character"] == item["textEdit"]["range"]["end"]["character"], ) ) @@ -760,12 +740,11 @@ class SolidLanguageServer(ABC): completion_item = multilspy_types.CompletionItem(**completion_item) completions_list.append(completion_item) - return [ - json.loads(json_repr) - for json_repr in set([json.dumps(item, sort_keys=True) for item in completions_list]) - ] + return [json.loads(json_repr) for json_repr in set(json.dumps(item, sort_keys=True) for item in completions_list)] - def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: + def request_document_symbols( + self, relative_file_path: str, include_body: bool = False + ) -> tuple[list[multilspy_types.UnifiedSymbolInformation], list[multilspy_types.UnifiedSymbolInformation]]: """ Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server to find symbols in the given file. Wait for the response and return the result. @@ -797,13 +776,12 @@ class SolidLanguageServer(ABC): self.logger.log(f"Requesting document symbols for {relative_file_path} from the Language Server", logging.DEBUG) response = self.server.send.document_symbol( - { - "textDocument": { - "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() - } - } + {"textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}} + ) + self.logger.log( + f"Received {len(response) if response is not None else None} document symbols for {relative_file_path} from the Language Server", + logging.DEBUG, ) - self.logger.log(f"Received {len(response) if response is not None else None} document symbols for {relative_file_path} from the Language Server", logging.DEBUG) def turn_item_into_symbol_with_children(item: GenericDocumentSymbol): item = cast(multilspy_types.UnifiedSymbolInformation, item) @@ -815,18 +793,18 @@ class SolidLanguageServer(ABC): assert "range" in item tree_location = multilspy_types.Location( uri=uri, - range=item['range'], + range=item["range"], absolutePath=absolute_path, relativePath=relative_file_path, ) - item['location'] = tree_location + item["location"] = tree_location location = item["location"] if "absolutePath" not in location: location["absolutePath"] = absolute_path if "relativePath" not in location: location["relativePath"] = relative_file_path if include_body: - item['body'] = self.retrieve_symbol_body(item) + item["body"] = self.retrieve_symbol_body(item) # handle missing selectionRange if "selectionRange" not in item: if "range" in item: @@ -838,9 +816,9 @@ class SolidLanguageServer(ABC): child["parent"] = item item[LSPConstants.CHILDREN] = children - flat_all_symbol_list: List[multilspy_types.UnifiedSymbolInformation] = [] + flat_all_symbol_list: list[multilspy_types.UnifiedSymbolInformation] = [] assert isinstance(response, list), f"Unexpected response from Language Server: {response}" - root_nodes: List[multilspy_types.UnifiedSymbolInformation] = [] + root_nodes: list[multilspy_types.UnifiedSymbolInformation] = [] for root_item in response: if "range" not in root_item and "location" not in root_item: if root_item["kind"] in [SymbolKind.File, SymbolKind.Module]: @@ -861,9 +839,9 @@ class SolidLanguageServer(ABC): if LSPConstants.CHILDREN in root_symbol: # TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well - def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> List[multilspy_types.UnifiedSymbolInformation]: + def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> list[multilspy_types.UnifiedSymbolInformation]: node = cast(multilspy_types.UnifiedSymbolInformation, node) - l: List[multilspy_types.UnifiedSymbolInformation] = [] + l: list[multilspy_types.UnifiedSymbolInformation] = [] turn_item_into_symbol_with_children(node) assert LSPConstants.CHILDREN in node children = node[LSPConstants.CHILDREN] @@ -883,9 +861,11 @@ class SolidLanguageServer(ABC): self._cache_has_changed = True return result - def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + def request_full_symbol_tree( + self, within_relative_path: str | None = None, include_body: bool = False + ) -> list[multilspy_types.UnifiedSymbolInformation]: """ - Will go through all files in the project or within a relative path and build a tree of symbols. + Will go through all files in the project or within a relative path and build a tree of symbols. Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search. For each file, a symbol of kind File (2) will be created. For directories, a symbol of kind Package (4) will be created. @@ -902,21 +882,23 @@ class SolidLanguageServer(ABC): :return: A list of root symbols representing the top-level packages/modules in the project. """ - if within_relative_path is not None: within_abs_path = os.path.join(self.repository_root_path, within_relative_path) if not os.path.exists(within_abs_path): raise FileNotFoundError(f"File or directory not found: {within_abs_path}") if os.path.isfile(within_abs_path): if self.is_ignored_path(within_relative_path): - self.logger.log(f"You passed a file explicitly, but it is ignored. This is probably an error. File: {within_relative_path}", logging.ERROR) + self.logger.log( + f"You passed a file explicitly, but it is ignored. This is probably an error. File: {within_relative_path}", + logging.ERROR, + ) return [] else: _, root_nodes = self.request_document_symbols(within_relative_path, include_body=include_body) return root_nodes # Helper function to recursively process directories - def process_directory(rel_dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]: + def process_directory(rel_dir_path: str) -> list[multilspy_types.UnifiedSymbolInformation]: abs_dir_path = self.repository_root_path if rel_dir_path == "." else os.path.join(self.repository_root_path, rel_dir_path) abs_dir_path = os.path.realpath(abs_dir_path) @@ -931,7 +913,7 @@ class SolidLanguageServer(ABC): return [] # Create package symbol for directory - package_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore + package_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore name=os.path.basename(abs_dir_path), kind=multilspy_types.SymbolKind.Package, location=multilspy_types.Location( @@ -940,7 +922,7 @@ class SolidLanguageServer(ABC): absolutePath=str(abs_dir_path), relativePath=str(Path(abs_dir_path).resolve().relative_to(self.repository_root_path)), ), - children=[] + children=[], ) result.append(package_symbol) @@ -964,7 +946,7 @@ class SolidLanguageServer(ABC): file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)) with self.open_file(file_rel_path) as file_data: fileRange = self._get_range_from_file_content(file_data.contents) - file_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore + file_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore name=os.path.splitext(contained_dir_or_file_name)[0], kind=multilspy_types.SymbolKind.File, range=fileRange, @@ -985,7 +967,7 @@ class SolidLanguageServer(ABC): package_symbol["children"].append(file_symbol) # TODO: Not sure if this is actually still needed given recent changes to relative path handling - def fix_relative_path(nodes: List[multilspy_types.UnifiedSymbolInformation]): + def fix_relative_path(nodes: list[multilspy_types.UnifiedSymbolInformation]): for node in nodes: if "location" in node and "relativePath" in node["location"]: path = Path(node["location"]["relativePath"]) @@ -1015,8 +997,7 @@ class SolidLanguageServer(ABC): end_line = len(lines) end_column = len(lines[-1]) return multilspy_types.Range( - start=multilspy_types.Position(line=0, character=0), - end=multilspy_types.Position(line=end_line, character=end_column) + start=multilspy_types.Position(line=0, character=0), end=multilspy_types.Position(line=end_line, character=end_column) ) def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: @@ -1038,12 +1019,14 @@ class SolidLanguageServer(ABC): assert "location" in child assert "selectionRange" in child path = Path(child["location"]["absolutePath"]).resolve().relative_to(self.repository_root_path) - result[str(path)].append(( - child["name"], - child["kind"], - child["selectionRange"]["start"]["line"], - child["selectionRange"]["start"]["character"] - )) + result[str(path)].append( + ( + child["name"], + child["kind"], + child["selectionRange"]["start"]["line"], + child["selectionRange"]["start"]["character"], + ) + ) # For package/directory symbols, process their children for child in symbol["children"]: process_symbol(child) @@ -1063,15 +1046,10 @@ class SolidLanguageServer(ABC): for root in document_roots: try: result.append( - ( root["name"], - root["kind"], - root["selectionRange"]["start"]["line"], - root["selectionRange"]["start"]["character"],) + (root["name"], root["kind"], root["selectionRange"]["start"]["line"], root["selectionRange"]["start"]["character"]) ) except KeyError as e: - raise KeyError( - f"Could not process symbol of name {root.get('name', 'unknown')} in {relative_file_path=}" - ) from e + raise KeyError(f"Could not process symbol of name {root.get('name', 'unknown')} in {relative_file_path=}") from e return result def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: @@ -1091,7 +1069,7 @@ class SolidLanguageServer(ABC): else: return self.request_dir_overview(within_relative_path) - def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: + def request_hover(self, relative_file_path: str, line: int, column: int) -> multilspy_types.Hover | None: """ Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server to find the hover information at the given line and column in the given file. Wait for the response and return the result. @@ -1105,9 +1083,7 @@ class SolidLanguageServer(ABC): with self.open_file(relative_file_path): response = self.server.send.hover( { - "textDocument": { - "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() - }, + "textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}, "position": { "line": line, "character": column, @@ -1124,7 +1100,9 @@ class SolidLanguageServer(ABC): # ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA -------------------- - def retrieve_symbol_body(self, symbol: multilspy_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation) -> str: + def retrieve_symbol_body( + self, symbol: multilspy_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation + ) -> str: """ Load the body of the given symbol. If the body is already contained in the symbol, just return it. """ @@ -1138,7 +1116,7 @@ class SolidLanguageServer(ABC): assert "relativePath" in symbol["location"] symbol_file = self.retrieve_full_file_content(symbol["location"]["relativePath"]) symbol_lines = symbol_file.split("\n") - symbol_body = "\n".join(symbol_lines[symbol_start_line:symbol_end_line+1]) + symbol_body = "\n".join(symbol_lines[symbol_start_line : symbol_end_line + 1]) # remove leading indentation symbol_start_column = symbol["location"]["range"]["start"]["character"] @@ -1163,12 +1141,12 @@ class SolidLanguageServer(ABC): return rel_file_paths def search_files_for_pattern( - self, - pattern: re.Pattern | str, - context_lines_before: int = 0, - context_lines_after: int = 0, - paths_include_glob: str | None = None, - paths_exclude_glob: str | None = None, + self, + pattern: re.Pattern | str, + context_lines_before: int = 0, + context_lines_after: int = 0, + paths_include_glob: str | None = None, + paths_exclude_glob: str | None = None, ) -> list[MatchedConsecutiveLines]: """ Search for a pattern across all files analyzed by the Language Server. @@ -1191,19 +1169,19 @@ class SolidLanguageServer(ABC): context_lines_before=context_lines_before, context_lines_after=context_lines_after, paths_include_glob=paths_include_glob, - paths_exclude_glob=paths_exclude_glob + paths_exclude_glob=paths_exclude_glob, ) def request_referencing_symbols( - self, - relative_file_path: str, - line: int, - column: int, - include_imports: bool = True, - include_self: bool = False, - include_body: bool = False, - include_file_symbols: bool = False, - ) -> List[ReferenceInSymbol]: + self, + relative_file_path: str, + line: int, + column: int, + include_imports: bool = True, + include_self: bool = False, + include_body: bool = False, + include_file_symbols: bool = False, + ) -> list[ReferenceInSymbol]: """ Finds all symbols that reference the symbol at the given location. This is similar to request_references but filters to only include symbols @@ -1244,9 +1222,7 @@ class SolidLanguageServer(ABC): with self.open_file(ref_path) as file_data: # Get the containing symbol for this reference - containing_symbol = self.request_containing_symbol( - ref_path, ref_line, ref_col, include_body=include_body - ) + containing_symbol = self.request_containing_symbol(ref_path, ref_line, ref_col, include_body=include_body) if containing_symbol is None: # TODO: HORRIBLE HACK! I don't know how to do it better for now... # THIS IS BOUND TO BREAK IN MANY CASES! IT IS ALSO SPECIFIC TO PYTHON! @@ -1276,7 +1252,7 @@ class SolidLanguageServer(ABC): if containing_symbol is None and include_file_symbols: self.logger.log( f"Could not find containing symbol for {ref_path}:{ref_line}:{ref_col}. Returning file symbol instead", - logging.WARNING + logging.WARNING, ) fileRange = self._get_range_from_file_content(file_data.contents) location = multilspy_types.Location( @@ -1301,7 +1277,7 @@ class SolidLanguageServer(ABC): children=[], body=body, ) - if containing_symbol is None or not include_file_symbols and containing_symbol["kind"] == multilspy_types.SymbolKind.File: + if containing_symbol is None or (not include_file_symbols and containing_symbol["kind"] == multilspy_types.SymbolKind.File): continue assert "location" in containing_symbol @@ -1309,31 +1285,31 @@ class SolidLanguageServer(ABC): # Checking for self-reference if ( - containing_symbol["location"]["relativePath"] == relative_file_path - and containing_symbol["selectionRange"]["start"]["line"] == ref_line - and containing_symbol["selectionRange"]["start"]["character"] == ref_col + containing_symbol["location"]["relativePath"] == relative_file_path + and containing_symbol["selectionRange"]["start"]["line"] == ref_line + and containing_symbol["selectionRange"]["start"]["character"] == ref_col ): incoming_symbol = containing_symbol if include_self: result.append(ReferenceInSymbol(symbol=containing_symbol, line=ref_line, character=ref_col)) continue - else: - self.logger.log(f"Found self-reference for {incoming_symbol['name']}, skipping it since {include_self=}", logging.DEBUG) - continue + self.logger.log(f"Found self-reference for {incoming_symbol['name']}, skipping it since {include_self=}", logging.DEBUG) + continue # checking whether reference is an import # This is neither really safe nor elegant, but if we don't do it, # there is no way to distinguish between definitions and imports as import is not a symbol-type # and we get the type referenced symbol resulting from imports... - if (not include_imports \ - and incoming_symbol is not None \ - and containing_symbol["name"] == incoming_symbol["name"] \ - and containing_symbol["kind"] == incoming_symbol["kind"] \ - ): + if ( + not include_imports + and incoming_symbol is not None + and containing_symbol["name"] == incoming_symbol["name"] + and containing_symbol["kind"] == incoming_symbol["kind"] + ): self.logger.log( f"Found import of referenced symbol {incoming_symbol['name']}" f"in {containing_symbol['location']['relativePath']}, skipping", - logging.DEBUG + logging.DEBUG, ) continue @@ -1342,12 +1318,12 @@ class SolidLanguageServer(ABC): return result def request_containing_symbol( - self, - relative_file_path: str, - line: int, - column: Optional[int] = None, - strict: bool = False, - include_body: bool = False, + self, + relative_file_path: str, + line: int, + column: int | None = None, + strict: bool = False, + include_body: bool = False, ) -> multilspy_types.UnifiedSymbolInformation | None: """ Finds the first symbol containing the position for the given file. @@ -1377,9 +1353,7 @@ class SolidLanguageServer(ABC): """ # checking if the line is empty, unfortunately ugly and duplicating code, but I don't want to refactor with self.open_file(relative_file_path): - absolute_file_path = str( - PurePath(self.repository_root_path, relative_file_path) - ) + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) content = FileUtils.read_file(self.logger, absolute_file_path) if content.split("\n")[line].strip() == "": self.logger.log( @@ -1411,11 +1385,7 @@ class SolidLanguageServer(ABC): location["uri"] = Path(absolute_file_path).as_uri() # Allowed container kinds, currently only for Python - container_symbol_kinds = { - multilspy_types.SymbolKind.Method, - multilspy_types.SymbolKind.Function, - multilspy_types.SymbolKind.Class - } + container_symbol_kinds = {multilspy_types.SymbolKind.Method, multilspy_types.SymbolKind.Function, multilspy_types.SymbolKind.Class} def is_position_in_range(line: int, range_d: multilspy_types.Range) -> bool: start = range_d["start"] @@ -1434,11 +1404,11 @@ class SolidLanguageServer(ABC): # Only consider containers that are not one-liners (otherwise we may get imports) candidate_containers = [ - s for s in symbols if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"] - ] - var_containers = [ - s for s in symbols if s["kind"] == multilspy_types.SymbolKind.Variable + s + for s in symbols + if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"] ] + var_containers = [s for s in symbols if s["kind"] == multilspy_types.SymbolKind.Variable] candidate_containers.extend(var_containers) if not candidate_containers: @@ -1461,7 +1431,9 @@ class SolidLanguageServer(ABC): else: return None - def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None: + def request_container_of_symbol( + self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False + ) -> multilspy_types.UnifiedSymbolInformation | None: """ Finds the container of the given symbol if there is one. If the parent attribute is present, the parent is returned without further searching. @@ -1482,12 +1454,12 @@ class SolidLanguageServer(ABC): ) def request_defining_symbol( - self, - relative_file_path: str, - line: int, - column: int, - include_body: bool = False, - ) -> Optional[multilspy_types.UnifiedSymbolInformation]: + self, + relative_file_path: str, + line: int, + column: int, + include_body: bool = False, + ) -> multilspy_types.UnifiedSymbolInformation | None: """ Finds the symbol that defines the symbol at the given location. @@ -1519,9 +1491,7 @@ class SolidLanguageServer(ABC): def_col = definition["range"]["start"]["character"] # Find the symbol at or containing this location - defining_symbol = self.request_containing_symbol( - def_path, def_line, def_col, strict=False, include_body=include_body - ) + defining_symbol = self.request_containing_symbol(def_path, def_line, def_col, strict=False, include_body=include_body) return defining_symbol @@ -1587,7 +1557,7 @@ class SolidLanguageServer(ABC): logging.ERROR, ) - def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]: + def request_workspace_symbol(self, query: str) -> list[multilspy_types.UnifiedSymbolInformation] | None: """ Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server to find symbols across the whole workspace. Wait for the response and return the result. @@ -1602,7 +1572,7 @@ class SolidLanguageServer(ABC): assert isinstance(response, list) - ret: List[multilspy_types.UnifiedSymbolInformation] = [] + ret: list[multilspy_types.UnifiedSymbolInformation] = [] for item in response: assert isinstance(item, dict) @@ -1620,7 +1590,10 @@ class SolidLanguageServer(ABC): :return: self for method chaining """ - self.logger.log(f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}", logging.INFO) + self.logger.log( + f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}", + logging.INFO, + ) self._server_context = self._start_server_process() return self diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index b34571f..0bc3255 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -5,16 +5,29 @@ import os import subprocess import threading import time +from collections.abc import Callable from dataclasses import dataclass from queue import Queue -from typing import Any, Callable, Dict, Optional +from typing import Any import psutil from multilspy.lsp_protocol_handler.lsp_requests import LspNotification from multilspy.lsp_protocol_handler.lsp_types import ErrorCodes -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo, StringDict, content_length, ENCODING, Error, MessageType, \ - make_notification, PayloadLike, make_response, make_error_response, make_request, create_message +from multilspy.lsp_protocol_handler.server import ( + ENCODING, + Error, + MessageType, + PayloadLike, + ProcessLaunchInfo, + StringDict, + content_length, + create_message, + make_error_response, + make_notification, + make_request, + make_response, +) from multilspy.multilspy_exceptions import MultilspyException from solidlsp.lsp_request import SolidLspRequest @@ -25,8 +38,8 @@ class Request: @dataclass class Result: - payload: Optional[PayloadLike] = None - error: Optional[Error] = None + payload: PayloadLike | None = None + error: Error | None = None def is_error(self) -> bool: return self.error is not None @@ -81,13 +94,14 @@ class SolidLanguageServerHandler: language server process in an independent process group. Default is `True`. Setting it to `False` means that the language server process will be in the same process group as the the current process, and any SIGINT and SIGTERM signals will be sent to both processes. + """ def __init__( - self, - process_launch_info: ProcessLaunchInfo, - logger: Optional[Callable[[str, str, StringDict | str], None]] = None, - start_independent_lsp_process=True, + self, + process_launch_info: ProcessLaunchInfo, + logger: Callable[[str, str, StringDict | str], None] | None = None, + start_independent_lsp_process=True, ) -> None: """ Params: @@ -103,7 +117,7 @@ class SolidLanguageServerHandler: self._received_shutdown = False self.request_id = 1 - self._response_handlers: Dict[Any, Request] = {} + self._response_handlers: dict[Any, Request] = {} self.on_request_handlers = {} self.on_notification_handlers = {} self.logger = logger @@ -141,7 +155,7 @@ class SolidLanguageServerHandler: env=child_proc_env, cwd=self.process_launch_info.cwd, start_new_session=self.start_independent_lsp_process, - shell=True + shell=True, ) # Check if process terminated immediately @@ -149,7 +163,7 @@ class SolidLanguageServerHandler: log.error("Language server has already terminated/could not be started") # Process has already terminated stderr_data = self.process.stderr.read() - error_message = stderr_data.decode('utf-8', errors='replace') + error_message = stderr_data.decode("utf-8", errors="replace") raise RuntimeError(f"Process terminated immediately with code {self.process.returncode}. Error: {error_message}") # start threads to read stdout and stderr of the process @@ -284,7 +298,7 @@ class SolidLanguageServerHandler: # Process has terminated, check if we can still read pass - data = b'' + data = b"" while len(data) < num_bytes: chunk = stream.read(num_bytes - len(data)) if not chunk: @@ -332,7 +346,7 @@ class SolidLanguageServerHandler: line = self.process.stderr.readline() if not line: continue - self._log("LSP stderr: " + line.decode(ENCODING, errors='replace')) + self._log("LSP stderr: " + line.decode(ENCODING, errors="replace")) except (BrokenPipeError, ConnectionResetError): pass @@ -342,7 +356,7 @@ class SolidLanguageServerHandler: """ try: self._receive_payload(json.loads(body)) - except IOError as ex: + except OSError as ex: self._log(f"malformed {ENCODING}: {ex}") except UnicodeDecodeError as ex: self._log(f"malformed {ENCODING}: {ex}") @@ -368,7 +382,7 @@ class SolidLanguageServerHandler: except Exception as err: self._log(f"Error handling server payload: {err}") - def send_notification(self, method: str, params: Optional[dict] = None) -> None: + def send_notification(self, method: str, params: dict | None = None) -> None: """ Send notification pertaining to the given method to the server with the given parameters """ @@ -387,7 +401,7 @@ class SolidLanguageServerHandler: # Use lock to prevent race conditions on tasks and task_counter self._send_payload(make_error_response(request_id, err)) - def send_request(self, method: str, params: Optional[dict] = None) -> PayloadLike: + def send_request(self, method: str, params: dict | None = None) -> PayloadLike: """ Send request to the server, register the request id, and wait for the response """ @@ -406,10 +420,12 @@ class SolidLanguageServerHandler: self._log(f"Waiting for response to request {method} with params:\n{params}") result = request.get_result() - self._log(f"Processing result") + self._log("Processing result") if result.is_error(): - raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {result.error}") from result.error - + raise MultilspyException( + f"Could not process request {method} with params:\n{params}.\n Language server error: {result.error}" + ) from result.error + self._log(f"Returning non-error result, which is:\n{result.payload}") return result.payload @@ -472,7 +488,7 @@ class SolidLanguageServerHandler: request_id, Error( ErrorCodes.MethodNotFound, - "method '{}' not handled on client.".format(method), + f"method '{method}' not handled on client.", ), ) return diff --git a/src/solidlsp/lsp_request.py b/src/solidlsp/lsp_request.py index 984407b..6320fe5 100644 --- a/src/solidlsp/lsp_request.py +++ b/src/solidlsp/lsp_request.py @@ -1,4 +1,5 @@ -from typing import List, Union +from typing import Union + from multilspy.lsp_protocol_handler import lsp_types @@ -6,258 +7,230 @@ class SolidLspRequest: def __init__(self, send_request): self.send_request = send_request - def implementation( - self, params: lsp_types.ImplementationParams - ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + def implementation(self, params: lsp_types.ImplementationParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the implementation locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Definition} or a - Thenable that resolves to such.""" + Thenable that resolves to such. + """ return self.send_request("textDocument/implementation", params) def type_definition( - self, params: lsp_types.TypeDefinitionParams - ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + self, params: lsp_types.TypeDefinitionParams + ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the type definition locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Definition} or a - Thenable that resolves to such.""" + Thenable that resolves to such. + """ return self.send_request("textDocument/typeDefinition", params) - def document_color( - self, params: lsp_types.DocumentColorParams - ) -> List["lsp_types.ColorInformation"]: + def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]: """A request to list all color symbols found in a given text document. The request's parameter is of type {@link DocumentColorParams} the response is of type {@link ColorInformation ColorInformation[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return self.send_request("textDocument/documentColor", params) - def color_presentation( - self, params: lsp_types.ColorPresentationParams - ) -> List["lsp_types.ColorPresentation"]: + def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]: """A request to list all presentation for a color. The request's parameter is of type {@link ColorPresentationParams} the response is of type {@link ColorInformation ColorInformation[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return self.send_request("textDocument/colorPresentation", params) - def folding_range( - self, params: lsp_types.FoldingRangeParams - ) -> Union[List["lsp_types.FoldingRange"], None]: + def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None: """A request to provide folding ranges in a document. The request's parameter is of type {@link FoldingRangeParams}, the response is of type {@link FoldingRangeList} or a Thenable - that resolves to such.""" + that resolves to such. + """ return self.send_request("textDocument/foldingRange", params) - def declaration( - self, params: lsp_types.DeclarationParams - ) -> Union["lsp_types.Declaration", List["lsp_types.LocationLink"], None]: + def declaration(self, params: lsp_types.DeclarationParams) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]: """A request to resolve the type definition locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Declaration} or a typed array of {@link DeclarationLink} or a Thenable that resolves - to such.""" + to such. + """ return self.send_request("textDocument/declaration", params) - def selection_range( - self, params: lsp_types.SelectionRangeParams - ) -> Union[List["lsp_types.SelectionRange"], None]: + def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None: """A request to provide selection ranges in a document. The request's parameter is of type {@link SelectionRangeParams}, the response is of type {@link SelectionRange SelectionRange[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return self.send_request("textDocument/selectionRange", params) - def prepare_call_hierarchy( - self, params: lsp_types.CallHierarchyPrepareParams - ) -> Union[List["lsp_types.CallHierarchyItem"], None]: + def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None: """A request to result a `CallHierarchyItem` in a document at a given position. Can be used as an input to an incoming or outgoing call hierarchy. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_request("textDocument/prepareCallHierarchy", params) - def incoming_calls( - self, params: lsp_types.CallHierarchyIncomingCallsParams - ) -> Union[List["lsp_types.CallHierarchyIncomingCall"], None]: + def incoming_calls(self, params: lsp_types.CallHierarchyIncomingCallsParams) -> list["lsp_types.CallHierarchyIncomingCall"] | None: """A request to resolve the incoming calls for a given `CallHierarchyItem`. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_request("callHierarchy/incomingCalls", params) - def outgoing_calls( - self, params: lsp_types.CallHierarchyOutgoingCallsParams - ) -> Union[List["lsp_types.CallHierarchyOutgoingCall"], None]: + def outgoing_calls(self, params: lsp_types.CallHierarchyOutgoingCallsParams) -> list["lsp_types.CallHierarchyOutgoingCall"] | None: """A request to resolve the outgoing calls for a given `CallHierarchyItem`. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_request("callHierarchy/outgoingCalls", params) - def semantic_tokens_full( - self, params: lsp_types.SemanticTokensParams - ) -> Union["lsp_types.SemanticTokens", None]: + def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" return self.send_request("textDocument/semanticTokens/full", params) def semantic_tokens_delta( - self, params: lsp_types.SemanticTokensDeltaParams + self, params: lsp_types.SemanticTokensDeltaParams ) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]: """@since 3.16.0""" return self.send_request("textDocument/semanticTokens/full/delta", params) - def semantic_tokens_range( - self, params: lsp_types.SemanticTokensRangeParams - ) -> Union["lsp_types.SemanticTokens", None]: + def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" return self.send_request("textDocument/semanticTokens/range", params) - def linked_editing_range( - self, params: lsp_types.LinkedEditingRangeParams - ) -> Union["lsp_types.LinkedEditingRanges", None]: + def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]: """A request to provide ranges that can be edited together. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_request("textDocument/linkedEditingRange", params) - def will_create_files( - self, params: lsp_types.CreateFilesParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The will create files request is sent from the client to the server before files are actually created as long as the creation is triggered from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_request("workspace/willCreateFiles", params) - def will_rename_files( - self, params: lsp_types.RenameFilesParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The will rename files request is sent from the client to the server before files are actually renamed as long as the rename is triggered from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_request("workspace/willRenameFiles", params) - def will_delete_files( - self, params: lsp_types.DeleteFilesParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The did delete files notification is sent from the client to the server when files were deleted from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_request("workspace/willDeleteFiles", params) - def moniker( - self, params: lsp_types.MonikerParams - ) -> Union[List["lsp_types.Moniker"], None]: + def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None: """A request to get the moniker of a symbol at a given text document position. The request parameter is of type {@link TextDocumentPositionParams}. - The response is of type {@link Moniker Moniker[]} or `null`.""" + The response is of type {@link Moniker Moniker[]} or `null`. + """ return self.send_request("textDocument/moniker", params) - def prepare_type_hierarchy( - self, params: lsp_types.TypeHierarchyPrepareParams - ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to result a `TypeHierarchyItem` in a document at a given position. Can be used as an input to a subtypes or supertypes type hierarchy. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_request("textDocument/prepareTypeHierarchy", params) - def type_hierarchy_supertypes( - self, params: lsp_types.TypeHierarchySupertypesParams - ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + def type_hierarchy_supertypes(self, params: lsp_types.TypeHierarchySupertypesParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to resolve the supertypes for a given `TypeHierarchyItem`. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_request("typeHierarchy/supertypes", params) - def type_hierarchy_subtypes( - self, params: lsp_types.TypeHierarchySubtypesParams - ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to resolve the subtypes for a given `TypeHierarchyItem`. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_request("typeHierarchy/subtypes", params) - def inline_value( - self, params: lsp_types.InlineValueParams - ) -> Union[List["lsp_types.InlineValue"], None]: + def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None: """A request to provide inline values in a document. The request's parameter is of type {@link InlineValueParams}, the response is of type {@link InlineValue InlineValue[]} or a Thenable that resolves to such. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_request("textDocument/inlineValue", params) - def inlay_hint( - self, params: lsp_types.InlayHintParams - ) -> Union[List["lsp_types.InlayHint"], None]: + def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None: """A request to provide inlay hints in a document. The request's parameter is of type {@link InlayHintsParams}, the response is of type {@link InlayHint InlayHint[]} or a Thenable that resolves to such. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_request("textDocument/inlayHint", params) - def resolve_inlay_hint( - self, params: lsp_types.InlayHint - ) -> "lsp_types.InlayHint": + def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint": """A request to resolve additional properties for an inlay hint. The request's parameter is of type {@link InlayHint}, the response is of type {@link InlayHint} or a Thenable that resolves to such. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_request("inlayHint/resolve", params) - def text_document_diagnostic( - self, params: lsp_types.DocumentDiagnosticParams - ) -> "lsp_types.DocumentDiagnosticReport": + def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport": """The document diagnostic request definition. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_request("textDocument/diagnostic", params) - def workspace_diagnostic( - self, params: lsp_types.WorkspaceDiagnosticParams - ) -> "lsp_types.WorkspaceDiagnosticReport": + def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport": """The workspace diagnostic request definition. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_request("workspace/diagnostic", params) - def initialize( - self, params: lsp_types.InitializeParams - ) -> "lsp_types.InitializeResult": + def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult": """The initialize request is sent from the client to the server. It is sent once as the request after starting up the server. The requests parameter is of type {@link InitializeParams} the response if of type {@link InitializeResult} of a Thenable that - resolves to such.""" + resolves to such. + """ return self.send_request("initialize", params) def shutdown(self) -> None: """A shutdown request is sent from the client to the server. It is sent once when the client decides to shutdown the server. The only notification that is sent after a shutdown request - is the exit event.""" + is the exit event. + """ return self.send_request("shutdown") - def will_save_wait_until( - self, params: lsp_types.WillSaveTextDocumentParams - ) -> Union[List["lsp_types.TextEdit"], None]: + def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None: """A document will save request is sent from the client to the server before the document is actually saved. The request can return an array of TextEdits which will be applied to the text document before it is saved. Please note that clients might drop results if computing the text edits took too long or if a server constantly fails on this request. This is done to keep the save fast and - reliable.""" + reliable. + """ return self.send_request("textDocument/willSaveWaitUntil", params) - def completion( - self, params: lsp_types.CompletionParams - ) -> Union[List["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: + def completion(self, params: lsp_types.CompletionParams) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: """Request to request completion at a given text document position. The request's parameter is of type {@link TextDocumentPosition} the response is of type {@link CompletionItem CompletionItem[]} or {@link CompletionList} @@ -270,85 +243,72 @@ class SolidLspRequest: """ return self.send_request("textDocument/completion", params) - def resolve_completion_item( - self, params: lsp_types.CompletionItem - ) -> "lsp_types.CompletionItem": + def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem": """Request to resolve additional information for a given completion item.The request's parameter is of type {@link CompletionItem} the response - is of type {@link CompletionItem} or a Thenable that resolves to such.""" + is of type {@link CompletionItem} or a Thenable that resolves to such. + """ return self.send_request("completionItem/resolve", params) - def hover( - self, params: lsp_types.HoverParams - ) -> Union["lsp_types.Hover", None]: + def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]: """Request to request hover information at a given text document position. The request's parameter is of type {@link TextDocumentPosition} the response is of - type {@link Hover} or a Thenable that resolves to such.""" + type {@link Hover} or a Thenable that resolves to such. + """ return self.send_request("textDocument/hover", params) - def signature_help( - self, params: lsp_types.SignatureHelpParams - ) -> Union["lsp_types.SignatureHelp", None]: + def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]: return self.send_request("textDocument/signatureHelp", params) - def definition( - self, params: lsp_types.DefinitionParams - ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the definition location of a symbol at a given text document position. The request's parameter is of type [TextDocumentPosition] (#TextDocumentPosition) the response is of either type {@link Definition} or a typed array of {@link DefinitionLink} or a Thenable that resolves - to such.""" + to such. + """ return self.send_request("textDocument/definition", params) - def references( - self, params: lsp_types.ReferenceParams - ) -> Union[List["lsp_types.Location"], None]: + def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None: """A request to resolve project-wide references for the symbol denoted by the given text document position. The request's parameter is of type {@link ReferenceParams} the response is of type - {@link Location Location[]} or a Thenable that resolves to such.""" + {@link Location Location[]} or a Thenable that resolves to such. + """ return self.send_request("textDocument/references", params) - def document_highlight( - self, params: lsp_types.DocumentHighlightParams - ) -> Union[List["lsp_types.DocumentHighlight"], None]: + def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None: """Request to resolve a {@link DocumentHighlight} for a given text document position. The request's parameter is of type [TextDocumentPosition] (#TextDocumentPosition) the request response is of type [DocumentHighlight[]] - (#DocumentHighlight) or a Thenable that resolves to such.""" + (#DocumentHighlight) or a Thenable that resolves to such. + """ return self.send_request("textDocument/documentHighlight", params) def document_symbol( - self, params: lsp_types.DocumentSymbolParams - ) -> Union[ - List["lsp_types.SymbolInformation"], List["lsp_types.DocumentSymbol"], None - ]: + self, params: lsp_types.DocumentSymbolParams + ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.DocumentSymbol"] | None: """A request to list all symbols found in a given text document. The request's parameter is of type {@link TextDocumentIdentifier} the response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return self.send_request("textDocument/documentSymbol", params) - def code_action( - self, params: lsp_types.CodeActionParams - ) -> Union[List[Union["lsp_types.Command", "lsp_types.CodeAction"]], None]: + def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None: """A request to provide commands for the given text document and range.""" return self.send_request("textDocument/codeAction", params) - def resolve_code_action( - self, params: lsp_types.CodeAction - ) -> "lsp_types.CodeAction": + def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction": """Request to resolve additional information for a given code action.The request's parameter is of type {@link CodeAction} the response - is of type {@link CodeAction} or a Thenable that resolves to such.""" + is of type {@link CodeAction} or a Thenable that resolves to such. + """ return self.send_request("codeAction/resolve", params) def workspace_symbol( - self, params: lsp_types.WorkspaceSymbolParams - ) -> Union[ - List["lsp_types.SymbolInformation"], List["lsp_types.WorkspaceSymbol"], None - ]: + self, params: lsp_types.WorkspaceSymbolParams + ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.WorkspaceSymbol"] | None: """A request to list project-wide symbols matching the query string given by the {@link WorkspaceSymbolParams}. The response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable that @@ -360,76 +320,58 @@ class SolidLspRequest: """ return self.send_request("workspace/symbol", params) - def resolve_workspace_symbol( - self, params: lsp_types.WorkspaceSymbol - ) -> "lsp_types.WorkspaceSymbol": + def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol": """A request to resolve the range inside the workspace symbol's location. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_request("workspaceSymbol/resolve", params) - def code_lens( - self, params: lsp_types.CodeLensParams - ) -> Union[List["lsp_types.CodeLens"], None]: + def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None: """A request to provide code lens for the given text document.""" return self.send_request("textDocument/codeLens", params) - def resolve_code_lens( - self, params: lsp_types.CodeLens - ) -> "lsp_types.CodeLens": + def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens": """A request to resolve a command for a given code lens.""" return self.send_request("codeLens/resolve", params) - def document_link( - self, params: lsp_types.DocumentLinkParams - ) -> Union[List["lsp_types.DocumentLink"], None]: + def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None: """A request to provide document links""" return self.send_request("textDocument/documentLink", params) - def resolve_document_link( - self, params: lsp_types.DocumentLink - ) -> "lsp_types.DocumentLink": + def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink": """Request to resolve additional information for a given document link. The request's parameter is of type {@link DocumentLink} the response - is of type {@link DocumentLink} or a Thenable that resolves to such.""" + is of type {@link DocumentLink} or a Thenable that resolves to such. + """ return self.send_request("documentLink/resolve", params) - def formatting( - self, params: lsp_types.DocumentFormattingParams - ) -> Union[List["lsp_types.TextEdit"], None]: + def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to to format a whole document.""" return self.send_request("textDocument/formatting", params) - def range_formatting( - self, params: lsp_types.DocumentRangeFormattingParams - ) -> Union[List["lsp_types.TextEdit"], None]: + def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to to format a range in a document.""" return self.send_request("textDocument/rangeFormatting", params) - def on_type_formatting( - self, params: lsp_types.DocumentOnTypeFormattingParams - ) -> Union[List["lsp_types.TextEdit"], None]: + def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to format a document on type.""" return self.send_request("textDocument/onTypeFormatting", params) - def rename( - self, params: lsp_types.RenameParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]: """A request to rename a symbol.""" return self.send_request("textDocument/rename", params) - def prepare_rename( - self, params: lsp_types.PrepareRenameParams - ) -> Union["lsp_types.PrepareRenameResult", None]: + def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]: """A request to test and perform the setup necessary for a rename. - @since 3.16 - support for default behavior""" + @since 3.16 - support for default behavior + """ return self.send_request("textDocument/prepareRename", params) - def execute_command( - self, params: lsp_types.ExecuteCommandParams - ) -> Union["lsp_types.LSPAny", None]: + def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]: """A request send from the client to the server to execute a command. The request might return - a workspace edit which the client will apply to the workspace.""" + a workspace edit which the client will apply to the workspace. + """ return self.send_request("workspace/executeCommand", params) From 27826d89c6831c2091a43aeeeabdd6fb7c339f31 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 16:25:19 +0200 Subject: [PATCH 139/174] Move components from multilspy to solidlsp --- src/multilspy/__init__.py | 2 +- src/multilspy/language_server.py | 47 ++++++++++--------- .../clangd_language_server.py | 12 ++--- .../dart_language_server.py | 5 +- .../eclipse_jdtls/eclipse_jdtls.py | 14 +++--- src/multilspy/language_servers/gopls/gopls.py | 13 ++--- .../intelephense/intelephense.py | 10 ++-- .../jedi_language_server/jedi_server.py | 10 ++-- .../kotlin_language_server.py | 13 +++-- .../language_servers/omnisharp/omnisharp.py | 12 ++--- .../pyright_language_server/pyright_server.py | 11 ++--- .../rust_analyzer/rust_analyzer.py | 12 ++--- .../language_servers/solargraph/solargraph.py | 10 ++-- .../typescript_language_server.py | 10 ++-- src/serena/agent.py | 6 +-- src/serena/symbol.py | 2 +- src/serena/util/inspection.py | 2 +- .../clangd_language_server.py | 10 ++-- .../dart_language_server.py | 6 +-- .../eclipse_jdtls/eclipse_jdtls.py | 12 ++--- src/solidlsp/language_servers/gopls/gopls.py | 8 ++-- .../intelephense/intelephense.py | 10 ++-- .../jedi_language_server/jedi_server.py | 8 ++-- .../kotlin_language_server.py | 10 ++-- .../language_servers/omnisharp/omnisharp.py | 12 ++--- .../pyright_language_server/pyright_server.py | 8 ++-- .../rust_analyzer/rust_analyzer.py | 10 ++-- .../language_servers/solargraph/solargraph.py | 8 ++-- .../typescript_language_server.py | 10 ++-- src/solidlsp/ls.py | 28 ++++++----- src/solidlsp/ls_handler.py | 8 ++-- .../lsp_protocol_handler/lsp_constants.py | 0 .../lsp_protocol_handler/lsp_requests.py | 3 +- .../lsp_protocol_handler/lsp_types.py | 0 .../lsp_protocol_handler/server.py | 0 src/solidlsp/lsp_request.py | 2 +- .../multilspy_config.py | 0 .../multilspy_exceptions.py | 0 .../multilspy_logger.py | 0 .../multilspy_settings.py | 0 .../multilspy_types.py | 0 .../multilspy_utils.py | 6 +-- src/{multilspy => solidlsp}/type_helpers.py | 0 test/conftest.py | 4 +- test/multilspy/go/test_go_basic.py | 4 +- test/multilspy/java/test_java_basic.py | 4 +- test/multilspy/php/test_php_basic.py | 2 +- test/multilspy/python/test_python_basic.py | 2 +- .../test_retrieval_with_ignored_dirs.py | 2 +- .../multilspy/python/test_symbol_retrieval.py | 4 +- test/multilspy/rust/test_rust_basic.py | 4 +- .../typescript/test_typescript_basic.py | 4 +- test/serena/test_serena_agent.py | 2 +- test/serena/test_symbol_editing.py | 2 +- 54 files changed, 192 insertions(+), 192 deletions(-) rename src/{multilspy => solidlsp}/lsp_protocol_handler/lsp_constants.py (100%) rename src/{multilspy => solidlsp}/lsp_protocol_handler/lsp_requests.py (99%) rename src/{multilspy => solidlsp}/lsp_protocol_handler/lsp_types.py (100%) rename src/{multilspy => solidlsp}/lsp_protocol_handler/server.py (100%) rename src/{multilspy => solidlsp}/multilspy_config.py (100%) rename src/{multilspy => solidlsp}/multilspy_exceptions.py (100%) rename src/{multilspy => solidlsp}/multilspy_logger.py (100%) rename src/{multilspy => solidlsp}/multilspy_settings.py (100%) rename src/{multilspy => solidlsp}/multilspy_types.py (100%) rename src/{multilspy => solidlsp}/multilspy_utils.py (98%) rename src/{multilspy => solidlsp}/type_helpers.py (100%) diff --git a/src/multilspy/__init__.py b/src/multilspy/__init__.py index a8229e9..0bd1758 100644 --- a/src/multilspy/__init__.py +++ b/src/multilspy/__init__.py @@ -2,7 +2,7 @@ This module contains the multilspy API """ -from . import multilspy_types as Types +from solidlsp import multilspy_types as Types from .language_server import LanguageServer, SyncLanguageServer __all__ = ["LanguageServer", "Types", "SyncLanguageServer"] diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index db9c1cb..b584b89 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -19,28 +19,26 @@ from collections import defaultdict from contextlib import asynccontextmanager, contextmanager from copy import copy from pathlib import Path, PurePath -import time from typing import AsyncIterator, Dict, Iterator, List, Optional, Tuple, Union, cast import pathspec import tqdm -from . import multilspy_types -from .lsp_protocol_handler import lsp_types as LSPTypes -from .lsp_protocol_handler.lsp_constants import LSPConstants -from .lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind -from .lsp_protocol_handler.server import ( +from solidlsp import multilspy_types +from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes, lsp_types +from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants +from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind +from solidlsp.lsp_protocol_handler.server import ( Error, LanguageServerHandler, ProcessLaunchInfo, StringDict, ) -from .multilspy_config import Language, MultilspyConfig -from .multilspy_exceptions import MultilspyException -from .multilspy_logger import MultilspyLogger -from .multilspy_utils import FileUtils, PathUtils, TextUtils -from .type_helpers import ensure_all_methods_implemented -from .lsp_protocol_handler import lsp_types +from solidlsp.multilspy_config import Language, MultilspyConfig +from solidlsp.multilspy_exceptions import MultilspyException +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_utils import FileUtils, PathUtils, TextUtils +from solidlsp.type_helpers import ensure_all_methods_implemented # Serena dependencies # We will need to watch out for circular imports, but it's probably better to not @@ -49,7 +47,7 @@ from .lsp_protocol_handler import lsp_types # since it caches (in-memory) file contents, so we can avoid reading from disk. # Moreover, the way we want to use the language server (for retrieving actual content), # it makes sense to have more content-related utils directly in it. -from serena.text_utils import LineType, MatchedConsecutiveLines, TextLine, search_files +from serena.text_utils import MatchedConsecutiveLines, search_files @@ -217,7 +215,8 @@ class LanguageServer: self.language = Language(language_id) # load cache first to prevent any racing conditions due to asyncio stuff - self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]]] = {} + self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[ + multilspy_types.UnifiedSymbolInformation]]]] = {} """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" self._cache_lock = threading.Lock() self._cache_has_changed: bool = False @@ -629,7 +628,8 @@ class LanguageServer: return ret # Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses - async def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[lsp_types.Location] | None: + async def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[ + lsp_types.Location] | None: return await self.server.send.references( { "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, @@ -830,7 +830,8 @@ class LanguageServer: for json_repr in set([json.dumps(item, sort_keys=True) for item in completions_list]) ] - async def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: + async def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[ + multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: """ Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server to find symbols in the given file. Wait for the response and return the result. @@ -926,7 +927,8 @@ class LanguageServer: if LSPConstants.CHILDREN in root_symbol: # TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well - def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> List[multilspy_types.UnifiedSymbolInformation]: + def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> List[ + multilspy_types.UnifiedSymbolInformation]: node = cast(multilspy_types.UnifiedSymbolInformation, node) l: List[multilspy_types.UnifiedSymbolInformation] = [] turn_item_into_symbol_with_children(node) @@ -948,7 +950,8 @@ class LanguageServer: self._cache_has_changed = True return result - async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[ + multilspy_types.UnifiedSymbolInformation]: """ Will go through all files in the project or within a relative path and build a tree of symbols. Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search. @@ -1817,7 +1820,7 @@ class SyncLanguageServer: self.language_server.request_references(file_path, line, column), self.loop ).result(timeout=self.timeout) except Exception as e: - from multilspy.lsp_protocol_handler.server import Error + from solidlsp.lsp_protocol_handler.server import Error if isinstance(e, Error) and getattr(e, 'code', None) == -32603: raise RuntimeError( f"LSP internal error (-32603) when requesting references for {file_path}:{line}:{column}. " @@ -1865,7 +1868,8 @@ class SyncLanguageServer: ).result(timeout=self.timeout) return result - def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: + def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[ + multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: """ Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server to find symbols in the given file. Wait for the response and return the result. @@ -1879,7 +1883,8 @@ class SyncLanguageServer: ).result() return result - def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[ + multilspy_types.UnifiedSymbolInformation]: """ Will go through all files in the project and build a tree of symbols. Note: this may be slow the first time it is called. diff --git a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py b/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py index e28aa2c..6960b9f 100644 --- a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py @@ -11,13 +11,13 @@ import pathlib from contextlib import asynccontextmanager from typing import AsyncIterator -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_utils import FileUtils +from solidlsp.multilspy_utils import PlatformUtils class ClangdLanguageServer(LanguageServer): diff --git a/src/multilspy/language_servers/dart_language_server/dart_language_server.py b/src/multilspy/language_servers/dart_language_server/dart_language_server.py index cd8ea51..8f9aaab 100644 --- a/src/multilspy/language_servers/dart_language_server/dart_language_server.py +++ b/src/multilspy/language_servers/dart_language_server/dart_language_server.py @@ -2,13 +2,12 @@ from contextlib import asynccontextmanager import logging import os import pathlib -import shutil import stat from typing import AsyncIterator from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo import json -from multilspy.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.multilspy_utils import FileUtils, PlatformUtils class DartLanguageServer(LanguageServer): diff --git a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py index a88dc1b..103c976 100644 --- a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -16,14 +16,14 @@ from typing import AsyncIterator from overrides import override -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_settings import MultilspySettings -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_settings import MultilspySettings +from solidlsp.multilspy_utils import FileUtils +from solidlsp.multilspy_utils import PlatformUtils from pathlib import PurePath diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py index 0e5574a..117f5c2 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/multilspy/language_servers/gopls/gopls.py @@ -5,18 +5,15 @@ import os import pathlib import subprocess from contextlib import asynccontextmanager -from typing import AsyncIterator, List +from typing import AsyncIterator from overrides import override -from multilspy import multilspy_types -from multilspy.lsp_protocol_handler import lsp_types -from multilspy.multilspy_exceptions import MultilspyException -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import Error, ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.multilspy_config import MultilspyConfig class Gopls(LanguageServer): diff --git a/src/multilspy/language_servers/intelephense/intelephense.py b/src/multilspy/language_servers/intelephense/intelephense.py index d2ad911..5a023fc 100644 --- a/src/multilspy/language_servers/intelephense/intelephense.py +++ b/src/multilspy/language_servers/intelephense/intelephense.py @@ -15,12 +15,12 @@ from typing import AsyncIterator from overrides import override -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import PlatformUtils, PlatformId +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_utils import PlatformUtils, PlatformId class Intelephense(LanguageServer): """ diff --git a/src/multilspy/language_servers/jedi_language_server/jedi_server.py b/src/multilspy/language_servers/jedi_language_server/jedi_server.py index 28c9d91..6df8365 100644 --- a/src/multilspy/language_servers/jedi_language_server/jedi_server.py +++ b/src/multilspy/language_servers/jedi_language_server/jedi_server.py @@ -7,15 +7,15 @@ import logging import os import pathlib from contextlib import asynccontextmanager -from typing import AsyncIterator, Tuple +from typing import AsyncIterator from overrides import override -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.multilspy_config import MultilspyConfig class JediServer(LanguageServer): diff --git a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py b/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py index ca4e866..0b2906c 100644 --- a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py @@ -2,7 +2,6 @@ Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. """ -import asyncio import dataclasses import json import logging @@ -12,13 +11,13 @@ import pathlib from contextlib import asynccontextmanager from typing import AsyncIterator -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_utils import FileUtils +from solidlsp.multilspy_utils import PlatformUtils @dataclasses.dataclass diff --git a/src/multilspy/language_servers/omnisharp/omnisharp.py b/src/multilspy/language_servers/omnisharp/omnisharp.py index df3cf0c..b376de7 100644 --- a/src/multilspy/language_servers/omnisharp/omnisharp.py +++ b/src/multilspy/language_servers/omnisharp/omnisharp.py @@ -13,13 +13,13 @@ from typing import AsyncIterator, Iterable from overrides import override -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_exceptions import MultilspyException -from multilspy.multilspy_utils import FileUtils, PlatformUtils, PlatformId, DotnetVersion +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_exceptions import MultilspyException +from solidlsp.multilspy_utils import FileUtils, PlatformUtils, PlatformId, DotnetVersion def breadth_first_file_scan(root) -> Iterable[str]: diff --git a/src/multilspy/language_servers/pyright_language_server/pyright_server.py b/src/multilspy/language_servers/pyright_language_server/pyright_server.py index 4885583..0cb9533 100644 --- a/src/multilspy/language_servers/pyright_language_server/pyright_server.py +++ b/src/multilspy/language_servers/pyright_language_server/pyright_server.py @@ -3,21 +3,20 @@ Provides Python specific instantiation of the LanguageServer class. Contains var """ import asyncio -import json import logging import os import pathlib import re from contextlib import asynccontextmanager -from typing import AsyncIterator, Tuple +from typing import AsyncIterator from overrides import override -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.multilspy_config import MultilspyConfig class PyrightServer(LanguageServer): diff --git a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py b/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py index 65f3598..60d7103 100644 --- a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py @@ -13,13 +13,13 @@ from typing import AsyncIterator from overrides import override -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_utils import FileUtils +from solidlsp.multilspy_utils import PlatformUtils class RustAnalyzer(LanguageServer): diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py index 099ff9d..5f3d135 100644 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ b/src/multilspy/language_servers/solargraph/solargraph.py @@ -13,13 +13,11 @@ import pathlib from contextlib import asynccontextmanager from typing import AsyncIterator, override -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_utils import FileUtils -from multilspy.multilspy_utils import PlatformUtils, PlatformId +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.multilspy_config import MultilspyConfig class Solargraph(LanguageServer): diff --git a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py index 479936c..edd3b30 100644 --- a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py @@ -16,11 +16,11 @@ from time import sleep from overrides import override from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import PlatformId, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_utils import PlatformId, PlatformUtils # Platform-specific imports if os.name != 'nt': # Unix-like systems diff --git a/src/serena/agent.py b/src/serena/agent.py index ef6e226..4952aa2 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -34,9 +34,9 @@ from sensai.util.logging import LOG_DEFAULT_FORMAT, FallbackHandler from sensai.util.string import ToStringMixin, dict_string from multilspy import SyncLanguageServer -from multilspy.multilspy_config import Language, MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_types import SymbolKind +from solidlsp.multilspy_config import Language, MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_types import SymbolKind from serena import serena_version from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import ( diff --git a/src/serena/symbol.py b/src/serena/symbol.py index f7cb194..334671d 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -11,7 +11,7 @@ from sensai.util.string import ToStringMixin from multilspy import SyncLanguageServer from multilspy.language_server import ReferenceInSymbol as LSPReferenceInSymbol -from multilspy.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation +from solidlsp.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation if TYPE_CHECKING: from .agent import SerenaAgent diff --git a/src/serena/util/inspection.py b/src/serena/util/inspection.py index 509da8e..1e5abd5 100644 --- a/src/serena/util/inspection.py +++ b/src/serena/util/inspection.py @@ -3,7 +3,7 @@ import os from collections.abc import Generator from typing import TypeVar -from multilspy.multilspy_config import Language +from solidlsp.multilspy_config import Language from serena.util.file_system import find_all_non_ignored_files T = TypeVar("T") diff --git a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py index 54d364e..c3d3497 100644 --- a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py @@ -9,11 +9,11 @@ import pathlib import stat import threading -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_utils import FileUtils, PlatformUtils from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py index ec81c8c..710f40a 100644 --- a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py @@ -4,9 +4,9 @@ import os import pathlib import stat -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_utils import FileUtils, PlatformUtils from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py index 8bce13b..eb0bafc 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -15,12 +15,12 @@ from pathlib import PurePath from overrides import override -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_settings import MultilspySettings -from multilspy.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_settings import MultilspySettings +from solidlsp.multilspy_utils import FileUtils, PlatformUtils from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/gopls/gopls.py b/src/solidlsp/language_servers/gopls/gopls.py index 78f15ca..04e423a 100644 --- a/src/solidlsp/language_servers/gopls/gopls.py +++ b/src/solidlsp/language_servers/gopls/gopls.py @@ -7,10 +7,10 @@ import threading from overrides import override -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense/intelephense.py index fa41573..d78e483 100644 --- a/src/solidlsp/language_servers/intelephense/intelephense.py +++ b/src/solidlsp/language_servers/intelephense/intelephense.py @@ -12,11 +12,11 @@ from time import sleep from overrides import override -from multilspy.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import PlatformId, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_utils import PlatformId, PlatformUtils from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py index 65fbe62..e935954 100644 --- a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py +++ b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py @@ -9,10 +9,10 @@ import pathlib from overrides import override -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py index bdb1333..bab71d1 100644 --- a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py @@ -9,11 +9,11 @@ import os import pathlib import stat -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_utils import FileUtils, PlatformUtils from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/omnisharp/omnisharp.py b/src/solidlsp/language_servers/omnisharp/omnisharp.py index cc10f58..f558033 100644 --- a/src/solidlsp/language_servers/omnisharp/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp/omnisharp.py @@ -12,12 +12,12 @@ from collections.abc import Iterable from overrides import override -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_exceptions import MultilspyException -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_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.multilspy_config import MultilspyConfig +from solidlsp.multilspy_exceptions import MultilspyException +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py index 1307e0e..7081109 100644 --- a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py @@ -10,10 +10,10 @@ import threading from overrides import override -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py index e0b1a8c..a5a1423 100644 --- a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py @@ -11,11 +11,11 @@ import threading from overrides import override -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_utils import FileUtils, PlatformUtils from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph/solargraph.py index 96fc704..c3ea619 100644 --- a/src/solidlsp/language_servers/solargraph/solargraph.py +++ b/src/solidlsp/language_servers/solargraph/solargraph.py @@ -12,10 +12,10 @@ import subprocess import threading from typing import override -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.ls import SolidLanguageServer diff --git a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py index 7d5fae6..1d6c3f7 100644 --- a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py @@ -13,11 +13,11 @@ from time import sleep from overrides import override -from multilspy.lsp_protocol_handler.lsp_types import InitializeParams -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo -from multilspy.multilspy_config import MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import PlatformId, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.multilspy_config import MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_utils import PlatformId, PlatformUtils from solidlsp.ls import SolidLanguageServer # Platform-specific imports diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 11c9feb..2ff2edf 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -16,21 +16,20 @@ from typing import Self, cast import pathspec import tqdm -from multilspy import multilspy_types +from solidlsp import multilspy_types from multilspy.language_server import GenericDocumentSymbol, LSPFileBuffer, ReferenceInSymbol -from multilspy.lsp_protocol_handler import lsp_types -from multilspy.lsp_protocol_handler import lsp_types as LSPTypes -from multilspy.lsp_protocol_handler.lsp_constants import LSPConstants -from multilspy.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind -from multilspy.lsp_protocol_handler.server import ( +from solidlsp.lsp_protocol_handler import lsp_types, lsp_types as LSPTypes +from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants +from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind +from solidlsp.lsp_protocol_handler.server import ( Error, ProcessLaunchInfo, StringDict, ) -from multilspy.multilspy_config import Language, MultilspyConfig -from multilspy.multilspy_exceptions import MultilspyException -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_utils import FileUtils, PathUtils, TextUtils +from solidlsp.multilspy_config import Language, MultilspyConfig +from solidlsp.multilspy_exceptions import MultilspyException +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_utils import FileUtils, PathUtils, TextUtils from serena.text_utils import MatchedConsecutiveLines, search_files from solidlsp.ls_handler import SolidLanguageServerHandler @@ -175,7 +174,8 @@ class SolidLanguageServer(ABC): # load cache first to prevent any racing conditions due to asyncio stuff self._document_symbols_cache: dict[ - str, tuple[str, tuple[list[multilspy_types.UnifiedSymbolInformation], list[multilspy_types.UnifiedSymbolInformation]]] + str, tuple[str, tuple[list[multilspy_types.UnifiedSymbolInformation], list[ + multilspy_types.UnifiedSymbolInformation]]] ] = {} """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" self._cache_lock = threading.Lock() @@ -543,7 +543,8 @@ class SolidLanguageServer(ABC): return ret # Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses - def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None: + def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[ + lsp_types.Location] | None: return self.server.send.references( { "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, @@ -839,7 +840,8 @@ class SolidLanguageServer(ABC): if LSPConstants.CHILDREN in root_symbol: # TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well - def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> list[multilspy_types.UnifiedSymbolInformation]: + def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> list[ + multilspy_types.UnifiedSymbolInformation]: node = cast(multilspy_types.UnifiedSymbolInformation, node) l: list[multilspy_types.UnifiedSymbolInformation] = [] turn_item_into_symbol_with_children(node) diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 0bc3255..a091043 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -12,9 +12,9 @@ from typing import Any import psutil -from multilspy.lsp_protocol_handler.lsp_requests import LspNotification -from multilspy.lsp_protocol_handler.lsp_types import ErrorCodes -from multilspy.lsp_protocol_handler.server import ( +from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification +from solidlsp.lsp_protocol_handler.lsp_types import ErrorCodes +from solidlsp.lsp_protocol_handler.server import ( ENCODING, Error, MessageType, @@ -28,7 +28,7 @@ from multilspy.lsp_protocol_handler.server import ( make_request, make_response, ) -from multilspy.multilspy_exceptions import MultilspyException +from solidlsp.multilspy_exceptions import MultilspyException from solidlsp.lsp_request import SolidLspRequest log = logging.getLogger(__name__) diff --git a/src/multilspy/lsp_protocol_handler/lsp_constants.py b/src/solidlsp/lsp_protocol_handler/lsp_constants.py similarity index 100% rename from src/multilspy/lsp_protocol_handler/lsp_constants.py rename to src/solidlsp/lsp_protocol_handler/lsp_constants.py diff --git a/src/multilspy/lsp_protocol_handler/lsp_requests.py b/src/solidlsp/lsp_protocol_handler/lsp_requests.py similarity index 99% rename from src/multilspy/lsp_protocol_handler/lsp_requests.py rename to src/solidlsp/lsp_protocol_handler/lsp_requests.py index 852df21..206f252 100644 --- a/src/multilspy/lsp_protocol_handler/lsp_requests.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_requests.py @@ -30,7 +30,8 @@ SOFTWARE. """ from typing import List, Union -from multilspy.lsp_protocol_handler import lsp_types +from solidlsp.lsp_protocol_handler import lsp_types + class LspRequest: def __init__(self, send_request): diff --git a/src/multilspy/lsp_protocol_handler/lsp_types.py b/src/solidlsp/lsp_protocol_handler/lsp_types.py similarity index 100% rename from src/multilspy/lsp_protocol_handler/lsp_types.py rename to src/solidlsp/lsp_protocol_handler/lsp_types.py diff --git a/src/multilspy/lsp_protocol_handler/server.py b/src/solidlsp/lsp_protocol_handler/server.py similarity index 100% rename from src/multilspy/lsp_protocol_handler/server.py rename to src/solidlsp/lsp_protocol_handler/server.py diff --git a/src/solidlsp/lsp_request.py b/src/solidlsp/lsp_request.py index 6320fe5..11fc67b 100644 --- a/src/solidlsp/lsp_request.py +++ b/src/solidlsp/lsp_request.py @@ -1,6 +1,6 @@ from typing import Union -from multilspy.lsp_protocol_handler import lsp_types +from solidlsp.lsp_protocol_handler import lsp_types class SolidLspRequest: diff --git a/src/multilspy/multilspy_config.py b/src/solidlsp/multilspy_config.py similarity index 100% rename from src/multilspy/multilspy_config.py rename to src/solidlsp/multilspy_config.py diff --git a/src/multilspy/multilspy_exceptions.py b/src/solidlsp/multilspy_exceptions.py similarity index 100% rename from src/multilspy/multilspy_exceptions.py rename to src/solidlsp/multilspy_exceptions.py diff --git a/src/multilspy/multilspy_logger.py b/src/solidlsp/multilspy_logger.py similarity index 100% rename from src/multilspy/multilspy_logger.py rename to src/solidlsp/multilspy_logger.py diff --git a/src/multilspy/multilspy_settings.py b/src/solidlsp/multilspy_settings.py similarity index 100% rename from src/multilspy/multilspy_settings.py rename to src/solidlsp/multilspy_settings.py diff --git a/src/multilspy/multilspy_types.py b/src/solidlsp/multilspy_types.py similarity index 100% rename from src/multilspy/multilspy_types.py rename to src/solidlsp/multilspy_types.py diff --git a/src/multilspy/multilspy_utils.py b/src/solidlsp/multilspy_utils.py similarity index 98% rename from src/multilspy/multilspy_utils.py rename to src/solidlsp/multilspy_utils.py index 20959c4..92cca5f 100644 --- a/src/multilspy/multilspy_utils.py +++ b/src/solidlsp/multilspy_utils.py @@ -14,10 +14,10 @@ import platform import subprocess from enum import Enum -from multilspy.multilspy_exceptions import MultilspyException +from solidlsp.multilspy_exceptions import MultilspyException from pathlib import PurePath, Path -from multilspy.multilspy_logger import MultilspyLogger -from multilspy.multilspy_types import UnifiedSymbolInformation +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_types import UnifiedSymbolInformation class TextUtils: diff --git a/src/multilspy/type_helpers.py b/src/solidlsp/type_helpers.py similarity index 100% rename from src/multilspy/type_helpers.py rename to src/solidlsp/type_helpers.py diff --git a/test/conftest.py b/test/conftest.py index 75bc8c7..9630cc6 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -6,8 +6,8 @@ import pytest from sensai.util.logging import configure from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language, MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_config import Language, MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger from serena.constants import USE_SOLID_LSP from serena.util.file_system import GitignoreParser from solidlsp.ls import SolidLanguageServer diff --git a/test/multilspy/go/test_go_basic.py b/test/multilspy/go/test_go_basic.py index 6b5e316..535b3ad 100644 --- a/test/multilspy/go/test_go_basic.py +++ b/test/multilspy/go/test_go_basic.py @@ -3,8 +3,8 @@ import os import pytest from multilspy import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_utils import SymbolUtils +from solidlsp.multilspy_config import Language +from solidlsp.multilspy_utils import SymbolUtils @pytest.mark.go diff --git a/test/multilspy/java/test_java_basic.py b/test/multilspy/java/test_java_basic.py index e7ff5a8..2403d12 100644 --- a/test/multilspy/java/test_java_basic.py +++ b/test/multilspy/java/test_java_basic.py @@ -3,8 +3,8 @@ import os import pytest from multilspy import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_utils import SymbolUtils +from solidlsp.multilspy_config import Language +from solidlsp.multilspy_utils import SymbolUtils @pytest.mark.java diff --git a/test/multilspy/php/test_php_basic.py b/test/multilspy/php/test_php_basic.py index 942281f..2bfed7b 100644 --- a/test/multilspy/php/test_php_basic.py +++ b/test/multilspy/php/test_php_basic.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language +from solidlsp.multilspy_config import Language @pytest.mark.php diff --git a/test/multilspy/python/test_python_basic.py b/test/multilspy/python/test_python_basic.py index 5af4f95..20ddabb 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/multilspy/python/test_python_basic.py @@ -10,7 +10,7 @@ import os import pytest from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language +from solidlsp.multilspy_config import Language from serena.text_utils import LineType diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 0db668e..f6b25c9 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language +from solidlsp.multilspy_config import Language from test.conftest import create_ls # This mark will be applied to all tests in this module diff --git a/test/multilspy/python/test_symbol_retrieval.py b/test/multilspy/python/test_symbol_retrieval.py index ba5bd61..80229ef 100644 --- a/test/multilspy/python/test_symbol_retrieval.py +++ b/test/multilspy/python/test_symbol_retrieval.py @@ -11,8 +11,8 @@ import os import pytest from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_types import SymbolKind +from solidlsp.multilspy_config import Language +from solidlsp.multilspy_types import SymbolKind pytestmark = pytest.mark.python diff --git a/test/multilspy/rust/test_rust_basic.py b/test/multilspy/rust/test_rust_basic.py index 02e9d50..20aa6e0 100644 --- a/test/multilspy/rust/test_rust_basic.py +++ b/test/multilspy/rust/test_rust_basic.py @@ -3,8 +3,8 @@ import os import pytest from multilspy import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_utils import SymbolUtils +from solidlsp.multilspy_config import Language +from solidlsp.multilspy_utils import SymbolUtils @pytest.mark.rust diff --git a/test/multilspy/typescript/test_typescript_basic.py b/test/multilspy/typescript/test_typescript_basic.py index 442b5a1..fca1e54 100644 --- a/test/multilspy/typescript/test_typescript_basic.py +++ b/test/multilspy/typescript/test_typescript_basic.py @@ -3,8 +3,8 @@ import os import pytest from multilspy import SyncLanguageServer -from multilspy.multilspy_config import Language -from multilspy.multilspy_utils import SymbolUtils +from solidlsp.multilspy_config import Language +from solidlsp.multilspy_utils import SymbolUtils @pytest.mark.typescript diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 853d86a..1642b38 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -5,7 +5,7 @@ from dataclasses import dataclass import pytest -from multilspy.multilspy_config import Language +from solidlsp.multilspy_config import Language from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase from serena.process_isolated_agent import ProcessIsolatedSerenaAgent from test.conftest import get_repo_path diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index 12be047..ba00ad1 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -11,7 +11,7 @@ from typing import Literal import pytest -from multilspy.multilspy_config import Language +from solidlsp.multilspy_config import Language from serena.symbol import CodeDiff from src.serena.symbol import SymbolManager from test.conftest import create_ls, get_repo_path From c2593a1171fb616569c54905d2acd707ef8f82e8 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 16:34:56 +0200 Subject: [PATCH 140/174] Apply black & ruff to modules copied from multilspy --- pyproject.toml | 4 + src/serena/agent.py | 6 +- src/serena/util/inspection.py | 2 +- .../clangd_language_server.py | 2 +- .../dart_language_server.py | 2 +- .../eclipse_jdtls/eclipse_jdtls.py | 2 +- src/solidlsp/language_servers/gopls/gopls.py | 2 +- .../intelephense/intelephense.py | 2 +- .../jedi_language_server/jedi_server.py | 2 +- .../kotlin_language_server.py | 2 +- .../language_servers/omnisharp/omnisharp.py | 2 +- .../pyright_language_server/pyright_server.py | 2 +- .../rust_analyzer/rust_analyzer.py | 2 +- .../language_servers/solargraph/solargraph.py | 2 +- .../typescript_language_server.py | 2 +- src/solidlsp/ls.py | 18 +- src/solidlsp/ls_handler.py | 2 +- .../lsp_protocol_handler/lsp_constants.py | 1 + .../lsp_protocol_handler/lsp_requests.py | 399 ++++---- .../lsp_protocol_handler/lsp_types.py | 865 +++++++++--------- src/solidlsp/lsp_protocol_handler/server.py | 90 +- src/solidlsp/multilspy_config.py | 11 +- src/solidlsp/multilspy_exceptions.py | 3 +- src/solidlsp/multilspy_logger.py | 3 +- src/solidlsp/multilspy_settings.py | 2 + src/solidlsp/multilspy_types.py | 68 +- src/solidlsp/multilspy_utils.py | 99 +- src/solidlsp/type_helpers.py | 11 +- test/conftest.py | 4 +- test/multilspy/python/test_python_basic.py | 2 +- test/serena/test_serena_agent.py | 2 +- test/serena/test_symbol_editing.py | 2 +- 32 files changed, 816 insertions(+), 802 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0b0f9f9..d3ba68a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -241,6 +241,10 @@ ignore = [ "SIM105", # forbids empty/general except clause "SIM113", # wants to enforce use of enumerate "E712", # forbids equality comparison with True/False + "UP007", # forbids some uses of Union + "TID252", # forbids relative imports + "B904", # forces use of raise from other_exception + "RUF012", # forbids mutable attributes as ClassVar ] unfixable = [ "F841", diff --git a/src/serena/agent.py b/src/serena/agent.py index 4952aa2..97bf63e 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -34,9 +34,6 @@ from sensai.util.logging import LOG_DEFAULT_FORMAT, FallbackHandler from sensai.util.string import ToStringMixin, dict_string from multilspy import SyncLanguageServer -from solidlsp.multilspy_config import Language, MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_types import SymbolKind from serena import serena_version from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import ( @@ -57,6 +54,9 @@ from serena.util.inspection import determine_programming_language_composition, i from serena.util.shell import execute_shell_command from serena.util.thread import ExecutionResult, execute_with_timeout from solidlsp.ls import SolidLanguageServer +from solidlsp.multilspy_config import Language, MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.multilspy_types import SymbolKind if TYPE_CHECKING: from serena.gui_log_viewer import GuiLogViewerHandler diff --git a/src/serena/util/inspection.py b/src/serena/util/inspection.py index 1e5abd5..4847fd1 100644 --- a/src/serena/util/inspection.py +++ b/src/serena/util/inspection.py @@ -3,8 +3,8 @@ import os from collections.abc import Generator from typing import TypeVar -from solidlsp.multilspy_config import Language from serena.util.file_system import find_all_non_ignored_files +from solidlsp.multilspy_config import Language T = TypeVar("T") diff --git a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py index c3d3497..a61ba13 100644 --- a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py @@ -9,12 +9,12 @@ import pathlib import stat import threading +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_utils import FileUtils, PlatformUtils -from solidlsp.ls import SolidLanguageServer class ClangdLanguageServer(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py index 710f40a..bf40699 100644 --- a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py @@ -4,10 +4,10 @@ import os import pathlib import stat +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_utils import FileUtils, PlatformUtils -from solidlsp.ls import SolidLanguageServer class DartLanguageServer(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py index eb0bafc..bd63594 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -15,13 +15,13 @@ from pathlib import PurePath from overrides import override +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_settings import MultilspySettings from solidlsp.multilspy_utils import FileUtils, PlatformUtils -from solidlsp.ls import SolidLanguageServer @dataclasses.dataclass diff --git a/src/solidlsp/language_servers/gopls/gopls.py b/src/solidlsp/language_servers/gopls/gopls.py index 04e423a..cd7f6e0 100644 --- a/src/solidlsp/language_servers/gopls/gopls.py +++ b/src/solidlsp/language_servers/gopls/gopls.py @@ -7,11 +7,11 @@ import threading from overrides import override +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.ls import SolidLanguageServer class Gopls(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense/intelephense.py index d78e483..7e7e8db 100644 --- a/src/solidlsp/language_servers/intelephense/intelephense.py +++ b/src/solidlsp/language_servers/intelephense/intelephense.py @@ -12,12 +12,12 @@ from time import sleep from overrides import override +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_utils import PlatformId, PlatformUtils -from solidlsp.ls import SolidLanguageServer class Intelephense(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py index e935954..9ec7554 100644 --- a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py +++ b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py @@ -9,11 +9,11 @@ import pathlib from overrides import override +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.ls import SolidLanguageServer class JediServer(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py index bab71d1..cf4022c 100644 --- a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py @@ -9,12 +9,12 @@ import os import pathlib import stat +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_utils import FileUtils, PlatformUtils -from solidlsp.ls import SolidLanguageServer @dataclasses.dataclass diff --git a/src/solidlsp/language_servers/omnisharp/omnisharp.py b/src/solidlsp/language_servers/omnisharp/omnisharp.py index f558033..b8b33ea 100644 --- a/src/solidlsp/language_servers/omnisharp/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp/omnisharp.py @@ -12,13 +12,13 @@ from collections.abc import Iterable from overrides import override +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_exceptions import MultilspyException from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils -from solidlsp.ls import SolidLanguageServer def breadth_first_file_scan(root) -> Iterable[str]: diff --git a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py index 7081109..b7ddd07 100644 --- a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py @@ -10,11 +10,11 @@ import threading from overrides import override +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.ls import SolidLanguageServer class PyrightServer(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py index a5a1423..d88cc59 100644 --- a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py @@ -11,12 +11,12 @@ import threading from overrides import override +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_utils import FileUtils, PlatformUtils -from solidlsp.ls import SolidLanguageServer class RustAnalyzer(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph/solargraph.py index c3ea619..f733e30 100644 --- a/src/solidlsp/language_servers/solargraph/solargraph.py +++ b/src/solidlsp/language_servers/solargraph/solargraph.py @@ -12,11 +12,11 @@ import subprocess import threading from typing import override +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.ls import SolidLanguageServer class Solargraph(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py index 1d6c3f7..3fe2d86 100644 --- a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py @@ -13,12 +13,12 @@ from time import sleep from overrides import override +from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.multilspy_config import MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_utils import PlatformId, PlatformUtils -from solidlsp.ls import SolidLanguageServer # Platform-specific imports if os.name != "nt": # Unix-like systems diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 2ff2edf..cc792db 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -16,9 +16,12 @@ from typing import Self, cast import pathspec import tqdm -from solidlsp import multilspy_types from multilspy.language_server import GenericDocumentSymbol, LSPFileBuffer, ReferenceInSymbol -from solidlsp.lsp_protocol_handler import lsp_types, lsp_types as LSPTypes +from serena.text_utils import MatchedConsecutiveLines, search_files +from solidlsp import multilspy_types +from solidlsp.ls_handler import SolidLanguageServerHandler +from solidlsp.lsp_protocol_handler import lsp_types +from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind from solidlsp.lsp_protocol_handler.server import ( @@ -30,8 +33,6 @@ from solidlsp.multilspy_config import Language, MultilspyConfig from solidlsp.multilspy_exceptions import MultilspyException from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_utils import FileUtils, PathUtils, TextUtils -from serena.text_utils import MatchedConsecutiveLines, search_files -from solidlsp.ls_handler import SolidLanguageServerHandler class SolidLanguageServer(ABC): @@ -174,8 +175,7 @@ class SolidLanguageServer(ABC): # load cache first to prevent any racing conditions due to asyncio stuff self._document_symbols_cache: dict[ - str, tuple[str, tuple[list[multilspy_types.UnifiedSymbolInformation], list[ - multilspy_types.UnifiedSymbolInformation]]] + str, tuple[str, tuple[list[multilspy_types.UnifiedSymbolInformation], list[multilspy_types.UnifiedSymbolInformation]]] ] = {} """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" self._cache_lock = threading.Lock() @@ -543,8 +543,7 @@ class SolidLanguageServer(ABC): return ret # Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses - def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[ - lsp_types.Location] | None: + def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None: return self.server.send.references( { "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, @@ -840,8 +839,7 @@ class SolidLanguageServer(ABC): if LSPConstants.CHILDREN in root_symbol: # TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well - def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> list[ - multilspy_types.UnifiedSymbolInformation]: + def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> list[multilspy_types.UnifiedSymbolInformation]: node = cast(multilspy_types.UnifiedSymbolInformation, node) l: list[multilspy_types.UnifiedSymbolInformation] = [] turn_item_into_symbol_with_children(node) diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index a091043..e4c8b68 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -28,8 +28,8 @@ from solidlsp.lsp_protocol_handler.server import ( make_request, make_response, ) -from solidlsp.multilspy_exceptions import MultilspyException from solidlsp.lsp_request import SolidLspRequest +from solidlsp.multilspy_exceptions import MultilspyException log = logging.getLogger(__name__) diff --git a/src/solidlsp/lsp_protocol_handler/lsp_constants.py b/src/solidlsp/lsp_protocol_handler/lsp_constants.py index 4ca5c3d..329c003 100644 --- a/src/solidlsp/lsp_protocol_handler/lsp_constants.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_constants.py @@ -2,6 +2,7 @@ This module contains constants used in the LSP protocol. """ + class LSPConstants: """ This class contains constants used in the LSP protocol. diff --git a/src/solidlsp/lsp_protocol_handler/lsp_requests.py b/src/solidlsp/lsp_protocol_handler/lsp_requests.py index 206f252..383e456 100644 --- a/src/solidlsp/lsp_protocol_handler/lsp_requests.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_requests.py @@ -29,7 +29,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from typing import List, Union +from typing import Union + from solidlsp.lsp_protocol_handler import lsp_types @@ -39,96 +40,94 @@ class LspRequest: async def implementation( self, params: lsp_types.ImplementationParams - ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the implementation locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Definition} or a - Thenable that resolves to such.""" + Thenable that resolves to such. + """ return await self.send_request("textDocument/implementation", params) async def type_definition( self, params: lsp_types.TypeDefinitionParams - ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the type definition locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Definition} or a - Thenable that resolves to such.""" + Thenable that resolves to such. + """ return await self.send_request("textDocument/typeDefinition", params) - async def document_color( - self, params: lsp_types.DocumentColorParams - ) -> List["lsp_types.ColorInformation"]: + async def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]: """A request to list all color symbols found in a given text document. The request's parameter is of type {@link DocumentColorParams} the response is of type {@link ColorInformation ColorInformation[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return await self.send_request("textDocument/documentColor", params) - async def color_presentation( - self, params: lsp_types.ColorPresentationParams - ) -> List["lsp_types.ColorPresentation"]: + async def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]: """A request to list all presentation for a color. The request's parameter is of type {@link ColorPresentationParams} the response is of type {@link ColorInformation ColorInformation[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return await self.send_request("textDocument/colorPresentation", params) - async def folding_range( - self, params: lsp_types.FoldingRangeParams - ) -> Union[List["lsp_types.FoldingRange"], None]: + async def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None: """A request to provide folding ranges in a document. The request's parameter is of type {@link FoldingRangeParams}, the response is of type {@link FoldingRangeList} or a Thenable - that resolves to such.""" + that resolves to such. + """ return await self.send_request("textDocument/foldingRange", params) async def declaration( self, params: lsp_types.DeclarationParams - ) -> Union["lsp_types.Declaration", List["lsp_types.LocationLink"], None]: + ) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]: """A request to resolve the type definition locations of a symbol at a given text document position. The request's parameter is of type [TextDocumentPositionParams] (#TextDocumentPositionParams) the response is of type {@link Declaration} or a typed array of {@link DeclarationLink} or a Thenable that resolves - to such.""" + to such. + """ return await self.send_request("textDocument/declaration", params) - async def selection_range( - self, params: lsp_types.SelectionRangeParams - ) -> Union[List["lsp_types.SelectionRange"], None]: + async def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None: """A request to provide selection ranges in a document. The request's parameter is of type {@link SelectionRangeParams}, the response is of type {@link SelectionRange SelectionRange[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return await self.send_request("textDocument/selectionRange", params) - async def prepare_call_hierarchy( - self, params: lsp_types.CallHierarchyPrepareParams - ) -> Union[List["lsp_types.CallHierarchyItem"], None]: + async def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None: """A request to result a `CallHierarchyItem` in a document at a given position. Can be used as an input to an incoming or outgoing call hierarchy. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("textDocument/prepareCallHierarchy", params) async def incoming_calls( self, params: lsp_types.CallHierarchyIncomingCallsParams - ) -> Union[List["lsp_types.CallHierarchyIncomingCall"], None]: + ) -> list["lsp_types.CallHierarchyIncomingCall"] | None: """A request to resolve the incoming calls for a given `CallHierarchyItem`. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("callHierarchy/incomingCalls", params) async def outgoing_calls( self, params: lsp_types.CallHierarchyOutgoingCallsParams - ) -> Union[List["lsp_types.CallHierarchyOutgoingCall"], None]: + ) -> list["lsp_types.CallHierarchyOutgoingCall"] | None: """A request to resolve the outgoing calls for a given `CallHierarchyItem`. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("callHierarchy/outgoingCalls", params) - async def semantic_tokens_full( - self, params: lsp_types.SemanticTokensParams - ) -> Union["lsp_types.SemanticTokens", None]: + async def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" return await self.send_request("textDocument/semanticTokens/full", params) @@ -138,157 +137,143 @@ class LspRequest: """@since 3.16.0""" return await self.send_request("textDocument/semanticTokens/full/delta", params) - async def semantic_tokens_range( - self, params: lsp_types.SemanticTokensRangeParams - ) -> Union["lsp_types.SemanticTokens", None]: + async def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" return await self.send_request("textDocument/semanticTokens/range", params) - async def linked_editing_range( - self, params: lsp_types.LinkedEditingRangeParams - ) -> Union["lsp_types.LinkedEditingRanges", None]: + async def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]: """A request to provide ranges that can be edited together. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("textDocument/linkedEditingRange", params) - async def will_create_files( - self, params: lsp_types.CreateFilesParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + async def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The will create files request is sent from the client to the server before files are actually created as long as the creation is triggered from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("workspace/willCreateFiles", params) - async def will_rename_files( - self, params: lsp_types.RenameFilesParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + async def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The will rename files request is sent from the client to the server before files are actually renamed as long as the rename is triggered from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("workspace/willRenameFiles", params) - async def will_delete_files( - self, params: lsp_types.DeleteFilesParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + async def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The did delete files notification is sent from the client to the server when files were deleted from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return await self.send_request("workspace/willDeleteFiles", params) - async def moniker( - self, params: lsp_types.MonikerParams - ) -> Union[List["lsp_types.Moniker"], None]: + async def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None: """A request to get the moniker of a symbol at a given text document position. The request parameter is of type {@link TextDocumentPositionParams}. - The response is of type {@link Moniker Moniker[]} or `null`.""" + The response is of type {@link Moniker Moniker[]} or `null`. + """ return await self.send_request("textDocument/moniker", params) - async def prepare_type_hierarchy( - self, params: lsp_types.TypeHierarchyPrepareParams - ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + async def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to result a `TypeHierarchyItem` in a document at a given position. Can be used as an input to a subtypes or supertypes type hierarchy. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("textDocument/prepareTypeHierarchy", params) async def type_hierarchy_supertypes( self, params: lsp_types.TypeHierarchySupertypesParams - ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + ) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to resolve the supertypes for a given `TypeHierarchyItem`. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("typeHierarchy/supertypes", params) - async def type_hierarchy_subtypes( - self, params: lsp_types.TypeHierarchySubtypesParams - ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + async def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to resolve the subtypes for a given `TypeHierarchyItem`. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("typeHierarchy/subtypes", params) - async def inline_value( - self, params: lsp_types.InlineValueParams - ) -> Union[List["lsp_types.InlineValue"], None]: + async def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None: """A request to provide inline values in a document. The request's parameter is of type {@link InlineValueParams}, the response is of type {@link InlineValue InlineValue[]} or a Thenable that resolves to such. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("textDocument/inlineValue", params) - async def inlay_hint( - self, params: lsp_types.InlayHintParams - ) -> Union[List["lsp_types.InlayHint"], None]: + async def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None: """A request to provide inlay hints in a document. The request's parameter is of type {@link InlayHintsParams}, the response is of type {@link InlayHint InlayHint[]} or a Thenable that resolves to such. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("textDocument/inlayHint", params) - async def resolve_inlay_hint( - self, params: lsp_types.InlayHint - ) -> "lsp_types.InlayHint": + async def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint": """A request to resolve additional properties for an inlay hint. The request's parameter is of type {@link InlayHint}, the response is of type {@link InlayHint} or a Thenable that resolves to such. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("inlayHint/resolve", params) - async def text_document_diagnostic( - self, params: lsp_types.DocumentDiagnosticParams - ) -> "lsp_types.DocumentDiagnosticReport": + async def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport": """The document diagnostic request definition. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("textDocument/diagnostic", params) - async def workspace_diagnostic( - self, params: lsp_types.WorkspaceDiagnosticParams - ) -> "lsp_types.WorkspaceDiagnosticReport": + async def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport": """The workspace diagnostic request definition. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("workspace/diagnostic", params) - async def initialize( - self, params: lsp_types.InitializeParams - ) -> "lsp_types.InitializeResult": + async def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult": """The initialize request is sent from the client to the server. It is sent once as the request after starting up the server. The requests parameter is of type {@link InitializeParams} the response if of type {@link InitializeResult} of a Thenable that - resolves to such.""" + resolves to such. + """ return await self.send_request("initialize", params) async def shutdown(self) -> None: """A shutdown request is sent from the client to the server. It is sent once when the client decides to shutdown the server. The only notification that is sent after a shutdown request - is the exit event.""" + is the exit event. + """ return await self.send_request("shutdown") - async def will_save_wait_until( - self, params: lsp_types.WillSaveTextDocumentParams - ) -> Union[List["lsp_types.TextEdit"], None]: + async def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None: """A document will save request is sent from the client to the server before the document is actually saved. The request can return an array of TextEdits which will be applied to the text document before it is saved. Please note that clients might drop results if computing the text edits took too long or if a server constantly fails on this request. This is done to keep the save fast and - reliable.""" + reliable. + """ return await self.send_request("textDocument/willSaveWaitUntil", params) async def completion( self, params: lsp_types.CompletionParams - ) -> Union[List["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: + ) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: """Request to request completion at a given text document position. The request's parameter is of type {@link TextDocumentPosition} the response is of type {@link CompletionItem CompletionItem[]} or {@link CompletionList} @@ -301,85 +286,72 @@ class LspRequest: """ return await self.send_request("textDocument/completion", params) - async def resolve_completion_item( - self, params: lsp_types.CompletionItem - ) -> "lsp_types.CompletionItem": + async def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem": """Request to resolve additional information for a given completion item.The request's parameter is of type {@link CompletionItem} the response - is of type {@link CompletionItem} or a Thenable that resolves to such.""" + is of type {@link CompletionItem} or a Thenable that resolves to such. + """ return await self.send_request("completionItem/resolve", params) - async def hover( - self, params: lsp_types.HoverParams - ) -> Union["lsp_types.Hover", None]: + async def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]: """Request to request hover information at a given text document position. The request's parameter is of type {@link TextDocumentPosition} the response is of - type {@link Hover} or a Thenable that resolves to such.""" + type {@link Hover} or a Thenable that resolves to such. + """ return await self.send_request("textDocument/hover", params) - async def signature_help( - self, params: lsp_types.SignatureHelpParams - ) -> Union["lsp_types.SignatureHelp", None]: + async def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]: return await self.send_request("textDocument/signatureHelp", params) - async def definition( - self, params: lsp_types.DefinitionParams - ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + async def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the definition location of a symbol at a given text document position. The request's parameter is of type [TextDocumentPosition] (#TextDocumentPosition) the response is of either type {@link Definition} or a typed array of {@link DefinitionLink} or a Thenable that resolves - to such.""" + to such. + """ return await self.send_request("textDocument/definition", params) - async def references( - self, params: lsp_types.ReferenceParams - ) -> Union[List["lsp_types.Location"], None]: + async def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None: """A request to resolve project-wide references for the symbol denoted by the given text document position. The request's parameter is of type {@link ReferenceParams} the response is of type - {@link Location Location[]} or a Thenable that resolves to such.""" + {@link Location Location[]} or a Thenable that resolves to such. + """ return await self.send_request("textDocument/references", params) - async def document_highlight( - self, params: lsp_types.DocumentHighlightParams - ) -> Union[List["lsp_types.DocumentHighlight"], None]: + async def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None: """Request to resolve a {@link DocumentHighlight} for a given text document position. The request's parameter is of type [TextDocumentPosition] (#TextDocumentPosition) the request response is of type [DocumentHighlight[]] - (#DocumentHighlight) or a Thenable that resolves to such.""" + (#DocumentHighlight) or a Thenable that resolves to such. + """ return await self.send_request("textDocument/documentHighlight", params) async def document_symbol( self, params: lsp_types.DocumentSymbolParams - ) -> Union[ - List["lsp_types.SymbolInformation"], List["lsp_types.DocumentSymbol"], None - ]: + ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.DocumentSymbol"] | None: """A request to list all symbols found in a given text document. The request's parameter is of type {@link TextDocumentIdentifier} the response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable - that resolves to such.""" + that resolves to such. + """ return await self.send_request("textDocument/documentSymbol", params) - async def code_action( - self, params: lsp_types.CodeActionParams - ) -> Union[List[Union["lsp_types.Command", "lsp_types.CodeAction"]], None]: + async def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None: """A request to provide commands for the given text document and range.""" return await self.send_request("textDocument/codeAction", params) - async def resolve_code_action( - self, params: lsp_types.CodeAction - ) -> "lsp_types.CodeAction": + async def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction": """Request to resolve additional information for a given code action.The request's parameter is of type {@link CodeAction} the response - is of type {@link CodeAction} or a Thenable that resolves to such.""" + is of type {@link CodeAction} or a Thenable that resolves to such. + """ return await self.send_request("codeAction/resolve", params) async def workspace_symbol( self, params: lsp_types.WorkspaceSymbolParams - ) -> Union[ - List["lsp_types.SymbolInformation"], List["lsp_types.WorkspaceSymbol"], None - ]: + ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.WorkspaceSymbol"] | None: """A request to list project-wide symbols matching the query string given by the {@link WorkspaceSymbolParams}. The response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable that @@ -391,78 +363,60 @@ class LspRequest: """ return await self.send_request("workspace/symbol", params) - async def resolve_workspace_symbol( - self, params: lsp_types.WorkspaceSymbol - ) -> "lsp_types.WorkspaceSymbol": + async def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol": """A request to resolve the range inside the workspace symbol's location. - @since 3.17.0""" + @since 3.17.0 + """ return await self.send_request("workspaceSymbol/resolve", params) - async def code_lens( - self, params: lsp_types.CodeLensParams - ) -> Union[List["lsp_types.CodeLens"], None]: + async def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None: """A request to provide code lens for the given text document.""" return await self.send_request("textDocument/codeLens", params) - async def resolve_code_lens( - self, params: lsp_types.CodeLens - ) -> "lsp_types.CodeLens": + async def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens": """A request to resolve a command for a given code lens.""" return await self.send_request("codeLens/resolve", params) - async def document_link( - self, params: lsp_types.DocumentLinkParams - ) -> Union[List["lsp_types.DocumentLink"], None]: + async def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None: """A request to provide document links""" return await self.send_request("textDocument/documentLink", params) - async def resolve_document_link( - self, params: lsp_types.DocumentLink - ) -> "lsp_types.DocumentLink": + async def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink": """Request to resolve additional information for a given document link. The request's parameter is of type {@link DocumentLink} the response - is of type {@link DocumentLink} or a Thenable that resolves to such.""" + is of type {@link DocumentLink} or a Thenable that resolves to such. + """ return await self.send_request("documentLink/resolve", params) - async def formatting( - self, params: lsp_types.DocumentFormattingParams - ) -> Union[List["lsp_types.TextEdit"], None]: + async def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to to format a whole document.""" return await self.send_request("textDocument/formatting", params) - async def range_formatting( - self, params: lsp_types.DocumentRangeFormattingParams - ) -> Union[List["lsp_types.TextEdit"], None]: + async def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to to format a range in a document.""" return await self.send_request("textDocument/rangeFormatting", params) - async def on_type_formatting( - self, params: lsp_types.DocumentOnTypeFormattingParams - ) -> Union[List["lsp_types.TextEdit"], None]: + async def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to format a document on type.""" return await self.send_request("textDocument/onTypeFormatting", params) - async def rename( - self, params: lsp_types.RenameParams - ) -> Union["lsp_types.WorkspaceEdit", None]: + async def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]: """A request to rename a symbol.""" return await self.send_request("textDocument/rename", params) - async def prepare_rename( - self, params: lsp_types.PrepareRenameParams - ) -> Union["lsp_types.PrepareRenameResult", None]: + async def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]: """A request to test and perform the setup necessary for a rename. - @since 3.16 - support for default behavior""" + @since 3.16 - support for default behavior + """ return await self.send_request("textDocument/prepareRename", params) - async def execute_command( - self, params: lsp_types.ExecuteCommandParams - ) -> Union["lsp_types.LSPAny", None]: + async def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]: """A request send from the client to the server to execute a command. The request might return - a workspace edit which the client will apply to the workspace.""" + a workspace edit which the client will apply to the workspace. + """ return await self.send_request("workspace/executeCommand", params) @@ -470,92 +424,87 @@ class LspNotification: def __init__(self, send_notification): self.send_notification = send_notification - def did_change_workspace_folders( - self, params: lsp_types.DidChangeWorkspaceFoldersParams - ) -> None: + def did_change_workspace_folders(self, params: lsp_types.DidChangeWorkspaceFoldersParams) -> None: """The `workspace/didChangeWorkspaceFolders` notification is sent from the client to the server when the workspace - folder configuration changes.""" + folder configuration changes. + """ return self.send_notification("workspace/didChangeWorkspaceFolders", params) - def cancel_work_done_progress( - self, params: lsp_types.WorkDoneProgressCancelParams - ) -> None: + def cancel_work_done_progress(self, params: lsp_types.WorkDoneProgressCancelParams) -> None: """The `window/workDoneProgress/cancel` notification is sent from the client to the server to cancel a progress - initiated on the server side.""" + initiated on the server side. + """ return self.send_notification("window/workDoneProgress/cancel", params) def did_create_files(self, params: lsp_types.CreateFilesParams) -> None: """The did create files notification is sent from the client to the server when files were created from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_notification("workspace/didCreateFiles", params) def did_rename_files(self, params: lsp_types.RenameFilesParams) -> None: """The did rename files notification is sent from the client to the server when files were renamed from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_notification("workspace/didRenameFiles", params) def did_delete_files(self, params: lsp_types.DeleteFilesParams) -> None: """The will delete files request is sent from the client to the server before files are actually deleted as long as the deletion is triggered from within the client. - @since 3.16.0""" + @since 3.16.0 + """ return self.send_notification("workspace/didDeleteFiles", params) - def did_open_notebook_document( - self, params: lsp_types.DidOpenNotebookDocumentParams - ) -> None: + def did_open_notebook_document(self, params: lsp_types.DidOpenNotebookDocumentParams) -> None: """A notification sent when a notebook opens. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_notification("notebookDocument/didOpen", params) - def did_change_notebook_document( - self, params: lsp_types.DidChangeNotebookDocumentParams - ) -> None: + def did_change_notebook_document(self, params: lsp_types.DidChangeNotebookDocumentParams) -> None: return self.send_notification("notebookDocument/didChange", params) - def did_save_notebook_document( - self, params: lsp_types.DidSaveNotebookDocumentParams - ) -> None: + def did_save_notebook_document(self, params: lsp_types.DidSaveNotebookDocumentParams) -> None: """A notification sent when a notebook document is saved. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_notification("notebookDocument/didSave", params) - def did_close_notebook_document( - self, params: lsp_types.DidCloseNotebookDocumentParams - ) -> None: + def did_close_notebook_document(self, params: lsp_types.DidCloseNotebookDocumentParams) -> None: """A notification sent when a notebook closes. - @since 3.17.0""" + @since 3.17.0 + """ return self.send_notification("notebookDocument/didClose", params) def initialized(self, params: lsp_types.InitializedParams) -> None: """The initialized notification is sent from the client to the server after the client is fully initialized and the server - is allowed to send requests from the server to the client.""" + is allowed to send requests from the server to the client. + """ return self.send_notification("initialized", params) def exit(self) -> None: """The exit event is sent from the client to the server to - ask the server to exit its process.""" + ask the server to exit its process. + """ return self.send_notification("exit") - def workspace_did_change_configuration( - self, params: lsp_types.DidChangeConfigurationParams - ) -> None: + def workspace_did_change_configuration(self, params: lsp_types.DidChangeConfigurationParams) -> None: """The configuration change notification is sent from the client to the server when the client's configuration has changed. The notification contains - the changed configuration as defined by the language client.""" + the changed configuration as defined by the language client. + """ return self.send_notification("workspace/didChangeConfiguration", params) - def did_open_text_document( - self, params: lsp_types.DidOpenTextDocumentParams - ) -> None: + def did_open_text_document(self, params: lsp_types.DidOpenTextDocumentParams) -> None: """The document open notification is sent from the client to the server to signal newly opened text documents. The document's truth is now managed by the client and the server must not try to read the document's truth using the document's @@ -563,47 +512,43 @@ class LspNotification: mean that its content is presented in an editor. An open notification must not be sent more than once without a corresponding close notification send before. This means open and close notification must be balanced and the max open count - is one.""" + is one. + """ return self.send_notification("textDocument/didOpen", params) - def did_change_text_document( - self, params: lsp_types.DidChangeTextDocumentParams - ) -> None: + def did_change_text_document(self, params: lsp_types.DidChangeTextDocumentParams) -> None: """The document change notification is sent from the client to the server to signal - changes to a text document.""" + changes to a text document. + """ return self.send_notification("textDocument/didChange", params) - def did_close_text_document( - self, params: lsp_types.DidCloseTextDocumentParams - ) -> None: + def did_close_text_document(self, params: lsp_types.DidCloseTextDocumentParams) -> None: """The document close notification is sent from the client to the server when the document got closed in the client. The document's truth now exists where the document's uri points to (e.g. if the document's uri is a file uri the truth now exists on disk). As with the open notification the close notification is about managing the document's content. Receiving a close notification doesn't mean that the document was open in an editor before. A close - notification requires a previous open notification to be sent.""" + notification requires a previous open notification to be sent. + """ return self.send_notification("textDocument/didClose", params) - def did_save_text_document( - self, params: lsp_types.DidSaveTextDocumentParams - ) -> None: + def did_save_text_document(self, params: lsp_types.DidSaveTextDocumentParams) -> None: """The document save notification is sent from the client to the server when - the document got saved in the client.""" + the document got saved in the client. + """ return self.send_notification("textDocument/didSave", params) - def will_save_text_document( - self, params: lsp_types.WillSaveTextDocumentParams - ) -> None: + def will_save_text_document(self, params: lsp_types.WillSaveTextDocumentParams) -> None: """A document will save notification is sent from the client to the server before - the document is actually saved.""" + the document is actually saved. + """ return self.send_notification("textDocument/willSave", params) - def did_change_watched_files( - self, params: lsp_types.DidChangeWatchedFilesParams - ) -> None: + def did_change_watched_files(self, params: lsp_types.DidChangeWatchedFilesParams) -> None: """The watched files notification is sent from the client to the server when - the client detects changes to file watched by the language client.""" + the client detects changes to file watched by the language client. + """ return self.send_notification("workspace/didChangeWatchedFiles", params) def set_trace(self, params: lsp_types.SetTraceParams) -> None: diff --git a/src/solidlsp/lsp_protocol_handler/lsp_types.py b/src/solidlsp/lsp_protocol_handler/lsp_types.py index 6d6a754..5488c35 100644 --- a/src/solidlsp/lsp_protocol_handler/lsp_types.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_types.py @@ -30,8 +30,9 @@ SOFTWARE. """ from enum import Enum, IntEnum, IntFlag -from typing import Dict, List, Literal, Union -from typing_extensions import NotRequired, TypedDict +from typing import Literal, NotRequired, Union + +from typing_extensions import TypedDict URI = str DocumentUri = str @@ -44,7 +45,8 @@ class SemanticTokenTypes(Enum): an clients can specify additional token types via the corresponding client capabilities. - @since 3.16.0""" + @since 3.16.0 + """ Namespace = "namespace" Type = "type" @@ -79,7 +81,8 @@ class SemanticTokenModifiers(Enum): an clients can specify additional token types via the corresponding client capabilities. - @since 3.16.0""" + @since 3.16.0 + """ Declaration = "declaration" Definition = "definition" @@ -96,7 +99,8 @@ class SemanticTokenModifiers(Enum): class DocumentDiagnosticReportKind(Enum): """The document diagnostic report kinds. - @since 3.17.0""" + @since 3.17.0 + """ Full = "full" """ A diagnostic report with a full @@ -191,8 +195,8 @@ class SymbolKind(IntEnum): Event = 24 Operator = 25 TypeParameter = 26 - - @classmethod + + @classmethod def from_int(cls, value: int) -> "SymbolKind": for symbol_kind in cls: if symbol_kind.value == value: @@ -203,7 +207,8 @@ class SymbolKind(IntEnum): class SymbolTag(IntEnum): """Symbol tags are extra annotations that tweak the rendering of a symbol. - @since 3.16""" + @since 3.16 + """ Deprecated = 1 """ Render a symbol as obsolete, usually using a strike-out. """ @@ -212,7 +217,8 @@ class SymbolTag(IntEnum): class UniquenessLevel(Enum): """Moniker uniqueness level to define scope of the moniker. - @since 3.16.0""" + @since 3.16.0 + """ Document = "document" """ The moniker is only unique inside a document """ @@ -229,7 +235,8 @@ class UniquenessLevel(Enum): class MonikerKind(Enum): """The moniker kind. - @since 3.16.0""" + @since 3.16.0 + """ Import = "import" """ The moniker represent a symbol that is imported into a project """ @@ -243,7 +250,8 @@ class MonikerKind(Enum): class InlayHintKind(IntEnum): """Inlay hint kinds. - @since 3.17.0""" + @since 3.17.0 + """ Type = 1 """ An inlay hint that for a type annotation. """ @@ -266,7 +274,8 @@ class MessageType(IntEnum): class TextDocumentSyncKind(IntEnum): """Defines how the host (editor) should sync - document changes to the language server.""" + document changes to the language server. + """ None_ = 0 """ Documents should not be synced at all. """ @@ -325,7 +334,8 @@ class CompletionItemTag(IntEnum): """Completion item tags are extra annotations that tweak the rendering of a completion item. - @since 3.15.0""" + @since 3.15.0 + """ Deprecated = 1 """ Render a completion as obsolete, usually using a strike-out. """ @@ -333,7 +343,8 @@ class CompletionItemTag(IntEnum): class InsertTextFormat(IntEnum): """Defines whether the insert text in a completion item should be interpreted as - plain text or a snippet.""" + plain text or a snippet. + """ PlainText = 1 """ The primary text to be inserted is treated as a plain string. """ @@ -352,7 +363,8 @@ class InsertTextMode(IntEnum): """How whitespace and indentation is handled during completion item insertion. - @since 3.16.0""" + @since 3.16.0 + """ AsIs = 1 """ The insertion or replace strings is taken as it is. If the @@ -449,7 +461,8 @@ class MarkupKind(Enum): result literals like `Hover`, `ParameterInfo` or `CompletionItem`. Please note that `MarkupKinds` must not start with a `$`. This kinds - are reserved for internal usage.""" + are reserved for internal usage. + """ PlainText = "plaintext" """ Plain text is supported as a content format """ @@ -460,7 +473,8 @@ class MarkupKind(Enum): class PositionEncodingKind(Enum): """A set of predefined position encoding kinds. - @since 3.17.0""" + @since 3.17.0 + """ UTF8 = "utf-8" """ Character offsets count UTF-8 code units. """ @@ -513,7 +527,8 @@ class DiagnosticSeverity(IntEnum): class DiagnosticTag(IntEnum): """The diagnostic tags. - @since 3.15.0""" + @since 3.15.0 + """ Unnecessary = 1 """ Unused or unnecessary code. @@ -542,7 +557,8 @@ class CompletionTriggerKind(IntEnum): class SignatureHelpTriggerKind(IntEnum): """How a signature help was triggered. - @since 3.15.0""" + @since 3.15.0 + """ Invoked = 1 """ Signature help was invoked manually by the user or by a command. """ @@ -555,7 +571,8 @@ class SignatureHelpTriggerKind(IntEnum): class CodeActionTriggerKind(IntEnum): """The reason why code actions were requested. - @since 3.17.0""" + @since 3.17.0 + """ Invoked = 1 """ Code actions were explicitly requested by the user or by an extension. """ @@ -570,7 +587,8 @@ class FileOperationPatternKind(Enum): """A pattern kind describing if a glob pattern matches a file a folder or both. - @since 3.16.0""" + @since 3.16.0 + """ File = "file" """ The pattern matches a file only. """ @@ -581,7 +599,8 @@ class FileOperationPatternKind(Enum): class NotebookCellKind(IntEnum): """A notebook cell kind. - @since 3.17.0""" + @since 3.17.0 + """ Markup = 1 """ A markup-cell is formatted source that is used for display. """ @@ -624,7 +643,7 @@ class TokenFormat(Enum): Relative = "relative" -Definition = Union["Location", List["Location"]] +Definition = Union["Location", list["Location"]] """ The definition of a symbol represented as one or many {@link Location locations}. For most programming languages there is only one location at which a symbol is defined. @@ -638,7 +657,7 @@ DefinitionLink = "LocationLink" Provides additional metadata over normal {@link Location location} definitions, including the range of the defining symbol """ -LSPArray = List["LSPAny"] +LSPArray = list["LSPAny"] """ LSP arrays. @since 3.17.0 """ @@ -650,7 +669,7 @@ convenience it is allowed and assumed that all these properties are optional as well. @since 3.17.0 """ -Declaration = Union["Location", List["Location"]] +Declaration = Union["Location", list["Location"]] """ The declaration of a symbol representation as one or many {@link Location locations}. """ DeclarationLink = "LocationLink" @@ -662,9 +681,7 @@ the declaring symbol. Servers should prefer returning `DeclarationLink` over `Declaration` if supported by the client. """ -InlineValue = Union[ - "InlineValueText", "InlineValueVariableLookup", "InlineValueEvaluatableExpression" -] +InlineValue = Union["InlineValueText", "InlineValueVariableLookup", "InlineValueEvaluatableExpression"] """ Inline value information can be provided by different means: - directly as a text value (class InlineValueText). - as a name to use for a variable lookup (class InlineValueVariableLookup) @@ -673,9 +690,7 @@ The InlineValue types combines all inline value types into one type. @since 3.17.0 """ -DocumentDiagnosticReport = Union[ - "RelatedFullDocumentDiagnosticReport", "RelatedUnchangedDocumentDiagnosticReport" -] +DocumentDiagnosticReport = Union["RelatedFullDocumentDiagnosticReport", "RelatedUnchangedDocumentDiagnosticReport"] """ The result of a document diagnostic pull request. A report can either be a full report containing all diagnostics for the requested document or an unchanged report indicating that nothing @@ -684,14 +699,12 @@ pull request. @since 3.17.0 """ -PrepareRenameResult = Union[ - "Range", "__PrepareRenameResult_Type_1", "__PrepareRenameResult_Type_2" -] +PrepareRenameResult = Union["Range", "__PrepareRenameResult_Type_1", "__PrepareRenameResult_Type_2"] -DocumentSelector = List["DocumentFilter"] +DocumentSelector = list["DocumentFilter"] """ A document selector is the combination of one or many document filters. -@sample `let sel:DocumentSelector = [{ language: 'typescript' }, { language: 'json', pattern: '**∕tsconfig.json' }]`; +@sample `let sel:DocumentSelector = [{ language: 'typescript' }, { language: 'json', pattern: '**/tsconfig.json' }]`; The use of a string as a document filter is deprecated @since 3.16.0. """ @@ -708,9 +721,7 @@ WorkspaceDocumentDiagnosticReport = Union[ @since 3.17.0 """ -TextDocumentContentChangeEvent = Union[ - "__TextDocumentContentChangeEvent_Type_1", "__TextDocumentContentChangeEvent_Type_2" -] +TextDocumentContentChangeEvent = Union["__TextDocumentContentChangeEvent_Type_1", "__TextDocumentContentChangeEvent_Type_2"] """ An event describing a change to a text document. If only a text is provided it is considered to be the full content of the document. """ @@ -734,7 +745,7 @@ a notebook cell document. @since 3.17.0 - proposed support for NotebookCellTextDocumentFilter. """ -LSPObject = Dict[str, "LSPAny"] +LSPObject = dict[str, "LSPAny"] """ LSP object definition. @since 3.17.0 """ @@ -756,7 +767,7 @@ Glob patterns can have the following syntax: - `*` to match one or more characters in a path segment - `?` to match on one character in a path segment - `**` to match any number of path segments, including none -- `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) +- `{}` to group sub patterns into an OR expression. (e.g. `**\u200b/*.{ts,js}` matches all TypeScript and JavaScript files) - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, â€Ļ) - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) @@ -781,7 +792,7 @@ Pattern = str - `*` to match one or more characters in a path segment - `?` to match on one character in a path segment - `**` to match any number of path segments, including none -- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) +- `{}` to group conditions (e.g. `**\u200b/*.{ts,js}` matches all TypeScript and JavaScript files) - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, â€Ļ) - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) @@ -802,7 +813,8 @@ class ImplementationParams(TypedDict): class Location(TypedDict): """Represents a location inside a resource, such as a line - inside a text file.""" + inside a text file. + """ uri: "DocumentUri" range: "Range" @@ -858,7 +870,7 @@ class DidChangeWorkspaceFoldersParams(TypedDict): class ConfigurationParams(TypedDict): """The parameters of a configuration request.""" - items: List["ConfigurationItem"] + items: list["ConfigurationItem"] class DocumentColorParams(TypedDict): @@ -916,7 +928,7 @@ class ColorPresentation(TypedDict): """ An {@link TextEdit edit} which is applied to a document when selecting this presentation for the color. When `falsy` the {@link ColorPresentation.label label} is used. """ - additionalTextEdits: NotRequired[List["TextEdit"]] + additionalTextEdits: NotRequired[list["TextEdit"]] """ An optional array of additional {@link TextEdit text edits} that are applied when selecting this color presentation. Edits must not overlap with the main {@link ColorPresentation.textEdit edit} nor with themselves. """ @@ -1007,7 +1019,7 @@ class SelectionRangeParams(TypedDict): textDocument: "TextDocumentIdentifier" """ The text document. """ - positions: List["Position"] + positions: list["Position"] """ The positions inside the text document. """ workDoneToken: NotRequired["ProgressToken"] """ An optional token that a server can use to report work done progress. """ @@ -1018,7 +1030,8 @@ class SelectionRangeParams(TypedDict): class SelectionRange(TypedDict): """A selection range represents a part of a selection hierarchy. A selection range - may have a parent selection range that contains it.""" + may have a parent selection range that contains it. + """ range: "Range" """ The {@link Range range} of this selection range. """ @@ -1048,7 +1061,8 @@ class WorkDoneProgressCancelParams(TypedDict): class CallHierarchyPrepareParams(TypedDict): """The parameter of a `textDocument/prepareCallHierarchy` request. - @since 3.16.0""" + @since 3.16.0 + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -1062,13 +1076,14 @@ class CallHierarchyItem(TypedDict): """Represents programming constructs like functions or constructors in the context of call hierarchy. - @since 3.16.0""" + @since 3.16.0 + """ name: str """ The name of this item. """ kind: "SymbolKind" """ The kind of this item. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this item. """ detail: NotRequired[str] """ More detail for this item, e.g. the signature of a function. """ @@ -1087,7 +1102,8 @@ class CallHierarchyItem(TypedDict): class CallHierarchyRegistrationOptions(TypedDict): """Call hierarchy options used during static or dynamic registration. - @since 3.16.0""" + @since 3.16.0 + """ documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null @@ -1100,7 +1116,8 @@ class CallHierarchyRegistrationOptions(TypedDict): class CallHierarchyIncomingCallsParams(TypedDict): """The parameter of a `callHierarchy/incomingCalls` request. - @since 3.16.0""" + @since 3.16.0 + """ item: "CallHierarchyItem" workDoneToken: NotRequired["ProgressToken"] @@ -1117,7 +1134,7 @@ CallHierarchyIncomingCall = TypedDict( "from": "CallHierarchyItem", # The ranges at which the calls appear. This is relative to the caller # denoted by {@link CallHierarchyIncomingCall.from `this.from`}. - "fromRanges": List["Range"], + "fromRanges": list["Range"], }, ) """ Represents an incoming call, e.g. a caller of a method or constructor. @@ -1128,7 +1145,8 @@ CallHierarchyIncomingCall = TypedDict( class CallHierarchyOutgoingCallsParams(TypedDict): """The parameter of a `callHierarchy/outgoingCalls` request. - @since 3.16.0""" + @since 3.16.0 + """ item: "CallHierarchyItem" workDoneToken: NotRequired["ProgressToken"] @@ -1141,11 +1159,12 @@ class CallHierarchyOutgoingCallsParams(TypedDict): class CallHierarchyOutgoingCall(TypedDict): """Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. - @since 3.16.0""" + @since 3.16.0 + """ to: "CallHierarchyItem" """ The item that is called. """ - fromRanges: List["Range"] + fromRanges: list["Range"] """ The range at which this item is called. This is the range relative to the caller, e.g the item passed to {@link CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls `provideCallHierarchyOutgoingCalls`} and not {@link CallHierarchyOutgoingCall.to `this.to`}. """ @@ -1171,14 +1190,14 @@ class SemanticTokens(TypedDict): the client will include the result id in the next semantic token request. A server can then instead of computing all semantic tokens again simply send a delta. """ - data: List[Uint] + data: list[Uint] """ The actual tokens. """ class SemanticTokensPartialResult(TypedDict): """@since 3.16.0""" - data: List[Uint] + data: list[Uint] class SemanticTokensRegistrationOptions(TypedDict): @@ -1189,7 +1208,7 @@ class SemanticTokensRegistrationOptions(TypedDict): the document selector provided on the client side will be used. """ legend: "SemanticTokensLegend" """ The legend used by the server """ - range: NotRequired[Union[bool, dict]] + range: NotRequired[bool | dict] """ Server supports providing semantic tokens for a specific range of a document. """ full: NotRequired[Union[bool, "__SemanticTokensOptions_full_Type_1"]] @@ -1218,14 +1237,14 @@ class SemanticTokensDelta(TypedDict): """@since 3.16.0""" resultId: NotRequired[str] - edits: List["SemanticTokensEdit"] + edits: list["SemanticTokensEdit"] """ The semantic token edits to transform a previous result into a new result. """ class SemanticTokensDeltaPartialResult(TypedDict): """@since 3.16.0""" - edits: List["SemanticTokensEdit"] + edits: list["SemanticTokensEdit"] class SemanticTokensRangeParams(TypedDict): @@ -1245,7 +1264,8 @@ class SemanticTokensRangeParams(TypedDict): class ShowDocumentParams(TypedDict): """Params to show a document. - @since 3.16.0""" + @since 3.16.0 + """ uri: "URI" """ The document uri to show. """ @@ -1268,7 +1288,8 @@ class ShowDocumentParams(TypedDict): class ShowDocumentResult(TypedDict): """The result of a showDocument request. - @since 3.16.0""" + @since 3.16.0 + """ success: bool """ A boolean indicating if the show was successful. """ @@ -1286,9 +1307,10 @@ class LinkedEditingRangeParams(TypedDict): class LinkedEditingRanges(TypedDict): """The result of a linked editing range request. - @since 3.16.0""" + @since 3.16.0 + """ - ranges: List["Range"] + ranges: list["Range"] """ A list of ranges that can be edited together. The ranges must have identical length and contain identical text content. The ranges cannot overlap. """ wordPattern: NotRequired[str] @@ -1310,9 +1332,10 @@ class CreateFilesParams(TypedDict): """The parameters sent in notifications/requests for user-initiated creation of files. - @since 3.16.0""" + @since 3.16.0 + """ - files: List["FileCreate"] + files: list["FileCreate"] """ An array of all files/folders created in this operation. """ @@ -1328,13 +1351,12 @@ class WorkspaceEdit(TypedDict): An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into file a.txt) will cause failure of the operation. How the client recovers from the failure is described by - the client capability: `workspace.workspaceEdit.failureHandling`""" + the client capability: `workspace.workspaceEdit.failureHandling` + """ - changes: NotRequired[Dict["DocumentUri", List["TextEdit"]]] + changes: NotRequired[dict["DocumentUri", list["TextEdit"]]] """ Holds changes to existing resources. """ - documentChanges: NotRequired[ - List[Union["TextDocumentEdit", "CreateFile", "RenameFile", "DeleteFile"]] - ] + documentChanges: NotRequired[list[Union["TextDocumentEdit", "CreateFile", "RenameFile", "DeleteFile"]]] """ Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes are either an array of `TextDocumentEdit`s to express changes to n different text documents where each text document edit addresses a specific version of a text document. Or it can contain @@ -1345,9 +1367,7 @@ class WorkspaceEdit(TypedDict): If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s using the `changes` property are supported. """ - changeAnnotations: NotRequired[ - Dict["ChangeAnnotationIdentifier", "ChangeAnnotation"] - ] + changeAnnotations: NotRequired[dict["ChangeAnnotationIdentifier", "ChangeAnnotation"]] """ A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and delete file / folder operations. @@ -1359,9 +1379,10 @@ class WorkspaceEdit(TypedDict): class FileOperationRegistrationOptions(TypedDict): """The options to register for file operations. - @since 3.16.0""" + @since 3.16.0 + """ - filters: List["FileOperationFilter"] + filters: list["FileOperationFilter"] """ The actual filters. """ @@ -1369,9 +1390,10 @@ class RenameFilesParams(TypedDict): """The parameters sent in notifications/requests for user-initiated renames of files. - @since 3.16.0""" + @since 3.16.0 + """ - files: List["FileRename"] + files: list["FileRename"] """ An array of all files/folders renamed in this operation. When a folder is renamed, only the folder will be included, and not its children. """ @@ -1380,9 +1402,10 @@ class DeleteFilesParams(TypedDict): """The parameters sent in notifications/requests for user-initiated deletes of files. - @since 3.16.0""" + @since 3.16.0 + """ - files: List["FileDelete"] + files: list["FileDelete"] """ An array of all files/folders deleted in this operation. """ @@ -1401,7 +1424,8 @@ class MonikerParams(TypedDict): class Moniker(TypedDict): """Moniker definition to match LSIF 0.5 moniker definition. - @since 3.16.0""" + @since 3.16.0 + """ scheme: str """ The scheme of the moniker. For example tsc or .Net """ @@ -1423,7 +1447,8 @@ class MonikerRegistrationOptions(TypedDict): class TypeHierarchyPrepareParams(TypedDict): """The parameter of a `textDocument/prepareTypeHierarchy` request. - @since 3.17.0""" + @since 3.17.0 + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -1440,7 +1465,7 @@ class TypeHierarchyItem(TypedDict): """ The name of this item. """ kind: "SymbolKind" """ The kind of this item. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this item. """ detail: NotRequired[str] """ More detail for this item, e.g. the signature of a function. """ @@ -1463,7 +1488,8 @@ class TypeHierarchyItem(TypedDict): class TypeHierarchyRegistrationOptions(TypedDict): """Type hierarchy options used during static or dynamic registration. - @since 3.17.0""" + @since 3.17.0 + """ documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null @@ -1476,7 +1502,8 @@ class TypeHierarchyRegistrationOptions(TypedDict): class TypeHierarchySupertypesParams(TypedDict): """The parameter of a `typeHierarchy/supertypes` request. - @since 3.17.0""" + @since 3.17.0 + """ item: "TypeHierarchyItem" workDoneToken: NotRequired["ProgressToken"] @@ -1489,7 +1516,8 @@ class TypeHierarchySupertypesParams(TypedDict): class TypeHierarchySubtypesParams(TypedDict): """The parameter of a `typeHierarchy/subtypes` request. - @since 3.17.0""" + @since 3.17.0 + """ item: "TypeHierarchyItem" workDoneToken: NotRequired["ProgressToken"] @@ -1502,7 +1530,8 @@ class TypeHierarchySubtypesParams(TypedDict): class InlineValueParams(TypedDict): """A parameter literal used in inline value requests. - @since 3.17.0""" + @since 3.17.0 + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -1518,7 +1547,8 @@ class InlineValueParams(TypedDict): class InlineValueRegistrationOptions(TypedDict): """Inline value options used during static or dynamic registration. - @since 3.17.0""" + @since 3.17.0 + """ documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null @@ -1531,7 +1561,8 @@ class InlineValueRegistrationOptions(TypedDict): class InlayHintParams(TypedDict): """A parameter literal used in inlay hint requests. - @since 3.17.0""" + @since 3.17.0 + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -1544,11 +1575,12 @@ class InlayHintParams(TypedDict): class InlayHint(TypedDict): """Inlay hint information. - @since 3.17.0""" + @since 3.17.0 + """ position: "Position" """ The position of this hint. """ - label: Union[str, List["InlayHintLabelPart"]] + label: str | list["InlayHintLabelPart"] """ The label of this hint. A human readable string or an array of InlayHintLabelPart label parts. @@ -1556,7 +1588,7 @@ class InlayHint(TypedDict): kind: NotRequired["InlayHintKind"] """ The kind of this hint. Can be omitted in which case the client should fall back to a reasonable default. """ - textEdits: NotRequired[List["TextEdit"]] + textEdits: NotRequired[list["TextEdit"]] """ Optional text edits that are performed when accepting this inlay hint. *Note* that edits are expected to change the document so that the inlay @@ -1584,7 +1616,8 @@ class InlayHint(TypedDict): class InlayHintRegistrationOptions(TypedDict): """Inlay hint options used during static or dynamic registration. - @since 3.17.0""" + @since 3.17.0 + """ resolveProvider: NotRequired[bool] """ The server provides support to resolve additional @@ -1600,7 +1633,8 @@ class InlayHintRegistrationOptions(TypedDict): class DocumentDiagnosticParams(TypedDict): """Parameters of the document diagnostic request. - @since 3.17.0""" + @since 3.17.0 + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -1618,9 +1652,10 @@ class DocumentDiagnosticParams(TypedDict): class DocumentDiagnosticReportPartialResult(TypedDict): """A partial result for a document diagnostic report. - @since 3.17.0""" + @since 3.17.0 + """ - relatedDocuments: Dict[ + relatedDocuments: dict[ "DocumentUri", Union["FullDocumentDiagnosticReport", "UnchangedDocumentDiagnosticReport"], ] @@ -1629,7 +1664,8 @@ class DocumentDiagnosticReportPartialResult(TypedDict): class DiagnosticServerCancellationData(TypedDict): """Cancellation data returned from a diagnostic request. - @since 3.17.0""" + @since 3.17.0 + """ retriggerRequest: bool @@ -1637,7 +1673,8 @@ class DiagnosticServerCancellationData(TypedDict): class DiagnosticRegistrationOptions(TypedDict): """Diagnostic registration options. - @since 3.17.0""" + @since 3.17.0 + """ documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null @@ -1660,11 +1697,12 @@ class DiagnosticRegistrationOptions(TypedDict): class WorkspaceDiagnosticParams(TypedDict): """Parameters of the workspace diagnostic request. - @since 3.17.0""" + @since 3.17.0 + """ identifier: NotRequired[str] """ The additional identifier provided during registration. """ - previousResultIds: List["PreviousResultId"] + previousResultIds: list["PreviousResultId"] """ The currently known diagnostic reports with their previous result ids. """ workDoneToken: NotRequired["ProgressToken"] @@ -1677,27 +1715,30 @@ class WorkspaceDiagnosticParams(TypedDict): class WorkspaceDiagnosticReport(TypedDict): """A workspace diagnostic report. - @since 3.17.0""" + @since 3.17.0 + """ - items: List["WorkspaceDocumentDiagnosticReport"] + items: list["WorkspaceDocumentDiagnosticReport"] class WorkspaceDiagnosticReportPartialResult(TypedDict): """A partial result for a workspace diagnostic report. - @since 3.17.0""" + @since 3.17.0 + """ - items: List["WorkspaceDocumentDiagnosticReport"] + items: list["WorkspaceDocumentDiagnosticReport"] class DidOpenNotebookDocumentParams(TypedDict): """The params sent in an open notebook document notification. - @since 3.17.0""" + @since 3.17.0 + """ notebookDocument: "NotebookDocument" """ The notebook document that got opened. """ - cellTextDocuments: List["TextDocumentItem"] + cellTextDocuments: list["TextDocumentItem"] """ The text documents that represent the content of a notebook cell. """ @@ -1705,7 +1746,8 @@ class DidOpenNotebookDocumentParams(TypedDict): class DidChangeNotebookDocumentParams(TypedDict): """The params sent in a change notebook document notification. - @since 3.17.0""" + @since 3.17.0 + """ notebookDocument: "VersionedNotebookDocumentIdentifier" """ The notebook document that did change. The version number points @@ -1731,7 +1773,8 @@ class DidChangeNotebookDocumentParams(TypedDict): class DidSaveNotebookDocumentParams(TypedDict): """The params sent in a save notebook document notification. - @since 3.17.0""" + @since 3.17.0 + """ notebookDocument: "NotebookDocumentIdentifier" """ The notebook document that got saved. """ @@ -1740,25 +1783,26 @@ class DidSaveNotebookDocumentParams(TypedDict): class DidCloseNotebookDocumentParams(TypedDict): """The params sent in a close notebook document notification. - @since 3.17.0""" + @since 3.17.0 + """ notebookDocument: "NotebookDocumentIdentifier" """ The notebook document that got closed. """ - cellTextDocuments: List["TextDocumentIdentifier"] + cellTextDocuments: list["TextDocumentIdentifier"] """ The text documents that represent the content of a notebook cell that got closed. """ class RegistrationParams(TypedDict): - registrations: List["Registration"] + registrations: list["Registration"] class UnregistrationParams(TypedDict): - unregisterations: List["Unregistration"] + unregisterations: list["Unregistration"] class InitializeParams(TypedDict): - processId: Union[int, None] + processId: int | None """ The process Id of the parent process that started the server. @@ -1777,7 +1821,7 @@ class InitializeParams(TypedDict): (See https://en.wikipedia.org/wiki/IETF_language_tag) @since 3.16.0 """ - rootPath: NotRequired[Union[str, None]] + rootPath: NotRequired[str | None] """ The rootPath of the workspace. Is null if no folder is open. @@ -1794,7 +1838,7 @@ class InitializeParams(TypedDict): """ User provided initialization options. """ trace: NotRequired["TraceValues"] """ The initial trace setting. If omitted trace is disabled ('off'). """ - workspaceFolders: NotRequired[Union[List["WorkspaceFolder"], None]] + workspaceFolders: NotRequired[list["WorkspaceFolder"] | None] """ The workspace folders configured in the client when the server starts. This property is only available if the client supports workspace folders. @@ -1817,7 +1861,8 @@ class InitializeResult(TypedDict): class InitializeError(TypedDict): """The data type of the ResponseError if the - initialize request fails.""" + initialize request fails. + """ retry: bool """ Indicates whether the client execute the following retry logic: @@ -1838,7 +1883,7 @@ class DidChangeConfigurationParams(TypedDict): class DidChangeConfigurationRegistrationOptions(TypedDict): - section: NotRequired[Union[str, List[str]]] + section: NotRequired[str | list[str]] class ShowMessageParams(TypedDict): @@ -1855,7 +1900,7 @@ class ShowMessageRequestParams(TypedDict): """ The message type. See {@link MessageType} """ message: str """ The actual message. """ - actions: NotRequired[List["MessageActionItem"]] + actions: NotRequired[list["MessageActionItem"]] """ The message action items to present. """ @@ -1887,7 +1932,7 @@ class DidChangeTextDocumentParams(TypedDict): """ The document that did change. The version number points to the version after all provided content changes have been applied. """ - contentChanges: List["TextDocumentContentChangeEvent"] + contentChanges: list["TextDocumentContentChangeEvent"] """ The actual content changes. The content changes describe single state changes to the document. So if there are two content changes c1 (at array index 0) and c2 (at array index 1) for a document in state S then c1 moves the document from @@ -1961,14 +2006,14 @@ class TextEdit(TypedDict): class DidChangeWatchedFilesParams(TypedDict): """The watched files change notification's parameters.""" - changes: List["FileEvent"] + changes: list["FileEvent"] """ The actual file events. """ class DidChangeWatchedFilesRegistrationOptions(TypedDict): """Describe options to be used when registered for text document change events.""" - watchers: List["FileSystemWatcher"] + watchers: list["FileSystemWatcher"] """ The watchers to register. """ @@ -1981,7 +2026,7 @@ class PublishDiagnosticsParams(TypedDict): """ Optional the version number of the document the diagnostics are published for. @since 3.15.0 """ - diagnostics: List["Diagnostic"] + diagnostics: list["Diagnostic"] """ An array of diagnostic information items. """ @@ -2004,7 +2049,8 @@ class CompletionParams(TypedDict): class CompletionItem(TypedDict): """A completion item represents a text snippet that is - proposed to complete text that is being typed.""" + proposed to complete text that is being typed. + """ label: str """ The label of this completion item. @@ -2021,7 +2067,7 @@ class CompletionItem(TypedDict): kind: NotRequired["CompletionItemKind"] """ The kind of this completion item. Based of the kind an icon is chosen by the editor. """ - tags: NotRequired[List["CompletionItemTag"]] + tags: NotRequired[list["CompletionItemTag"]] """ Tags for this completion item. @since 3.15.0 """ @@ -2104,7 +2150,7 @@ class CompletionItem(TypedDict): property is used as a text. @since 3.17.0 """ - additionalTextEdits: NotRequired[List["TextEdit"]] + additionalTextEdits: NotRequired[list["TextEdit"]] """ An optional array of additional {@link TextEdit text edits} that are applied when selecting this completion. Edits must not overlap (including the same insert position) with the main {@link CompletionItem.textEdit edit} nor with themselves. @@ -2112,7 +2158,7 @@ class CompletionItem(TypedDict): Additional text edits should be used to change text unrelated to the current cursor position (for example adding an import statement at the top of the file if the completion item will insert an unqualified type). """ - commitCharacters: NotRequired[List[str]] + commitCharacters: NotRequired[list[str]] """ An optional set of characters that when pressed while this completion is active will accept it first and then type that character. *Note* that all commit characters should have `length=1` and that superfluous characters will be ignored. """ @@ -2127,7 +2173,8 @@ class CompletionItem(TypedDict): class CompletionList(TypedDict): """Represents a collection of {@link CompletionItem completion items} to be presented - in the editor.""" + in the editor. + """ isIncomplete: bool """ This list it not complete. Further typing results in recomputing this list. @@ -2148,7 +2195,7 @@ class CompletionList(TypedDict): capability. @since 3.17.0 """ - items: List["CompletionItem"] + items: list["CompletionItem"] """ The completion items. """ @@ -2158,7 +2205,7 @@ class CompletionRegistrationOptions(TypedDict): documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null the document selector provided on the client side will be used. """ - triggerCharacters: NotRequired[List[str]] + triggerCharacters: NotRequired[list[str]] """ Most tools trigger completion request automatically without explicitly requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they do so when the user starts to type an identifier. For example if the user types `c` in a JavaScript file @@ -2167,7 +2214,7 @@ class CompletionRegistrationOptions(TypedDict): If code complete should automatically be trigger on characters not being valid inside an identifier (for example `.` in JavaScript) list them in `triggerCharacters`. """ - allCommitCharacters: NotRequired[List[str]] + allCommitCharacters: NotRequired[list[str]] """ The list of all possible characters that commit a completion. This field can be used if clients don't support individual commit characters per completion item. See `ClientCapabilities.textDocument.completion.completionItem.commitCharactersSupport` @@ -2200,7 +2247,7 @@ class HoverParams(TypedDict): class Hover(TypedDict): """The result of a hover request.""" - contents: Union["MarkupContent", "MarkedString", List["MarkedString"]] + contents: Union["MarkupContent", "MarkedString", list["MarkedString"]] """ The hover's content """ range: NotRequired["Range"] """ An optional range inside the text document that is used to @@ -2234,9 +2281,10 @@ class SignatureHelpParams(TypedDict): class SignatureHelp(TypedDict): """Signature help represents the signature of something callable. There can be multiple signature but only one - active and only one active parameter.""" + active and only one active parameter. + """ - signatures: List["SignatureInformation"] + signatures: list["SignatureInformation"] """ One or more signatures. """ activeSignature: NotRequired[Uint] """ The active signature. If omitted or the value lies outside the @@ -2264,9 +2312,9 @@ class SignatureHelpRegistrationOptions(TypedDict): documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null the document selector provided on the client side will be used. """ - triggerCharacters: NotRequired[List[str]] + triggerCharacters: NotRequired[list[str]] """ List of characters that trigger signature help automatically. """ - retriggerCharacters: NotRequired[List[str]] + retriggerCharacters: NotRequired[list[str]] """ List of characters that re-trigger signature help. These trigger characters are only active when signature help is already showing. All trigger characters @@ -2337,7 +2385,8 @@ class DocumentHighlightParams(TypedDict): class DocumentHighlight(TypedDict): """A document highlight is a range inside a text document which deserves special attention. Usually a document highlight is visualized by changing - the background color of its range.""" + the background color of its range. + """ range: "Range" """ The range this highlight applies to. """ @@ -2367,7 +2416,8 @@ class DocumentSymbolParams(TypedDict): class SymbolInformation(TypedDict): """Represents information about programming constructs like variables, classes, - interfaces etc.""" + interfaces etc. + """ deprecated: NotRequired[bool] """ Indicates if this symbol is deprecated. @@ -2387,7 +2437,7 @@ class SymbolInformation(TypedDict): """ The name of this symbol. """ kind: "SymbolKind" """ The kind of this symbol. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this symbol. @since 3.16.0 """ @@ -2402,7 +2452,8 @@ class DocumentSymbol(TypedDict): """Represents programming constructs like variables, classes, interfaces etc. that appear in a document. Document symbols can be hierarchical and they have two ranges: one that encloses its definition and one that points to - its most interesting range, e.g. the range of an identifier.""" + its most interesting range, e.g. the range of an identifier. + """ name: str """ The name of this symbol. Will be displayed in the user interface and therefore must not be @@ -2411,7 +2462,7 @@ class DocumentSymbol(TypedDict): """ More detail for this symbol, e.g the signature of a function. """ kind: "SymbolKind" """ The kind of this symbol. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this document symbol. @since 3.16.0 """ @@ -2461,13 +2512,14 @@ class Command(TypedDict): """Represents a reference to a command. Provides a title which will be used to represent a command in the UI and, optionally, an array of arguments which will be passed to the command handler - function when invoked.""" + function when invoked. + """ title: str """ Title of the command, like `save`. """ command: str """ The identifier of the actual command handler. """ - arguments: NotRequired[List["LSPAny"]] + arguments: NotRequired[list["LSPAny"]] """ Arguments that the command handler should be invoked with. """ @@ -2485,7 +2537,7 @@ class CodeAction(TypedDict): """ The kind of the code action. Used to filter code actions. """ - diagnostics: NotRequired[List["Diagnostic"]] + diagnostics: NotRequired[list["Diagnostic"]] """ The diagnostics that this code action resolves. """ isPreferred: NotRequired[bool] """ Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted @@ -2530,7 +2582,7 @@ class CodeActionRegistrationOptions(TypedDict): documentSelector: Union["DocumentSelector", None] """ A document selector to identify the scope of the registration. If set to null the document selector provided on the client side will be used. """ - codeActionKinds: NotRequired[List["CodeActionKind"]] + codeActionKinds: NotRequired[list["CodeActionKind"]] """ CodeActionKinds that this server may return. The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server @@ -2560,7 +2612,8 @@ class WorkspaceSymbol(TypedDict): See also SymbolInformation. - @since 3.17.0""" + @since 3.17.0 + """ location: Union["Location", "__WorkspaceSymbol_location_Type_1"] """ The location of the symbol. Whether a server is allowed to @@ -2575,7 +2628,7 @@ class WorkspaceSymbol(TypedDict): """ The name of this symbol. """ kind: "SymbolKind" """ The kind of this symbol. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this symbol. @since 3.16.0 """ @@ -2613,7 +2666,8 @@ class CodeLens(TypedDict): source text, like the number of references, a way to run tests, etc. A code lens is _unresolved_ when no command is associated to it. For performance - reasons the creation of a code lens and resolving should be done in two stages.""" + reasons the creation of a code lens and resolving should be done in two stages. + """ range: "Range" """ The range in which this code lens is valid. Should only span a single line. """ @@ -2649,7 +2703,8 @@ class DocumentLinkParams(TypedDict): class DocumentLink(TypedDict): """A document link is a range in a text document that links to an internal or external resource, like another - text document or a web site.""" + text document or a web site. + """ range: "Range" """ The range this link applies to. """ @@ -2744,7 +2799,7 @@ class DocumentOnTypeFormattingRegistrationOptions(TypedDict): the document selector provided on the client side will be used. """ firstTriggerCharacter: str """ A character on which formatting should be triggered, like `{`. """ - moreTriggerCharacter: NotRequired[List[str]] + moreTriggerCharacter: NotRequired[list[str]] """ More trigger characters. """ @@ -2789,7 +2844,7 @@ class ExecuteCommandParams(TypedDict): command: str """ The identifier of the actual command handler. """ - arguments: NotRequired[List["LSPAny"]] + arguments: NotRequired[list["LSPAny"]] """ Arguments that the command should be invoked with. """ workDoneToken: NotRequired["ProgressToken"] """ An optional token that a server can use to report work done progress. """ @@ -2798,7 +2853,7 @@ class ExecuteCommandParams(TypedDict): class ExecuteCommandRegistrationOptions(TypedDict): """Registration options for a {@link ExecuteCommandRequest}.""" - commands: List[str] + commands: list[str] """ The commands to be executed on the server """ @@ -2816,7 +2871,8 @@ class ApplyWorkspaceEditParams(TypedDict): class ApplyWorkspaceEditResult(TypedDict): """The result returned from the apply workspace edit request. - @since 3.17 renamed from ApplyWorkspaceEditResponse""" + @since 3.17 renamed from ApplyWorkspaceEditResponse + """ applied: bool """ Indicates whether the edit was applied or not. """ @@ -2895,7 +2951,7 @@ class LogTraceParams(TypedDict): class CancelParams(TypedDict): - id: Union[int, str] + id: int | str """ The request id to cancel. """ @@ -2908,7 +2964,8 @@ class ProgressParams(TypedDict): class TextDocumentPositionParams(TypedDict): """A parameter literal used in requests to pass a text document and a position inside that - document.""" + document. + """ textDocument: "TextDocumentIdentifier" """ The text document. """ @@ -2929,7 +2986,8 @@ class PartialResultParams(TypedDict): class LocationLink(TypedDict): """Represents the connection of two locations. Provides additional metadata over normal {@link Location locations}, - including an origin range.""" + including an origin range. + """ originSelectionRange: NotRequired["Range"] """ Span of the origin of this link. @@ -2958,7 +3016,8 @@ class Range(TypedDict): start: { line: 5, character: 23 } end : { line 6, character : 0 } } - ```""" + ``` + """ start: "Position" """ The range's start position. """ @@ -2972,7 +3031,8 @@ class ImplementationOptions(TypedDict): class StaticRegistrationOptions(TypedDict): """Static registration options to be returned in the initialize - request.""" + request. + """ id: NotRequired[str] """ The id used to register the request. The id can be used to deregister @@ -2986,9 +3046,9 @@ class TypeDefinitionOptions(TypedDict): class WorkspaceFoldersChangeEvent(TypedDict): """The workspace folder change event.""" - added: List["WorkspaceFolder"] + added: list["WorkspaceFolder"] """ The array of added workspace folders """ - removed: List["WorkspaceFolder"] + removed: list["WorkspaceFolder"] """ The array of the removed workspace folders """ @@ -3032,7 +3092,7 @@ class DeclarationOptions(TypedDict): class Position(TypedDict): - """Position in a text document expressed as zero-based line and character + r"""Position in a text document expressed as zero-based line and character offset. Prior to 3.17 the offsets were always based on a UTF-16 string representation. So a string of the form `a𐐀b` the character offset of the character `a` is 0, the character offset of `𐐀` is 1 and the character @@ -3058,7 +3118,8 @@ class Position(TypedDict): Positions are line end character agnostic. So you can not specify a position that denotes `\r|\n` or `\n|` where `|` represents the character offset. - @since 3.17.0 - support for negotiated position encoding.""" + @since 3.17.0 - support for negotiated position encoding. + """ line: Uint """ Line position in a document (zero-based). @@ -3082,7 +3143,8 @@ class SelectionRangeOptions(TypedDict): class CallHierarchyOptions(TypedDict): """Call hierarchy options used during static registration. - @since 3.16.0""" + @since 3.16.0 + """ workDoneProgress: NotRequired[bool] @@ -3092,7 +3154,7 @@ class SemanticTokensOptions(TypedDict): legend: "SemanticTokensLegend" """ The legend used by the server """ - range: NotRequired[Union[bool, dict]] + range: NotRequired[bool | dict] """ Server supports providing semantic tokens for a specific range of a document. """ full: NotRequired[Union[bool, "__SemanticTokensOptions_full_Type_2"]] @@ -3107,7 +3169,7 @@ class SemanticTokensEdit(TypedDict): """ The start offset of the edit. """ deleteCount: Uint """ The count of elements to remove. """ - data: NotRequired[List[Uint]] + data: NotRequired[list[Uint]] """ The elements to insert. """ @@ -3118,7 +3180,8 @@ class LinkedEditingRangeOptions(TypedDict): class FileCreate(TypedDict): """Represents information on a file/folder create. - @since 3.16.0""" + @since 3.16.0 + """ uri: str """ A file:// URI for the location of the file/folder being created. """ @@ -3128,11 +3191,12 @@ class TextDocumentEdit(TypedDict): """Describes textual changes on a text document. A TextDocumentEdit describes all changes on a document version Si and after they are applied move the document to version Si+1. So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any - kind of ordering. However the edits must be non overlapping.""" + kind of ordering. However the edits must be non overlapping. + """ textDocument: "OptionalVersionedTextDocumentIdentifier" """ The text document to change. """ - edits: List[Union["TextEdit", "AnnotatedTextEdit"]] + edits: list[Union["TextEdit", "AnnotatedTextEdit"]] """ The edits to be applied. @since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a @@ -3189,7 +3253,8 @@ class DeleteFile(TypedDict): class ChangeAnnotation(TypedDict): """Additional information that describes document changes. - @since 3.16.0""" + @since 3.16.0 + """ label: str """ A human-readable string describing the actual change. The string @@ -3206,7 +3271,8 @@ class FileOperationFilter(TypedDict): """A filter to describe in which file operation requests or notifications the server is interested in receiving. - @since 3.16.0""" + @since 3.16.0 + """ scheme: NotRequired[str] """ A Uri scheme like `file` or `untitled`. """ @@ -3217,7 +3283,8 @@ class FileOperationFilter(TypedDict): class FileRename(TypedDict): """Represents information on a file/folder rename. - @since 3.16.0""" + @since 3.16.0 + """ oldUri: str """ A file:// URI for the original location of the file/folder being renamed. """ @@ -3228,7 +3295,8 @@ class FileRename(TypedDict): class FileDelete(TypedDict): """Represents information on a file/folder delete. - @since 3.16.0""" + @since 3.16.0 + """ uri: str """ A file:// URI for the location of the file/folder being deleted. """ @@ -3241,7 +3309,8 @@ class MonikerOptions(TypedDict): class TypeHierarchyOptions(TypedDict): """Type hierarchy options used during static registration. - @since 3.17.0""" + @since 3.17.0 + """ workDoneProgress: NotRequired[bool] @@ -3259,7 +3328,8 @@ class InlineValueContext(TypedDict): class InlineValueText(TypedDict): """Provide inline value as text. - @since 3.17.0""" + @since 3.17.0 + """ range: "Range" """ The document range for which the inline value applies. """ @@ -3272,7 +3342,8 @@ class InlineValueVariableLookup(TypedDict): If only a range is specified, the variable name will be extracted from the underlying document. An optional variable name can be used to override the extracted name. - @since 3.17.0""" + @since 3.17.0 + """ range: "Range" """ The document range for which the inline value applies. @@ -3288,7 +3359,8 @@ class InlineValueEvaluatableExpression(TypedDict): If only a range is specified, the expression will be extracted from the underlying document. An optional expression can be used to override the extracted expression. - @since 3.17.0""" + @since 3.17.0 + """ range: "Range" """ The document range for which the inline value applies. @@ -3300,7 +3372,8 @@ class InlineValueEvaluatableExpression(TypedDict): class InlineValueOptions(TypedDict): """Inline value options used during static registration. - @since 3.17.0""" + @since 3.17.0 + """ workDoneProgress: NotRequired[bool] @@ -3309,7 +3382,8 @@ class InlayHintLabelPart(TypedDict): """An inlay hint label part allows for interactive and composite labels of inlay hints. - @since 3.17.0""" + @since 3.17.0 + """ value: str """ The value of this label part. """ @@ -3337,7 +3411,7 @@ class InlayHintLabelPart(TypedDict): class MarkupContent(TypedDict): - """A `MarkupContent` literal represents a string value which content is interpreted base on its + r"""A `MarkupContent` literal represents a string value which content is interpreted base on its kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues. @@ -3358,7 +3432,8 @@ class MarkupContent(TypedDict): ``` *Please Note* that clients might sanitize the return markdown. A client could decide to - remove HTML from the markdown to avoid script execution.""" + remove HTML from the markdown to avoid script execution. + """ kind: "MarkupKind" """ The type of the Markup """ @@ -3369,7 +3444,8 @@ class MarkupContent(TypedDict): class InlayHintOptions(TypedDict): """Inlay hint options used during static registration. - @since 3.17.0""" + @since 3.17.0 + """ resolveProvider: NotRequired[bool] """ The server provides support to resolve additional @@ -3380,10 +3456,11 @@ class InlayHintOptions(TypedDict): class RelatedFullDocumentDiagnosticReport(TypedDict): """A full diagnostic report with a set of related documents. - @since 3.17.0""" + @since 3.17.0 + """ relatedDocuments: NotRequired[ - Dict[ + dict[ "DocumentUri", Union["FullDocumentDiagnosticReport", "UnchangedDocumentDiagnosticReport"], ] @@ -3401,17 +3478,18 @@ class RelatedFullDocumentDiagnosticReport(TypedDict): """ An optional result id. If provided it will be sent on the next diagnostic request for the same document. """ - items: List["Diagnostic"] + items: list["Diagnostic"] """ The actual items. """ class RelatedUnchangedDocumentDiagnosticReport(TypedDict): """An unchanged diagnostic report with a set of related documents. - @since 3.17.0""" + @since 3.17.0 + """ relatedDocuments: NotRequired[ - Dict[ + dict[ "DocumentUri", Union["FullDocumentDiagnosticReport", "UnchangedDocumentDiagnosticReport"], ] @@ -3436,7 +3514,8 @@ class RelatedUnchangedDocumentDiagnosticReport(TypedDict): class FullDocumentDiagnosticReport(TypedDict): """A diagnostic report with a full set of problems. - @since 3.17.0""" + @since 3.17.0 + """ kind: Literal["full"] """ A full document diagnostic report. """ @@ -3444,7 +3523,7 @@ class FullDocumentDiagnosticReport(TypedDict): """ An optional result id. If provided it will be sent on the next diagnostic request for the same document. """ - items: List["Diagnostic"] + items: list["Diagnostic"] """ The actual items. """ @@ -3452,7 +3531,8 @@ class UnchangedDocumentDiagnosticReport(TypedDict): """A diagnostic report indicating that the last returned report is still accurate. - @since 3.17.0""" + @since 3.17.0 + """ kind: Literal["unchanged"] """ A document diagnostic report indicating @@ -3467,7 +3547,8 @@ class UnchangedDocumentDiagnosticReport(TypedDict): class DiagnosticOptions(TypedDict): """Diagnostic options. - @since 3.17.0""" + @since 3.17.0 + """ identifier: NotRequired[str] """ An optional identifier under which the diagnostics are @@ -3485,7 +3566,8 @@ class DiagnosticOptions(TypedDict): class PreviousResultId(TypedDict): """A previous result id in a workspace pull request. - @since 3.17.0""" + @since 3.17.0 + """ uri: "DocumentUri" """ The URI for which the client knowns a @@ -3497,7 +3579,8 @@ class PreviousResultId(TypedDict): class NotebookDocument(TypedDict): """A notebook document. - @since 3.17.0""" + @since 3.17.0 + """ uri: "URI" """ The notebook document's uri. """ @@ -3511,13 +3594,14 @@ class NotebookDocument(TypedDict): document. Note: should always be an object literal (e.g. LSPObject) """ - cells: List["NotebookCell"] + cells: list["NotebookCell"] """ The cells of a notebook. """ class TextDocumentItem(TypedDict): """An item to transfer a text document from the client to the - server.""" + server. + """ uri: "DocumentUri" """ The text document's uri. """ @@ -3533,7 +3617,8 @@ class TextDocumentItem(TypedDict): class VersionedNotebookDocumentIdentifier(TypedDict): """A versioned notebook document identifier. - @since 3.17.0""" + @since 3.17.0 + """ version: int """ The version number of this notebook document. """ @@ -3544,7 +3629,8 @@ class VersionedNotebookDocumentIdentifier(TypedDict): class NotebookDocumentChangeEvent(TypedDict): """A change event for a notebook document. - @since 3.17.0""" + @since 3.17.0 + """ metadata: NotRequired["LSPObject"] """ The changed meta data if any. @@ -3557,7 +3643,8 @@ class NotebookDocumentChangeEvent(TypedDict): class NotebookDocumentIdentifier(TypedDict): """A literal to identify a notebook document in the client. - @since 3.17.0""" + @since 3.17.0 + """ uri: "URI" """ The notebook document's uri. """ @@ -3586,7 +3673,7 @@ class Unregistration(TypedDict): class WorkspaceFoldersInitializeParams(TypedDict): - workspaceFolders: NotRequired[Union[List["WorkspaceFolder"], None]] + workspaceFolders: NotRequired[list["WorkspaceFolder"] | None] """ The workspace folders configured in the client when the server starts. This property is only available if the client supports workspace folders. @@ -3598,7 +3685,8 @@ class WorkspaceFoldersInitializeParams(TypedDict): class ServerCapabilities(TypedDict): """Defines the capabilities provided by a language - server.""" + server. + """ positionEncoding: NotRequired["PositionEncodingKind"] """ The position encoding the server picked from the encodings offered @@ -3610,15 +3698,11 @@ class ServerCapabilities(TypedDict): If omitted it defaults to 'utf-16'. @since 3.17.0 """ - textDocumentSync: NotRequired[ - Union["TextDocumentSyncOptions", "TextDocumentSyncKind"] - ] + textDocumentSync: NotRequired[Union["TextDocumentSyncOptions", "TextDocumentSyncKind"]] """ Defines how text documents are synced. Is either a detailed structure defining each notification or for backwards compatibility the TextDocumentSyncKind number. """ - notebookDocumentSync: NotRequired[ - Union["NotebookDocumentSyncOptions", "NotebookDocumentSyncRegistrationOptions"] - ] + notebookDocumentSync: NotRequired[Union["NotebookDocumentSyncOptions", "NotebookDocumentSyncRegistrationOptions"]] """ Defines how notebook documents are synced. @since 3.17.0 """ @@ -3628,19 +3712,13 @@ class ServerCapabilities(TypedDict): """ The server provides hover support. """ signatureHelpProvider: NotRequired["SignatureHelpOptions"] """ The server provides signature help support. """ - declarationProvider: NotRequired[ - Union[bool, "DeclarationOptions", "DeclarationRegistrationOptions"] - ] + declarationProvider: NotRequired[Union[bool, "DeclarationOptions", "DeclarationRegistrationOptions"]] """ The server provides Goto Declaration support. """ definitionProvider: NotRequired[Union[bool, "DefinitionOptions"]] """ The server provides goto definition support. """ - typeDefinitionProvider: NotRequired[ - Union[bool, "TypeDefinitionOptions", "TypeDefinitionRegistrationOptions"] - ] + typeDefinitionProvider: NotRequired[Union[bool, "TypeDefinitionOptions", "TypeDefinitionRegistrationOptions"]] """ The server provides Goto Type Definition support. """ - implementationProvider: NotRequired[ - Union[bool, "ImplementationOptions", "ImplementationRegistrationOptions"] - ] + implementationProvider: NotRequired[Union[bool, "ImplementationOptions", "ImplementationRegistrationOptions"]] """ The server provides Goto Implementation support. """ referencesProvider: NotRequired[Union[bool, "ReferenceOptions"]] """ The server provides find references support. """ @@ -3656,17 +3734,13 @@ class ServerCapabilities(TypedDict): """ The server provides code lens. """ documentLinkProvider: NotRequired["DocumentLinkOptions"] """ The server provides document link support. """ - colorProvider: NotRequired[ - Union[bool, "DocumentColorOptions", "DocumentColorRegistrationOptions"] - ] + colorProvider: NotRequired[Union[bool, "DocumentColorOptions", "DocumentColorRegistrationOptions"]] """ The server provides color provider support. """ workspaceSymbolProvider: NotRequired[Union[bool, "WorkspaceSymbolOptions"]] """ The server provides workspace symbol support. """ documentFormattingProvider: NotRequired[Union[bool, "DocumentFormattingOptions"]] """ The server provides document formatting. """ - documentRangeFormattingProvider: NotRequired[ - Union[bool, "DocumentRangeFormattingOptions"] - ] + documentRangeFormattingProvider: NotRequired[Union[bool, "DocumentRangeFormattingOptions"]] """ The server provides document range formatting. """ documentOnTypeFormattingProvider: NotRequired["DocumentOnTypeFormattingOptions"] """ The server provides document formatting on typing. """ @@ -3674,63 +3748,41 @@ class ServerCapabilities(TypedDict): """ The server provides rename support. RenameOptions may only be specified if the client states that it supports `prepareSupport` in its initial `initialize` request. """ - foldingRangeProvider: NotRequired[ - Union[bool, "FoldingRangeOptions", "FoldingRangeRegistrationOptions"] - ] + foldingRangeProvider: NotRequired[Union[bool, "FoldingRangeOptions", "FoldingRangeRegistrationOptions"]] """ The server provides folding provider support. """ - selectionRangeProvider: NotRequired[ - Union[bool, "SelectionRangeOptions", "SelectionRangeRegistrationOptions"] - ] + selectionRangeProvider: NotRequired[Union[bool, "SelectionRangeOptions", "SelectionRangeRegistrationOptions"]] """ The server provides selection range support. """ executeCommandProvider: NotRequired["ExecuteCommandOptions"] """ The server provides execute command support. """ - callHierarchyProvider: NotRequired[ - Union[bool, "CallHierarchyOptions", "CallHierarchyRegistrationOptions"] - ] + callHierarchyProvider: NotRequired[Union[bool, "CallHierarchyOptions", "CallHierarchyRegistrationOptions"]] """ The server provides call hierarchy support. @since 3.16.0 """ - linkedEditingRangeProvider: NotRequired[ - Union[ - bool, "LinkedEditingRangeOptions", "LinkedEditingRangeRegistrationOptions" - ] - ] + linkedEditingRangeProvider: NotRequired[Union[bool, "LinkedEditingRangeOptions", "LinkedEditingRangeRegistrationOptions"]] """ The server provides linked editing range support. @since 3.16.0 """ - semanticTokensProvider: NotRequired[ - Union["SemanticTokensOptions", "SemanticTokensRegistrationOptions"] - ] + semanticTokensProvider: NotRequired[Union["SemanticTokensOptions", "SemanticTokensRegistrationOptions"]] """ The server provides semantic tokens support. @since 3.16.0 """ - monikerProvider: NotRequired[ - Union[bool, "MonikerOptions", "MonikerRegistrationOptions"] - ] + monikerProvider: NotRequired[Union[bool, "MonikerOptions", "MonikerRegistrationOptions"]] """ The server provides moniker support. @since 3.16.0 """ - typeHierarchyProvider: NotRequired[ - Union[bool, "TypeHierarchyOptions", "TypeHierarchyRegistrationOptions"] - ] + typeHierarchyProvider: NotRequired[Union[bool, "TypeHierarchyOptions", "TypeHierarchyRegistrationOptions"]] """ The server provides type hierarchy support. @since 3.17.0 """ - inlineValueProvider: NotRequired[ - Union[bool, "InlineValueOptions", "InlineValueRegistrationOptions"] - ] + inlineValueProvider: NotRequired[Union[bool, "InlineValueOptions", "InlineValueRegistrationOptions"]] """ The server provides inline values. @since 3.17.0 """ - inlayHintProvider: NotRequired[ - Union[bool, "InlayHintOptions", "InlayHintRegistrationOptions"] - ] + inlayHintProvider: NotRequired[Union[bool, "InlayHintOptions", "InlayHintRegistrationOptions"]] """ The server provides inlay hints. @since 3.17.0 """ - diagnosticProvider: NotRequired[ - Union["DiagnosticOptions", "DiagnosticRegistrationOptions"] - ] + diagnosticProvider: NotRequired[Union["DiagnosticOptions", "DiagnosticRegistrationOptions"]] """ The server has support for pull model diagnostics. @since 3.17.0 """ @@ -3778,14 +3830,15 @@ class FileSystemWatcher(TypedDict): class Diagnostic(TypedDict): """Represents a diagnostic, such as a compiler error or warning. Diagnostic objects - are only valid in the scope of a resource.""" + are only valid in the scope of a resource. + """ range: "Range" """ The range at which the message applies """ severity: NotRequired["DiagnosticSeverity"] """ The diagnostic's severity. Can be omitted. If omitted it is up to the client to interpret diagnostics as error, warning, info or hint. """ - code: NotRequired[Union[int, str]] + code: NotRequired[int | str] """ The diagnostic's code, which usually appear in the user interface. """ codeDescription: NotRequired["CodeDescription"] """ An optional property to describe the error code. @@ -3798,11 +3851,11 @@ class Diagnostic(TypedDict): appears in the user interface. """ message: str """ The diagnostic's message. It usually appears in the user interface """ - tags: NotRequired[List["DiagnosticTag"]] + tags: NotRequired[list["DiagnosticTag"]] """ Additional metadata about the diagnostic. @since 3.15.0 """ - relatedInformation: NotRequired[List["DiagnosticRelatedInformation"]] + relatedInformation: NotRequired[list["DiagnosticRelatedInformation"]] """ An array of related diagnostic information, e.g. when symbol-names within a scope collide all definitions can be marked via this property. """ data: NotRequired["LSPAny"] @@ -3825,7 +3878,8 @@ class CompletionContext(TypedDict): class CompletionItemLabelDetails(TypedDict): """Additional details for a completion item label. - @since 3.17.0""" + @since 3.17.0 + """ detail: NotRequired[str] """ An optional string which is rendered less prominently directly after {@link CompletionItem.label label}, @@ -3838,7 +3892,8 @@ class CompletionItemLabelDetails(TypedDict): class InsertReplaceEdit(TypedDict): """A special text edit to provide an insert and a replace operation. - @since 3.16.0""" + @since 3.16.0 + """ newText: str """ The string to be inserted. """ @@ -3851,7 +3906,7 @@ class InsertReplaceEdit(TypedDict): class CompletionOptions(TypedDict): """Completion options.""" - triggerCharacters: NotRequired[List[str]] + triggerCharacters: NotRequired[list[str]] """ Most tools trigger completion request automatically without explicitly requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they do so when the user starts to type an identifier. For example if the user types `c` in a JavaScript file @@ -3860,7 +3915,7 @@ class CompletionOptions(TypedDict): If code complete should automatically be trigger on characters not being valid inside an identifier (for example `.` in JavaScript) list them in `triggerCharacters`. """ - allCommitCharacters: NotRequired[List[str]] + allCommitCharacters: NotRequired[list[str]] """ The list of all possible characters that commit a completion. This field can be used if clients don't support individual commit characters per completion item. See `ClientCapabilities.textDocument.completion.completionItem.commitCharactersSupport` @@ -3889,7 +3944,8 @@ class HoverOptions(TypedDict): class SignatureHelpContext(TypedDict): """Additional information about the context in which a signature help request was triggered. - @since 3.15.0""" + @since 3.15.0 + """ triggerKind: "SignatureHelpTriggerKind" """ Action that caused signature help to be triggered. """ @@ -3912,7 +3968,8 @@ class SignatureHelpContext(TypedDict): class SignatureInformation(TypedDict): """Represents the signature of something callable. A signature can have a label, like a function-name, a doc-comment, and - a set of parameters.""" + a set of parameters. + """ label: str """ The label of this signature. Will be shown in @@ -3920,7 +3977,7 @@ class SignatureInformation(TypedDict): documentation: NotRequired[Union[str, "MarkupContent"]] """ The human-readable doc-comment of this signature. Will be shown in the UI but can be omitted. """ - parameters: NotRequired[List["ParameterInformation"]] + parameters: NotRequired[list["ParameterInformation"]] """ The parameters of this signature. """ activeParameter: NotRequired[Uint] """ The index of the active parameter. @@ -3933,9 +3990,9 @@ class SignatureInformation(TypedDict): class SignatureHelpOptions(TypedDict): """Server Capabilities for a {@link SignatureHelpRequest}.""" - triggerCharacters: NotRequired[List[str]] + triggerCharacters: NotRequired[list[str]] """ List of characters that trigger signature help automatically. """ - retriggerCharacters: NotRequired[List[str]] + retriggerCharacters: NotRequired[list[str]] """ List of characters that re-trigger signature help. These trigger characters are only active when signature help is already showing. All trigger characters @@ -3953,7 +4010,8 @@ class DefinitionOptions(TypedDict): class ReferenceContext(TypedDict): """Value-object that contains additional information when - requesting references.""" + requesting references. + """ includeDeclaration: bool """ Include the declaration of the current symbol. """ @@ -3978,7 +4036,7 @@ class BaseSymbolInformation(TypedDict): """ The name of this symbol. """ kind: "SymbolKind" """ The kind of this symbol. """ - tags: NotRequired[List["SymbolTag"]] + tags: NotRequired[list["SymbolTag"]] """ Tags for this symbol. @since 3.16.0 """ @@ -4002,15 +4060,16 @@ class DocumentSymbolOptions(TypedDict): class CodeActionContext(TypedDict): """Contains additional diagnostic information about the context in which - a {@link CodeActionProvider.provideCodeActions code action} is run.""" + a {@link CodeActionProvider.provideCodeActions code action} is run. + """ - diagnostics: List["Diagnostic"] + diagnostics: list["Diagnostic"] """ An array of diagnostics known on the client side overlapping the range provided to the `textDocument/codeAction` request. They are provided so that the server knows which errors are currently presented to the user for the given range. There is no guarantee that these accurately reflect the error state of the resource. The primary parameter to compute code actions is the provided range. """ - only: NotRequired[List["CodeActionKind"]] + only: NotRequired[list["CodeActionKind"]] """ Requested kind of actions to return. Actions not of this kind are filtered out by the client before being shown. So servers @@ -4024,7 +4083,7 @@ class CodeActionContext(TypedDict): class CodeActionOptions(TypedDict): """Provider options for a {@link CodeActionRequest}.""" - codeActionKinds: NotRequired[List["CodeActionKind"]] + codeActionKinds: NotRequired[list["CodeActionKind"]] """ CodeActionKinds that this server may return. The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server @@ -4102,7 +4161,7 @@ class DocumentOnTypeFormattingOptions(TypedDict): firstTriggerCharacter: str """ A character on which formatting should be triggered, like `{`. """ - moreTriggerCharacter: NotRequired[List[str]] + moreTriggerCharacter: NotRequired[list[str]] """ More trigger characters. """ @@ -4119,7 +4178,7 @@ class RenameOptions(TypedDict): class ExecuteCommandOptions(TypedDict): """The server capabilities of a {@link ExecuteCommandRequest}.""" - commands: List[str] + commands: list[str] """ The commands to be executed on the server """ workDoneProgress: NotRequired[bool] @@ -4127,16 +4186,16 @@ class ExecuteCommandOptions(TypedDict): class SemanticTokensLegend(TypedDict): """@since 3.16.0""" - tokenTypes: List[str] + tokenTypes: list[str] """ The token types a server uses. """ - tokenModifiers: List[str] + tokenModifiers: list[str] """ The token modifiers a server uses. """ class OptionalVersionedTextDocumentIdentifier(TypedDict): """A text document identifier to optionally denote a specific version of a text document.""" - version: Union[int, None] + version: int | None """ The version number of this document. If a versioned text document identifier is sent from the server to the client and the file is not open in the editor (the server has not received an open notification before) the server can send @@ -4149,7 +4208,8 @@ class OptionalVersionedTextDocumentIdentifier(TypedDict): class AnnotatedTextEdit(TypedDict): """A special text edit with an additional change annotation. - @since 3.16.0.""" + @since 3.16.0. + """ annotationId: "ChangeAnnotationIdentifier" """ The actual identifier of the change annotation """ @@ -4203,14 +4263,15 @@ class FileOperationPattern(TypedDict): """A pattern to describe in which file operation requests or notifications the server is interested in receiving. - @since 3.16.0""" + @since 3.16.0 + """ glob: str """ The glob pattern to match. Glob patterns can have the following syntax: - `*` to match one or more characters in a path segment - `?` to match on one character in a path segment - `**` to match any number of path segments, including none - - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) + - `{}` to group sub patterns into an OR expression. (e.g. `**\u200b/*.{ts,js}` matches all TypeScript and JavaScript files) - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, â€Ļ) - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) """ matches: NotRequired["FileOperationPatternKind"] @@ -4224,11 +4285,12 @@ class FileOperationPattern(TypedDict): class WorkspaceFullDocumentDiagnosticReport(TypedDict): """A full document diagnostic report for a workspace diagnostic result. - @since 3.17.0""" + @since 3.17.0 + """ uri: "DocumentUri" """ The URI for which diagnostic information is reported. """ - version: Union[int, None] + version: int | None """ The version number for which the diagnostics are reported. If the document is not marked as open `null` can be provided. """ kind: Literal["full"] @@ -4237,18 +4299,19 @@ class WorkspaceFullDocumentDiagnosticReport(TypedDict): """ An optional result id. If provided it will be sent on the next diagnostic request for the same document. """ - items: List["Diagnostic"] + items: list["Diagnostic"] """ The actual items. """ class WorkspaceUnchangedDocumentDiagnosticReport(TypedDict): """An unchanged document diagnostic report for a workspace diagnostic result. - @since 3.17.0""" + @since 3.17.0 + """ uri: "DocumentUri" """ The URI for which diagnostic information is reported. """ - version: Union[int, None] + version: int | None """ The version number for which the diagnostics are reported. If the document is not marked as open `null` can be provided. """ kind: Literal["unchanged"] @@ -4268,7 +4331,8 @@ class NotebookCell(TypedDict): cells and can therefore be used to uniquely identify a notebook cell or the cell's text document. - @since 3.17.0""" + @since 3.17.0 + """ kind: "NotebookCellKind" """ The cell's kind """ @@ -4288,13 +4352,14 @@ class NotebookCellArrayChange(TypedDict): """A change describing how to move a `NotebookCell` array from state S to S'. - @since 3.17.0""" + @since 3.17.0 + """ start: Uint """ The start oftest of the cell that changed. """ deleteCount: Uint """ The deleted cells """ - cells: NotRequired[List["NotebookCell"]] + cells: NotRequired[list["NotebookCell"]] """ The new cells, if any """ @@ -4350,9 +4415,10 @@ class NotebookDocumentSyncOptions(TypedDict): document that contain at least one matching cell will be synced. - @since 3.17.0""" + @since 3.17.0 + """ - notebookSelector: List[ + notebookSelector: list[ Union[ "__NotebookDocumentSyncOptions_notebookSelector_Type_1", "__NotebookDocumentSyncOptions_notebookSelector_Type_2", @@ -4367,9 +4433,10 @@ class NotebookDocumentSyncOptions(TypedDict): class NotebookDocumentSyncRegistrationOptions(TypedDict): """Registration options specific to a notebook. - @since 3.17.0""" + @since 3.17.0 + """ - notebookSelector: List[ + notebookSelector: list[ Union[ "__NotebookDocumentSyncOptions_notebookSelector_Type_3", "__NotebookDocumentSyncOptions_notebookSelector_Type_4", @@ -4387,7 +4454,7 @@ class NotebookDocumentSyncRegistrationOptions(TypedDict): class WorkspaceFoldersServerCapabilities(TypedDict): supported: NotRequired[bool] """ The server has support for workspace folders """ - changeNotifications: NotRequired[Union[str, bool]] + changeNotifications: NotRequired[str | bool] """ Whether the server wants to receive workspace folder change notifications. @@ -4400,7 +4467,8 @@ class WorkspaceFoldersServerCapabilities(TypedDict): class FileOperationOptions(TypedDict): """Options for notifications/requests for user operations on files. - @since 3.16.0""" + @since 3.16.0 + """ didCreate: NotRequired["FileOperationRegistrationOptions"] """ The server is interested in receiving didCreateFiles notifications. """ @@ -4419,7 +4487,8 @@ class FileOperationOptions(TypedDict): class CodeDescription(TypedDict): """Structure to capture a description for an error code. - @since 3.16.0""" + @since 3.16.0 + """ href: "URI" """ An URI to open with more information about the diagnostic error. """ @@ -4428,7 +4497,8 @@ class CodeDescription(TypedDict): class DiagnosticRelatedInformation(TypedDict): """Represents a related message and source code location for a diagnostic. This should be used to point to code locations that cause or related to a diagnostics, e.g when duplicating - a symbol in a scope.""" + a symbol in a scope. + """ location: "Location" """ The location of this related diagnostic information. """ @@ -4438,9 +4508,10 @@ class DiagnosticRelatedInformation(TypedDict): class ParameterInformation(TypedDict): """Represents a parameter of a callable-signature. A parameter can - have a label and a doc-comment.""" + have a label and a doc-comment. + """ - label: Union[str, List[Union[Uint, Uint]]] + label: str | list[Uint | Uint] """ The label of this parameter information. Either a string or an inclusive start and exclusive end offsets within its containing @@ -4458,7 +4529,8 @@ class NotebookCellTextDocumentFilter(TypedDict): """A notebook cell text document filter denotes a cell text document by different properties. - @since 3.17.0""" + @since 3.17.0 + """ notebook: Union[str, "NotebookDocumentFilter"] """ A filter that matches against the notebook @@ -4475,7 +4547,8 @@ class NotebookCellTextDocumentFilter(TypedDict): class FileOperationPatternOptions(TypedDict): """Matching options for the file operation pattern. - @since 3.16.0""" + @since 3.16.0 + """ ignoreCase: NotRequired[bool] """ The pattern should be matched ignoring casing. """ @@ -4644,7 +4717,8 @@ class TextDocumentClientCapabilities(TypedDict): class NotebookDocumentClientCapabilities(TypedDict): """Capabilities specific to the notebook document support. - @since 3.17.0""" + @since 3.17.0 + """ synchronization: "NotebookDocumentSyncClientCapabilities" """ Capabilities specific to notebook document synchronization @@ -4676,11 +4750,10 @@ class WindowClientCapabilities(TypedDict): class GeneralClientCapabilities(TypedDict): """General client capabilities. - @since 3.16.0""" + @since 3.16.0 + """ - staleRequestSupport: NotRequired[ - "__GeneralClientCapabilities_staleRequestSupport_Type_1" - ] + staleRequestSupport: NotRequired["__GeneralClientCapabilities_staleRequestSupport_Type_1"] """ Client capability that signals how the client handles stale requests (e.g. a request for which the client will not process the response @@ -4695,7 +4768,7 @@ class GeneralClientCapabilities(TypedDict): """ Client capabilities specific to the client's markdown parser. @since 3.16.0 """ - positionEncodings: NotRequired[List["PositionEncodingKind"]] + positionEncodings: NotRequired[list["PositionEncodingKind"]] """ The position encodings supported by the client. Client and server have to agree on the same position encoding to ensure that offsets (e.g. character position in a line) are interpreted the same on both @@ -4721,7 +4794,8 @@ class RelativePattern(TypedDict): relatively to a base URI. The common value for a `baseUri` is a workspace folder root, but it can be another absolute URI as well. - @since 3.17.0""" + @since 3.17.0 + """ baseUri: Union["WorkspaceFolder", "URI"] """ A workspace folder or a base URI to which this pattern will be matched @@ -4733,7 +4807,7 @@ class RelativePattern(TypedDict): class WorkspaceEditClientCapabilities(TypedDict): documentChanges: NotRequired[bool] """ The client supports versioned document changes in `WorkspaceEdit`s """ - resourceOperations: NotRequired[List["ResourceOperationKind"]] + resourceOperations: NotRequired[list["ResourceOperationKind"]] """ The resource operations the client supports. Clients should at least support 'create', 'rename' and 'delete' files and folders. @@ -4751,9 +4825,7 @@ class WorkspaceEditClientCapabilities(TypedDict): character. @since 3.16.0 """ - changeAnnotationSupport: NotRequired[ - "__WorkspaceEditClientCapabilities_changeAnnotationSupport_Type_1" - ] + changeAnnotationSupport: NotRequired["__WorkspaceEditClientCapabilities_changeAnnotationSupport_Type_1"] """ Whether the client in general supports change annotations on text edits, create file, rename file and delete file changes. @@ -4789,9 +4861,7 @@ class WorkspaceSymbolClientCapabilities(TypedDict): Clients supporting tags have to handle unknown tags gracefully. @since 3.16.0 """ - resolveSupport: NotRequired[ - "__WorkspaceSymbolClientCapabilities_resolveSupport_Type_1" - ] + resolveSupport: NotRequired["__WorkspaceSymbolClientCapabilities_resolveSupport_Type_1"] """ The client support partial workspace symbols. The client will send the request `workspaceSymbol/resolve` to the server to resolve additional properties. @@ -4838,7 +4908,8 @@ class FileOperationClientCapabilities(TypedDict): These events do not come from the file system, they come from user operations like renaming a file in the UI. - @since 3.16.0""" + @since 3.16.0 + """ dynamicRegistration: NotRequired[bool] """ Whether the client supports dynamic registration for file requests/notifications. """ @@ -4859,7 +4930,8 @@ class FileOperationClientCapabilities(TypedDict): class InlineValueWorkspaceClientCapabilities(TypedDict): """Client workspace capabilities specific to inline values. - @since 3.17.0""" + @since 3.17.0 + """ refreshSupport: NotRequired[bool] """ Whether the client implementation supports a refresh request sent from the @@ -4874,7 +4946,8 @@ class InlineValueWorkspaceClientCapabilities(TypedDict): class InlayHintWorkspaceClientCapabilities(TypedDict): """Client workspace capabilities specific to inlay hints. - @since 3.17.0""" + @since 3.17.0 + """ refreshSupport: NotRequired[bool] """ Whether the client implementation supports a refresh request sent from @@ -4889,7 +4962,8 @@ class InlayHintWorkspaceClientCapabilities(TypedDict): class DiagnosticWorkspaceClientCapabilities(TypedDict): """Workspace client capabilities specific to diagnostic pull requests. - @since 3.17.0""" + @since 3.17.0 + """ refreshSupport: NotRequired[bool] """ Whether the client implementation supports a refresh request sent from @@ -4922,9 +4996,7 @@ class CompletionClientCapabilities(TypedDict): completionItem: NotRequired["__CompletionClientCapabilities_completionItem_Type_1"] """ The client supports the following `CompletionItem` specific capabilities. """ - completionItemKind: NotRequired[ - "__CompletionClientCapabilities_completionItemKind_Type_1" - ] + completionItemKind: NotRequired["__CompletionClientCapabilities_completionItemKind_Type_1"] insertTextMode: NotRequired["InsertTextMode"] """ Defines how the client handles whitespace and indentation when accepting a completion item that uses multi line @@ -4944,7 +5016,7 @@ class CompletionClientCapabilities(TypedDict): class HoverClientCapabilities(TypedDict): dynamicRegistration: NotRequired[bool] """ Whether hover supports dynamic registration. """ - contentFormat: NotRequired[List["MarkupKind"]] + contentFormat: NotRequired[list["MarkupKind"]] """ Client supports the following content formats for the content property. The order describes the preferred format of the client. """ @@ -4954,9 +5026,7 @@ class SignatureHelpClientCapabilities(TypedDict): dynamicRegistration: NotRequired[bool] """ Whether signature help supports dynamic registration. """ - signatureInformation: NotRequired[ - "__SignatureHelpClientCapabilities_signatureInformation_Type_1" - ] + signatureInformation: NotRequired["__SignatureHelpClientCapabilities_signatureInformation_Type_1"] """ The client supports the following `SignatureInformation` specific properties. """ contextSupport: NotRequired[bool] @@ -5058,9 +5128,7 @@ class CodeActionClientCapabilities(TypedDict): dynamicRegistration: NotRequired[bool] """ Whether code action supports dynamic registration. """ - codeActionLiteralSupport: NotRequired[ - "__CodeActionClientCapabilities_codeActionLiteralSupport_Type_1" - ] + codeActionLiteralSupport: NotRequired["__CodeActionClientCapabilities_codeActionLiteralSupport_Type_1"] """ The client support code action literals of type `CodeAction` as a valid response of the `textDocument/codeAction` request. If the property is not set the request can only return `Command` literals. @@ -5180,9 +5248,7 @@ class FoldingRangeClientCapabilities(TypedDict): """ If set, the client signals that it only supports folding complete lines. If set, client will ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange. """ - foldingRangeKind: NotRequired[ - "__FoldingRangeClientCapabilities_foldingRangeKind_Type_1" - ] + foldingRangeKind: NotRequired["__FoldingRangeClientCapabilities_foldingRangeKind_Type_1"] """ Specific options for the folding range kind. @since 3.17.0 """ @@ -5251,11 +5317,11 @@ class SemanticTokensClientCapabilities(TypedDict): `request.range` are both set to true but the server only provides a range provider the client might not render a minimap correctly or might even decide to not show any semantic tokens at all. """ - tokenTypes: List[str] + tokenTypes: list[str] """ The token types that the client supports. """ - tokenModifiers: List[str] + tokenModifiers: list[str] """ The token modifiers that the client supports. """ - formats: List["TokenFormat"] + formats: list["TokenFormat"] """ The token formats the clients supports. """ overlappingTokenSupport: NotRequired[bool] """ Whether the client supports tokens that can overlap each other. """ @@ -5284,7 +5350,8 @@ class SemanticTokensClientCapabilities(TypedDict): class LinkedEditingRangeClientCapabilities(TypedDict): """Client capabilities for the linked editing range request. - @since 3.16.0""" + @since 3.16.0 + """ dynamicRegistration: NotRequired[bool] """ Whether implementation supports dynamic registration. If this is set to `true` @@ -5295,7 +5362,8 @@ class LinkedEditingRangeClientCapabilities(TypedDict): class MonikerClientCapabilities(TypedDict): """Client capabilities specific to the moniker request. - @since 3.16.0""" + @since 3.16.0 + """ dynamicRegistration: NotRequired[bool] """ Whether moniker supports dynamic registration. If this is set to `true` @@ -5315,7 +5383,8 @@ class TypeHierarchyClientCapabilities(TypedDict): class InlineValueClientCapabilities(TypedDict): """Client capabilities specific to inline values. - @since 3.17.0""" + @since 3.17.0 + """ dynamicRegistration: NotRequired[bool] """ Whether implementation supports dynamic registration for inline value providers. """ @@ -5324,7 +5393,8 @@ class InlineValueClientCapabilities(TypedDict): class InlayHintClientCapabilities(TypedDict): """Inlay hint client capabilities. - @since 3.17.0""" + @since 3.17.0 + """ dynamicRegistration: NotRequired[bool] """ Whether inlay hints support dynamic registration. """ @@ -5336,7 +5406,8 @@ class InlayHintClientCapabilities(TypedDict): class DiagnosticClientCapabilities(TypedDict): """Client capabilities specific to diagnostic pull requests. - @since 3.17.0""" + @since 3.17.0 + """ dynamicRegistration: NotRequired[bool] """ Whether implementation supports dynamic registration. If this is set to `true` @@ -5349,7 +5420,8 @@ class DiagnosticClientCapabilities(TypedDict): class NotebookDocumentSyncClientCapabilities(TypedDict): """Notebook specific client capabilities. - @since 3.17.0""" + @since 3.17.0 + """ dynamicRegistration: NotRequired[bool] """ Whether implementation supports dynamic registration. If this is @@ -5363,16 +5435,15 @@ class NotebookDocumentSyncClientCapabilities(TypedDict): class ShowMessageRequestClientCapabilities(TypedDict): """Show message request client capabilities""" - messageActionItem: NotRequired[ - "__ShowMessageRequestClientCapabilities_messageActionItem_Type_1" - ] + messageActionItem: NotRequired["__ShowMessageRequestClientCapabilities_messageActionItem_Type_1"] """ Capabilities specific to the `MessageActionItem` type. """ class ShowDocumentClientCapabilities(TypedDict): """Client capabilities for the showDocument request. - @since 3.16.0""" + @since 3.16.0 + """ support: bool """ The client has support for the showDocument @@ -5382,7 +5453,8 @@ class ShowDocumentClientCapabilities(TypedDict): class RegularExpressionsClientCapabilities(TypedDict): """Client capabilities specific to regular expressions. - @since 3.16.0""" + @since 3.16.0 + """ engine: str """ The engine's name. """ @@ -5393,13 +5465,14 @@ class RegularExpressionsClientCapabilities(TypedDict): class MarkdownClientCapabilities(TypedDict): """Client capabilities specific to the used markdown parser. - @since 3.16.0""" + @since 3.16.0 + """ parser: str """ The name of the parser. """ version: NotRequired[str] """ The version of the parser. """ - allowedTags: NotRequired[List[str]] + allowedTags: NotRequired[list[str]] """ A list of HTML tags that the client allows / supports in Markdown. @@ -5412,10 +5485,8 @@ class __CodeActionClientCapabilities_codeActionLiteralSupport_Type_1(TypedDict): set. """ -class __CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind_Type_1( - TypedDict -): - valueSet: List["CodeActionKind"] +class __CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind_Type_1(TypedDict): + valueSet: list["CodeActionKind"] """ The code action kind values the client supports. When this property exists the client also guarantees that it will handle values outside its set gracefully and falls back @@ -5423,7 +5494,7 @@ class __CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind_Typ class __CodeActionClientCapabilities_resolveSupport_Type_1(TypedDict): - properties: List[str] + properties: list[str] """ The properties that a client can resolve lazily. """ @@ -5435,7 +5506,7 @@ class __CodeAction_disabled_Type_1(TypedDict): class __CompletionClientCapabilities_completionItemKind_Type_1(TypedDict): - valueSet: NotRequired[List["CompletionItemKind"]] + valueSet: NotRequired[list["CompletionItemKind"]] """ The completion item kind values the client supports. When this property exists the client also guarantees that it will handle values outside its set gracefully and falls back @@ -5456,16 +5527,14 @@ class __CompletionClientCapabilities_completionItem_Type_1(TypedDict): that is typing in one will update others too. """ commitCharactersSupport: NotRequired[bool] """ Client supports commit characters on a completion item. """ - documentationFormat: NotRequired[List["MarkupKind"]] + documentationFormat: NotRequired[list["MarkupKind"]] """ Client supports the following content formats for the documentation property. The order describes the preferred format of the client. """ deprecatedSupport: NotRequired[bool] """ Client supports the deprecated property on a completion item. """ preselectSupport: NotRequired[bool] """ Client supports the preselect property on a completion item. """ - tagSupport: NotRequired[ - "__CompletionClientCapabilities_completionItem_tagSupport_Type_1" - ] + tagSupport: NotRequired["__CompletionClientCapabilities_completionItem_tagSupport_Type_1"] """ Client supports the tag property on a completion item. Clients supporting tags have to handle unknown tags gracefully. Clients especially need to preserve unknown tags when sending a completion item back to the server in @@ -5477,17 +5546,13 @@ class __CompletionClientCapabilities_completionItem_Type_1(TypedDict): completion item is inserted in the text or should replace text. @since 3.16.0 """ - resolveSupport: NotRequired[ - "__CompletionClientCapabilities_completionItem_resolveSupport_Type_1" - ] + resolveSupport: NotRequired["__CompletionClientCapabilities_completionItem_resolveSupport_Type_1"] """ Indicates which properties a client can resolve lazily on a completion item. Before version 3.16.0 only the predefined properties `documentation` and `details` could be resolved lazily. @since 3.16.0 """ - insertTextModeSupport: NotRequired[ - "__CompletionClientCapabilities_completionItem_insertTextModeSupport_Type_1" - ] + insertTextModeSupport: NotRequired["__CompletionClientCapabilities_completionItem_insertTextModeSupport_Type_1"] """ The client supports the `insertTextMode` property on a completion item to override the whitespace handling mode as defined by the client (see `insertTextMode`). @@ -5500,24 +5565,22 @@ class __CompletionClientCapabilities_completionItem_Type_1(TypedDict): @since 3.17.0 """ -class __CompletionClientCapabilities_completionItem_insertTextModeSupport_Type_1( - TypedDict -): - valueSet: List["InsertTextMode"] +class __CompletionClientCapabilities_completionItem_insertTextModeSupport_Type_1(TypedDict): + valueSet: list["InsertTextMode"] class __CompletionClientCapabilities_completionItem_resolveSupport_Type_1(TypedDict): - properties: List[str] + properties: list[str] """ The properties that a client can resolve lazily. """ class __CompletionClientCapabilities_completionItem_tagSupport_Type_1(TypedDict): - valueSet: List["CompletionItemTag"] + valueSet: list["CompletionItemTag"] """ The tags supported by the client. """ class __CompletionClientCapabilities_completionList_Type_1(TypedDict): - itemDefaults: NotRequired[List[str]] + itemDefaults: NotRequired[list[str]] """ The client supports the following itemDefaults on a completion list. @@ -5529,13 +5592,11 @@ class __CompletionClientCapabilities_completionList_Type_1(TypedDict): class __CompletionList_itemDefaults_Type_1(TypedDict): - commitCharacters: NotRequired[List[str]] + commitCharacters: NotRequired[list[str]] """ A default commit character set. @since 3.17.0 """ - editRange: NotRequired[ - Union["Range", "__CompletionList_itemDefaults_editRange_Type_1"] - ] + editRange: NotRequired[Union["Range", "__CompletionList_itemDefaults_editRange_Type_1"]] """ A default edit range. @since 3.17.0 """ @@ -5577,7 +5638,7 @@ class __CompletionOptions_completionItem_Type_2(TypedDict): class __DocumentSymbolClientCapabilities_symbolKind_Type_1(TypedDict): - valueSet: NotRequired[List["SymbolKind"]] + valueSet: NotRequired[list["SymbolKind"]] """ The symbol kind values the client supports. When this property exists the client also guarantees that it will handle values outside its set gracefully and falls back @@ -5589,12 +5650,12 @@ class __DocumentSymbolClientCapabilities_symbolKind_Type_1(TypedDict): class __DocumentSymbolClientCapabilities_tagSupport_Type_1(TypedDict): - valueSet: List["SymbolTag"] + valueSet: list["SymbolTag"] """ The tags supported by the client. """ class __FoldingRangeClientCapabilities_foldingRangeKind_Type_1(TypedDict): - valueSet: NotRequired[List["FoldingRangeKind"]] + valueSet: NotRequired[list["FoldingRangeKind"]] """ The folding range kind values the client supports. When this property exists the client also guarantees that it will handle values outside its set gracefully and falls back @@ -5612,7 +5673,7 @@ class __FoldingRangeClientCapabilities_foldingRange_Type_1(TypedDict): class __GeneralClientCapabilities_staleRequestSupport_Type_1(TypedDict): cancel: bool """ The client will actively cancel the request. """ - retryOnContentModified: List[str] + retryOnContentModified: list[str] """ The list of requests for which the client will retry the request if it receives a response with error code `ContentModified` """ @@ -5626,7 +5687,7 @@ class __InitializeResult_serverInfo_Type_1(TypedDict): class __InlayHintClientCapabilities_resolveSupport_Type_1(TypedDict): - properties: List[str] + properties: list[str] """ The properties that a client can resolve lazily. """ @@ -5639,27 +5700,25 @@ class __NotebookDocumentChangeEvent_cells_Type_1(TypedDict): structure: NotRequired["__NotebookDocumentChangeEvent_cells_structure_Type_1"] """ Changes to the cell structure to add or remove cells. """ - data: NotRequired[List["NotebookCell"]] + data: NotRequired[list["NotebookCell"]] """ Changes to notebook cells properties like its kind, execution summary or metadata. """ - textContent: NotRequired[ - List["__NotebookDocumentChangeEvent_cells_textContent_Type_1"] - ] + textContent: NotRequired[list["__NotebookDocumentChangeEvent_cells_textContent_Type_1"]] """ Changes to the text content of notebook cells. """ class __NotebookDocumentChangeEvent_cells_structure_Type_1(TypedDict): array: "NotebookCellArrayChange" """ The change to the cell array. """ - didOpen: NotRequired[List["TextDocumentItem"]] + didOpen: NotRequired[list["TextDocumentItem"]] """ Additional opened cell text documents. """ - didClose: NotRequired[List["TextDocumentIdentifier"]] + didClose: NotRequired[list["TextDocumentIdentifier"]] """ Additional closed cell text documents. """ class __NotebookDocumentChangeEvent_cells_textContent_Type_1(TypedDict): document: "VersionedTextDocumentIdentifier" - changes: List["TextDocumentContentChangeEvent"] + changes: list["TextDocumentContentChangeEvent"] class __NotebookDocumentFilter_Type_1(TypedDict): @@ -5694,9 +5753,7 @@ class __NotebookDocumentSyncOptions_notebookSelector_Type_1(TypedDict): """ The notebook to be synced If a string value is provided it matches against the notebook type. '*' matches every notebook. """ - cells: NotRequired[ - List["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_1"] - ] + cells: NotRequired[list["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_1"]] """ The cells of the matching notebook to be synced. """ @@ -5705,7 +5762,7 @@ class __NotebookDocumentSyncOptions_notebookSelector_Type_2(TypedDict): """ The notebook to be synced If a string value is provided it matches against the notebook type. '*' matches every notebook. """ - cells: List["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_2"] + cells: list["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_2"] """ The cells of the matching notebook to be synced. """ @@ -5714,9 +5771,7 @@ class __NotebookDocumentSyncOptions_notebookSelector_Type_3(TypedDict): """ The notebook to be synced If a string value is provided it matches against the notebook type. '*' matches every notebook. """ - cells: NotRequired[ - List["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_3"] - ] + cells: NotRequired[list["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_3"]] """ The cells of the matching notebook to be synced. """ @@ -5725,7 +5780,7 @@ class __NotebookDocumentSyncOptions_notebookSelector_Type_4(TypedDict): """ The notebook to be synced If a string value is provided it matches against the notebook type. '*' matches every notebook. """ - cells: List["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_4"] + cells: list["__NotebookDocumentSyncOptions_notebookSelector_cells_Type_4"] """ The cells of the matching notebook to be synced. """ @@ -5755,17 +5810,15 @@ class __PrepareRenameResult_Type_2(TypedDict): class __PublishDiagnosticsClientCapabilities_tagSupport_Type_1(TypedDict): - valueSet: List["DiagnosticTag"] + valueSet: list["DiagnosticTag"] """ The tags supported by the client. """ class __SemanticTokensClientCapabilities_requests_Type_1(TypedDict): - range: NotRequired[Union[bool, dict]] + range: NotRequired[bool | dict] """ The client will send the `textDocument/semanticTokens/range` request if the server provides a corresponding handler. """ - full: NotRequired[ - Union[bool, "__SemanticTokensClientCapabilities_requests_full_Type_1"] - ] + full: NotRequired[Union[bool, "__SemanticTokensClientCapabilities_requests_full_Type_1"]] """ The client will send the `textDocument/semanticTokens/full` request if the server provides a corresponding handler. """ @@ -5805,12 +5858,10 @@ class __ShowMessageRequestClientCapabilities_messageActionItem_Type_1(TypedDict) class __SignatureHelpClientCapabilities_signatureInformation_Type_1(TypedDict): - documentationFormat: NotRequired[List["MarkupKind"]] + documentationFormat: NotRequired[list["MarkupKind"]] """ Client supports the following content formats for the documentation property. The order describes the preferred format of the client. """ - parameterInformation: NotRequired[ - "__SignatureHelpClientCapabilities_signatureInformation_parameterInformation_Type_1" - ] + parameterInformation: NotRequired["__SignatureHelpClientCapabilities_signatureInformation_parameterInformation_Type_1"] """ Client capabilities specific to parameter information. """ activeParameterSupport: NotRequired[bool] """ The client supports the `activeParameter` property on `SignatureInformation` @@ -5819,9 +5870,7 @@ class __SignatureHelpClientCapabilities_signatureInformation_Type_1(TypedDict): @since 3.16.0 """ -class __SignatureHelpClientCapabilities_signatureInformation_parameterInformation_Type_1( - TypedDict -): +class __SignatureHelpClientCapabilities_signatureInformation_parameterInformation_Type_1(TypedDict): labelOffsetSupport: NotRequired[bool] """ The client supports processing label offsets instead of a simple label string. @@ -5880,13 +5929,13 @@ class __WorkspaceEditClientCapabilities_changeAnnotationSupport_Type_1(TypedDict class __WorkspaceSymbolClientCapabilities_resolveSupport_Type_1(TypedDict): - properties: List[str] + properties: list[str] """ The properties that a client can resolve lazily. Usually `location.range` """ class __WorkspaceSymbolClientCapabilities_symbolKind_Type_1(TypedDict): - valueSet: NotRequired[List["SymbolKind"]] + valueSet: NotRequired[list["SymbolKind"]] """ The symbol kind values the client supports. When this property exists the client also guarantees that it will handle values outside its set gracefully and falls back @@ -5898,7 +5947,7 @@ class __WorkspaceSymbolClientCapabilities_symbolKind_Type_1(TypedDict): class __WorkspaceSymbolClientCapabilities_tagSupport_Type_1(TypedDict): - valueSet: List["SymbolTag"] + valueSet: list["SymbolTag"] """ The tags supported by the client. """ diff --git a/src/solidlsp/lsp_protocol_handler/server.py b/src/solidlsp/lsp_protocol_handler/server.py index a224143..c628d43 100644 --- a/src/solidlsp/lsp_protocol_handler/server.py +++ b/src/solidlsp/lsp_protocol_handler/server.py @@ -1,8 +1,8 @@ """ -This file provides the implementation of the JSON-RPC client, that launches and +This file provides the implementation of the JSON-RPC client, that launches and communicates with the language server. -The initial implementation of this file was obtained from +The initial implementation of this file was obtained from https://github.com/predragnikolic/OLSP under the MIT License with the following terms: MIT License @@ -34,16 +34,17 @@ import json import logging import os import threading +from collections.abc import Callable +from typing import Any, Union import psutil -from typing import Any, Callable, Dict, List, Optional, Union +from ..multilspy_exceptions import MultilspyException from .lsp_requests import LspNotification, LspRequest from .lsp_types import ErrorCodes -from ..multilspy_exceptions import MultilspyException -StringDict = Dict[str, Any] -PayloadLike = Union[List[StringDict], StringDict, None] +StringDict = dict[str, Any] +PayloadLike = Union[list[StringDict], StringDict, None] CONTENT_LENGTH = "Content-Length: " ENCODING = "utf-8" log = logging.getLogger(__name__) @@ -59,7 +60,7 @@ class ProcessLaunchInfo: cmd: str # The environment variables to set for the process - env: Dict[str, str] = dataclasses.field(default_factory=dict) + env: dict[str, str] = dataclasses.field(default_factory=dict) # The working directory for the process cwd: str = os.getcwd() @@ -120,8 +121,8 @@ class MessageType: class Request: def __init__(self) -> None: self.cv = asyncio.Condition() - self.result: Optional[PayloadLike] = None - self.error: Optional[Error] = None + self.result: PayloadLike | None = None + self.error: Error | None = None async def on_result(self, params: PayloadLike) -> None: self.result = params @@ -134,14 +135,14 @@ class Request: self.cv.notify() -def content_length(line: bytes) -> Optional[int]: +def content_length(line: bytes) -> int | None: if line.startswith(b"Content-Length: "): _, value = line.split(b"Content-Length: ") value = value.strip() try: return int(value) except ValueError: - raise ValueError("Invalid Content-Length header: {}".format(value)) + raise ValueError(f"Invalid Content-Length header: {value}") return None @@ -181,12 +182,13 @@ class LanguageServerHandler: language server process in an independent process group. Default is `True`. Setting it to `False` means that the language server process will be in the same process group as the the current process, and any SIGINT and SIGTERM signals will be sent to both processes. + """ def __init__( self, process_launch_info: ProcessLaunchInfo, - logger: Optional[Callable[[str, str, StringDict | str], None]] = None, + logger: Callable[[str, str, StringDict | str], None] | None = None, start_independent_lsp_process=True, ) -> None: """ @@ -203,7 +205,7 @@ class LanguageServerHandler: self._received_shutdown = False self.request_id = 1 - self._response_handlers: Dict[Any, Request] = {} + self._response_handlers: dict[Any, Request] = {} self.on_request_handlers = {} self.on_notification_handlers = {} self.logger = logger @@ -218,8 +220,6 @@ class LanguageServerHandler: self._response_handlers_lock = threading.Lock() self._tasks_lock = threading.Lock() - - def is_running(self) -> bool: """ Checks if the language server process is currently running. @@ -250,7 +250,7 @@ class LanguageServerHandler: log.error("Language server has already terminated/could not be started") # Process has already terminated stderr_data = await self.process.stderr.read() - error_message = stderr_data.decode('utf-8', errors='replace') + error_message = stderr_data.decode("utf-8", errors="replace") raise RuntimeError(f"Process terminated immediately with code {self.process.returncode}. Error: {error_message}") self.loop = asyncio.get_event_loop() @@ -262,21 +262,19 @@ class LanguageServerHandler: self.tasks[self.task_counter] = self.loop.create_task(self.run_forever_stderr()) self.task_counter += 1 - - async def stop(self) -> None: """ Sends the terminate signal to the language server process and waits for it to exit, with a timeout, killing it if necessary """ # First cancel all tasks await self._cancel_pending_tasks() - + process = self.process self.process = None - + if not process: return - + # Clean up the process await self._cleanup_process(process) @@ -294,7 +292,7 @@ class LanguageServerHandler: if pending_tasks: try: await asyncio.wait_for(asyncio.gather(*pending_tasks, return_exceptions=True), timeout=5.0) - except (asyncio.TimeoutError, Exception): + except (TimeoutError, Exception): pass # Clear tasks dictionary under lock @@ -306,18 +304,18 @@ class LanguageServerHandler: # Close stdin first to prevent deadlocks # See: https://bugs.python.org/issue35539 self._safely_close_pipe(process.stdin) - + # Terminate/kill the process if it's still running if process.returncode is None: await self._terminate_or_kill_process(process) - + # Close stdout and stderr pipes after process has exited # This is essential to prevent "I/O operation on closed pipe" errors and # "Event loop is closed" errors during garbage collection # See: https://bugs.python.org/issue41320 and https://github.com/python/cpython/issues/88050 self._safely_close_pipe(process.stdout) self._safely_close_pipe(process.stderr) - + # Small delay to ensure OS has released file handles await asyncio.sleep(0.5) @@ -333,11 +331,11 @@ class LanguageServerHandler: """Try to terminate the process gracefully, then forcefully if necessary.""" # First try to terminate the process tree gracefully self._signal_process_tree(process, terminate=True) - + # Wait for the process to exit (with timeout) try: await asyncio.wait_for(process.wait(), timeout=10) - except (asyncio.TimeoutError, Exception): + except (TimeoutError, Exception): # If termination failed, forcefully kill the process tree self._signal_process_tree(process, terminate=False) try: @@ -349,14 +347,14 @@ class LanguageServerHandler: def _signal_process_tree(self, process, terminate=True): """Send signal (terminate or kill) to the process and all its children.""" signal_method = "terminate" if terminate else "kill" - + # Try to get the parent process parent = None try: parent = psutil.Process(process.pid) except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): pass - + # If we have the parent process and it's running, signal the entire tree if parent and parent.is_running(): # Signal children first @@ -365,7 +363,7 @@ class LanguageServerHandler: getattr(child, signal_method)() except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): pass - + # Then signal the parent try: getattr(parent, signal_method)() @@ -378,7 +376,6 @@ class LanguageServerHandler: except Exception: pass - async def shutdown(self) -> None: """ Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit @@ -433,7 +430,6 @@ class LanguageServerHandler: pass return self._received_shutdown - async def run_forever_stderr(self) -> None: """ Continuously read from the language server process stderr and log the messages @@ -443,7 +439,7 @@ class LanguageServerHandler: line = await self.process.stderr.readline() if not line: continue - self._log("LSP stderr: " + line.decode(ENCODING, errors='replace')) + self._log("LSP stderr: " + line.decode(ENCODING, errors="replace")) except (BrokenPipeError, ConnectionResetError, StopLoopException): pass @@ -453,7 +449,7 @@ class LanguageServerHandler: """ try: await self._receive_payload(json.loads(body)) - except IOError as ex: + except OSError as ex: self._log(f"malformed {ENCODING}: {ex}") except UnicodeDecodeError as ex: self._log(f"malformed {ENCODING}: {ex}") @@ -479,7 +475,7 @@ class LanguageServerHandler: except Exception as err: self._log(f"Error handling server payload: {err}") - def send_notification(self, method: str, params: Optional[dict] = None) -> None: + def send_notification(self, method: str, params: dict | None = None) -> None: """ Send notification pertaining to the given method to the server with the given parameters """ @@ -491,25 +487,19 @@ class LanguageServerHandler: """ # Use lock to prevent race conditions on tasks and task_counter with self._tasks_lock: - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( - self._send_payload(make_response(request_id, params)) - ) + self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._send_payload(make_response(request_id, params))) self.task_counter += 1 - def send_error_response(self, request_id: Any, err: Error) -> None: """ Send error response to the given request id to the server with the given error """ # Use lock to prevent race conditions on tasks and task_counter with self._tasks_lock: - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task( - self._send_payload(make_error_response(request_id, err)) - ) + self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._send_payload(make_error_response(request_id, err))) self.task_counter += 1 - - async def send_request(self, method: str, params: Optional[dict] = None) -> PayloadLike: + async def send_request(self, method: str, params: dict | None = None) -> PayloadLike: """ Send request to the server, register the request id, and wait for the response """ @@ -527,13 +517,14 @@ class LanguageServerHandler: await self._send_payload(make_request(method, request_id, params)) self._log(f"Waiting for asyncio condition for request {method} with params:\n{params}") await request.cv.wait() - self._log(f"Finished waiting, processing result") + self._log("Finished waiting, processing result") if isinstance(request.error, Error): - raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {request.error}") from request.error + raise MultilspyException( + f"Could not process request {method} with params:\n{params}.\n Language server error: {request.error}" + ) from request.error self._log(f"Returning non-error result, which is:\n{request.result}") return request.result - def _send_payload_sync(self, payload: StringDict) -> None: """ Send the payload to the server by writing to its stdin synchronously @@ -554,7 +545,6 @@ class LanguageServerHandler: self.logger("client", "logger", f"Failed to write to stdin: {e}") return - async def _send_payload(self, payload: StringDict) -> None: """ Send the payload to the server by writing to its stdin asynchronously. @@ -575,7 +565,6 @@ class LanguageServerHandler: self.logger("client", "logger", f"Failed to write to stdin: {e}") return - def on_request(self, method: str, cb) -> None: """ Register the callback function to handle requests from the server to the client for the given method @@ -602,7 +591,6 @@ class LanguageServerHandler: else: await request.on_error(Error(ErrorCodes.InvalidRequest, "")) - async def _request_handler(self, response: StringDict) -> None: """ Handle the request received from the server: call the appropriate callback function and return the result @@ -616,7 +604,7 @@ class LanguageServerHandler: request_id, Error( ErrorCodes.MethodNotFound, - "method '{}' not handled on client.".format(method), + f"method '{method}' not handled on client.", ), ) return diff --git a/src/solidlsp/multilspy_config.py b/src/solidlsp/multilspy_config.py index c1d77ae..f676e52 100644 --- a/src/solidlsp/multilspy_config.py +++ b/src/solidlsp/multilspy_config.py @@ -1,10 +1,10 @@ """ Configuration parameters for Multilspy. """ + import fnmatch -from enum import Enum -from typing import List from dataclasses import dataclass, field +from enum import Enum class FilenameMatcher: @@ -77,6 +77,7 @@ class MultilspyConfig: """ Configuration parameters """ + code_language: Language trace_lsp_communication: bool = False start_independent_lsp_process: bool = True @@ -89,7 +90,5 @@ class MultilspyConfig: Create a MultilspyConfig instance from a dictionary """ import inspect - return cls(**{ - k: v for k, v in env.items() - if k in inspect.signature(cls).parameters - }) + + return cls(**{k: v for k, v in env.items() if k in inspect.signature(cls).parameters}) diff --git a/src/solidlsp/multilspy_exceptions.py b/src/solidlsp/multilspy_exceptions.py index d645d13..1dc12ed 100644 --- a/src/solidlsp/multilspy_exceptions.py +++ b/src/solidlsp/multilspy_exceptions.py @@ -2,6 +2,7 @@ This module contains the exceptions raised by the Multilspy framework. """ + class MultilspyException(Exception): """ Exceptions raised by the Multilspy framework. @@ -11,4 +12,4 @@ class MultilspyException(Exception): """ Initializes the exception with the given message. """ - super().__init__(message) \ No newline at end of file + super().__init__(message) diff --git a/src/solidlsp/multilspy_logger.py b/src/solidlsp/multilspy_logger.py index c6135a5..993aa3b 100644 --- a/src/solidlsp/multilspy_logger.py +++ b/src/solidlsp/multilspy_logger.py @@ -1,9 +1,11 @@ """ Multilspy logger module. """ + import inspect import logging from datetime import datetime + from pydantic import BaseModel @@ -34,7 +36,6 @@ class MultilspyLogger: """ Log the debug and sanitized messages using the logger """ - debug_message = debug_message.replace("'", '"').replace("\n", " ") sanitized_error_message = sanitized_error_message.replace("'", '"').replace("\n", " ") diff --git a/src/solidlsp/multilspy_settings.py b/src/solidlsp/multilspy_settings.py index 5d7ebfa..b404030 100644 --- a/src/solidlsp/multilspy_settings.py +++ b/src/solidlsp/multilspy_settings.py @@ -5,10 +5,12 @@ Defines the settings for multilspy. import os import pathlib + class MultilspySettings: """ Provides the various settings for multilspy. """ + @staticmethod def get_language_server_directory() -> str: """Returns the directory for language servers""" diff --git a/src/solidlsp/multilspy_types.py b/src/solidlsp/multilspy_types.py index dad0116..3d4d465 100644 --- a/src/solidlsp/multilspy_types.py +++ b/src/solidlsp/multilspy_types.py @@ -4,16 +4,19 @@ Defines wrapper objects around the types returned by LSP to ensure decoupling be from __future__ import annotations -from enum import IntEnum, Enum -from typing_extensions import NotRequired, TypedDict, List, Dict, Union +from enum import Enum, IntEnum +from typing import NotRequired, Union + +from typing_extensions import TypedDict URI = str DocumentUri = str Uint = int RegExp = str + class Position(TypedDict): - """Position in a text document expressed as zero-based line and character + r"""Position in a text document expressed as zero-based line and character offset. Prior to 3.17 the offsets were always based on a UTF-16 string representation. So a string of the form `a𐐀b` the character offset of the character `a` is 0, the character offset of `𐐀` is 1 and the character @@ -39,7 +42,8 @@ class Position(TypedDict): Positions are line end character agnostic. So you can not specify a position that denotes `\r|\n` or `\n|` where `|` represents the character offset. - @since 3.17.0 - support for negotiated position encoding.""" + @since 3.17.0 - support for negotiated position encoding. + """ line: Uint """ Line position in a document (zero-based). @@ -67,7 +71,8 @@ class Range(TypedDict): start: { line: 5, character: 23 } end : { line 6, character : 0 } } - ```""" + ``` + """ start: Position """ The range's start position. """ @@ -77,12 +82,14 @@ class Range(TypedDict): class Location(TypedDict): """Represents a location inside a resource, such as a line - inside a text file.""" + inside a text file. + """ uri: DocumentUri range: Range absolutePath: str - relativePath: Union[str, None] + relativePath: str | None + class CompletionItemKind(IntEnum): """The kind of a completion entry.""" @@ -113,9 +120,11 @@ class CompletionItemKind(IntEnum): Operator = 24 TypeParameter = 25 + class CompletionItem(TypedDict): """A completion item represents a text snippet that is - proposed to complete text that is being typed.""" + proposed to complete text that is being typed. + """ completionText: str """ The completionText of this completion item. @@ -131,6 +140,7 @@ class CompletionItem(TypedDict): """ A human-readable string with additional information about this item, like type or symbol information. """ + class SymbolKind(IntEnum): """A symbol kind.""" @@ -161,17 +171,21 @@ class SymbolKind(IntEnum): Operator = 25 TypeParameter = 26 + class SymbolTag(IntEnum): """Symbol tags are extra annotations that tweak the rendering of a symbol. - @since 3.16""" + @since 3.16 + """ Deprecated = 1 """ Render a symbol as obsolete, usually using a strike-out. """ + class UnifiedSymbolInformation(TypedDict): """Represents information about programming constructs like variables, classes, - interfaces etc.""" + interfaces etc. + """ deprecated: NotRequired[bool] """ Indicates if this symbol is deprecated. @@ -191,7 +205,7 @@ class UnifiedSymbolInformation(TypedDict): """ The name of this symbol. """ kind: SymbolKind """ The kind of this symbol. """ - tags: NotRequired[List[SymbolTag]] + tags: NotRequired[list[SymbolTag]] """ Tags for this symbol. @since 3.16.0 """ @@ -207,7 +221,7 @@ class UnifiedSymbolInformation(TypedDict): detail: NotRequired[str] """ More detail for this symbol, e.g the signature of a function. """ - + range: NotRequired[Range] """ The range enclosing this symbol not including leading/trailing whitespace but everything else like comments. This information is typically used to determine if the clients cursor is @@ -215,37 +229,40 @@ class UnifiedSymbolInformation(TypedDict): selectionRange: NotRequired[Range] """ The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. Must be contained by the `range`. """ - + body: NotRequired[str] """ The body of the symbol. """ - - children: List[UnifiedSymbolInformation] + + children: list[UnifiedSymbolInformation] """ The children of the symbol. Added to be compatible with `lsp_types.DocumentSymbol`, since it is sometimes useful to have the children of the symbol as a user-facing feature.""" - + parent: NotRequired[UnifiedSymbolInformation | None] """The parent of the symbol, if there is any. Added with Serena, not part of the LSP. All symbols except the root packages will have a parent. """ - + class MarkupKind(Enum): """Describes the content type that a client supports in various result literals like `Hover`, `ParameterInfo` or `CompletionItem`. Please note that `MarkupKinds` must not start with a `$`. This kinds - are reserved for internal usage.""" + are reserved for internal usage. + """ PlainText = "plaintext" """ Plain text is supported as a content format """ Markdown = "markdown" """ Markdown is supported as a content format """ + class __MarkedString_Type_1(TypedDict): language: str value: str + MarkedString = Union[str, "__MarkedString_Type_1"] """ MarkedString can be used to render human readable text. It is either a markdown string or a code-block that provides a language and a code snippet. The language identifier @@ -260,8 +277,9 @@ ${value} Note that markdown strings will be sanitized - that means html will be escaped. @deprecated use MarkupContent instead. """ + class MarkupContent(TypedDict): - """A `MarkupContent` literal represents a string value which content is interpreted base on its + r"""A `MarkupContent` literal represents a string value which content is interpreted base on its kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues. @@ -282,18 +300,20 @@ class MarkupContent(TypedDict): ``` *Please Note* that clients might sanitize the return markdown. A client could decide to - remove HTML from the markdown to avoid script execution.""" + remove HTML from the markdown to avoid script execution. + """ - kind: "MarkupKind" + kind: MarkupKind """ The type of the Markup """ value: str """ The content itself """ + class Hover(TypedDict): """The result of a hover request.""" - contents: Union["MarkupContent", "MarkedString", List["MarkedString"]] + contents: MarkupContent | MarkedString | list[MarkedString] """ The hover's content """ - range: NotRequired["Range"] + range: NotRequired[Range] """ An optional range inside the text document that is used to - visualize the hover, e.g. by changing the background color. """ \ No newline at end of file + visualize the hover, e.g. by changing the background color. """ diff --git a/src/solidlsp/multilspy_utils.py b/src/solidlsp/multilspy_utils.py index 92cca5f..e1b71c2 100644 --- a/src/solidlsp/multilspy_utils.py +++ b/src/solidlsp/multilspy_utils.py @@ -5,17 +5,16 @@ This file contains various utility functions like I/O operations, handling paths import gzip import logging import os -from typing import Tuple, Union -import requests -import shutil -import uuid - import platform +import shutil import subprocess +import uuid from enum import Enum +from pathlib import Path, PurePath + +import requests from solidlsp.multilspy_exceptions import MultilspyException -from pathlib import PurePath, Path from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_types import UnifiedSymbolInformation @@ -24,8 +23,9 @@ class TextUtils: """ Utilities for text operations. """ + @staticmethod - def get_line_col_from_index(text: str, index: int) -> Tuple[int, int]: + def get_line_col_from_index(text: str, index: int) -> tuple[int, int]: """ Returns the zero-indexed line and column number of the given index in the given text """ @@ -33,7 +33,7 @@ class TextUtils: c = 0 idx = 0 while idx < index: - if text[idx] == '\n': + if text[idx] == "\n": l += 1 c = 0 else: @@ -41,7 +41,7 @@ class TextUtils: idx += 1 return l, c - + @staticmethod def get_index_from_line_col(text: str, line: int, col: int) -> int: """ @@ -55,35 +55,35 @@ class TextUtils: idx += 1 idx += col return idx - + @staticmethod - def _get_updated_position_from_line_and_column_and_edit(l: int, c: int, text_to_be_inserted: str) -> Tuple[int, int]: + def _get_updated_position_from_line_and_column_and_edit(l: int, c: int, text_to_be_inserted: str) -> tuple[int, int]: """ Utility function to get the position of the cursor after inserting text at a given line and column. """ - num_newlines_in_gen_text = text_to_be_inserted.count('\n') + num_newlines_in_gen_text = text_to_be_inserted.count("\n") if num_newlines_in_gen_text > 0: l += num_newlines_in_gen_text - c = len(text_to_be_inserted.split('\n')[-1]) + c = len(text_to_be_inserted.split("\n")[-1]) else: c += len(text_to_be_inserted) return (l, c) - + @staticmethod - def delete_text_between_positions(text: str, start_line: int, start_col: int, end_line: int, end_col: int) -> Tuple[str, str]: + def delete_text_between_positions(text: str, start_line: int, start_col: int, end_line: int, end_col: int) -> tuple[str, str]: """ Deletes the text between the given start and end positions. Returns the modified text and the deleted text. """ del_start_idx = TextUtils.get_index_from_line_col(text, start_line, start_col) del_end_idx = TextUtils.get_index_from_line_col(text, end_line, end_col) - + deleted_text = text[del_start_idx:del_end_idx] new_text = text[:del_start_idx] + text[del_end_idx:] return new_text, deleted_text - + @staticmethod - def insert_text_at_position(text: str, line: int, col: int, text_to_be_inserted: str) -> Tuple[str, int, int]: + def insert_text_at_position(text: str, line: int, col: int, text_to_be_inserted: str) -> tuple[str, int, int]: """ Inserts the given text at the given line and column. Returns the modified text and the new line and column. @@ -98,6 +98,7 @@ class PathUtils: """ Utilities for platform-agnostic path operations. """ + @staticmethod def uri_to_path(uri: str) -> str: """ @@ -106,14 +107,15 @@ class PathUtils: This method was obtained from https://stackoverflow.com/a/61922504 """ try: - from urllib.parse import urlparse, unquote + from urllib.parse import unquote, urlparse from urllib.request import url2pathname except ImportError: # backwards compatibility - from urlparse import urlparse from urllib import unquote, url2pathname + + from urlparse import urlparse parsed = urlparse(uri) - host = "{0}{0}{mnt}{0}".format(os.path.sep, mnt=parsed.netloc) + host = f"{os.path.sep}{os.path.sep}{parsed.netloc}{os.path.sep}" return os.path.normpath(os.path.join(host, url2pathname(unquote(parsed.path)))) @staticmethod @@ -126,10 +128,10 @@ class PathUtils: @staticmethod def is_glob_pattern(pattern: str) -> bool: """Check if a pattern contains glob-specific characters.""" - return any(c in pattern for c in '*?[]!') + return any(c in pattern for c in "*?[]!") @staticmethod - def get_relative_path(path: str, base_path: str) -> Union[str, None]: + def get_relative_path(path: str, base_path: str) -> str | None: """ Gets relative path if it's possible (paths should be on the same drive), returns `None` otherwise. @@ -153,12 +155,12 @@ class FileUtils: logger.log(f"File read '{file_path}' failed: File does not exist.", logging.ERROR) raise MultilspyException(f"File read '{file_path}' failed: File does not exist.") try: - with open(file_path, "r", encoding="utf-8") as inp_file: + with open(file_path, encoding="utf-8") as inp_file: return inp_file.read() except Exception as exc: logger.log(f"File read '{file_path}' failed to read with encoding 'utf-8': {exc}", logging.ERROR) raise MultilspyException("File read failed.") from None - + @staticmethod def download_file(logger: MultilspyLogger, url: str, target_path: str) -> None: """ @@ -215,6 +217,7 @@ class PlatformId(str, Enum): """ multilspy supported platforms """ + WIN_x86 = "win-x86" WIN_x64 = "win-x64" WIN_arm64 = "win-arm64" @@ -232,6 +235,7 @@ class DotnetVersion(str, Enum): """ multilspy supported dotnet versions """ + V4 = "4" V6 = "6" V7 = "7" @@ -260,7 +264,7 @@ class PlatformUtils: platform_id = system_map[system] + "-" + machine_map[machine] if system == "Linux" and bitness == "64bit": libc = platform.libc_ver()[0] - if libc != 'glibc': + if libc != "glibc": platform_id += "-" + libc return PlatformId(platform_id) else: @@ -274,13 +278,13 @@ class PlatformUtils: class SYSTEM_INFO(ctypes.Structure): class _U(ctypes.Union): class _S(ctypes.Structure): - _fields_ = [("wProcessorArchitecture", wintypes.WORD), - ("wReserved", wintypes.WORD)] - _fields_ = [("dwOemId", wintypes.DWORD), - ("s", _S)] + _fields_ = [("wProcessorArchitecture", wintypes.WORD), ("wReserved", wintypes.WORD)] + + _fields_ = [("dwOemId", wintypes.DWORD), ("s", _S)] _anonymous_ = ("s",) - _fields_ = [("u", _U), + _fields_ = [ + ("u", _U), ("dwPageSize", wintypes.DWORD), ("lpMinimumApplicationAddress", wintypes.LPVOID), ("lpMaximumApplicationAddress", wintypes.LPVOID), @@ -289,22 +293,22 @@ class PlatformUtils: ("dwProcessorType", wintypes.DWORD), ("dwAllocationGranularity", wintypes.DWORD), ("wProcessorLevel", wintypes.WORD), - ("wProcessorRevision", wintypes.WORD)] + ("wProcessorRevision", wintypes.WORD), + ] _anonymous_ = ("u",) sys_info = SYSTEM_INFO() ctypes.windll.kernel32.GetNativeSystemInfo(ctypes.byref(sys_info)) arch_map = { - 9: 'AMD64', - 5: 'ARM', - 12: 'arm64', - 6: 'Intel Itanium-based', - 0: 'i386', + 9: "AMD64", + 5: "ARM", + 12: "arm64", + 6: "Intel Itanium-based", + 0: "i386", } - return arch_map.get(sys_info.wProcessorArchitecture, f'Unknown ({sys_info.wProcessorArchitecture})') - + return arch_map.get(sys_info.wProcessorArchitecture, f"Unknown ({sys_info.wProcessorArchitecture})") @staticmethod def get_dotnet_version() -> DotnetVersion: @@ -314,14 +318,14 @@ class PlatformUtils: try: result = subprocess.run(["dotnet", "--list-runtimes"], capture_output=True, check=True) available_version_cmd_output = [] - for line in result.stdout.decode('utf-8').split('\n'): - if line.startswith('Microsoft.NETCore.App'): - version_cmd_output = line.split(' ')[1] + for line in result.stdout.decode("utf-8").split("\n"): + if line.startswith("Microsoft.NETCore.App"): + version_cmd_output = line.split(" ")[1] available_version_cmd_output.append(version_cmd_output) - + if not available_version_cmd_output: raise MultilspyException("dotnet not found on the system") - + # Check for supported versions in order of preference (latest first) for version_cmd_output in available_version_cmd_output: if version_cmd_output.startswith("8"): @@ -335,9 +339,11 @@ class PlatformUtils: for version_cmd_output in available_version_cmd_output: if version_cmd_output.startswith("4"): return DotnetVersion.V4 - + # If no supported version found, raise exception with all available versions - raise MultilspyException(f"No supported dotnet version found. Available versions: {', '.join(available_version_cmd_output)}. Supported versions: 4, 6, 7, 8") + raise MultilspyException( + f"No supported dotnet version found. Available versions: {', '.join(available_version_cmd_output)}. Supported versions: 4, 6, 7, 8" + ) except (FileNotFoundError, subprocess.CalledProcessError): try: result = subprocess.run(["mono", "--version"], capture_output=True, check=True) @@ -346,7 +352,6 @@ class PlatformUtils: raise MultilspyException("dotnet or mono not found on the system") - class SymbolUtils: @staticmethod def symbol_tree_contains_name(roots: list[UnifiedSymbolInformation], name: str) -> bool: diff --git a/src/solidlsp/type_helpers.py b/src/solidlsp/type_helpers.py index 0a805e4..53a10de 100644 --- a/src/solidlsp/type_helpers.py +++ b/src/solidlsp/type_helpers.py @@ -3,14 +3,15 @@ This module provides type-helpers used across multilspy implementation """ import inspect - -from typing import Callable, TypeVar, Type +from collections.abc import Callable +from typing import TypeVar R = TypeVar("R", bound=object) + def ensure_all_methods_implemented( - source_cls: Type[object], -) -> Callable[[Type[R]], Type[R]]: + source_cls: type[object], +) -> Callable[[type[R]], type[R]]: """ A decorator to ensure that all methods of source_cls class are implemented in the decorated class. """ @@ -24,4 +25,4 @@ def ensure_all_methods_implemented( return target_cls - return check_all_methods_implemented \ No newline at end of file + return check_all_methods_implemented diff --git a/test/conftest.py b/test/conftest.py index 9630cc6..0eeda4a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -6,11 +6,11 @@ import pytest from sensai.util.logging import configure from multilspy.language_server import SyncLanguageServer -from solidlsp.multilspy_config import Language, MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger from serena.constants import USE_SOLID_LSP from serena.util.file_system import GitignoreParser from solidlsp.ls import SolidLanguageServer +from solidlsp.multilspy_config import Language, MultilspyConfig +from solidlsp.multilspy_logger import MultilspyLogger configure(level=logging.DEBUG) diff --git a/test/multilspy/python/test_python_basic.py b/test/multilspy/python/test_python_basic.py index 20ddabb..a3edbea 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/multilspy/python/test_python_basic.py @@ -10,8 +10,8 @@ import os import pytest from multilspy.language_server import SyncLanguageServer -from solidlsp.multilspy_config import Language from serena.text_utils import LineType +from solidlsp.multilspy_config import Language @pytest.mark.python diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 1642b38..a31ccd4 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -5,9 +5,9 @@ from dataclasses import dataclass import pytest -from solidlsp.multilspy_config import Language from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase from serena.process_isolated_agent import ProcessIsolatedSerenaAgent +from solidlsp.multilspy_config import Language from test.conftest import get_repo_path diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index ba00ad1..bf6e2a3 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -11,8 +11,8 @@ from typing import Literal import pytest -from solidlsp.multilspy_config import Language from serena.symbol import CodeDiff +from solidlsp.multilspy_config import Language from src.serena.symbol import SymbolManager from test.conftest import create_ls, get_repo_path From 541998e912f3657b7828c3e87b5fe118bb0ebafe Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 16:38:11 +0200 Subject: [PATCH 141/174] Move definitions from multilspy.language_server to solidlsp --- src/multilspy/language_server.py | 38 ---------------------------- src/solidlsp/ls.py | 43 ++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index b584b89..e6faf7a 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -51,44 +51,6 @@ from serena.text_utils import MatchedConsecutiveLines, search_files -GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, multilspy_types.UnifiedSymbolInformation] - -@dataclasses.dataclass(kw_only=True) -class ReferenceInSymbol: - """A symbol retrieved when requesting reference to a symbol, together with the location of the reference""" - symbol: multilspy_types.UnifiedSymbolInformation - line: int - character: int - -@dataclasses.dataclass -class LSPFileBuffer: - """ - This class is used to store the contents of an open LSP file in memory. - """ - - # uri of the file - uri: str - - # The contents of the file - contents: str - - # The version of the file - version: int - - # The language id of the file - language_id: str - - # reference count of the file - ref_count: int - - # --------------------------------- MODIFICATIONS BY MISCHA --------------------------------- - - content_hash: str = "" - - def __post_init__(self): - self.content_hash = hashlib.md5(self.contents.encode('utf-8')).hexdigest() - - class LanguageServer: """ The LanguageServer class provides a language agnostic interface to the Language Server Protocol. diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index cc792db..610c60e 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1,3 +1,5 @@ +import dataclasses +import hashlib import json import logging import os @@ -11,12 +13,11 @@ from collections.abc import Iterator from contextlib import contextmanager from copy import copy from pathlib import Path, PurePath -from typing import Self, cast +from typing import Self, Union, cast import pathspec import tqdm -from multilspy.language_server import GenericDocumentSymbol, LSPFileBuffer, ReferenceInSymbol from serena.text_utils import MatchedConsecutiveLines, search_files from solidlsp import multilspy_types from solidlsp.ls_handler import SolidLanguageServerHandler @@ -34,6 +35,44 @@ from solidlsp.multilspy_exceptions import MultilspyException from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_utils import FileUtils, PathUtils, TextUtils +GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, multilspy_types.UnifiedSymbolInformation] + + +@dataclasses.dataclass(kw_only=True) +class ReferenceInSymbol: + """A symbol retrieved when requesting reference to a symbol, together with the location of the reference""" + + symbol: multilspy_types.UnifiedSymbolInformation + line: int + character: int + + +@dataclasses.dataclass +class LSPFileBuffer: + """ + This class is used to store the contents of an open LSP file in memory. + """ + + # uri of the file + uri: str + + # The contents of the file + contents: str + + # The version of the file + version: int + + # The language id of the file + language_id: str + + # reference count of the file + ref_count: int + + content_hash: str = "" + + def __post_init__(self): + self.content_hash = hashlib.md5(self.contents.encode("utf-8")).hexdigest() + class SolidLanguageServer(ABC): """ From 1bdc0240401ab07dd6b7ac8b0641c0d2e8bb8e61 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 16:57:31 +0200 Subject: [PATCH 142/174] Change imports to use SolidLanguageServer instead of SyncLanguageServer --- src/serena/agent.py | 2 +- src/serena/symbol.py | 4 ++-- src/solidlsp/__init__.py | 1 + test/conftest.py | 2 +- test/multilspy/go/test_go_basic.py | 2 +- test/multilspy/java/test_java_basic.py | 2 +- test/multilspy/php/test_php_basic.py | 2 +- test/multilspy/python/test_python_basic.py | 2 +- test/multilspy/python/test_retrieval_with_ignored_dirs.py | 2 +- test/multilspy/python/test_symbol_retrieval.py | 2 +- test/multilspy/rust/test_rust_basic.py | 2 +- test/multilspy/typescript/test_typescript_basic.py | 2 +- 12 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 src/solidlsp/__init__.py diff --git a/src/serena/agent.py b/src/serena/agent.py index 97bf63e..1ac3ffb 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -33,7 +33,7 @@ from sensai.util import logging from sensai.util.logging import LOG_DEFAULT_FORMAT, FallbackHandler from sensai.util.string import ToStringMixin, dict_string -from multilspy import SyncLanguageServer +from solidlsp import SolidLanguageServer as SyncLanguageServer from serena import serena_version from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import ( diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 334671d..ec94db2 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -9,8 +9,8 @@ from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Self, Union from sensai.util.string import ToStringMixin -from multilspy import SyncLanguageServer -from multilspy.language_server import ReferenceInSymbol as LSPReferenceInSymbol +from solidlsp import SolidLanguageServer as SyncLanguageServer +from solidlsp.ls import ReferenceInSymbol as LSPReferenceInSymbol from solidlsp.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation if TYPE_CHECKING: diff --git a/src/solidlsp/__init__.py b/src/solidlsp/__init__.py new file mode 100644 index 0000000..fbd65b1 --- /dev/null +++ b/src/solidlsp/__init__.py @@ -0,0 +1 @@ +from .ls import SolidLanguageServer diff --git a/test/conftest.py b/test/conftest.py index 0eeda4a..8e086cf 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,7 +5,7 @@ from typing import cast import pytest from sensai.util.logging import configure -from multilspy.language_server import SyncLanguageServer +from solidlsp import SolidLanguageServer as SyncLanguageServer from serena.constants import USE_SOLID_LSP from serena.util.file_system import GitignoreParser from solidlsp.ls import SolidLanguageServer diff --git a/test/multilspy/go/test_go_basic.py b/test/multilspy/go/test_go_basic.py index 535b3ad..e5d82de 100644 --- a/test/multilspy/go/test_go_basic.py +++ b/test/multilspy/go/test_go_basic.py @@ -2,7 +2,7 @@ import os import pytest -from multilspy import SyncLanguageServer +from solidlsp import SolidLanguageServer as SyncLanguageServer from solidlsp.multilspy_config import Language from solidlsp.multilspy_utils import SymbolUtils diff --git a/test/multilspy/java/test_java_basic.py b/test/multilspy/java/test_java_basic.py index 2403d12..679a7a8 100644 --- a/test/multilspy/java/test_java_basic.py +++ b/test/multilspy/java/test_java_basic.py @@ -2,7 +2,7 @@ import os import pytest -from multilspy import SyncLanguageServer +from solidlsp import SolidLanguageServer as SyncLanguageServer from solidlsp.multilspy_config import Language from solidlsp.multilspy_utils import SymbolUtils diff --git a/test/multilspy/php/test_php_basic.py b/test/multilspy/php/test_php_basic.py index 2bfed7b..ff4bf32 100644 --- a/test/multilspy/php/test_php_basic.py +++ b/test/multilspy/php/test_php_basic.py @@ -2,7 +2,7 @@ from pathlib import Path import pytest -from multilspy.language_server import SyncLanguageServer +from solidlsp import SolidLanguageServer as SyncLanguageServer from solidlsp.multilspy_config import Language diff --git a/test/multilspy/python/test_python_basic.py b/test/multilspy/python/test_python_basic.py index a3edbea..c5522e5 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/multilspy/python/test_python_basic.py @@ -9,7 +9,7 @@ import os import pytest -from multilspy.language_server import SyncLanguageServer +from solidlsp import SolidLanguageServer as SyncLanguageServer from serena.text_utils import LineType from solidlsp.multilspy_config import Language diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index f6b25c9..d0446f9 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest -from multilspy.language_server import SyncLanguageServer +from solidlsp import SolidLanguageServer as SyncLanguageServer from solidlsp.multilspy_config import Language from test.conftest import create_ls diff --git a/test/multilspy/python/test_symbol_retrieval.py b/test/multilspy/python/test_symbol_retrieval.py index 80229ef..5bca8f6 100644 --- a/test/multilspy/python/test_symbol_retrieval.py +++ b/test/multilspy/python/test_symbol_retrieval.py @@ -10,7 +10,7 @@ import os import pytest -from multilspy.language_server import SyncLanguageServer +from solidlsp import SolidLanguageServer as SyncLanguageServer from solidlsp.multilspy_config import Language from solidlsp.multilspy_types import SymbolKind diff --git a/test/multilspy/rust/test_rust_basic.py b/test/multilspy/rust/test_rust_basic.py index 20aa6e0..ffe1245 100644 --- a/test/multilspy/rust/test_rust_basic.py +++ b/test/multilspy/rust/test_rust_basic.py @@ -2,7 +2,7 @@ import os import pytest -from multilspy import SyncLanguageServer +from solidlsp import SolidLanguageServer as SyncLanguageServer from solidlsp.multilspy_config import Language from solidlsp.multilspy_utils import SymbolUtils diff --git a/test/multilspy/typescript/test_typescript_basic.py b/test/multilspy/typescript/test_typescript_basic.py index fca1e54..e135f88 100644 --- a/test/multilspy/typescript/test_typescript_basic.py +++ b/test/multilspy/typescript/test_typescript_basic.py @@ -2,7 +2,7 @@ import os import pytest -from multilspy import SyncLanguageServer +from solidlsp import SolidLanguageServer as SyncLanguageServer from solidlsp.multilspy_config import Language from solidlsp.multilspy_utils import SymbolUtils From 605f8fba71cfd4c134ae568a45fdea5eb85241f0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 16:57:49 +0200 Subject: [PATCH 143/174] Remove multilspy package --- src/multilspy/.syncCommitId.remote | 1 - src/multilspy/.syncCommitId.this | 1 - src/multilspy/__init__.py | 8 - src/multilspy/language_server.py | 2311 ----------------- .../clangd_language_server.py | 190 -- .../initialize_params.json | 36 - .../runtime_dependencies.json | 29 - .../dart_language_server.py | 142 - .../initialize_params.json | 23 - .../runtime_dependencies.json | 45 - .../eclipse_jdtls/eclipse_jdtls.py | 422 --- .../eclipse_jdtls/initialize_params.json | 849 ------ .../eclipse_jdtls/runtime_dependencies.json | 72 - src/multilspy/language_servers/gopls/gopls.py | 151 -- .../gopls/initialize_params.json | 46 - .../intelephense/initialize_params.json | 36 - .../intelephense/intelephense.py | 200 -- .../intelephense/runtime_dependencies.json | 10 - .../initialize_params.json | 905 ------- .../jedi_language_server/jedi_server.py | 123 - .../initialize_params.json | 521 ---- .../kotlin_language_server.py | 226 -- .../runtime_dependencies.json | 41 - .../omnisharp/initialize_params.json | 631 ----- .../language_servers/omnisharp/omnisharp.py | 406 --- .../omnisharp/runtime_dependencies.json | 441 ---- .../workspace_did_change_configuration.json | 111 - .../pyright_language_server/pyright_server.py | 245 -- .../rust_analyzer/initialize_params.json | 917 ------- .../rust_analyzer/runtime_dependencies.json | 29 - .../rust_analyzer/rust_analyzer.py | 185 -- .../solargraph/initialize_params.json | 15 - .../solargraph/runtime_dependencies.json | 11 - .../language_servers/solargraph/solargraph.py | 187 -- .../initialize_params.json | 906 ------- .../runtime_dependencies.json | 16 - .../typescript_language_server.py | 245 -- 37 files changed, 10733 deletions(-) delete mode 100644 src/multilspy/.syncCommitId.remote delete mode 100644 src/multilspy/.syncCommitId.this delete mode 100644 src/multilspy/__init__.py delete mode 100644 src/multilspy/language_server.py delete mode 100644 src/multilspy/language_servers/clangd_language_server/clangd_language_server.py delete mode 100644 src/multilspy/language_servers/clangd_language_server/initialize_params.json delete mode 100644 src/multilspy/language_servers/clangd_language_server/runtime_dependencies.json delete mode 100644 src/multilspy/language_servers/dart_language_server/dart_language_server.py delete mode 100644 src/multilspy/language_servers/dart_language_server/initialize_params.json delete mode 100644 src/multilspy/language_servers/dart_language_server/runtime_dependencies.json delete mode 100644 src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py delete mode 100644 src/multilspy/language_servers/eclipse_jdtls/initialize_params.json delete mode 100644 src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json delete mode 100644 src/multilspy/language_servers/gopls/gopls.py delete mode 100644 src/multilspy/language_servers/gopls/initialize_params.json delete mode 100644 src/multilspy/language_servers/intelephense/initialize_params.json delete mode 100644 src/multilspy/language_servers/intelephense/intelephense.py delete mode 100644 src/multilspy/language_servers/intelephense/runtime_dependencies.json delete mode 100644 src/multilspy/language_servers/jedi_language_server/initialize_params.json delete mode 100644 src/multilspy/language_servers/jedi_language_server/jedi_server.py delete mode 100644 src/multilspy/language_servers/kotlin_language_server/initialize_params.json delete mode 100644 src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py delete mode 100644 src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json delete mode 100644 src/multilspy/language_servers/omnisharp/initialize_params.json delete mode 100644 src/multilspy/language_servers/omnisharp/omnisharp.py delete mode 100644 src/multilspy/language_servers/omnisharp/runtime_dependencies.json delete mode 100644 src/multilspy/language_servers/omnisharp/workspace_did_change_configuration.json delete mode 100644 src/multilspy/language_servers/pyright_language_server/pyright_server.py delete mode 100644 src/multilspy/language_servers/rust_analyzer/initialize_params.json delete mode 100644 src/multilspy/language_servers/rust_analyzer/runtime_dependencies.json delete mode 100644 src/multilspy/language_servers/rust_analyzer/rust_analyzer.py delete mode 100644 src/multilspy/language_servers/solargraph/initialize_params.json delete mode 100644 src/multilspy/language_servers/solargraph/runtime_dependencies.json delete mode 100644 src/multilspy/language_servers/solargraph/solargraph.py delete mode 100644 src/multilspy/language_servers/typescript_language_server/initialize_params.json delete mode 100644 src/multilspy/language_servers/typescript_language_server/runtime_dependencies.json delete mode 100644 src/multilspy/language_servers/typescript_language_server/typescript_language_server.py diff --git a/src/multilspy/.syncCommitId.remote b/src/multilspy/.syncCommitId.remote deleted file mode 100644 index 20a711e..0000000 --- a/src/multilspy/.syncCommitId.remote +++ /dev/null @@ -1 +0,0 @@ -606542ed8766bfbcc6490b3115eb0aea78c693e5 \ No newline at end of file diff --git a/src/multilspy/.syncCommitId.this b/src/multilspy/.syncCommitId.this deleted file mode 100644 index 8467819..0000000 --- a/src/multilspy/.syncCommitId.this +++ /dev/null @@ -1 +0,0 @@ -2f5651bd7846ebb6abde121223233bf975ce98ee \ No newline at end of file diff --git a/src/multilspy/__init__.py b/src/multilspy/__init__.py deleted file mode 100644 index 0bd1758..0000000 --- a/src/multilspy/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -This module contains the multilspy API -""" - -from solidlsp import multilspy_types as Types -from .language_server import LanguageServer, SyncLanguageServer - -__all__ = ["LanguageServer", "Types", "SyncLanguageServer"] diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py deleted file mode 100644 index e6faf7a..0000000 --- a/src/multilspy/language_server.py +++ /dev/null @@ -1,2311 +0,0 @@ -""" -This file contains the main interface and the public API for multilspy. -The abstract class LanguageServer provides a factory method, creator that is -intended for creating instantiations of language specific clients. -The details of Language Specific configuration are not exposed to the user. -""" - -import asyncio -import dataclasses -import hashlib -import json -import logging -import os -import pathlib -import pickle -import re -import threading -from collections import defaultdict -from contextlib import asynccontextmanager, contextmanager -from copy import copy -from pathlib import Path, PurePath -from typing import AsyncIterator, Dict, Iterator, List, Optional, Tuple, Union, cast - -import pathspec -import tqdm - -from solidlsp import multilspy_types -from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes, lsp_types -from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants -from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind -from solidlsp.lsp_protocol_handler.server import ( - Error, - LanguageServerHandler, - ProcessLaunchInfo, - StringDict, -) -from solidlsp.multilspy_config import Language, MultilspyConfig -from solidlsp.multilspy_exceptions import MultilspyException -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_utils import FileUtils, PathUtils, TextUtils -from solidlsp.type_helpers import ensure_all_methods_implemented - -# Serena dependencies -# We will need to watch out for circular imports, but it's probably better to not -# move all generic util code from serena into multilspy. -# It does however make sense to integrate many text-related utils into the language server -# since it caches (in-memory) file contents, so we can avoid reading from disk. -# Moreover, the way we want to use the language server (for retrieving actual content), -# it makes sense to have more content-related utils directly in it. -from serena.text_utils import MatchedConsecutiveLines, search_files - - - -class LanguageServer: - """ - The LanguageServer class provides a language agnostic interface to the Language Server Protocol. - It is used to communicate with Language Servers of different programming languages. - """ - - # To be overridden and extended by subclasses - def is_ignored_dirname(self, dirname: str) -> bool: - """ - A language-specific condition for directories that should always be ignored. For example, venv - in Python and node_modules in JS/TS should be ignored always. - """ - return dirname.startswith('.') - - @classmethod - def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str) -> "LanguageServer": - """ - Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. - - If language is Java, then ensure that jdk-17.0.6 or higher is installed, `java` is in PATH, and JAVA_HOME is set to the installation directory. - If language is JS/TS, then ensure that node (v18.16.0 or higher) is installed and in PATH. - - :param repository_root_path: The root path of the repository. - :param config: The Multilspy configuration. - :param logger: The logger to use. - :return LanguageServer: A language specific LanguageServer instance. - """ - if config.code_language == Language.PYTHON: - from multilspy.language_servers.pyright_language_server.pyright_server import ( - PyrightServer, - ) - - return PyrightServer(config, logger, repository_root_path) - # It used to be jedi, but pyright is a bit faster, and also more actively maintained - # Keeping the previous code for reference - # from multilspy.language_servers.jedi_language_server.jedi_server import ( - # JediServer, - # ) - - # return JediServer(config, logger, repository_root_path) - elif config.code_language == Language.JAVA: - from multilspy.language_servers.eclipse_jdtls.eclipse_jdtls import ( - EclipseJDTLS, - ) - - return EclipseJDTLS(config, logger, repository_root_path) - elif config.code_language == Language.KOTLIN: - from multilspy.language_servers.kotlin_language_server.kotlin_language_server import ( - KotlinLanguageServer, - ) - - return KotlinLanguageServer(config, logger, repository_root_path) - elif config.code_language == Language.RUST: - from multilspy.language_servers.rust_analyzer.rust_analyzer import ( - RustAnalyzer, - ) - - return RustAnalyzer(config, logger, repository_root_path) - elif config.code_language == Language.CSHARP: - from multilspy.language_servers.omnisharp.omnisharp import OmniSharp - - return OmniSharp(config, logger, repository_root_path) - elif config.code_language in [Language.TYPESCRIPT, Language.JAVASCRIPT]: - from multilspy.language_servers.typescript_language_server.typescript_language_server import ( - TypeScriptLanguageServer, - ) - return TypeScriptLanguageServer(config, logger, repository_root_path) - elif config.code_language == Language.GO: - from multilspy.language_servers.gopls.gopls import Gopls - - return Gopls(config, logger, repository_root_path) - elif config.code_language == Language.RUBY: - from multilspy.language_servers.solargraph.solargraph import Solargraph - - return Solargraph(config, logger, repository_root_path) - elif config.code_language == Language.DART: - from multilspy.language_servers.dart_language_server.dart_language_server import DartLanguageServer - - return DartLanguageServer(config, logger, repository_root_path) - elif config.code_language == Language.CPP: - from multilspy.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer - - return ClangdLanguageServer(config, logger, repository_root_path) - elif config.code_language == Language.PHP: - from multilspy.language_servers.intelephense.intelephense import Intelephense - - return Intelephense(config, logger, repository_root_path) - else: - logger.log(f"Language {config.code_language} is not supported", logging.ERROR) - raise MultilspyException(f"Language {config.code_language} is not supported") - - def __init__( - self, - config: MultilspyConfig, - logger: MultilspyLogger, - repository_root_path: str, - process_launch_info: ProcessLaunchInfo, - language_id: str, - ): - """ - Initializes a LanguageServer instance. - - Do not instantiate this class directly. Use `LanguageServer.create` method instead. - - :param config: The Multilspy configuration. - :param logger: The logger to use. - :param repository_root_path: The root path of the repository. - :param process_launch_info: Each language server has a specific command used to start the server. - This parameter is the command to launch the language server process. - The command must pass appropriate flags to the binary, so that it runs in the stdio mode, - as opposed to HTTP, TCP modes supported by some language servers. - """ - if type(self) == LanguageServer: - raise MultilspyException( - "LanguageServer is an abstract class and cannot be instantiated directly. Use LanguageServer.create method instead." - ) - - self.logger = logger - self.repository_root_path: str = repository_root_path - self.logger.log(f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) - - self.language_id = language_id - self.open_file_buffers: Dict[str, LSPFileBuffer] = {} - self.language = Language(language_id) - - # load cache first to prevent any racing conditions due to asyncio stuff - self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[ - multilspy_types.UnifiedSymbolInformation]]]] = {} - """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" - self._cache_lock = threading.Lock() - self._cache_has_changed: bool = False - self.load_cache() - - self.server_started = False - self.completions_available = asyncio.Event() - if config.trace_lsp_communication: - def logging_fn(source: str, target: str, msg: StringDict | str): - self.logger.log(f"LSP: {source} -> {target}: {str(msg)[:90]}...", self.logger.logger.level) - else: - logging_fn = None - - - # cmd is obtained from the child classes, which provide the language specific command to start the language server - # LanguageServerHandler provides the functionality to start the language server and communicate with it - self.logger.log(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) - self.server = LanguageServerHandler( - process_launch_info, - logger=logging_fn, - start_independent_lsp_process=config.start_independent_lsp_process, - ) - - - # Set up the pathspec matcher for the ignored paths - # for all absolute paths in ignored_paths, convert them to relative paths - processed_patterns = [] - for pattern in set(config.ignored_paths): - # Normalize separators (pathspec expects forward slashes) - pattern = pattern.replace(os.path.sep, '/') - processed_patterns.append(pattern) - self.logger.log(f"Processing {len(processed_patterns)} ignored paths from the config", logging.DEBUG) - - # Create a pathspec matcher from the processed patterns - self._ignore_spec = pathspec.PathSpec.from_lines( - pathspec.patterns.GitWildMatchPattern, - processed_patterns - ) - - def get_ignore_spec(self) -> pathspec.PathSpec: - """Returns the pathspec matcher for the paths that were configured to be ignored through - the multilspy config. - - This is is a subset of the full language-specific ignore spec that determines - which files are relevant for the language server. - - This matcher is useful for operations outside of the language server, - such as when searching for relevant non-language files in the project. - """ - return self._ignore_spec - - def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool: - """ - Determine if a path should be ignored based on file type - and ignore patterns. - - :param relative_path: Relative path to check - :param ignore_unsupported_files: whether files that are not supported source files should be ignored - - :return: True if the path should be ignored, False otherwise - """ - abs_path = os.path.join(self.repository_root_path, relative_path) - if not os.path.exists(abs_path): - raise FileNotFoundError(f"File {abs_path} not found, the ignore check cannot be performed") - - # Check file extension if it's a file - is_file = os.path.isfile(abs_path) - if is_file and ignore_unsupported_files: - fn_matcher = self.language.get_source_fn_matcher() - if not fn_matcher.is_relevant_filename(abs_path): - return True - - # Create normalized path for consistent handling - rel_path = Path(relative_path) - - # Check each part of the path against always fulfilled ignore conditions - dir_parts = rel_path.parts - if is_file: - dir_parts = dir_parts[:-1] - for part in dir_parts: - if not part: # Skip empty parts (e.g., from leading '/') - continue - if self.is_ignored_dirname(part): - return True - - # Use pathspec for gitignore-style pattern matching - # Normalize path separators for pathspec (it expects forward slashes) - normalized_path = str(rel_path).replace(os.path.sep, '/') - - # pathspec can't handle the matching of directories if they don't end with a slash! - # see https://github.com/cpburnz/python-pathspec/issues/89 - if os.path.isdir(os.path.join(self.repository_root_path, normalized_path)) and not normalized_path.endswith('/'): - normalized_path = normalized_path + '/' - - # Use the pathspec matcher to check if the path matches any ignore pattern - if self.get_ignore_spec().match_file(normalized_path): - return True - - return False - - async def _shutdown(self, timeout: float = 5.0): - """ - A robust shutdown process designed to terminate cleanly on all platforms, including Windows, - by explicitly closing all I/O pipes. - """ - if not self.server.is_running(): - self.logger.log("Server process not running, skipping shutdown.", logging.DEBUG) - return - - self.logger.log(f"Initiating final robust shutdown with a {timeout}s timeout...", logging.INFO) - process = self.server.process - reader_tasks = list(self.server.tasks.values()) - - # --- Main Shutdown Logic --- - try: - # Stage 1: Graceful Termination Request - # Send LSP shutdown and close stdin to signal no more input. - try: - await asyncio.wait_for(self.server.shutdown(), timeout=2.0) - if process.stdin and not process.stdin.is_closing(): - process.stdin.close() - except Exception: - pass # Ignore errors here, we are proceeding to terminate anyway. - - # Stage 2: Terminate and Concurrently Drain stdout/stderr - process.terminate() - - # Wait for the process to exit AND for the output pipes to be drained. - # The reader tasks will exit when they hit EOF. - await asyncio.wait_for( - asyncio.gather(process.wait(), *reader_tasks), - timeout=timeout - 2.0 - ) - self.logger.log("Process terminated and output pipes drained.", logging.INFO) - - except asyncio.TimeoutError: - # Stage 3: Forceful Kill - self.logger.log("Graceful termination failed. Forcefully killing process...", logging.WARNING) - if self.server.is_running(): - try: - process.kill() - # Wait for the killed process to be reaped by the OS. - await process.wait() - except Exception as e: - self.logger.log(f"Error during forceful kill: {e}", logging.ERROR) - - except Exception as e: - self.logger.log(f"An unexpected error occurred during shutdown logic: {e}", logging.ERROR) - - finally: - # === STAGE 4: EXPLICIT TASK & PIPE CLEANUP === - self.logger.log("Performing final task cancellation and pipe handle cleanup...", logging.DEBUG) - - # 1. Cancel any lingering reader tasks. - for task in reader_tasks: - if not task.done(): - task.cancel() - # Wait for cancellations to complete. - await asyncio.gather(*reader_tasks, return_exceptions=True) - self.server.tasks = {} - - # 2. Explicitly close each pipe to release OS handles. - for pipe in [process.stdin, process.stdout, process.stderr]: - if pipe and hasattr(pipe, 'is_closing') and not pipe.is_closing(): - try: - pipe.close() - except Exception: - pass - - # 3. Null out the process object in the handler. - self.server.process = None - self.logger.log("Shutdown sequence fully finished.", logging.DEBUG) - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["LanguageServer"]: - """ - Starts the Language Server and yields the LanguageServer instance. - """ - self.server_started = True - try: - yield self - finally: - self.server_started = False - await self._shutdown() - - @contextmanager - def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]: - """ - Open a file in the Language Server. This is required before making any requests to the Language Server. - - :param relative_file_path: The relative path of the file to open. - """ - if not self.server_started: - self.logger.log( - "open_file called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") - - absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) - uri = pathlib.Path(absolute_file_path).as_uri() - - if uri in self.open_file_buffers: - assert self.open_file_buffers[uri].uri == uri - assert self.open_file_buffers[uri].ref_count >= 1 - - self.open_file_buffers[uri].ref_count += 1 - yield self.open_file_buffers[uri] - self.open_file_buffers[uri].ref_count -= 1 - else: - contents = FileUtils.read_file(self.logger, absolute_file_path) - - version = 0 - self.open_file_buffers[uri] = LSPFileBuffer(uri, contents, version, self.language_id, 1) - - self.server.notify.did_open_text_document( - { - LSPConstants.TEXT_DOCUMENT: { - LSPConstants.URI: uri, - LSPConstants.LANGUAGE_ID: self.language_id, - LSPConstants.VERSION: 0, - LSPConstants.TEXT: contents, - } - } - ) - yield self.open_file_buffers[uri] - self.open_file_buffers[uri].ref_count -= 1 - - if self.open_file_buffers[uri].ref_count == 0: - self.server.notify.did_close_text_document( - { - LSPConstants.TEXT_DOCUMENT: { - LSPConstants.URI: uri, - } - } - ) - del self.open_file_buffers[uri] - - def insert_text_at_position( - self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str - ) -> multilspy_types.Position: - """ - Insert text at the given line and column in the given file and return - the updated cursor position after inserting the text. - - :param relative_file_path: The relative path of the file to open. - :param line: The line number at which text should be inserted. - :param column: The column number at which text should be inserted. - :param text_to_be_inserted: The text to insert. - """ - if not self.server_started: - self.logger.log( - "insert_text_at_position called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") - - absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) - uri = pathlib.Path(absolute_file_path).as_uri() - - # Ensure the file is open - assert uri in self.open_file_buffers - - file_buffer = self.open_file_buffers[uri] - file_buffer.version += 1 - - new_contents, new_l, new_c = TextUtils.insert_text_at_position(file_buffer.contents, line, column, text_to_be_inserted) - file_buffer.contents = new_contents - self.server.notify.did_change_text_document( - { - LSPConstants.TEXT_DOCUMENT: { - LSPConstants.VERSION: file_buffer.version, - LSPConstants.URI: file_buffer.uri, - }, - LSPConstants.CONTENT_CHANGES: [ - { - LSPConstants.RANGE: { - "start": {"line": line, "character": column}, - "end": {"line": line, "character": column}, - }, - "text": text_to_be_inserted, - } - ], - } - ) - return multilspy_types.Position(line=new_l, character=new_c) - - def delete_text_between_positions( - self, - relative_file_path: str, - start: multilspy_types.Position, - end: multilspy_types.Position, - ) -> str: - """ - Delete text between the given start and end positions in the given file and return the deleted text. - """ - if not self.server_started: - self.logger.log( - "insert_text_at_position called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") - - absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) - uri = pathlib.Path(absolute_file_path).as_uri() - - # Ensure the file is open - assert uri in self.open_file_buffers - - file_buffer = self.open_file_buffers[uri] - file_buffer.version += 1 - new_contents, deleted_text = TextUtils.delete_text_between_positions(file_buffer.contents, start_line=start["line"], start_col=start["character"], end_line=end["line"], end_col=end["character"]) - file_buffer.contents = new_contents - self.server.notify.did_change_text_document( - { - LSPConstants.TEXT_DOCUMENT: { - LSPConstants.VERSION: file_buffer.version, - LSPConstants.URI: file_buffer.uri, - }, - LSPConstants.CONTENT_CHANGES: [{LSPConstants.RANGE: {"start": start, "end": end}, "text": ""}], - } - ) - return deleted_text - - async def _send_definition_request(self, definition_params: DefinitionParams) -> Union[Definition, List[LocationLink], None]: - return await self.server.send.definition(definition_params) - - async def request_definition( - self, relative_file_path: str, line: int, column: int - ) -> List[multilspy_types.Location]: - """ - Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server - for the symbol at the given line and column in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the symbol for which definition should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - - :return List[multilspy_types.Location]: A list of locations where the symbol is defined - """ - - if not self.server_started: - self.logger.log( - "find_function_definition called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") - - with self.open_file(relative_file_path): - # sending request to the language server and waiting for response - definition_params = cast(DefinitionParams, { - LSPConstants.TEXT_DOCUMENT: { - LSPConstants.URI: pathlib.Path( - str(PurePath(self.repository_root_path, relative_file_path)) - ).as_uri() - }, - LSPConstants.POSITION: { - LSPConstants.LINE: line, - LSPConstants.CHARACTER: column, - }, - }) - response = await self._send_definition_request(definition_params) - - ret: List[multilspy_types.Location] = [] - if isinstance(response, list): - # response is either of type Location[] or LocationLink[] - for item in response: - assert isinstance(item, dict) - if LSPConstants.URI in item and LSPConstants.RANGE in item: - new_item: multilspy_types.Location = {} - new_item.update(item) - new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) - new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) - ret.append(multilspy_types.Location(new_item)) - elif ( - LSPConstants.ORIGIN_SELECTION_RANGE in item - and LSPConstants.TARGET_URI in item - and LSPConstants.TARGET_RANGE in item - and LSPConstants.TARGET_SELECTION_RANGE in item - ): - new_item: multilspy_types.Location = {} - new_item["uri"] = item[LSPConstants.TARGET_URI] - new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) - new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) - new_item["range"] = item[LSPConstants.TARGET_SELECTION_RANGE] - ret.append(multilspy_types.Location(**new_item)) - else: - assert False, f"Unexpected response from Language Server: {item}" - elif isinstance(response, dict): - # response is of type Location - assert LSPConstants.URI in response - assert LSPConstants.RANGE in response - - new_item: multilspy_types.Location = {} - new_item.update(response) - new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) - new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) - ret.append(multilspy_types.Location(**new_item)) - elif response is None: - # Some language servers return None when they cannot find a definition - # This is expected for certain symbol types like generics or types with incomplete information - self.logger.log( - f"Language server returned None for definition request at {relative_file_path}:{line}:{column}", - logging.WARNING, - ) - else: - assert False, f"Unexpected response from Language Server: {response}" - - return ret - - # Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses - async def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[ - lsp_types.Location] | None: - return await self.server.send.references( - { - "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, - "position": {"line": line, "character": column}, - "context": {"includeDeclaration": False}, - } - ) - - async def request_references( - self, relative_file_path: str, line: int, column: int - ) -> List[multilspy_types.Location]: - """ - Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server - to find references to the symbol at the given line and column in the given file. Wait for the response and return the result. - Filters out references located in ignored directories. - - :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - - :return: A list of locations where the symbol is referenced (excluding ignored directories) - """ - - if not self.server_started: - self.logger.log( - "request_references called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") - - - with self.open_file(relative_file_path): - try: - response = await self._send_references_request(relative_file_path, line=line, column=column) - except Exception as e: - # Catch LSP internal error (-32603) and raise a more informative exception - if isinstance(e, Error) and getattr(e, 'code', None) == -32603: - raise RuntimeError( - f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. " - "This often occurs when requesting references for a symbol not referenced in the expected way. " - ) from e - raise - if response is None: - return [] - - ret: List[multilspy_types.Location] = [] - assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}" - for item in response: - assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}" - assert LSPConstants.URI in item - assert LSPConstants.RANGE in item - - abs_path = PathUtils.uri_to_path(item[LSPConstants.URI]) - rel_path = Path(abs_path).relative_to(self.repository_root_path) - if self.is_ignored_path(str(rel_path)): - self.logger.log(f"Ignoring reference in {rel_path} since it should be ignored", logging.DEBUG) - continue - - new_item: multilspy_types.Location = {} - new_item.update(item) - new_item["absolutePath"] = str(abs_path) - new_item["relativePath"] = str(rel_path) - ret.append(multilspy_types.Location(**new_item)) - - return ret - - async def request_references_with_content( - self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0 - ) -> List[MatchedConsecutiveLines]: - """ - Like request_references, but returns the content of the lines containing the references, not just the locations. - - :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - :param context_lines_before: The number of lines to include in the context before the line containing the reference - :param context_lines_after: The number of lines to include in the context after the line containing the reference - - :return: A list of MatchedConsecutiveLines objects, one for each reference. - """ - references = await self.request_references(relative_file_path, line, column) - return [self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) for ref in references] - - def retrieve_full_file_content(self, relative_file_path: str) -> str: - """ - Retrieve the full content of the given file. - """ - with self.open_file(relative_file_path) as file_data: - return file_data.contents - - def retrieve_content_around_line(self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0) -> MatchedConsecutiveLines: - """ - Retrieve the content of the given file around the given line. - - :param relative_file_path: The relative path of the file to retrieve the content from - :param line: The line number to retrieve the content around - :param context_lines_before: The number of lines to retrieve before the given line - :param context_lines_after: The number of lines to retrieve after the given line - - :return MatchedConsecutiveLines: A container with the desired lines. - """ - with self.open_file(relative_file_path) as file_data: - file_contents = file_data.contents - return MatchedConsecutiveLines.from_file_contents(file_contents, line=line, context_lines_before=context_lines_before, context_lines_after=context_lines_after, source_file_path=relative_file_path) - - - async def request_completions( - self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False - ) -> List[multilspy_types.CompletionItem]: - """ - Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server - to find completions at the given line and column in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the symbol for which completions should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - - :return List[multilspy_types.CompletionItem]: A list of completions - """ - with self.open_file(relative_file_path): - open_file_buffer = self.open_file_buffers[ - pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() - ] - completion_params: LSPTypes.CompletionParams = { - "position": {"line": line, "character": column}, - "textDocument": {"uri": open_file_buffer.uri}, - "context": {"triggerKind": LSPTypes.CompletionTriggerKind.Invoked}, - } - response: Union[List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None] = None - - num_retries = 0 - while response is None or (response["isIncomplete"] and num_retries < 30): - await self.completions_available.wait() - response: Union[ - List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None - ] = await self.server.send.completion(completion_params) - if isinstance(response, list): - response = {"items": response, "isIncomplete": False} - num_retries += 1 - - # TODO: Understand how to appropriately handle `isIncomplete` - if response is None or (response["isIncomplete"] and not(allow_incomplete)): - return [] - - if "items" in response: - response = response["items"] - - response: List[LSPTypes.CompletionItem] = response - - # TODO: Handle the case when the completion is a keyword - items = [item for item in response if item["kind"] != LSPTypes.CompletionItemKind.Keyword] - - completions_list: List[multilspy_types.CompletionItem] = [] - - for item in items: - assert "insertText" in item or "textEdit" in item - assert "kind" in item - completion_item = {} - if "detail" in item: - completion_item["detail"] = item["detail"] - - if "label" in item: - completion_item["completionText"] = item["label"] - completion_item["kind"] = item["kind"] - elif "insertText" in item: - completion_item["completionText"] = item["insertText"] - completion_item["kind"] = item["kind"] - elif "textEdit" in item and "newText" in item["textEdit"]: - completion_item["completionText"] = item["textEdit"]["newText"] - completion_item["kind"] = item["kind"] - elif "textEdit" in item and "range" in item["textEdit"]: - new_dot_lineno, new_dot_colno = ( - completion_params["position"]["line"], - completion_params["position"]["character"], - ) - assert all( - ( - item["textEdit"]["range"]["start"]["line"] == new_dot_lineno, - item["textEdit"]["range"]["start"]["character"] == new_dot_colno, - item["textEdit"]["range"]["start"]["line"] == item["textEdit"]["range"]["end"]["line"], - item["textEdit"]["range"]["start"]["character"] - == item["textEdit"]["range"]["end"]["character"], - ) - ) - - completion_item["completionText"] = item["textEdit"]["newText"] - completion_item["kind"] = item["kind"] - elif "textEdit" in item and "insert" in item["textEdit"]: - assert False - else: - assert False - - completion_item = multilspy_types.CompletionItem(**completion_item) - completions_list.append(completion_item) - - return [ - json.loads(json_repr) - for json_repr in set([json.dumps(item, sort_keys=True) for item in completions_list]) - ] - - async def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[ - multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: - """ - Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server - to find symbols in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the symbols - :param include_body: whether to include the body of the symbols in the result. - :return: A list of symbols in the file, and a list of root symbols that represent the tree structure of the symbols. - All symbols will have a location, a children, and a parent attribute, - where the parent attribute is None for root symbols. - Note that this is slightly different from the call to request_full_symbol_tree, - where the parent attribute will be the file symbol which in turn may have a package symbol as parent. - If you need a symbol tree that contains file symbols as well, you should use `request_full_symbol_tree` instead. - """ - # TODO: it's kinda dumb to not use the cache if include_body is False after include_body was True once - # Should be fixed in the future, it's a small performance optimization - cache_key = f"{relative_file_path}-{include_body}" - with self.open_file(relative_file_path) as file_data: - with self._cache_lock: - file_hash_and_result = self._document_symbols_cache.get(cache_key) - if file_hash_and_result is not None: - file_hash, result = file_hash_and_result - if file_hash == file_data.content_hash: - self.logger.log(f"Returning cached document symbols for {relative_file_path}", logging.DEBUG) - return result - else: - self.logger.log(f"Content for {relative_file_path} has changed. Will overwrite in-memory cache", logging.DEBUG) - else: - self.logger.log(f"No cache hit for symbols with {include_body=} in {relative_file_path}", logging.DEBUG) - - self.logger.log(f"Requesting document symbols for {relative_file_path} from the Language Server", logging.DEBUG) - response = await self.server.send.document_symbol( - { - "textDocument": { - "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() - } - } - ) - self.logger.log(f"Received {len(response) if response is not None else None} document symbols for {relative_file_path} from the Language Server", logging.DEBUG) - - def turn_item_into_symbol_with_children(item: GenericDocumentSymbol): - item = cast(multilspy_types.UnifiedSymbolInformation, item) - absolute_path = os.path.join(self.repository_root_path, relative_file_path) - - # handle missing entries in location - if "location" not in item: - uri = pathlib.Path(absolute_path).as_uri() - assert "range" in item - tree_location = multilspy_types.Location( - uri=uri, - range=item['range'], - absolutePath=absolute_path, - relativePath=relative_file_path, - ) - item['location'] = tree_location - location = item["location"] - if "absolutePath" not in location: - location["absolutePath"] = absolute_path - if "relativePath" not in location: - location["relativePath"] = relative_file_path - if include_body: - item['body'] = self.retrieve_symbol_body(item) - # handle missing selectionRange - if "selectionRange" not in item: - if "range" in item: - item["selectionRange"] = item["range"] - else: - item["selectionRange"] = item["location"]["range"] - children = item.get(LSPConstants.CHILDREN, []) - for child in children: - child["parent"] = item - item[LSPConstants.CHILDREN] = children - - flat_all_symbol_list: List[multilspy_types.UnifiedSymbolInformation] = [] - assert isinstance(response, list), f"Unexpected response from Language Server: {response}" - root_nodes: List[multilspy_types.UnifiedSymbolInformation] = [] - for root_item in response: - if "range" not in root_item and "location" not in root_item: - if root_item["kind"] in [SymbolKind.File, SymbolKind.Module]: - ... - - # mutation is more convenient than creating a new dict, - # so we cast and rename the var after the mutating call to turn_item_into_symbol_with_children - # which turned and item into a "symbol" - turn_item_into_symbol_with_children(root_item) - root_symbol = cast(multilspy_types.UnifiedSymbolInformation, root_item) - root_symbol["parent"] = None - - root_nodes.append(root_symbol) - assert isinstance(root_symbol, dict) - assert LSPConstants.NAME in root_symbol - assert LSPConstants.KIND in root_symbol - - if LSPConstants.CHILDREN in root_symbol: - # TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well - - def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> List[ - multilspy_types.UnifiedSymbolInformation]: - node = cast(multilspy_types.UnifiedSymbolInformation, node) - l: List[multilspy_types.UnifiedSymbolInformation] = [] - turn_item_into_symbol_with_children(node) - assert LSPConstants.CHILDREN in node - children = node[LSPConstants.CHILDREN] - l.append(node) - for child in children: - l.extend(visit_tree_nodes_and_build_tree_repr(child)) - return l - - flat_all_symbol_list.extend(visit_tree_nodes_and_build_tree_repr(root_symbol)) - else: - flat_all_symbol_list.append(multilspy_types.UnifiedSymbolInformation(**root_symbol)) - - result = flat_all_symbol_list, root_nodes - self.logger.log(f"Caching document symbols for {relative_file_path}", logging.DEBUG) - with self._cache_lock: - self._document_symbols_cache[cache_key] = (file_data.content_hash, result) - self._cache_has_changed = True - return result - - async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[ - multilspy_types.UnifiedSymbolInformation]: - """ - Will go through all files in the project or within a relative path and build a tree of symbols. - Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search. - - For each file, a symbol of kind File (2) will be created. For directories, a symbol of kind Package (4) will be created. - All symbols will have a children attribute, thereby representing the tree structure of all symbols in the project - that are within the repository. - All symbols except the root packages will have a parent attribute. - Will ignore directories starting with '.', language-specific defaults - and user-configured directories (e.g. from .gitignore). - - :param within_relative_path: pass a relative path to only consider symbols within this path. - If a file is passed, only the symbols within this file will be considered. - If a directory is passed, all files within this directory will be considered. - :param include_body: whether to include the body of the symbols in the result. - - :return: A list of root symbols representing the top-level packages/modules in the project. - """ - - if within_relative_path is not None: - within_abs_path = os.path.join(self.repository_root_path, within_relative_path) - if not os.path.exists(within_abs_path): - raise FileNotFoundError(f"File or directory not found: {within_abs_path}") - if os.path.isfile(within_abs_path): - if self.is_ignored_path(within_relative_path): - self.logger.log(f"You passed a file explicitly, but it is ignored. This is probably an error. File: {within_relative_path}", logging.ERROR) - return [] - else: - _, root_nodes = await self.request_document_symbols(within_relative_path, include_body=include_body) - return root_nodes - - # Helper function to recursively process directories - async def process_directory(rel_dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]: - abs_dir_path = self.repository_root_path if rel_dir_path == "." else os.path.join(self.repository_root_path, rel_dir_path) - abs_dir_path = os.path.realpath(abs_dir_path) - - if self.is_ignored_path(str(Path(abs_dir_path).relative_to(self.repository_root_path))): - self.logger.log(f"Skipping directory: {rel_dir_path}\n(because it should be ignored)", logging.DEBUG) - return [] - - result = [] - try: - contained_dir_or_file_names = os.listdir(abs_dir_path) - except OSError: - return [] - - # Create package symbol for directory - package_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore - name=os.path.basename(abs_dir_path), - kind=multilspy_types.SymbolKind.Package, - location=multilspy_types.Location( - uri=str(pathlib.Path(abs_dir_path).as_uri()), - range={"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}, - absolutePath=str(abs_dir_path), - relativePath=str(Path(abs_dir_path).resolve().relative_to(self.repository_root_path)), - ), - children=[] - ) - result.append(package_symbol) - - for contained_dir_or_file_name in contained_dir_or_file_names: - contained_dir_or_file_abs_path = os.path.join(abs_dir_path, contained_dir_or_file_name) - contained_dir_or_file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)) - if self.is_ignored_path(contained_dir_or_file_rel_path): - self.logger.log(f"Skipping item: {contained_dir_or_file_rel_path}\n(because it should be ignored)", logging.DEBUG) - continue - - if os.path.isdir(contained_dir_or_file_abs_path): - child_symbols = await process_directory(contained_dir_or_file_rel_path) - package_symbol["children"].extend(child_symbols) - for child in child_symbols: - child["parent"] = package_symbol - - elif os.path.isfile(contained_dir_or_file_abs_path): - _, file_root_nodes = await self.request_document_symbols(contained_dir_or_file_rel_path, include_body=include_body) - - # Create file symbol, link with children - file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)) - with self.open_file(file_rel_path) as file_data: - fileRange = self._get_range_from_file_content(file_data.contents) - file_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore - name=os.path.splitext(contained_dir_or_file_name)[0], - kind=multilspy_types.SymbolKind.File, - range=fileRange, - selectionRange=fileRange, - location=multilspy_types.Location( - uri=str(pathlib.Path(contained_dir_or_file_abs_path).as_uri()), - range=fileRange, - absolutePath=str(contained_dir_or_file_abs_path), - relativePath=str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)), - ), - children=file_root_nodes, - parent=package_symbol, - ) - for child in file_root_nodes: - child["parent"] = file_symbol - - # Link file symbol with package - package_symbol["children"].append(file_symbol) - - # TODO: Not sure if this is actually still needed given recent changes to relative path handling - def fix_relative_path(nodes: List[multilspy_types.UnifiedSymbolInformation]): - for node in nodes: - if "location" in node and "relativePath" in node["location"]: - path = Path(node["location"]["relativePath"]) - if path.is_absolute(): - try: - path = path.relative_to(self.repository_root_path) - node["location"]["relativePath"] = str(path) - except Exception: - pass - if "children" in node: - fix_relative_path(node["children"]) - - fix_relative_path(file_root_nodes) - - return result - - # Start from the root or the specified directory - start_rel_path = within_relative_path or "." - return await process_directory(start_rel_path) - - @staticmethod - def _get_range_from_file_content(file_content: str) -> multilspy_types.Range: - """ - Get the range for the given file. - """ - lines = file_content.split("\n") - end_line = len(lines) - end_column = len(lines[-1]) - return multilspy_types.Range( - start=multilspy_types.Position(line=0, character=0), - end=multilspy_types.Position(line=end_line, character=end_column) - ) - - async def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: - """ - An overview of the given directory. - - Maps relative paths of all contained files to info about top-level symbols in the file - (name, kind, line, column). - """ - symbol_tree = await self.request_full_symbol_tree(relative_dir_path) - # Initialize result dictionary - result: dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]] = defaultdict(list) - - # Helper function to process a symbol and its children - def process_symbol(symbol: multilspy_types.UnifiedSymbolInformation): - if symbol["kind"] == multilspy_types.SymbolKind.File: - # For file symbols, process their children (top-level symbols) - for child in symbol["children"]: - assert "location" in child - assert "selectionRange" in child - path = Path(child["location"]["absolutePath"]).resolve().relative_to(self.repository_root_path) - result[str(path)].append(( - child["name"], - child["kind"], - child["selectionRange"]["start"]["line"], - child["selectionRange"]["start"]["character"] - )) - # For package/directory symbols, process their children - for child in symbol["children"]: - process_symbol(child) - - # Process each root symbol - for root in symbol_tree: - process_symbol(root) - return result - - async def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: - """ - An overview of the given file. - Returns the list of tuples (name, kind, line, column) of all top-level symbols in the file. - """ - _, document_roots = await self.request_document_symbols(relative_file_path) - result = [] - for root in document_roots: - try: - result.append( - ( root["name"], - root["kind"], - root["selectionRange"]["start"]["line"], - root["selectionRange"]["start"]["character"],) - ) - except KeyError as e: - raise KeyError( - f"Could not process symbol of name {root.get('name', 'unknown')} in {relative_file_path=}" - ) from e - return result - - async def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: - """ - An overview of all symbols in the given file or directory. - - :param within_relative_path: the relative path to the file or directory to get the overview of. - :return: A mapping of all relative paths analyzed to lists of tuples (name, kind, line, column) of all top-level symbols in the corresponding file. - """ - abs_path = (Path(self.repository_root_path) / within_relative_path).resolve() - if not abs_path.exists(): - raise FileNotFoundError(f"File or directory not found: {abs_path}") - - if abs_path.is_file(): - symbols_overview = await self.request_document_overview(within_relative_path) - return {within_relative_path: symbols_overview} - else: - return await self.request_dir_overview(within_relative_path) - - async def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: - """ - Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server - to find the hover information at the given line and column in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the hover information - :param line: The line number of the symbol - :param column: The column number of the symbol - - :return None - """ - with self.open_file(relative_file_path): - response = await self.server.send.hover( - { - "textDocument": { - "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() - }, - "position": { - "line": line, - "character": column, - }, - } - ) - - if response is None: - return None - - assert isinstance(response, dict) - - return multilspy_types.Hover(**response) - - # ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA -------------------- - - def retrieve_symbol_body(self, symbol: multilspy_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation) -> str: - """ - Load the body of the given symbol. If the body is already contained in the symbol, just return it. - """ - existing_body = symbol.get("body", None) - if existing_body: - return existing_body - - assert "location" in symbol - symbol_start_line = symbol["location"]["range"]["start"]["line"] - symbol_end_line = symbol["location"]["range"]["end"]["line"] - assert "relativePath" in symbol["location"] - symbol_file = self.retrieve_full_file_content(symbol["location"]["relativePath"]) - symbol_lines = symbol_file.split("\n") - symbol_body = "\n".join(symbol_lines[symbol_start_line:symbol_end_line+1]) - - # remove leading indentation - symbol_start_column = symbol["location"]["range"]["start"]["character"] - symbol_body = symbol_body[symbol_start_column:] - return symbol_body - - async def request_parsed_files(self) -> list[str]: - """Retrieves relative paths of all files analyzed by the Language Server.""" - if not self.server_started: - self.logger.log( - "request_parsed_files called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") - rel_file_paths = [] - for root, dirs, files in os.walk(self.repository_root_path): - dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d))] - for file in files: - rel_file_path = os.path.relpath(os.path.join(root, file), start=self.repository_root_path) - if not self.is_ignored_path(rel_file_path): - rel_file_paths.append(rel_file_path) - return rel_file_paths - - async def search_files_for_pattern( - self, - pattern: re.Pattern | str, - context_lines_before: int = 0, - context_lines_after: int = 0, - paths_include_glob: str | None = None, - paths_exclude_glob: str | None = None, - ) -> list[MatchedConsecutiveLines]: - """ - Search for a pattern across all files analyzed by the Language Server. - - :param pattern: Regular expression pattern to search for, either as a compiled Pattern or string - :param context_lines_before: Number of lines of context to include before each match - :param context_lines_after: Number of lines of context to include after each match - :param paths_include_glob: Glob pattern to filter which files to include in the search - :param paths_exclude_glob: Glob pattern to filter which files to exclude from the search. Takes precedence over paths_include_glob. - :return: List of matched consecutive lines with context - """ - if isinstance(pattern, str): - pattern = re.compile(pattern) - - relative_file_paths = await self.request_parsed_files() - return search_files( - relative_file_paths, - pattern, - file_reader=self.retrieve_full_file_content, - context_lines_before=context_lines_before, - context_lines_after=context_lines_after, - paths_include_glob=paths_include_glob, - paths_exclude_glob=paths_exclude_glob - ) - - async def request_referencing_symbols( - self, - relative_file_path: str, - line: int, - column: int, - include_imports: bool = True, - include_self: bool = False, - include_body: bool = False, - include_file_symbols: bool = False, - ) -> List[ReferenceInSymbol]: - """ - Finds all symbols that reference the symbol at the given location. - This is similar to request_references but filters to only include symbols - (functions, methods, classes, etc.) that reference the target symbol. - - :param relative_file_path: The relative path to the file. - :param line: The 0-indexed line number. - :param column: The 0-indexed column number. - :param include_imports: whether to also include imports as references. - Unfortunately, the LSP does not have an import type, so the references corresponding to imports - will not be easily distinguishable from definitions. - :param include_self: whether to include the references that is the "input symbol" itself. - Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition. - :param include_body: whether to include the body of the symbols in the result. - :param include_file_symbols: whether to include references that are file symbols. This - is often a fallback mechanism for when the reference cannot be resolved to a symbol. - :return: List of objects containing the symbol and the location of the reference. - """ - if not self.server_started: - self.logger.log( - "request_referencing_symbols called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") - - # First, get all references to the symbol - references = await self.request_references(relative_file_path, line, column) - if not references: - return [] - - # For each reference, find the containing symbol - result = [] - incoming_symbol = None - for ref in references: - ref_path = ref["relativePath"] - ref_line = ref["range"]["start"]["line"] - ref_col = ref["range"]["start"]["character"] - - with self.open_file(ref_path) as file_data: - # Get the containing symbol for this reference - containing_symbol = await self.request_containing_symbol( - ref_path, ref_line, ref_col, include_body=include_body - ) - if containing_symbol is None: - # TODO: HORRIBLE HACK! I don't know how to do it better for now... - # THIS IS BOUND TO BREAK IN MANY CASES! IT IS ALSO SPECIFIC TO PYTHON! - # Background: - # When a variable is used to change something, like - # - # instance = MyClass() - # instance.status = "new status" - # - # we can't find the containing symbol for the reference to `status` - # since there is no container on the line of the reference - # The hack is to try to find a variable symbol in the containing module - # by using the text of the reference to find the variable name (In a very heuristic way) - # and then look for a symbol with that name and kind Variable - ref_text = file_data.contents.split("\n")[ref_line] - if "." in ref_text: - containing_symbol_name = ref_text.split(".")[0] - all_symbols, _ = await self.request_document_symbols(ref_path) - for symbol in all_symbols: - if symbol["name"] == containing_symbol_name and symbol["kind"] == multilspy_types.SymbolKind.Variable: - containing_symbol = copy(symbol) - containing_symbol["location"] = ref - containing_symbol["range"] = ref["range"] - break - - # We failed retrieving the symbol, falling back to creating a file symbol - if containing_symbol is None and include_file_symbols: - self.logger.log( - f"Could not find containing symbol for {ref_path}:{ref_line}:{ref_col}. Returning file symbol instead", - logging.WARNING - ) - fileRange = self._get_range_from_file_content(file_data.contents) - location = multilspy_types.Location( - uri=str(pathlib.Path(os.path.join(self.repository_root_path, ref_path)).as_uri()), - range=fileRange, - absolutePath=str(os.path.join(self.repository_root_path, ref_path)), - relativePath=ref_path, - ) - name = os.path.splitext(os.path.basename(ref_path))[0] - - if include_body: - body = self.retrieve_full_file_content(ref_path) - else: - body = "" - - containing_symbol = multilspy_types.UnifiedSymbolInformation( - kind=multilspy_types.SymbolKind.File, - range=fileRange, - selectionRange=fileRange, - location=location, - name=name, - children=[], - body=body, - ) - if containing_symbol is None or not include_file_symbols and containing_symbol["kind"] == multilspy_types.SymbolKind.File: - continue - - assert "location" in containing_symbol - assert "selectionRange" in containing_symbol - - # Checking for self-reference - if ( - containing_symbol["location"]["relativePath"] == relative_file_path - and containing_symbol["selectionRange"]["start"]["line"] == ref_line - and containing_symbol["selectionRange"]["start"]["character"] == ref_col - ): - incoming_symbol = containing_symbol - if include_self: - result.append(ReferenceInSymbol(symbol=containing_symbol, line=ref_line, character=ref_col)) - continue - else: - self.logger.log(f"Found self-reference for {incoming_symbol['name']}, skipping it since {include_self=}", logging.DEBUG) - continue - - # checking whether reference is an import - # This is neither really safe nor elegant, but if we don't do it, - # there is no way to distinguish between definitions and imports as import is not a symbol-type - # and we get the type referenced symbol resulting from imports... - if (not include_imports \ - and incoming_symbol is not None \ - and containing_symbol["name"] == incoming_symbol["name"] \ - and containing_symbol["kind"] == incoming_symbol["kind"] \ - ): - self.logger.log( - f"Found import of referenced symbol {incoming_symbol['name']}" - f"in {containing_symbol['location']['relativePath']}, skipping", - logging.DEBUG - ) - continue - - result.append(ReferenceInSymbol(symbol=containing_symbol, line=ref_line, character=ref_col)) - - return result - - async def request_containing_symbol( - self, - relative_file_path: str, - line: int, - column: Optional[int] = None, - strict: bool = False, - include_body: bool = False, - ) -> multilspy_types.UnifiedSymbolInformation | None: - """ - Finds the first symbol containing the position for the given file. - For Python, container symbols are considered to be those with kinds corresponding to - functions, methods, or classes (typically: Function (12), Method (6), Class (5)). - - The method operates as follows: - - Request the document symbols for the file. - - Filter symbols to those that start at or before the given line. - - From these, first look for symbols whose range contains the (line, column). - - If one or more symbols contain the position, return the one with the greatest starting position - (i.e. the innermost container). - - If none (strictly) contain the position, return the symbol with the greatest starting position - among those above the given line. - - If no container candidates are found, return None. - - :param relative_file_path: The relative path to the Python file. - :param line: The 0-indexed line number. - :param column: The 0-indexed column (also called character). If not passed, the lookup will be based - only on the line. - :param strict: If True, the position must be strictly within the range of the symbol. - Setting to True is useful for example for finding the parent of a symbol, as with strict=False, - and the line pointing to a symbol itself, the containing symbol will be the symbol itself - (and not the parent). - :param include_body: Whether to include the body of the symbol in the result. - :return: The container symbol (if found) or None. - """ - # checking if the line is empty, unfortunately ugly and duplicating code, but I don't want to refactor - with self.open_file(relative_file_path): - absolute_file_path = str( - PurePath(self.repository_root_path, relative_file_path) - ) - content = FileUtils.read_file(self.logger, absolute_file_path) - if content.split("\n")[line].strip() == "": - self.logger.log( - f"Passing empty lines to request_container_symbol is currently not supported, {relative_file_path=}, {line=}", - logging.ERROR, - ) - return None - - symbols, _ = await self.request_document_symbols(relative_file_path) - - # make jedi and pyright api compatible - # the former has no location, the later has no range - # we will just always add location of the desired format to all symbols - for symbol in symbols: - if "location" not in symbol: - range = symbol["range"] - location = multilspy_types.Location( - uri=f"file:/{absolute_file_path}", - range=range, - absolutePath=absolute_file_path, - relativePath=relative_file_path, - ) - symbol["location"] = location - else: - location = symbol["location"] - assert "range" in location - location["absolutePath"] = absolute_file_path - location["relativePath"] = relative_file_path - location["uri"] = Path(absolute_file_path).as_uri() - - # Allowed container kinds, currently only for Python - container_symbol_kinds = { - multilspy_types.SymbolKind.Method, - multilspy_types.SymbolKind.Function, - multilspy_types.SymbolKind.Class - } - - def is_position_in_range(line: int, range_d: multilspy_types.Range) -> bool: - start = range_d["start"] - end = range_d["end"] - - column_condition = True - if strict: - line_condition = end["line"] >= line > start["line"] - if column is not None and line == start["line"]: - column_condition = column > start["character"] - else: - line_condition = end["line"] >= line >= start["line"] - if column is not None and line == start["line"]: - column_condition = column >= start["character"] - return line_condition and column_condition - - # Only consider containers that are not one-liners (otherwise we may get imports) - candidate_containers = [ - s for s in symbols if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"] - ] - var_containers = [ - s for s in symbols if s["kind"] == multilspy_types.SymbolKind.Variable - ] - candidate_containers.extend(var_containers) - - if not candidate_containers: - return None - - # From the candidates, find those whose range contains the given position. - containing_symbols = [] - for symbol in candidate_containers: - s_range = symbol["location"]["range"] - if not is_position_in_range(line, s_range): - continue - containing_symbols.append(symbol) - - if containing_symbols: - # Return the one with the greatest starting position (i.e. the innermost container). - containing_symbol = max(containing_symbols, key=lambda s: s["location"]["range"]["start"]["line"]) - if include_body: - containing_symbol["body"] = self.retrieve_symbol_body(containing_symbol) - return containing_symbol - else: - return None - - async def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None: - """ - Finds the container of the given symbol if there is one. If the parent attribute is present, the parent is returned - without further searching. - - :param symbol: The symbol to find the container of. - :param include_body: whether to include the body of the symbol in the result. - :return: The container of the given symbol or None if no container is found. - """ - if "parent" in symbol: - return symbol["parent"] - assert "location" in symbol, f"Symbol {symbol} has no location and no parent attribute" - return await self.request_containing_symbol( - symbol["location"]["relativePath"], - symbol["location"]["range"]["start"]["line"], - symbol["location"]["range"]["start"]["character"], - strict=True, - include_body=include_body, - ) - - async def request_defining_symbol( - self, - relative_file_path: str, - line: int, - column: int, - include_body: bool = False, - ) -> Optional[multilspy_types.UnifiedSymbolInformation]: - """ - Finds the symbol that defines the symbol at the given location. - - This method first finds the definition of the symbol at the given position, - then retrieves the full symbol information for that definition. - - :param relative_file_path: The relative path to the file. - :param line: The 0-indexed line number. - :param column: The 0-indexed column number. - :param include_body: whether to include the body of the symbol in the result. - :return: The symbol information for the definition, or None if not found. - """ - if not self.server_started: - self.logger.log( - "request_defining_symbol called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") - - # Get the definition location(s) - definitions = await self.request_definition(relative_file_path, line, column) - if not definitions: - return None - - # Use the first definition location - definition = definitions[0] - def_path = definition["relativePath"] - def_line = definition["range"]["start"]["line"] - def_col = definition["range"]["start"]["character"] - - # Find the symbol at or containing this location - defining_symbol = await self.request_containing_symbol( - def_path, def_line, def_col, strict=False, include_body=include_body - ) - - return defining_symbol - - @property - def cache_path(self) -> Path: - """ - The path to the cache file for the document symbols. - """ - return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v20-05-25.pkl" - - async def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None: - """Will go through the entire repository and "index" all files, meaning save their symbols to the cache. - - :param progress_bar: Whether to show a progress bar while indexing the repository. - :param save_after_n_files: How many files to process before saving a checkpoint of the cache. - """ - parsed_files = await self.request_parsed_files() - files_processed = 0 - pbar = tqdm.tqdm(parsed_files, disable=not progress_bar) - for relative_file_path in pbar: - pbar.set_description(f"Indexing ({os.path.basename(relative_file_path)})") - await self.request_document_symbols(relative_file_path, include_body=False) - await self.request_document_symbols(relative_file_path, include_body=True) - files_processed += 1 - if files_processed % save_after_n_files == 0: - self.save_cache() - self.save_cache() - - def save_cache(self): - with self._cache_lock: - if not self._cache_has_changed: - self.logger.log("No changes to document symbols cache, skipping save", logging.DEBUG) - return - - self.logger.log(f"Saving updated document symbols cache to {self.cache_path}", logging.INFO) - self.cache_path.parent.mkdir(parents=True, exist_ok=True) - try: - with open(self.cache_path, "wb") as f: - pickle.dump(self._document_symbols_cache, f) - self._cache_has_changed = False - except Exception as e: - self.logger.log( - f"Failed to save document symbols cache to {self.cache_path}: {e}. " - "Note: this may have resulted in a corrupted cache file.", - logging.ERROR, - ) - - def load_cache(self): - if not self.cache_path.exists(): - return - - with self._cache_lock: - self.logger.log(f"Loading document symbols cache from {self.cache_path}", logging.INFO) - try: - with open(self.cache_path, "rb") as f: - self._document_symbols_cache = pickle.load(f) - self.logger.log(f"Loaded {len(self._document_symbols_cache)} document symbols from cache.", logging.INFO) - except Exception as e: - # cache often becomes corrupt, so just skip loading it - self.logger.log( - f"Failed to load document symbols cache from {self.cache_path}: {e}. Possible cause: the cache file is corrupted. " - "Check for any errors related to saving the cache in the logs.", - logging.ERROR, - ) - - - async def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]: - """ - Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server - to find symbols across the whole workspace. Wait for the response and return the result. - - :param query: The query string to filter symbols by - - :return: A list of matching symbols - """ - response = await self.server.send.workspace_symbol({"query": query}) - if response is None: - return None - - assert isinstance(response, list) - - ret: List[multilspy_types.UnifiedSymbolInformation] = [] - for item in response: - assert isinstance(item, dict) - - assert LSPConstants.NAME in item - assert LSPConstants.KIND in item - assert LSPConstants.LOCATION in item - - ret.append(multilspy_types.UnifiedSymbolInformation(**item)) - - return ret - -@ensure_all_methods_implemented(LanguageServer) -class SyncLanguageServer: - """ - The SyncLanguageServer class provides a language agnostic interface to the Language Server Protocol. - It is used to communicate with Language Servers of different programming languages. - """ - - def __init__(self, language_server: LanguageServer, timeout: Optional[float] = None): - """ - :param language_server: the async language server being wrapped - :param timeout: the timeout, in seconds, to use for requests to the language server. - """ - self.language_server = language_server - self.loop = None - self.loop_thread = None - self.timeout = timeout - - self._server_context = None - - self._shutdown_lock = threading.Lock() - self._is_shutting_down = False - - @property - def cache_path(self) -> Path: - """ - The path to the cache file for the document symbols. - """ - return self.language_server.cache_path - - @classmethod - def create( - cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, - timeout: Optional[float] = None - ) -> "SyncLanguageServer": - """ - Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. - - If language is Java, then ensure that jdk-17.0.6 or higher is installed, `java` is in PATH, and JAVA_HOME is set to the installation directory. - - :param repository_root_path: The root path of the repository (must be absolute). - :param config: The Multilspy configuration. - :param logger: The logger to use. - :param timeout: the timeout, in seconds, to use for requests; if None, use no timeout - - :return SyncLanguageServer: A language specific LanguageServer instance. - """ - return SyncLanguageServer(LanguageServer.create(config, logger, repository_root_path), timeout=timeout) - - @property - def repository_root_path(self) -> str: - return self.language_server.repository_root_path - - @contextmanager - def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]: - """ - Open a file in the Language Server. This is required before making any requests to the Language Server. - - :param relative_file_path: The relative path of the file to open. - """ - with self.language_server.open_file(relative_file_path) as file_buffer: - yield file_buffer - - def insert_text_at_position( - self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str - ) -> multilspy_types.Position: - """ - Insert text at the given line and column in the given file and return - the updated cursor position after inserting the text. - - :param relative_file_path: The relative path of the file to open. - :param line: The line number at which text should be inserted. - :param column: The column number at which text should be inserted. - :param text_to_be_inserted: The text to insert. - """ - return self.language_server.insert_text_at_position(relative_file_path, line, column, text_to_be_inserted) - - def delete_text_between_positions( - self, - relative_file_path: str, - start: multilspy_types.Position, - end: multilspy_types.Position, - ) -> str: - """ - Delete text between the given start and end positions in the given file and return the deleted text. - """ - return self.language_server.delete_text_between_positions(relative_file_path, start, end) - - @contextmanager - def start_server(self) -> Iterator["SyncLanguageServer"]: - """ - Starts the language server process and connects to it. - - :return: None - """ - self.start() - yield self - self.language_server.logger.log("SyncLS Startup: exiting LS context", logging.DEBUG) - self.stop() - - def request_definition(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: - """ - Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server - for the symbol at the given line and column in the given file. Wait for the response and return the result. - - :param file_path: The path of the file relative to the repository root. - :param line: The line number (zero-indexed). - :param column: The column number (zero-indexed). - - :return: A list of Locations defining the symbol. - """ - if not self.is_running(): - raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") - - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_definition(file_path, line, column), self.loop - ).result(timeout=self.timeout) - return result - - def request_references(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: - """ - Raises a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server - for the symbol at the given line and column in the given file. Wait for the response and return the result. - - :param file_path: The path of the file relative to the repository root. - :param line: The line number (zero-indexed). - :param column: The column number (zero-indexed). - - :return: A list of Locations referencing the symbol. - """ - if not self.is_running(): - raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") - - try: - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_references(file_path, line, column), self.loop - ).result(timeout=self.timeout) - except Exception as e: - from solidlsp.lsp_protocol_handler.server import Error - if isinstance(e, Error) and getattr(e, 'code', None) == -32603: - raise RuntimeError( - f"LSP internal error (-32603) when requesting references for {file_path}:{line}:{column}. " - "This often occurs when requesting references for a symbol not referenced in the expected way. " - ) from e - raise - return result - - - def request_references_with_content( - self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0 - ) -> List[MatchedConsecutiveLines]: - """ - Like request_references, but returns the content of the lines containing the references, not just the locations. - - :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - :param context_lines_before: The number of lines to include in the context before the line containing the reference - :param context_lines_after: The number of lines to include in the context after the line containing the reference - - :return: A list of MatchedConsecutiveLines objects, one for each reference. - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_references_with_content(relative_file_path, line, column, context_lines_before, context_lines_after), self.loop - ).result() - return result - - def request_completions( - self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False - ) -> List[multilspy_types.CompletionItem]: - """ - Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server - to find completions at the given line and column in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the symbol for which completions should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol - - :return List[multilspy_types.CompletionItem]: A list of completions - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_completions(relative_file_path, line, column, allow_incomplete), - self.loop, - ).result(timeout=self.timeout) - return result - - def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[ - multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: - """ - Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server - to find symbols in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the symbols - :param include_body: whether to include the body of the symbols in the result. - :return: A list of symbols in the file, and a list of root symbols that represent the tree structure of the symbols. Each symbol in hierarchy starting from the roots has a children attribute. - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_document_symbols(relative_file_path, include_body), self.loop - ).result() - return result - - def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[ - multilspy_types.UnifiedSymbolInformation]: - """ - Will go through all files in the project and build a tree of symbols. Note: this may be slow the first time it is called. - - For each file, a symbol of kind Module (3) will be created. For directories, a symbol of kind Package (4) will be created. - All symbols will have a children attribute, thereby representing the tree structure of all symbols in the project - that are within the repository. - Will ignore directories starting with '.', language-specific defaults - and user-configured directories (e.g. from .gitignore). - - :param within_relative_path: pass a relative path to only consider symbols within this path. - If a file is passed, only the symbols within this file will be considered. - If a directory is passed, all files within this directory will be considered. - If None, the entire codebase will be considered. - :param include_body: whether to include the body of the symbols in the result. - - :return: A list of root symbols representing the top-level packages/modules in the project. - """ - if not self.is_running(): - raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") - - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_full_symbol_tree(within_relative_path, include_body), self.loop - ).result(timeout=self.timeout) - return result - - def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: - """ - An overview of the given directory. - - Maps relative paths of all contained files to info about top-level symbols in the file - (name, kind, line, column). - """ - if not self.is_running(): - raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") - - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_dir_overview(relative_dir_path), self.loop - ).result(timeout=self.timeout) - return result - - def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: - """ - An overview of the given file. - - Returns the list of tuples (name, kind, line, column) of all top-level symbols in the file. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_document_overview(relative_file_path), self.loop - ).result(timeout=self.timeout) - return result - - def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: - """ - An overview of all symbols in the given file or directory. - - :param within_relative_path: the relative path to the file or directory to get the overview of. - :return: A mapping of all relative paths analyzed to lists of tuples (name, kind, line, column) of all top-level symbols in the corresponding file. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_overview(within_relative_path), self.loop - ).result() - return result - - def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: - """ - Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server - to find the hover information at the given line and column in the given file. Wait for the response and return the result. - - :param relative_file_path: The relative path of the file that has the hover information - :param line: The line number of the symbol - :param column: The column number of the symbol - - :return None - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_hover(relative_file_path, line, column), self.loop - ).result(timeout=self.timeout) - return result - - def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]: - """ - Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server - to find symbols across the whole workspace. Wait for the response and return the result. - - :param query: The query string to filter symbols by - - :return Union[List[multilspy_types.UnifiedSymbolInformation], None]: A list of matching symbols - """ - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_workspace_symbol(query), self.loop - ).result(timeout=self.timeout) - return result - - # ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA -------------------- - - def retrieve_symbol_body(self, symbol: multilspy_types.UnifiedSymbolInformation) -> str: - """ - Load the body of the given symbol. If the body is already contained in the symbol, just return it. - - :param symbol: The symbol to retrieve the body of. - :return: The body of the symbol. - """ - return self.language_server.retrieve_symbol_body(symbol) - - def request_parsed_files(self) -> list[str]: - """Retrieves relative paths of all files analyzed by the Language Server.""" - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_parsed_files(), self.loop - ).result() - return result - - def request_referencing_symbols( - self, relative_file_path: str, line: int, column: int, - include_imports: bool = True, include_self: bool = False, - include_body: bool = False, - include_file_symbols: bool = False, - ) -> List[ReferenceInSymbol]: - """ - Finds all symbols that reference the symbol at the given location. - This is similar to request_references but filters to only include symbols - (functions, methods, classes, etc.) that reference the target symbol. - - :param relative_file_path: The relative path to the file. - :param line: The 0-indexed line number. - :param column: The 0-indexed column number. - :param include_imports: whether to also include imports as references. - Unfortunately, the LSP does not have an import type, so the references corresponding to imports - will not be easily distinguishable from definitions. - :param include_self: whether to include the references that is the "input symbol" itself. - Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition. - :param include_body: whether to include the body of the symbols in the result. - :param include_file_symbols: whether to include references that are file symbols. This - is often a fallback mechanism for when the reference cannot be resolved to a symbol. - :return: List of objects containing the symbol and the location of the reference. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_referencing_symbols( - relative_file_path, - line, - column, - include_imports=include_imports, - include_self=include_self, - include_body=include_body, - include_file_symbols=include_file_symbols, - ), - self.loop - ).result(timeout=self.timeout) - return result - - def request_containing_symbol( - self, relative_file_path: str, line: int, - column: Optional[int] = None, strict: bool = False, - include_body: bool = False, - ) -> multilspy_types.UnifiedSymbolInformation | None: - """ - Finds the first symbol containing the position for the given file. - For Python, container symbols are considered to be those with kinds corresponding to - functions, methods, or classes (typically: Function (12), Method (6), Class (5)). - - The method operates as follows: - - Request the document symbols for the file. - - Filter symbols to those that start at or before the given line. - - From these, first look for symbols whose range contains the (line, column). - - If one or more symbols contain the position, return the one with the greatest starting position - (i.e. the innermost container). - - If none (strictly) contain the position, return the symbol with the greatest starting position - among those above the given line. - - If no container candidates are found, return None. - - :param relative_file_path: The relative path to the Python file. - :param line: The 0-indexed line number. - :param column: The 0-indexed column (also called character). If not passed, the lookup will be based - only on the line. - :param strict: If True, the position must be strictly within the range of the symbol. - Setting to true is useful for example for finding the parent of a symbol, as with strict=False, - and the line pointing to a symbol itself, the containing symbol will be the symbol itself - (and not the parent). - :param include_body: whether to include the body of the symbol in the result. - :return: The container symbol (if found) or None. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_containing_symbol(relative_file_path, line, column=column, strict=strict, include_body=include_body), self.loop - ).result(timeout=self.timeout) - return result - - def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None: - """ - Finds the container of the given symbol if there is one. - - :param symbol: The symbol to find the container of. - :param include_body: whether to include the body of the symbol in the result. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_container_of_symbol(symbol, include_body=include_body), self.loop - ).result(timeout=self.timeout) - return result - - def request_defining_symbol( - self, relative_file_path: str, line: int, column: int, - include_body: bool = False, - ) -> Optional[multilspy_types.UnifiedSymbolInformation]: - """ - Finds the symbol that defines the symbol at the given location. - - This method first finds the definition of the symbol at the given position, - then retrieves the full symbol information for that definition. - - :param relative_file_path: The relative path to the file. - :param line: The 0-indexed line number. - :param column: The 0-indexed column number. - :param include_body: whether to include the body of the symbol in the result. - :return: The symbol information for the definition, or None if not found. - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.request_defining_symbol(relative_file_path, line, column, include_body=include_body), self.loop - ).result(timeout=self.timeout) - return result - - def retrieve_full_file_content(self, relative_file_path: str) -> str: - """ - Retrieve the full content of the given file. - """ - return self.language_server.retrieve_full_file_content(relative_file_path) - - def retrieve_content_around_line(self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0) -> MatchedConsecutiveLines: - """ - Retrieve the content of the given file around the given line. - - :param relative_file_path: The relative path of the file to retrieve the content from - :param line: The line number to retrieve the content around - :param context_lines_before: The number of lines to retrieve before the given line - :param context_lines_after: The number of lines to retrieve after the given line - :return MatchedConsecutiveLines: A container with the desired lines. - """ - return self.language_server.retrieve_content_around_line(relative_file_path, line, context_lines_before, context_lines_after) - - def search_files_for_pattern( - self, - pattern: re.Pattern | str, - context_lines_before: int = 0, - context_lines_after: int = 0, - paths_include_glob: str | None = None, - paths_exclude_glob: str | None = None, - ) -> list[MatchedConsecutiveLines]: - """ - Search for a pattern across all files analyzed by the Language Server. - - :param pattern: Regular expression pattern to search for, either as a compiled Pattern or string - :param context_lines_before: Number of lines of context to include before each match - :param context_lines_after: Number of lines of context to include after each match - :param paths_include_glob: Glob pattern to filter which files to include in the search - :param paths_exclude_glob: Glob pattern to filter which files to exclude from the search. Takes precedence over paths_include_glob. - :return: List of matched consecutive lines with context - """ - assert self.loop - result = asyncio.run_coroutine_threadsafe( - self.language_server.search_files_for_pattern(pattern, context_lines_before, context_lines_after, paths_include_glob, paths_exclude_glob), self.loop - ).result(timeout=self.timeout) - return result - - def index_repository(self, progress_bar: bool = True, timeout: float | None = None, save_after_n_files: int = 10) -> None: - """ - Will go through the entire repository and "index" all files, meaning save their symbols to the cache. - - :param progress_bar: Whether to show a progress bar while indexing the repository. - :param save_after_n_files: How many files to process before saving a checkpoint of the cache. - :param timeout: since this operation can take a long time, the default timeout (the attribute of the LS instance) is not used. - If you want to provide a timeout for indexing, you can pass it here explicitly. - :return: - """ - assert self.loop - asyncio.run_coroutine_threadsafe( - self.language_server.index_repository(progress_bar=progress_bar, save_after_n_files=save_after_n_files), self.loop - ).result(timeout=timeout) - - def start(self) -> "SyncLanguageServer": - """ - Starts the language server process and connects to it. Call shutdown when ready. - - :return: self for method chaining - """ - self.language_server.logger.log(f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}", logging.INFO) - self.language_server.logger.log("Creating new event loop", logging.DEBUG) - self.loop = asyncio.new_event_loop() - self.language_server.logger.log("Creating new thread for event loop", logging.DEBUG) - self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True) - self.loop_thread.start() - self.language_server.logger.log("Starting server (async) context", logging.DEBUG) - self._server_context = self.language_server.start_server() - self.language_server.logger.log("Entering server context", logging.DEBUG) - asyncio.run_coroutine_threadsafe(self._server_context.__aenter__(), loop=self.loop).result(timeout=self.timeout) - return self - - def is_running(self) -> bool: - """ - Check if the language server is running. - """ - return self.loop is not None and self.loop_thread is not None and self.loop_thread.is_alive() - - async def _shutdown_and_stop_loop(self): - """A coroutine that performs the full shutdown and then stops the event loop.""" - try: - if self._server_context: - await self._server_context.__aexit__(None, None, None) - except Exception as e: - self.language_server.logger.log(f"Exception during async shutdown: {e}", logging.ERROR) - finally: - # Clean up tasks but DON'T stop the loop from here - # Let the main thread handle loop stopping to avoid coroutine deadlocks - if self.loop and self.loop.is_running(): - current_task = asyncio.current_task(self.loop) - pending_tasks = [task for task in asyncio.all_tasks(self.loop) - if not task.done() and task is not current_task] - - if pending_tasks: - self.language_server.logger.log(f"Cancelling {len(pending_tasks)} pending tasks", logging.DEBUG) - for task in pending_tasks: - try: - task.cancel() - except Exception: - pass - - self.language_server.logger.log("Async shutdown tasks completed, returning to main thread", logging.DEBUG) - # Note: NOT calling loop.stop() here - let main thread do it - - def stop(self, shutdown_timeout: float = 2.0) -> None: - """ - Stops the language server and robustly cleans up all associated resources, - including the asyncio event loop, to prevent hangs on process exit. - """ - # 1. Use a lock and flag to make the shutdown call thread-safe and idempotent, - # ensuring the shutdown logic runs only once. - with self._shutdown_lock: - if self._is_shutting_down: - self.language_server.logger.log("Already shutting down or stopped, skipping", logging.DEBUG) - return - self._is_shutting_down = True - - if not self.is_running(): - self.language_server.logger.log("Language server is not running, skipping", logging.DEBUG) - return - - self.language_server.logger.log("Initiating server shutdown...", logging.INFO) - self.save_cache() - - # Detect platform to choose shutdown strategy - is_windows = os.name == "nt" - - if not is_windows: - # 2. Graceful shutdown for Linux/Unix - try proper async cleanup first - self.language_server.logger.log("Starting graceful shutdown", logging.INFO) - shutdown_future = None - if self.loop and self.loop.is_running(): - shutdown_future = asyncio.run_coroutine_threadsafe(self._shutdown_and_stop_loop(), self.loop) - - # 3. Wait for the background thread to exit - if self.loop_thread: - self.language_server.logger.log("Waiting for event loop thread to exit", logging.INFO) - self.loop_thread.join(timeout=shutdown_timeout) - if self.loop_thread.is_alive(): - self.language_server.logger.log("Event loop thread did not terminate within timeout", logging.WARNING) - - # 4. Wait for the shutdown coroutine to complete if it was scheduled - if shutdown_future: - self.language_server.logger.log("Waiting for async shutdown to complete", logging.INFO) - try: - shutdown_future.result(timeout=shutdown_timeout) - self.language_server.logger.log("Graceful async shutdown completed", logging.DEBUG) - except Exception as e: - self.language_server.logger.log(f"Async shutdown failed: {e}", logging.WARNING) - - # 5. Close the loop properly after graceful shutdown - if self.loop and not self.loop.is_closed(): - self.language_server.logger.log("Closing event loop", logging.INFO) - try: - self.loop.close() - self.language_server.logger.log("Event loop closed successfully", logging.DEBUG) - except Exception as e: - self.language_server.logger.log(f"Event loop close failed: {e}", logging.WARNING) - - else: - # WINDOWS NUCLEAR OPTION: Skip graceful shutdown - go straight to nuclear - # Windows IocpProactor has too many issues with proper cleanup - self.language_server.logger.log("Windows detected - using nuclear shutdown to prevent zombies", logging.WARNING) - - # Force stop the loop immediately without waiting for async cleanup - if self.loop and self.loop.is_running(): - try: - self.loop.call_soon_threadsafe(lambda: setattr(self.loop, '_stopping', True)) - self.loop.call_soon_threadsafe(self.loop.stop) - except Exception: - pass - - # Force mark loop as closed IMMEDIATELY to prevent zombies - try: - if hasattr(self.loop, '_closed'): - self.loop._closed = True - if hasattr(self.loop, '_stopping'): - self.loop._stopping = True - if hasattr(self.loop, '_ready'): - self.loop._ready.clear() - except Exception: - pass - - # Give thread minimal time to exit, then abandon it - if self.loop_thread: - self.loop_thread.join(timeout=1.0) # Only 1 second on Windows - if self.loop_thread.is_alive(): - self.language_server.logger.log("Thread stuck - abandoning to prevent GC deadlock", logging.WARNING) - - # 6. Null out everything immediately - same for both platforms - self.loop = None - self.loop_thread = None - self._server_context = None - self._is_shutting_down = False - - shutdown_type = "Nuclear" if is_windows else "Graceful" - self.language_server.logger.log(f"{shutdown_type} shutdown complete - all references cleared", logging.INFO) - - def save_cache(self): - """ - Save the cache to a file. - """ - self.language_server.save_cache() - - def load_cache(self): - """ - Load the cache from a file. - """ - self.language_server.load_cache() - - def is_ignored_dirname(self, dirname: str) -> bool: - """ - A language-specific condition for directories that should be ignored always. For example, venv - in Python and node_modules in JS/TS should be ignored always. - """ - return self.language_server.is_ignored_dirname(dirname) - - def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool: - """ - Whether the given path should be ignored. - """ - return self.language_server.is_ignored_path(relative_path, ignore_unsupported_files=ignore_unsupported_files) - - def get_ignore_spec(self) -> pathspec.PathSpec: - """Returns the pathspec matcher for the paths that were configured to be ignored through - the multilspy config. - - This is is a subset of the full language-specific ignore spec that determines - which files are relevant for the language server. - - This matcher is useful for operations outside of the language server, - such as when searching for relevant non-language files in the project. - """ - return self.language_server.get_ignore_spec() diff --git a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py b/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py deleted file mode 100644 index 6960b9f..0000000 --- a/src/multilspy/language_servers/clangd_language_server/clangd_language_server.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. -""" - -import asyncio -import json -import logging -import os -import stat -import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator - -from solidlsp.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_utils import FileUtils -from solidlsp.multilspy_utils import PlatformUtils - - -class ClangdLanguageServer(LanguageServer): - """ - Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. - As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly. - Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example. - """ - - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - """ - 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) - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), - "cpp", - ) - self.server_ready = asyncio.Event() - - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: - """ - Setup runtime dependencies for ClangdLanguageServer. - """ - platform_id = PlatformUtils.get_platform_id() - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: - d = json.load(f) - del d["_description"] - - assert platform_id.value in [ - "linux-x64", - "win-x64", - "osx-arm64", - ], "Unsupported platform: " + platform_id.value - - runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] - assert len(runtime_dependencies) == 1 - # Select dependency matching the current platform - dependency = next((dep for dep in runtime_dependencies if dep["platformId"] == platform_id.value), None) - if dependency is None: - raise RuntimeError(f"No runtime dependency found for platform {platform_id.value}") - - clangd_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clangd") - clangd_executable_path = os.path.join(clangd_ls_dir, "clangd_19.1.2", "bin", dependency["binaryName"]) - if not os.path.exists(clangd_executable_path): - clangd_url = dependency["url"] - logger.log(f"Clangd executable not found at {clangd_executable_path}. Downloading from {clangd_url}", logging.INFO) - os.makedirs(clangd_ls_dir, exist_ok=True) - if dependency["archiveType"] == "zip": - FileUtils.download_and_extract_archive( - logger, clangd_url, clangd_ls_dir, dependency["archiveType"] - ) - else: - raise RuntimeError(f"Unsupported archive type: {dependency['archiveType']}") - if not os.path.exists(clangd_executable_path): - raise FileNotFoundError( - f"Clangd executable not found at {clangd_executable_path}.\n" - "Make sure you have installed clangd. See https://clangd.llvm.org/installation" - ) - os.chmod(clangd_executable_path, stat.S_IEXEC) - - return clangd_executable_path - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the clangd Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r") as f: - d = json.load(f) - - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["ClangdLanguageServer"]: - """ - Starts the Clangd Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - """ - async def register_capability_handler(params): - assert "registrations" in params - for registration in params["registrations"]: - if registration["method"] == "workspace/executeCommand": - self.initialize_searcher_command_available.set() - self.resolve_main_method_available.set() - return - - async def lang_status_handler(params): - # TODO: Should we wait for - # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} - # Before proceeding? - if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": - self.service_ready_event.set() - - async def execute_client_command_handler(params): - return [] - - async def do_nothing(params): - return - - async def check_experimental_status(params): - if params["quiescent"] == True: - self.server_ready.set() - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - self.server.on_request("client/registerCapability", register_capability_handler) - self.server.on_notification("language/status", lang_status_handler) - 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) - self.server.on_notification("experimental/serverStatus", check_experimental_status) - - async with super().start_server(): - self.logger.log("Starting Clangd server process", logging.INFO) - await 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, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": ['.', '<', '>', ':', '"', '/', '*'], - "resolveProvider": False, - } - - self.server.notify.initialized({}) - - self.completions_available.set() - # set ready flag - self.server_ready.set() - await self.server_ready.wait() - - yield self diff --git a/src/multilspy/language_servers/clangd_language_server/initialize_params.json b/src/multilspy/language_servers/clangd_language_server/initialize_params.json deleted file mode 100644 index 4330560..0000000 --- a/src/multilspy/language_servers/clangd_language_server/initialize_params.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - "textDocument": { - "synchronization": { - "didSave": true, - "dynamicRegistration": true - }, - "completion": { - "dynamicRegistration": true, - "completionItem": { - "snippetSupport": true - } - }, - "definition": { - "dynamicRegistration": true - } - }, - "workspace": { - "workspaceFolders": true, - "didChangeConfiguration": { - "dynamicRegistration": true - } - } - }, - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/multilspy/language_servers/clangd_language_server/runtime_dependencies.json b/src/multilspy/language_servers/clangd_language_server/runtime_dependencies.json deleted file mode 100644 index 53f5b7f..0000000 --- a/src/multilspy/language_servers/clangd_language_server/runtime_dependencies.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running Clangd.", - "runtimeDependencies": [ - { - "id": "Clangd", - "description": "Clangd for Linux (x64)", - "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip", - "platformId": "linux-x64", - "archiveType": "zip", - "binaryName": "clangd" - }, - { - "id": "Clangd", - "description": "Clangd for Windows (x64)", - "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip", - "platformId": "windows-x64", - "archiveType": "zip", - "binaryName": "clangd.exe" - }, - { - "id": "Clangd", - "description": "Clangd for macOS (Arm64)", - "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", - "platformId": "osx-arm64", - "archiveType": "zip", - "binaryName": "clangd" - } - ] -} diff --git a/src/multilspy/language_servers/dart_language_server/dart_language_server.py b/src/multilspy/language_servers/dart_language_server/dart_language_server.py deleted file mode 100644 index 8f9aaab..0000000 --- a/src/multilspy/language_servers/dart_language_server/dart_language_server.py +++ /dev/null @@ -1,142 +0,0 @@ -from contextlib import asynccontextmanager -import logging -import os -import pathlib -import stat -from typing import AsyncIterator -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -import json -from solidlsp.multilspy_utils import FileUtils, PlatformUtils - - -class DartLanguageServer(LanguageServer): - """ - Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. - """ - - def __init__(self, config, logger, repository_root_path): - """ - Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. - """ - - executable_path = self.setup_runtime_dependencies(logger) - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), - "dart", - ) - - def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str: - platform_id = PlatformUtils.get_platform_id() - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: - d = json.load(f) - del d["_description"] - - runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] - - assert len(runtime_dependencies) == 1 - dependency = runtime_dependencies[0] - - dart_ls_dir = os.path.join(os.path.dirname(__file__), "static", "dart-language-server") - dart_executable_path = os.path.join(dart_ls_dir, dependency["binaryName"]) - - if not os.path.exists(dart_ls_dir): - os.makedirs(dart_ls_dir) - FileUtils.download_and_extract_archive( - logger, dependency["url"], dart_ls_dir, dependency["archiveType"] - ) - - - assert os.path.exists(dart_executable_path) - os.chmod(dart_executable_path, stat.S_IEXEC) - - return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" - - - def _get_initialize_params(self, repository_absolute_path: str): - """ - Returns the initialize params for the Dart Language Server. - """ - with open( - os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r" - ) as f: - d = json.load(f) - - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path( - repository_absolute_path - ).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["DartLanguageServer"]: - """ - Start the language server and yield when the server is ready. - """ - - async def execute_client_command_handler(params): - return [] - - async def do_nothing(params): - return - - async def check_experimental_status(params): - pass - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - 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) - self.server.on_notification( - "experimental/serverStatus", check_experimental_status - ) - - async with super().start_server(): - self.logger.log( - "Starting dart-language-server server process", logging.INFO - ) - await 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, - ) - init_response = await self.server.send_request( - "initialize", initialize_params - ) - self.logger.log( - f"Received initialize response from dart-language-server: {init_response}", - logging.INFO, - ) - - self.server.notify.initialized({}) - - yield self diff --git a/src/multilspy/language_servers/dart_language_server/initialize_params.json b/src/multilspy/language_servers/dart_language_server/initialize_params.json deleted file mode 100644 index e90e408..0000000 --- a/src/multilspy/language_servers/dart_language_server/initialize_params.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "_description": "This file contains the initialization parameters for the Dart Language Server.", - "processId": "$processId", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": {}, - "initializationOptions": { - "onlyAnalyzeProjectsWithOpenFiles": false, - "suggestFromUnimportedLibraries": true, - "closingLabels": false, - "outline": false, - "flutterOutline": false, - "allowOpenUri": false - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] - -} \ No newline at end of file diff --git a/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json b/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json deleted file mode 100644 index 32e4929..0000000 --- a/src/multilspy/language_servers/dart_language_server/runtime_dependencies.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running Dart Language Server, downloaded from https://dart.dev/get-dart/archive", - "runtimeDependencies": [ - { - "id": "DartLanguageServer", - "description": "Dart Language Server for Linux (x64)", - "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip", - "platformId": "linux-x64", - "archiveType": "zip", - "binaryName": "dart-sdk/bin/dart" - }, - { - "id": "DartLanguageServer", - "description": "Dart Language Server for Windows (x64)", - "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip", - "platformId": "win-x64", - "archiveType": "zip", - "binaryName": "dart-sdk/bin/dart.exe" - }, - { - "id": "DartLanguageServer", - "description": "Dart Language Server for Windows (arm64)", - "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-arm64-release.zip", - "platformId": "win-arm64", - "archiveType": "zip", - "binaryName": "dart-sdk/bin/dart.exe" - }, - { - "id": "DartLanguageServer", - "description": "Dart Language Server for macOS (x64)", - "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip", - "platformId": "osx-x64", - "archiveType": "zip", - "binaryName": "dart-sdk/bin/dart" - }, - { - "id": "DartLanguageServer", - "description": "Dart Language Server for macOS (arm64)", - "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip", - "platformId": "osx-arm64", - "archiveType": "zip", - "binaryName": "dart-sdk/bin/dart" - } - ] -} \ No newline at end of file diff --git a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py deleted file mode 100644 index 103c976..0000000 --- a/src/multilspy/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ /dev/null @@ -1,422 +0,0 @@ -""" -Provides Java specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Java. -""" - -import asyncio -import dataclasses -import json -import logging -import os -import pathlib -import shutil -import stat -import uuid -from contextlib import asynccontextmanager -from typing import AsyncIterator - -from overrides import override - -from solidlsp.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_settings import MultilspySettings -from solidlsp.multilspy_utils import FileUtils -from solidlsp.multilspy_utils import PlatformUtils -from pathlib import PurePath - - -@dataclasses.dataclass -class RuntimeDependencyPaths: - """ - Stores the paths to the runtime dependencies of EclipseJDTLS - """ - - gradle_path: str - lombok_jar_path: str - jre_path: str - jre_home_path: str - jdtls_launcher_jar_path: str - jdtls_readonly_config_path: str - intellicode_jar_path: str - intellisense_members_path: str - - -class EclipseJDTLS(LanguageServer): - """ - The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class - """ - - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - """ - 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) - self.runtime_dependency_paths = runtime_dependency_paths - - # ws_dir is the workspace directory for the EclipseJDTLS server - ws_dir = str( - PurePath( - MultilspySettings.get_language_server_directory(), - "EclipseJDTLS", - "workspaces", - uuid.uuid4().hex, - ) - ) - - # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces - shared_cache_location = str( - PurePath(MultilspySettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex") - ) - - jre_path = self.runtime_dependency_paths.jre_path - lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path - - jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path - - os.makedirs(ws_dir, exist_ok=True) - - data_dir = str(PurePath(ws_dir, "data_dir")) - jdtls_config_path = str(PurePath(ws_dir, "config_path")) - - jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path - - if not os.path.exists(jdtls_config_path): - shutil.copytree(jdtls_readonly_config_path, jdtls_config_path) - - for static_path in [ - jre_path, - lombok_jar_path, - jdtls_launcher_jar, - jdtls_config_path, - jdtls_readonly_config_path, - ]: - assert os.path.exists(static_path), static_path - - # TODO: Add "self.runtime_dependency_paths.jre_home_path"/bin to $PATH as well - proc_env = {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path} - proc_cwd = repository_root_path - cmd = " ".join( - [ - jre_path, - "--add-modules=ALL-SYSTEM", - "--add-opens", - "java.base/java.util=ALL-UNNAMED", - "--add-opens", - "java.base/java.lang=ALL-UNNAMED", - "--add-opens", - "java.base/sun.nio.fs=ALL-UNNAMED", - "-Declipse.application=org.eclipse.jdt.ls.core.id1", - "-Dosgi.bundles.defaultStartLevel=4", - "-Declipse.product=org.eclipse.jdt.ls.core.product", - "-Djava.import.generatesMetadataFilesAtProjectRoot=false", - "-Dfile.encoding=utf8", - "-noverify", - "-XX:+UseParallelGC", - "-XX:GCTimeRatio=4", - "-XX:AdaptiveSizePolicyWeight=90", - "-Dsun.zip.disableMemoryMapping=true", - "-Djava.lsp.joinOnCompletion=true", - "-Xmx3G", - "-Xms100m", - "-Xlog:disable", - "-Dlog.level=ALL", - f'"-javaagent:{lombok_jar_path}"', - f'"-Djdt.core.sharedIndexLocation={shared_cache_location}"', - "-jar", - f'"{jdtls_launcher_jar}"', - "-configuration", - f'"{jdtls_config_path}"', - "-data", - f'"{data_dir}"', - ] - ) - - self.service_ready_event = asyncio.Event() - self.intellicode_enable_command_available = asyncio.Event() - self.initialize_searcher_command_available = asyncio.Event() - - super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java") - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - # Ignore common Java build directories from different build tools: - # - Maven: target - # - Gradle: build, .gradle - # - Eclipse: bin, .settings - # - IntelliJ IDEA: out, .idea - # - General: classes, dist, lib - return super().is_ignored_dirname(dirname) or dirname in [ - "target", # Maven - "build", # Gradle - "bin", # Eclipse - "out", # IntelliJ IDEA - "classes", # General - "dist", # General - "lib" # General - ] - - def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> RuntimeDependencyPaths: - """ - Setup runtime dependencies for EclipseJDTLS. - """ - platformId = PlatformUtils.get_platform_id() - - with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), "r", encoding="utf-8") as f: - runtimeDependencies = json.load(f) - del runtimeDependencies["_description"] - - os.makedirs(str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static")), exist_ok=True) - - # assert platformId.value in [ - # "linux-x64", - # "win-x64", - # ], "Only linux-x64 platform is supported for in multilspy at the moment" - - gradle_path = str( - PurePath( - os.path.abspath(os.path.dirname(__file__)), - "static/gradle-7.3.3", - ) - ) - - if not os.path.exists(gradle_path): - FileUtils.download_and_extract_archive( - logger, - runtimeDependencies["gradle"]["platform-agnostic"]["url"], - str(PurePath(gradle_path).parent), - runtimeDependencies["gradle"]["platform-agnostic"]["archiveType"], - ) - - assert os.path.exists(gradle_path) - - dependency = runtimeDependencies["vscode-java"][platformId.value] - vscode_java_path = str( - PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]) - ) - os.makedirs(vscode_java_path, exist_ok=True) - jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"])) - jre_path = str(PurePath(vscode_java_path, dependency["jre_path"])) - lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"])) - jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"])) - jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"])) - if not all( - [ - os.path.exists(vscode_java_path), - os.path.exists(jre_home_path), - os.path.exists(jre_path), - os.path.exists(lombok_jar_path), - os.path.exists(jdtls_launcher_jar_path), - os.path.exists(jdtls_readonly_config_path), - ] - ): - FileUtils.download_and_extract_archive( - logger, dependency["url"], vscode_java_path, dependency["archiveType"] - ) - - os.chmod(jre_path, stat.S_IEXEC) - - assert os.path.exists(vscode_java_path) - assert os.path.exists(jre_home_path) - assert os.path.exists(jre_path) - assert os.path.exists(lombok_jar_path) - assert os.path.exists(jdtls_launcher_jar_path) - assert os.path.exists(jdtls_readonly_config_path) - - dependency = runtimeDependencies["intellicode"]["platform-agnostic"] - intellicode_directory_path = str( - PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]) - ) - os.makedirs(intellicode_directory_path, exist_ok=True) - intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"])) - intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"])) - if not all( - [ - os.path.exists(intellicode_directory_path), - os.path.exists(intellicode_jar_path), - os.path.exists(intellisense_members_path), - ] - ): - FileUtils.download_and_extract_archive( - logger, dependency["url"], intellicode_directory_path, dependency["archiveType"] - ) - - assert os.path.exists(intellicode_directory_path) - assert os.path.exists(intellicode_jar_path) - assert os.path.exists(intellisense_members_path) - - return RuntimeDependencyPaths( - gradle_path=gradle_path, - lombok_jar_path=lombok_jar_path, - jre_path=jre_path, - jre_home_path=jre_home_path, - jdtls_launcher_jar_path=jdtls_launcher_jar_path, - jdtls_readonly_config_path=jdtls_readonly_config_path, - intellicode_jar_path=intellicode_jar_path, - intellisense_members_path=intellisense_members_path, - ) - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize parameters for the EclipseJDTLS server. - """ - # Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available - with open(str(PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: - d: InitializeParams = json.load(f) - - del d["_description"] - - if not os.path.isabs(repository_absolute_path): - repository_absolute_path = os.path.abspath(repository_absolute_path) - - assert d["processId"] == "os.getpid()" - d["processId"] = os.getpid() - - assert d["rootPath"] == "repository_absolute_path" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "pathlib.Path(repository_absolute_path).as_uri()" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["initializationOptions"]["workspaceFolders"] == "[pathlib.Path(repository_absolute_path).as_uri()]" - d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()] - - assert ( - d["workspaceFolders"] - == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' - ) - d["workspaceFolders"] = [ - { - "uri": pathlib.Path(repository_absolute_path).as_uri(), - "name": os.path.basename(repository_absolute_path), - } - ] - - assert d["initializationOptions"]["bundles"] == ["intellicode-core.jar"] - bundles = [self.runtime_dependency_paths.intellicode_jar_path] - d["initializationOptions"]["bundles"] = bundles - - assert d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] == [ - {"name": "JavaSE-21", "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": True} - ] - d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [ - {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True} - ] - - for runtime in d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]: - assert "name" in runtime - assert "path" in runtime - assert os.path.exists( - runtime["path"] - ), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist" - - assert d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] == "abs(static/gradle-7.3.3)" - d["initializationOptions"]["settings"]["java"]["import"]["gradle"][ - "home" - ] = self.runtime_dependency_paths.gradle_path - - d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"][ - "home" - ] = self.runtime_dependency_paths.jre_path - - return d - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["EclipseJDTLS"]: - """ - Starts the Eclipse JDTLS Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` - """ - - async def register_capability_handler(params): - assert "registrations" in params - for registration in params["registrations"]: - if registration["method"] == "textDocument/completion": - assert registration["registerOptions"]["resolveProvider"] == True - assert registration["registerOptions"]["triggerCharacters"] == [ - ".", - "@", - "#", - "*", - " ", - ] - self.completions_available.set() - if registration["method"] == "workspace/executeCommand": - if "java.intellicode.enable" in registration["registerOptions"]["commands"]: - self.intellicode_enable_command_available.set() - return - - async def lang_status_handler(params): - # TODO: Should we wait for - # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} - # Before proceeding? - if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": - self.service_ready_event.set() - - async def execute_client_command_handler(params): - assert params["command"] == "_java.reloadBundles.command" - assert params["arguments"] == [] - return [] - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - async def do_nothing(params): - return - - self.server.on_request("client/registerCapability", register_capability_handler) - self.server.on_notification("language/status", lang_status_handler) - 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) - - async with super().start_server(): - self.logger.log("Starting EclipseJDTLS server process", logging.INFO) - await 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, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" not in init_response["capabilities"] - assert "executeCommandProvider" not in init_response["capabilities"] - - self.server.notify.initialized({}) - - self.server.notify.workspace_did_change_configuration( - {"settings": initialize_params["initializationOptions"]["settings"]} - ) - - await self.intellicode_enable_command_available.wait() - - java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path - assert os.path.exists(java_intellisense_members_path) - intellicode_enable_result = await self.server.send.execute_command( - { - "command": "java.intellicode.enable", - "arguments": [True, java_intellisense_members_path], - } - ) - assert intellicode_enable_result - - # TODO: Add comments about why we wait here, and how this can be optimized - await self.service_ready_event.wait() - - yield self diff --git a/src/multilspy/language_servers/eclipse_jdtls/initialize_params.json b/src/multilspy/language_servers/eclipse_jdtls/initialize_params.json deleted file mode 100644 index b65b7b6..0000000 --- a/src/multilspy/language_servers/eclipse_jdtls/initialize_params.json +++ /dev/null @@ -1,849 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "clientInfo": { - "name": "Visual Studio Code - Insiders", - "version": "1.77.0-insider" - }, - "locale": "en", - "rootPath": "repository_absolute_path", - "rootUri": "pathlib.Path(repository_absolute_path).as_uri()", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "configuration": true, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": false, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": false, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": true - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "window": { - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": true - } - }, - "showDocument": { - "support": true - }, - "workDoneProgress": true - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0" - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - } - }, - "initializationOptions": { - "bundles": [ - "intellicode-core.jar" - ], - "workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]", - "settings": { - "java": { - "home": null, - "jdt": { - "ls": { - "java": { - "home": null - }, - "vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -Xlog:disable", - "lombokSupport": { - "enabled": true - }, - "protobufSupport": { - "enabled": true - }, - "androidSupport": { - "enabled": true - } - } - }, - "errors": { - "incompleteClasspath": { - "severity": "error" - } - }, - "configuration": { - "checkProjectSettingsExclusions": false, - "updateBuildConfiguration": "interactive", - "maven": { - "userSettings": null, - "globalSettings": null, - "notCoveredPluginExecutionSeverity": "warning", - "defaultMojoExecutionAction": "ignore" - }, - "workspaceCacheLimit": 90, - "runtimes": [ - { - "name": "JavaSE-21", - "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", - "default": true - } - ] - }, - "trace": { - "server": "verbose" - }, - "import": { - "maven": { - "enabled": true, - "offline": { - "enabled": false - }, - "disableTestClasspathFlag": false - }, - "gradle": { - "enabled": true, - "wrapper": { - "enabled": true - }, - "version": null, - "home": "abs(static/gradle-7.3.3)", - "java": { - "home": "abs(static/launch_jres/21.0.7-linux-x86_64)" - }, - "offline": { - "enabled": false - }, - "arguments": null, - "jvmArguments": null, - "user": { - "home": null - }, - "annotationProcessing": { - "enabled": true - } - }, - "exclusions": [ - "**/node_modules/**", - "**/.metadata/**", - "**/archetype-resources/**", - "**/META-INF/maven/**" - ], - "generatesMetadataFilesAtProjectRoot": false - }, - "maven": { - "downloadSources": true, - "updateSnapshots": true - }, - "eclipse": { - "downloadSources": true - }, - "referencesCodeLens": { - "enabled": true - }, - "signatureHelp": { - "enabled": true, - "description": { - "enabled": true - } - }, - "implementationsCodeLens": { - "enabled": true - }, - "format": { - "enabled": true, - "settings": { - "url": null, - "profile": null - }, - "comments": { - "enabled": true - }, - "onType": { - "enabled": true - }, - "insertSpaces": true, - "tabSize": 4 - }, - "saveActions": { - "organizeImports": false - }, - "project": { - "referencedLibraries": [ - "lib/**/*.jar" - ], - "importOnFirstTimeStartup": "automatic", - "importHint": true, - "resourceFilters": [ - "node_modules", - "\\.git" - ], - "encoding": "ignore", - "exportJar": { - "targetPath": "${workspaceFolder}/${workspaceFolderBasename}.jar" - } - }, - "contentProvider": { - "preferred": null - }, - "autobuild": { - "enabled": true - }, - "maxConcurrentBuilds": 1, - "recommendations": { - "dependency": { - "analytics": { - "show": true - } - } - }, - "completion": { - "maxResults": 0, - "enabled": true, - "guessMethodArguments": true, - "favoriteStaticMembers": [ - "org.junit.Assert.*", - "org.junit.Assume.*", - "org.junit.jupiter.api.Assertions.*", - "org.junit.jupiter.api.Assumptions.*", - "org.junit.jupiter.api.DynamicContainer.*", - "org.junit.jupiter.api.DynamicTest.*", - "org.mockito.Mockito.*", - "org.mockito.ArgumentMatchers.*", - "org.mockito.Answers.*" - ], - "filteredTypes": [ - "java.awt.*", - "com.sun.*", - "sun.*", - "jdk.*", - "org.graalvm.*", - "io.micrometer.shaded.*" - ], - "importOrder": [ - "#", - "java", - "javax", - "org", - "com", - "" - ], - "postfix": { - "enabled": false - }, - "matchCase": "off" - }, - "foldingRange": { - "enabled": true - }, - "progressReports": { - "enabled": false - }, - "codeGeneration": { - "hashCodeEquals": { - "useJava7Objects": false, - "useInstanceof": false - }, - "useBlocks": false, - "generateComments": false, - "toString": { - "template": "${object.className} [${member.name()}=${member.value}, ${otherMembers}]", - "codeStyle": "STRING_CONCATENATION", - "skipNullValues": false, - "listArrayContents": true, - "limitElements": 0 - }, - "insertionLocation": "afterCursor" - }, - "selectionRange": { - "enabled": true - }, - "showBuildStatusOnStart": { - "enabled": "notification" - }, - "server": { - "launchMode": "Standard" - }, - "sources": { - "organizeImports": { - "starThreshold": 99, - "staticStarThreshold": 99 - } - }, - "imports": { - "gradle": { - "wrapper": { - "checksums": [] - } - } - }, - "templates": { - "fileHeader": [], - "typeComment": [] - }, - "references": { - "includeAccessors": true, - "includeDecompiledSources": true - }, - "typeHierarchy": { - "lazyLoad": false - }, - "settings": { - "url": null - }, - "symbols": { - "includeSourceMethodDeclarations": false - }, - "quickfix": { - "showAt": "line" - }, - "inlayHints": { - "parameterNames": { - "enabled": "literals", - "exclusions": [] - } - }, - "codeAction": { - "sortMembers": { - "avoidVolatileChanges": true - } - }, - "compile": { - "nullAnalysis": { - "nonnull": [ - "javax.annotation.Nonnull", - "org.eclipse.jdt.annotation.NonNull", - "org.springframework.lang.NonNull" - ], - "nullable": [ - "javax.annotation.Nullable", - "org.eclipse.jdt.annotation.Nullable", - "org.springframework.lang.Nullable" - ], - "mode": "automatic" - } - }, - "cleanup": { - "actionsOnSave": [] - }, - "sharedIndexes": { - "enabled": "auto", - "location": "" - }, - "refactoring": { - "extract": { - "interface": { - "replace": true - } - } - }, - "debug": { - "logLevel": "verbose", - "settings": { - "showHex": false, - "showStaticVariables": false, - "showQualifiedNames": false, - "showLogicalStructure": true, - "showToString": true, - "maxStringLength": 0, - "numericPrecision": 0, - "hotCodeReplace": "manual", - "enableRunDebugCodeLens": true, - "forceBuildBeforeLaunch": true, - "onBuildFailureProceed": false, - "console": "integratedTerminal", - "exceptionBreakpoint": { - "skipClasses": [] - }, - "stepping": { - "skipClasses": [], - "skipSynthetics": false, - "skipStaticInitializers": false, - "skipConstructors": false - }, - "jdwp": { - "limitOfVariablesPerJdwpRequest": 100, - "requestTimeout": 3000, - "async": "auto" - }, - "vmArgs": "" - } - }, - "silentNotification": false, - "dependency": { - "showMembers": false, - "syncWithFolderExplorer": true, - "autoRefresh": true, - "refreshDelay": 2000, - "packagePresentation": "flat" - }, - "help": { - "firstView": "auto", - "showReleaseNotes": true, - "collectErrorLog": false - }, - "test": { - "defaultConfig": "", - "config": {} - } - } - }, - "extendedClientCapabilities": { - "progressReportProvider": false, - "classFileContentsSupport": true, - "overrideMethodsPromptSupport": true, - "hashCodeEqualsPromptSupport": true, - "advancedOrganizeImportsSupport": true, - "generateToStringPromptSupport": true, - "advancedGenerateAccessorsSupport": true, - "generateConstructorsPromptSupport": true, - "generateDelegateMethodsPromptSupport": true, - "advancedExtractRefactoringSupport": true, - "inferSelectionSupport": [ - "extractMethod", - "extractVariable", - "extractField" - ], - "moveRefactoringSupport": true, - "clientHoverProvider": true, - "clientDocumentSymbolProvider": true, - "gradleChecksumWrapperPromptSupport": true, - "resolveAdditionalTextEditsSupport": true, - "advancedIntroduceParameterRefactoringSupport": true, - "actionableRuntimeNotificationSupport": true, - "shouldLanguageServerExitOnShutdown": true, - "onCompletionItemSelectedCommand": "editor.action.triggerParameterHints", - "extractInterfaceSupport": true, - "advancedUpgradeGradleSupport": true - }, - "triggerFiles": [] - }, - "trace": "verbose", - "workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]" -} \ No newline at end of file diff --git a/src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json b/src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json deleted file mode 100644 index 1b00040..0000000 --- a/src/multilspy/language_servers/eclipse_jdtls/runtime_dependencies.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "_description": "This file lists the runtime dependencies for the Java Language Server", - "gradle": { - "platform-agnostic": { - "url": "https://services.gradle.org/distributions/gradle-7.3.3-bin.zip", - "archiveType": "zip", - "relative_extraction_path": "." - } - }, - "vscode-java": { - "darwin-arm64": { - "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", - "archiveType": "zip", - "relative_extraction_path": "vscode-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", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-macosx-aarch64", - "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_mac_arm" - }, - "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", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-macosx-x86_64", - "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_mac" - }, - "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", - "relative_extraction_path": "vscode-java" - }, - "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", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-linux-x86_64", - "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_linux" - }, - "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", - "relative_extraction_path": "vscode-java", - "jre_home_path": "extension/jre/21.0.7-win32-x86_64", - "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", - "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", - "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", - "jdtls_readonly_config_path": "extension/server/config_win" - } - }, - "intellicode": { - "platform-agnostic": { - "url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage", - "alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage", - "archiveType": "zip", - "relative_extraction_path": "intellicode", - "intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar", - "intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members" - } - } -} diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py deleted file mode 100644 index 117f5c2..0000000 --- a/src/multilspy/language_servers/gopls/gopls.py +++ /dev/null @@ -1,151 +0,0 @@ -import asyncio -import json -import logging -import os -import pathlib -import subprocess -from contextlib import asynccontextmanager -from typing import AsyncIterator - -from overrides import override - -from solidlsp.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.multilspy_config import MultilspyConfig - - -class Gopls(LanguageServer): - """ - Provides Go specific instantiation of the LanguageServer class using gopls. - """ - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - # For Go projects, we should ignore: - # - vendor: third-party dependencies vendored into the project - # - node_modules: if the project has JavaScript components - # - dist/build: common output directories - return super().is_ignored_dirname(dirname) or dirname in ["vendor", "node_modules", "dist", "build"] - - @staticmethod - def _get_go_version(): - """Get the installed Go version or None if not found.""" - try: - result = subprocess.run(['go', 'version'], capture_output=True, text=True) - if result.returncode == 0: - return result.stdout.strip() - except FileNotFoundError: - return None - return None - - @staticmethod - def _get_gopls_version(): - """Get the installed gopls version or None if not found.""" - try: - result = subprocess.run(['gopls', 'version'], capture_output=True, text=True) - if result.returncode == 0: - return result.stdout.strip() - except FileNotFoundError: - return None - return None - - @classmethod - def setup_runtime_dependency(cls): - """ - Check if required Go runtime dependencies are available. - Raises RuntimeError with helpful message if dependencies are missing. - """ - go_version = cls._get_go_version() - if not go_version: - raise RuntimeError("Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH.") - - gopls_version = cls._get_gopls_version() - if not gopls_version: - raise RuntimeError( - "Found a Go version but gopls is not installed.\n" - "Please install gopls as described in https://pkg.go.dev/golang.org/x/tools/gopls#section-readme\n\n" - "After installation, make sure it is added to your PATH (it might be installed in a different location than Go)." - ) - - return True - - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - self.setup_runtime_dependency() - - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), - "go", - ) - self.server_ready = asyncio.Event() - self.request_id = 0 - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the TypeScript Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: - d = json.load(f) - - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["Gopls"]: - """Start gopls server process""" - async def register_capability_handler(params): - return - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - async def do_nothing(params): - 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) - - async with super().start_server(): - self.logger.log("Starting gopls server process", logging.INFO) - await 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, - ) - init_response = await self.server.send.initialize(initialize_params) - - # Verify server capabilities - assert "textDocumentSync" in init_response["capabilities"] - assert "completionProvider" in init_response["capabilities"] - assert "definitionProvider" in init_response["capabilities"] - - self.server.notify.initialized({}) - self.completions_available.set() - - # gopls server is typically ready immediately after initialization - self.server_ready.set() - await self.server_ready.wait() - - yield self diff --git a/src/multilspy/language_servers/gopls/initialize_params.json b/src/multilspy/language_servers/gopls/initialize_params.json deleted file mode 100644 index 40592e1..0000000 --- a/src/multilspy/language_servers/gopls/initialize_params.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - "textDocument": { - "synchronization": { - "didSave": true, - "dynamicRegistration": true - }, - "completion": { - "dynamicRegistration": true, - "completionItem": { - "snippetSupport": true - } - }, - "definition": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": true, - "hierarchicalDocumentSymbolSupport": true, - "symbolKind": { - "valueSet": [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 - ] - } - } - }, - "workspace": { - "workspaceFolders": true, - "didChangeConfiguration": { - "dynamicRegistration": true - } - } - }, - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/multilspy/language_servers/intelephense/initialize_params.json b/src/multilspy/language_servers/intelephense/initialize_params.json deleted file mode 100644 index 4330560..0000000 --- a/src/multilspy/language_servers/intelephense/initialize_params.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - "textDocument": { - "synchronization": { - "didSave": true, - "dynamicRegistration": true - }, - "completion": { - "dynamicRegistration": true, - "completionItem": { - "snippetSupport": true - } - }, - "definition": { - "dynamicRegistration": true - } - }, - "workspace": { - "workspaceFolders": true, - "didChangeConfiguration": { - "dynamicRegistration": true - } - } - }, - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/multilspy/language_servers/intelephense/intelephense.py b/src/multilspy/language_servers/intelephense/intelephense.py deleted file mode 100644 index 5a023fc..0000000 --- a/src/multilspy/language_servers/intelephense/intelephense.py +++ /dev/null @@ -1,200 +0,0 @@ -""" -Provides PHP specific instantiation of the LanguageServer class using Intelephense. -""" - -import asyncio -import json -import shutil -import logging -import os -import subprocess -import pathlib -from contextlib import asynccontextmanager -from time import sleep -from typing import AsyncIterator - -from overrides import override - -from solidlsp.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_utils import PlatformUtils, PlatformId - -class Intelephense(LanguageServer): - """ - Provides PHP specific instantiation of the LanguageServer class using Intelephense. - """ - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - # For PHP projects, we should ignore: - # - vendor: third-party dependencies managed by Composer - # - node_modules: if the project has JavaScript components - # - cache: commonly used for caching - return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] - - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: - """ - Setup runtime dependencies for Intelephense. - """ - platform_id = PlatformUtils.get_platform_id() - - valid_platforms = [ - PlatformId.LINUX_x64, - PlatformId.LINUX_arm64, - PlatformId.OSX, - 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 multilspy PHP at the moment" - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: - d = json.load(f) - del d["_description"] - - runtime_dependencies = d.get("runtimeDependencies", []) - intelephense_ls_dir = os.path.join(os.path.dirname(__file__), "static", "php-lsp") - - # Verify both node and npm are installed - is_node_installed = shutil.which('node') is not None - assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." - is_npm_installed = shutil.which('npm') is not None - assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - - # Install intelephense if not already installed - if not os.path.exists(intelephense_ls_dir): - os.makedirs(intelephense_ls_dir, exist_ok=True) - for dependency in runtime_dependencies: - # Windows doesn't support the 'user' parameter and doesn't have pwd module - if PlatformUtils.get_platform_id().value.startswith("win"): - subprocess.run( - dependency["command"], - shell=True, - check=True, - cwd=intelephense_ls_dir, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - else: - # On Unix-like systems, run as non-root user - import pwd - user = pwd.getpwuid(os.getuid()).pw_name - subprocess.run( - dependency["command"], - shell=True, - check=True, - user=user, - cwd=intelephense_ls_dir, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - - intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") - assert os.path.exists(intelephense_executable_path), "intelephense executable not found. Please install intelephense and try again." - - return f"{intelephense_executable_path} --stdio" - - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - # Setup runtime dependencies before initializing - intelephense_cmd = self.setup_runtime_dependencies(logger, config) - - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), - "php" - ) - self.server_ready = asyncio.Event() - self.request_id = 0 - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the TypeScript Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: - d = json.load(f) - - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["Intelephense"]: - """Start Intelephense server process""" - async def register_capability_handler(params): - return - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - async def do_nothing(params): - 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) - - async with super().start_server(): - self.logger.log("Starting Intelephense server process", logging.INFO) - await 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, - ) - init_response = await self.server.send.initialize(initialize_params) - self.logger.log( - "After sent initialize params", - logging.INFO, - ) - - # Verify server capabilities - assert "textDocumentSync" in init_response["capabilities"] - assert "completionProvider" in init_response["capabilities"] - assert "definitionProvider" in init_response["capabilities"] - - self.server.notify.initialized({}) - self.completions_available.set() - - # Intelephense server is typically ready immediately after initialization - self.server_ready.set() - await self.server_ready.wait() - - yield self - - @override - # For some reason, the LS may need longer to process this, so we just retry - async def _send_references_request(self, relative_file_path: str, line: int, column: int): - # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is - # despite the LS having processed requests already. I don't know what causes this, but sleeping - # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. - # May be related to the time it takes to read the files or something like that. - # The sleeping doesn't seem to be needed on all systems - sleep(1) - return await super()._send_references_request(relative_file_path, line, column) - - @override - async def _send_definition_request(self, definition_params: DefinitionParams): - # TODO: same as above, also only a problem if the definition is in another file - sleep(1) - return await super()._send_definition_request(definition_params) diff --git a/src/multilspy/language_servers/intelephense/runtime_dependencies.json b/src/multilspy/language_servers/intelephense/runtime_dependencies.json deleted file mode 100644 index e3a6d7e..0000000 --- a/src/multilspy/language_servers/intelephense/runtime_dependencies.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running intelephense. Obtained from https://www.npmjs.com/package/intelephense", - "runtimeDependencies": [ - { - "id": "intelephense", - "description": "Intelephense package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", - "command": "npm install --prefix ./ intelephense@1.14.4" - } - ] -} diff --git a/src/multilspy/language_servers/jedi_language_server/initialize_params.json b/src/multilspy/language_servers/jedi_language_server/initialize_params.json deleted file mode 100644 index 5a04872..0000000 --- a/src/multilspy/language_servers/jedi_language_server/initialize_params.json +++ /dev/null @@ -1,905 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "clientInfo": { - "name": "Visual Studio Code - Insiders", - "version": "1.81.0-insider" - }, - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "configuration": true, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": true, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": true, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": false - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "window": { - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": true - } - }, - "showDocument": { - "support": true - }, - "workDoneProgress": true - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0", - "allowedTags": [ - "ul", - "li", - "p", - "code", - "blockquote", - "ol", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "hr", - "em", - "pre", - "table", - "thead", - "tbody", - "tr", - "th", - "td", - "div", - "del", - "a", - "strong", - "br", - "img", - "span" - ] - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - }, - "experimental": { - "snippetTextEdit": true, - "codeActionGroup": true, - "hoverActions": true, - "serverStatusNotification": true, - "colorDiagnosticOutput": true, - "openServerLogs": true, - "commands": { - "commands": [ - "rust-analyzer.runSingle", - "rust-analyzer.debugSingle", - "rust-analyzer.showReferences", - "rust-analyzer.gotoLocation", - "editor.action.triggerParameterHints" - ] - } - } - }, - "initializationOptions": { - "cargoRunner": null, - "runnables": { - "extraEnv": null, - "problemMatcher": [ - "$rustc" - ], - "command": null, - "extraArgs": [] - }, - "server": { - "path": null, - "extraEnv": null - }, - "trace": { - "server": "verbose", - "extension": true - }, - "debug": { - "engine": "auto", - "sourceFileMap": { - "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" - }, - "openDebugPane": false, - "engineSettings": {} - }, - "restartServerOnConfigChange": false, - "typing": { - "continueCommentsOnNewline": true, - "autoClosingAngleBrackets": { - "enable": false - } - }, - "diagnostics": { - "previewRustcOutput": false, - "useRustcErrorCode": false, - "disabled": [], - "enable": true, - "experimental": { - "enable": false - }, - "remapPrefix": {}, - "warningsAsHint": [], - "warningsAsInfo": [] - }, - "discoverProjectCommand": null, - "showUnlinkedFileNotification": true, - "showDependenciesExplorer": true, - "assist": { - "emitMustUse": false, - "expressionFillDefault": "todo" - }, - "cachePriming": { - "enable": true, - "numThreads": 0 - }, - "cargo": { - "autoreload": true, - "buildScripts": { - "enable": true, - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "overrideCommand": null, - "useRustcWrapper": true - }, - "cfgs": {}, - "extraArgs": [], - "extraEnv": {}, - "features": [], - "noDefaultFeatures": false, - "sysroot": "discover", - "sysrootSrc": null, - "target": null, - "unsetTest": [ - "core" - ] - }, - "checkOnSave": true, - "check": { - "allTargets": true, - "command": "check", - "extraArgs": [], - "extraEnv": {}, - "features": null, - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "noDefaultFeatures": null, - "overrideCommand": null, - "targets": null - }, - "completion": { - "autoimport": { - "enable": true - }, - "autoself": { - "enable": true - }, - "callable": { - "snippets": "fill_arguments" - }, - "limit": null, - "postfix": { - "enable": true - }, - "privateEditable": { - "enable": false - }, - "snippets": { - "custom": { - "Arc::new": { - "postfix": "arc", - "body": "Arc::new(${receiver})", - "requires": "std::sync::Arc", - "description": "Put the expression into an `Arc`", - "scope": "expr" - }, - "Rc::new": { - "postfix": "rc", - "body": "Rc::new(${receiver})", - "requires": "std::rc::Rc", - "description": "Put the expression into an `Rc`", - "scope": "expr" - }, - "Box::pin": { - "postfix": "pinbox", - "body": "Box::pin(${receiver})", - "requires": "std::boxed::Box", - "description": "Put the expression into a pinned `Box`", - "scope": "expr" - }, - "Ok": { - "postfix": "ok", - "body": "Ok(${receiver})", - "description": "Wrap the expression in a `Result::Ok`", - "scope": "expr" - }, - "Err": { - "postfix": "err", - "body": "Err(${receiver})", - "description": "Wrap the expression in a `Result::Err`", - "scope": "expr" - }, - "Some": { - "postfix": "some", - "body": "Some(${receiver})", - "description": "Wrap the expression in an `Option::Some`", - "scope": "expr" - } - } - } - }, - "files": { - "excludeDirs": [], - "watcher": "client" - }, - "highlightRelated": { - "breakPoints": { - "enable": true - }, - "closureCaptures": { - "enable": true - }, - "exitPoints": { - "enable": true - }, - "references": { - "enable": true - }, - "yieldPoints": { - "enable": true - } - }, - "hover": { - "actions": { - "debug": { - "enable": true - }, - "enable": true, - "gotoTypeDef": { - "enable": true - }, - "implementations": { - "enable": true - }, - "references": { - "enable": false - }, - "run": { - "enable": true - } - }, - "documentation": { - "enable": true, - "keywords": { - "enable": true - } - }, - "links": { - "enable": true - }, - "memoryLayout": { - "alignment": "hexadecimal", - "enable": true, - "niches": false, - "offset": "hexadecimal", - "size": "both" - } - }, - "imports": { - "granularity": { - "enforce": false, - "group": "crate" - }, - "group": { - "enable": true - }, - "merge": { - "glob": true - }, - "prefer": { - "no": { - "std": false - } - }, - "prefix": "plain" - }, - "inlayHints": { - "bindingModeHints": { - "enable": false - }, - "chainingHints": { - "enable": true - }, - "closingBraceHints": { - "enable": true, - "minLines": 25 - }, - "closureCaptureHints": { - "enable": false - }, - "closureReturnTypeHints": { - "enable": "never" - }, - "closureStyle": "impl_fn", - "discriminantHints": { - "enable": "never" - }, - "expressionAdjustmentHints": { - "enable": "never", - "hideOutsideUnsafe": false, - "mode": "prefix" - }, - "lifetimeElisionHints": { - "enable": "never", - "useParameterNames": false - }, - "maxLength": 25, - "parameterHints": { - "enable": true - }, - "reborrowHints": { - "enable": "never" - }, - "renderColons": true, - "typeHints": { - "enable": true, - "hideClosureInitialization": false, - "hideNamedConstructor": false - } - }, - "interpret": { - "tests": false - }, - "joinLines": { - "joinAssignments": true, - "joinElseIf": true, - "removeTrailingComma": true, - "unwrapTrivialBlock": true - }, - "lens": { - "debug": { - "enable": true - }, - "enable": true, - "forceCustomCommands": true, - "implementations": { - "enable": true - }, - "location": "above_name", - "references": { - "adt": { - "enable": false - }, - "enumVariant": { - "enable": false - }, - "method": { - "enable": false - }, - "trait": { - "enable": false - } - }, - "run": { - "enable": true - } - }, - "linkedProjects": [], - "lru": { - "capacity": null, - "query": { - "capacities": {} - } - }, - "notifications": { - "cargoTomlNotFound": true - }, - "numThreads": null, - "procMacro": { - "attributes": { - "enable": true - }, - "enable": true, - "ignored": {}, - "server": null - }, - "references": { - "excludeImports": false - }, - "rustc": { - "source": null - }, - "rustfmt": { - "extraArgs": [], - "overrideCommand": null, - "rangeFormatting": { - "enable": false - } - }, - "semanticHighlighting": { - "doc": { - "comment": { - "inject": { - "enable": true - } - } - }, - "nonStandardTokens": true, - "operator": { - "enable": true, - "specialization": { - "enable": false - } - }, - "punctuation": { - "enable": false, - "separate": { - "macro": { - "bang": false - } - }, - "specialization": { - "enable": false - } - }, - "strings": { - "enable": true - } - }, - "signatureInfo": { - "detail": "full", - "documentation": { - "enable": true - } - }, - "workspace": { - "symbols": { - "maxSymbols": 0 - } - } - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/multilspy/language_servers/jedi_language_server/jedi_server.py b/src/multilspy/language_servers/jedi_language_server/jedi_server.py deleted file mode 100644 index 6df8365..0000000 --- a/src/multilspy/language_servers/jedi_language_server/jedi_server.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. -""" - -import json -import logging -import os -import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator - -from overrides import override - -from solidlsp.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.multilspy_config import MultilspyConfig - - -class JediServer(LanguageServer): - """ - Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. - """ - - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - """ - 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", - ) - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the Jedi Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: - d = json.load(f) - - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["JediServer"]: - """ - Starts the JEDI Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` - """ - - async def execute_client_command_handler(params): - return [] - - async def do_nothing(params): - return - - async def check_experimental_status(params): - if params["quiescent"] == True: - self.completions_available.set() - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - 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) - self.server.on_notification("experimental/serverStatus", check_experimental_status) - - async with super().start_server(): - self.logger.log("Starting jedi-language-server server process", logging.INFO) - await 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, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": [".", "'", '"'], - "resolveProvider": True, - } - - self.server.notify.initialized({}) - - yield self diff --git a/src/multilspy/language_servers/kotlin_language_server/initialize_params.json b/src/multilspy/language_servers/kotlin_language_server/initialize_params.json deleted file mode 100644 index a7b2ec0..0000000 --- a/src/multilspy/language_servers/kotlin_language_server/initialize_params.json +++ /dev/null @@ -1,521 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "clientInfo": { - "name": "Multilspy Kotlin Client", - "version": "1.0.0" - }, - "locale": "en", - "rootPath": "repository_absolute_path", - "rootUri": "pathlib.Path(repository_absolute_path).as_uri()", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "configuration": true, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": false, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": false, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": true - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "window": { - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": true - } - }, - "showDocument": { - "support": true - }, - "workDoneProgress": true - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0" - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - } - }, - "initializationOptions": { - "workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]", - "storagePath": null, - "codegen": { - "enabled": false - }, - "compiler": { - "jvm": { - "target": "default" - } - }, - "completion": { - "snippets": { - "enabled": true - } - }, - "diagnostics": { - "enabled": true, - "level": 4, - "debounceTime": 250 - }, - "scripts": { - "enabled": true, - "buildScriptsEnabled": true - }, - "indexing": { - "enabled": true - }, - "externalSources": { - "useKlsScheme": false, - "autoConvertToKotlin": false - }, - "inlayHints": { - "typeHints": false, - "parameterHints": false, - "chainedHints": false - }, - "formatting": { - "formatter": "ktfmt", - "ktfmt": { - "style": "google", - "indent": 4, - "maxWidth": 100, - "continuationIndent": 8, - "removeUnusedImports": true - } - } - }, - "trace": "verbose", - "workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]" -} diff --git a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py b/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py deleted file mode 100644 index 0b2906c..0000000 --- a/src/multilspy/language_servers/kotlin_language_server/kotlin_language_server.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. -""" - -import dataclasses -import json -import logging -import os -import stat -import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator - -from solidlsp.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_utils import FileUtils -from solidlsp.multilspy_utils import PlatformUtils - - -@dataclasses.dataclass -class KotlinRuntimeDependencyPaths: - """ - Stores the paths to the runtime dependencies of Kotlin Language Server - """ - java_path: str - java_home_path: str - kotlin_executable_path: str - - -class KotlinLanguageServer(LanguageServer): - """ - Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. - """ - - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - """ - 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) - self.runtime_dependency_paths = runtime_dependency_paths - - # Create command to execute the Kotlin Language Server script - cmd = f'"{self.runtime_dependency_paths.kotlin_executable_path}"' - - # Set environment variables including JAVA_HOME - proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} - - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), - "kotlin", - ) - - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> KotlinRuntimeDependencyPaths: - """ - Setup runtime dependencies for Kotlin Language Server. - """ - 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 Kotlin in multilspy at the moment" - - # Load dependency information - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: - d = json.load(f) - del d["_description"] - - kotlin_dependency = d["runtimeDependency"] - java_dependency = d["java"][platform_id.value] - - # Setup paths for dependencies - static_dir = os.path.join(os.path.dirname(__file__), "static") - os.makedirs(static_dir, exist_ok=True) - - # Setup Java paths - 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"]) - - # 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"] - ) - # Make Java executable - if not platform_id.value.startswith("win-"): - os.chmod(java_path, 0o755) - - assert os.path.exists(java_path), f"Java executable not found at {java_path}" - - # Setup Kotlin Language Server paths - kotlin_ls_dir = os.path.join(static_dir, "server") - - # Get platform-specific executable script path - if platform_id.value.startswith("win-"): - kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server.bat") - else: - kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server") - - # 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"] - ) - - # Make script executable on Unix platforms - if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"): - os.chmod(kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) - - # 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) - else: - raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}") - - return KotlinRuntimeDependencyPaths( - java_path=java_path, - java_home_path=java_home_path, - kotlin_executable_path=kotlin_executable_path - ) - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the Kotlin Language Server. - """ - with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: - d: InitializeParams = json.load(f) - - del d["_description"] - - if not os.path.isabs(repository_absolute_path): - repository_absolute_path = os.path.abspath(repository_absolute_path) - - assert d["processId"] == "os.getpid()" - d["processId"] = os.getpid() - - assert d["rootPath"] == "repository_absolute_path" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "pathlib.Path(repository_absolute_path).as_uri()" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["initializationOptions"]["workspaceFolders"] == "[pathlib.Path(repository_absolute_path).as_uri()]" - d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()] - - assert ( - d["workspaceFolders"] - == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' - ) - d["workspaceFolders"] = [ - { - "uri": pathlib.Path(repository_absolute_path).as_uri(), - "name": os.path.basename(repository_absolute_path), - } - ] - - return d - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["KotlinLanguageServer"]: - """ - Starts the Kotlin Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - ``` - """ - async def execute_client_command_handler(params): - return [] - - async def do_nothing(params): - return - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - 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) - - async with super().start_server(): - self.logger.log("Starting Kotlin server process", logging.INFO) - await 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, - ) - init_response = await 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" - assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens" - - self.server.notify.initialized({}) - self.completions_available.set() - - yield self diff --git a/src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json b/src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json deleted file mode 100644 index 3568c9d..0000000 --- a/src/multilspy/language_servers/kotlin_language_server/runtime_dependencies.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for Kotlin Language Server from https://github.com/fwcd/kotlin-language-server", - "runtimeDependency": { - "id": "KotlinLsp", - "description": "Kotlin Language Server", - "url": "https://github.com/fwcd/kotlin-language-server/releases/download/1.3.13/server.zip", - "archiveType": "zip" - }, - "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" - } - } -} diff --git a/src/multilspy/language_servers/omnisharp/initialize_params.json b/src/multilspy/language_servers/omnisharp/initialize_params.json deleted file mode 100644 index f9ee86c..0000000 --- a/src/multilspy/language_servers/omnisharp/initialize_params.json +++ /dev/null @@ -1,631 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "clientInfo": { - "name": "Visual Studio Code - Insiders", - "version": "1.82.0-insider" - }, - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "configuration": false, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": true, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": true, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": false - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "window": { - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": true - } - }, - "showDocument": { - "support": true - }, - "workDoneProgress": true - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0", - "allowedTags": [ - "ul", - "li", - "p", - "code", - "blockquote", - "ol", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "hr", - "em", - "pre", - "table", - "thead", - "tbody", - "tr", - "th", - "td", - "div", - "del", - "a", - "strong", - "br", - "img", - "span" - ] - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - }, - "experimental": { - "snippetTextEdit": true, - "codeActionGroup": true, - "hoverActions": true, - "serverStatusNotification": true, - "colorDiagnosticOutput": true, - "openServerLogs": true, - "commands": { - "commands": [ - "editor.action.triggerParameterHints" - ] - } - } - }, - "initializationOptions": { - "RoslynExtensionsOptions": { - "EnableDecompilationSupport": false, - "EnableAnalyzersSupport": true, - "EnableImportCompletion": true, - "EnableAsyncCompletion": false, - "DocumentAnalysisTimeoutMs": 30000, - "DiagnosticWorkersThreadCount": 18, - "AnalyzeOpenDocumentsOnly": true, - "InlayHintsOptions": { - "EnableForParameters": false, - "ForLiteralParameters": false, - "ForIndexerParameters": false, - "ForObjectCreationParameters": false, - "ForOtherParameters": false, - "SuppressForParametersThatDifferOnlyBySuffix": false, - "SuppressForParametersThatMatchMethodIntent": false, - "SuppressForParametersThatMatchArgumentName": false, - "EnableForTypes": false, - "ForImplicitVariableTypes": false, - "ForLambdaParameterTypes": false, - "ForImplicitObjectCreation": false - }, - "LocationPaths": null - }, - "FormattingOptions": { - "OrganizeImports": false, - "EnableEditorConfigSupport": true, - "NewLine": "\n", - "UseTabs": false, - "TabSize": 4, - "IndentationSize": 4, - "SpacingAfterMethodDeclarationName": false, - "SeparateImportDirectiveGroups": false, - "SpaceWithinMethodDeclarationParenthesis": false, - "SpaceBetweenEmptyMethodDeclarationParentheses": false, - "SpaceAfterMethodCallName": false, - "SpaceWithinMethodCallParentheses": false, - "SpaceBetweenEmptyMethodCallParentheses": false, - "SpaceAfterControlFlowStatementKeyword": true, - "SpaceWithinExpressionParentheses": false, - "SpaceWithinCastParentheses": false, - "SpaceWithinOtherParentheses": false, - "SpaceAfterCast": false, - "SpaceBeforeOpenSquareBracket": false, - "SpaceBetweenEmptySquareBrackets": false, - "SpaceWithinSquareBrackets": false, - "SpaceAfterColonInBaseTypeDeclaration": true, - "SpaceAfterComma": true, - "SpaceAfterDot": false, - "SpaceAfterSemicolonsInForStatement": true, - "SpaceBeforeColonInBaseTypeDeclaration": true, - "SpaceBeforeComma": false, - "SpaceBeforeDot": false, - "SpaceBeforeSemicolonsInForStatement": false, - "SpacingAroundBinaryOperator": "single", - "IndentBraces": false, - "IndentBlock": true, - "IndentSwitchSection": true, - "IndentSwitchCaseSection": true, - "IndentSwitchCaseSectionWhenBlock": true, - "LabelPositioning": "oneLess", - "WrappingPreserveSingleLine": true, - "WrappingKeepStatementsOnSingleLine": true, - "NewLinesForBracesInTypes": true, - "NewLinesForBracesInMethods": true, - "NewLinesForBracesInProperties": true, - "NewLinesForBracesInAccessors": true, - "NewLinesForBracesInAnonymousMethods": true, - "NewLinesForBracesInControlBlocks": true, - "NewLinesForBracesInAnonymousTypes": true, - "NewLinesForBracesInObjectCollectionArrayInitializers": true, - "NewLinesForBracesInLambdaExpressionBody": true, - "NewLineForElse": true, - "NewLineForCatch": true, - "NewLineForFinally": true, - "NewLineForMembersInObjectInit": true, - "NewLineForMembersInAnonymousTypes": true, - "NewLineForClausesInQuery": true - }, - "FileOptions": { - "SystemExcludeSearchPatterns": [ - "**/node_modules/**/*", - "**/bin/**/*", - "**/obj/**/*", - "**/.git/**/*", - "**/.git", - "**/.svn", - "**/.hg", - "**/CVS", - "**/.DS_Store", - "**/Thumbs.db" - ], - "ExcludeSearchPatterns": [] - }, - "RenameOptions": { - "RenameOverloads": false, - "RenameInStrings": false, - "RenameInComments": false - }, - "ImplementTypeOptions": { - "InsertionBehavior": 0, - "PropertyGenerationBehavior": 0 - }, - "DotNetCliOptions": { - "LocationPaths": null - }, - "Plugins": { - "LocationPaths": null - } - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/multilspy/language_servers/omnisharp/omnisharp.py b/src/multilspy/language_servers/omnisharp/omnisharp.py deleted file mode 100644 index b376de7..0000000 --- a/src/multilspy/language_servers/omnisharp/omnisharp.py +++ /dev/null @@ -1,406 +0,0 @@ -""" -Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. -""" - -import asyncio -import json -import logging -import os -import pathlib -import stat -from contextlib import asynccontextmanager -from typing import AsyncIterator, Iterable - -from overrides import override - -from solidlsp.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_exceptions import MultilspyException -from solidlsp.multilspy_utils import FileUtils, PlatformUtils, PlatformId, DotnetVersion - - -def breadth_first_file_scan(root) -> Iterable[str]: - """ - This function was obtained from https://stackoverflow.com/questions/49654234/is-there-a-breadth-first-search-option-available-in-os-walk-or-equivalent-py - It traverses the directory tree in breadth first order. - """ - dirs = [root] - # while we has dirs to scan - while len(dirs): - next_dirs = [] - for parent in dirs: - # scan each dir - for f in os.listdir(parent): - # if there is a dir, then save for next ittr - # if it is a file then yield it (we'll return later) - ff = os.path.join(parent, f) - if os.path.isdir(ff): - next_dirs.append(ff) - else: - yield ff - - # once we've done all the current dirs then - # we set up the next itter as the child dirs - # from the current itter. - dirs = next_dirs - - -def find_least_depth_sln_file(root_dir) -> str | None: - for filename in breadth_first_file_scan(root_dir): - if filename.endswith(".sln"): - return filename - return None - - -class OmniSharp(LanguageServer): - """ - Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. - """ - - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - """ - Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. - """ - omnisharp_executable_path, dll_path = self.setupRuntimeDependencies(logger, config) - - slnfilename = find_least_depth_sln_file(repository_root_path) - if slnfilename is None: - logger.log("No *.sln file found in repository", logging.ERROR) - raise MultilspyException("No SLN file found in repository") - - cmd = " ".join( - [ - omnisharp_executable_path, - "-lsp", - "--encoding", - "ascii", - "-z", - "-s", - f'"{slnfilename}"', - "--hostPID", - str(os.getpid()), - "DotNet:enablePackageRestore=false", - "--loglevel", - "trace", - "--plugin", - dll_path, - "FileOptions:SystemExcludeSearchPatterns:0=**/.git", - "FileOptions:SystemExcludeSearchPatterns:1=**/.svn", - "FileOptions:SystemExcludeSearchPatterns:2=**/.hg", - "FileOptions:SystemExcludeSearchPatterns:3=**/CVS", - "FileOptions:SystemExcludeSearchPatterns:4=**/.DS_Store", - "FileOptions:SystemExcludeSearchPatterns:5=**/Thumbs.db", - "RoslynExtensionsOptions:EnableAnalyzersSupport=true", - "FormattingOptions:EnableEditorConfigSupport=true", - "RoslynExtensionsOptions:EnableImportCompletion=true", - "Sdk:IncludePrereleases=true", - "RoslynExtensionsOptions:AnalyzeOpenDocumentsOnly=true", - "formattingOptions:useTabs=false", - "formattingOptions:tabSize=4", - "formattingOptions:indentationSize=4", - ] - ) - super().__init__( - config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp" - ) - - self.definition_available = asyncio.Event() - self.references_available = asyncio.Event() - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj"] - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the Omnisharp Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: - d = json.load(f) - - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d - - def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> tuple[str, str]: - """ - Setup runtime dependencies for OmniSharp. - """ - platform_id = PlatformUtils.get_platform_id() - dotnet_version = PlatformUtils.get_dotnet_version() - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: - d = json.load(f) - del d["_description"] - - assert platform_id in [ - PlatformId.LINUX_x64, - PlatformId.WIN_x64, - ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" - assert dotnet_version in [ - DotnetVersion.V6, - DotnetVersion.V7, - DotnetVersion.V8 - ], "Only dotnet version 6 and 7 are supported in multilspy at the moment" - - # TODO: Do away with this assumption - # Currently, runtime binaries are not available for .Net 7 and .Net 8. Hence, we assume .Net 6 runtime binaries to be compatible with .Net 7, .Net 8 - if dotnet_version in [DotnetVersion.V7, DotnetVersion.V8]: - dotnet_version = DotnetVersion.V6 - - runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] - runtime_dependencies = [ - dependency - for dependency in runtime_dependencies - if not ("dotnet_version" in dependency) or dependency["dotnet_version"] == dotnet_version.value - ] - assert len(runtime_dependencies) == 2 - runtime_dependencies = { - runtime_dependencies[0]["id"]: runtime_dependencies[0], - runtime_dependencies[1]["id"]: runtime_dependencies[1], - } - - assert "OmniSharp" in runtime_dependencies - assert "RazorOmnisharp" in runtime_dependencies - - omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "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" - ) - 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, stat.S_IEXEC) - - razor_omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "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" - ) - razor_omnisharp_dll_path = os.path.join( - razor_omnisharp_ls_dir, runtime_dependencies["RazorOmnisharp"]["dll_path"] - ) - assert os.path.exists(razor_omnisharp_dll_path) - - return omnisharp_executable_path, razor_omnisharp_dll_path - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["OmniSharp"]: - """ - Starts the Omnisharp Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - """ - - async def register_capability_handler(params): - assert "registrations" in params - for registration in params["registrations"]: - if registration["method"] == "textDocument/definition": - self.definition_available.set() - if registration["method"] == "textDocument/references": - self.references_available.set() - if registration["method"] == "textDocument/completion": - self.completions_available.set() - - async def lang_status_handler(params): - # TODO: Should we wait for - # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} - # Before proceeding? - # if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": - # self.service_ready_event.set() - pass - - async def execute_client_command_handler(params): - return [] - - async def do_nothing(params): - return - - async def check_experimental_status(params): - if params["quiescent"] == True: - self.server_ready.set() - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - async def workspace_configuration_handler(params): - # TODO: We do not know the appropriate way to handle this request. Should ideally contact the OmniSharp dev team - return [ - { - "RoslynExtensionsOptions": { - "EnableDecompilationSupport": False, - "EnableAnalyzersSupport": True, - "EnableImportCompletion": True, - "EnableAsyncCompletion": False, - "DocumentAnalysisTimeoutMs": 30000, - "DiagnosticWorkersThreadCount": 18, - "AnalyzeOpenDocumentsOnly": True, - "InlayHintsOptions": { - "EnableForParameters": False, - "ForLiteralParameters": False, - "ForIndexerParameters": False, - "ForObjectCreationParameters": False, - "ForOtherParameters": False, - "SuppressForParametersThatDifferOnlyBySuffix": False, - "SuppressForParametersThatMatchMethodIntent": False, - "SuppressForParametersThatMatchArgumentName": False, - "EnableForTypes": False, - "ForImplicitVariableTypes": False, - "ForLambdaParameterTypes": False, - "ForImplicitObjectCreation": False, - }, - "LocationPaths": None, - }, - "FormattingOptions": { - "OrganizeImports": False, - "EnableEditorConfigSupport": True, - "NewLine": "\n", - "UseTabs": False, - "TabSize": 4, - "IndentationSize": 4, - "SpacingAfterMethodDeclarationName": False, - "SeparateImportDirectiveGroups": False, - "SpaceWithinMethodDeclarationParenthesis": False, - "SpaceBetweenEmptyMethodDeclarationParentheses": False, - "SpaceAfterMethodCallName": False, - "SpaceWithinMethodCallParentheses": False, - "SpaceBetweenEmptyMethodCallParentheses": False, - "SpaceAfterControlFlowStatementKeyword": True, - "SpaceWithinExpressionParentheses": False, - "SpaceWithinCastParentheses": False, - "SpaceWithinOtherParentheses": False, - "SpaceAfterCast": False, - "SpaceBeforeOpenSquareBracket": False, - "SpaceBetweenEmptySquareBrackets": False, - "SpaceWithinSquareBrackets": False, - "SpaceAfterColonInBaseTypeDeclaration": True, - "SpaceAfterComma": True, - "SpaceAfterDot": False, - "SpaceAfterSemicolonsInForStatement": True, - "SpaceBeforeColonInBaseTypeDeclaration": True, - "SpaceBeforeComma": False, - "SpaceBeforeDot": False, - "SpaceBeforeSemicolonsInForStatement": False, - "SpacingAroundBinaryOperator": "single", - "IndentBraces": False, - "IndentBlock": True, - "IndentSwitchSection": True, - "IndentSwitchCaseSection": True, - "IndentSwitchCaseSectionWhenBlock": True, - "LabelPositioning": "oneLess", - "WrappingPreserveSingleLine": True, - "WrappingKeepStatementsOnSingleLine": True, - "NewLinesForBracesInTypes": True, - "NewLinesForBracesInMethods": True, - "NewLinesForBracesInProperties": True, - "NewLinesForBracesInAccessors": True, - "NewLinesForBracesInAnonymousMethods": True, - "NewLinesForBracesInControlBlocks": True, - "NewLinesForBracesInAnonymousTypes": True, - "NewLinesForBracesInObjectCollectionArrayInitializers": True, - "NewLinesForBracesInLambdaExpressionBody": True, - "NewLineForElse": True, - "NewLineForCatch": True, - "NewLineForFinally": True, - "NewLineForMembersInObjectInit": True, - "NewLineForMembersInAnonymousTypes": True, - "NewLineForClausesInQuery": True, - }, - "FileOptions": { - "SystemExcludeSearchPatterns": [ - "**/node_modules/**/*", - "**/bin/**/*", - "**/obj/**/*", - "**/.git/**/*", - "**/.git", - "**/.svn", - "**/.hg", - "**/CVS", - "**/.DS_Store", - "**/Thumbs.db", - ], - "ExcludeSearchPatterns": [], - }, - "RenameOptions": { - "RenameOverloads": False, - "RenameInStrings": False, - "RenameInComments": False, - }, - "ImplementTypeOptions": { - "InsertionBehavior": 0, - "PropertyGenerationBehavior": 0, - }, - "DotNetCliOptions": {"LocationPaths": None}, - "Plugins": {"LocationPaths": None}, - } - ] - - self.server.on_request("client/registerCapability", register_capability_handler) - self.server.on_notification("language/status", lang_status_handler) - 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) - self.server.on_notification("experimental/serverStatus", check_experimental_status) - self.server.on_request("workspace/configuration", workspace_configuration_handler) - - async with super().start_server(): - self.logger.log("Starting OmniSharp server process", logging.INFO) - await 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, - ) - init_response = await self.server.send.initialize(initialize_params) - self.server.notify.initialized({}) - with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), "r", encoding="utf-8") as f: - self.server.notify.workspace_did_change_configuration({ - "settings": json.load(f) - }) - assert "capabilities" in init_response - if ( - "definitionProvider" in init_response["capabilities"] - and init_response["capabilities"]["definitionProvider"] - ): - self.definition_available.set() - if ( - "referencesProvider" in init_response["capabilities"] - and init_response["capabilities"]["referencesProvider"] - ): - self.references_available.set() - - await self.definition_available.wait() - await self.references_available.wait() - - yield self diff --git a/src/multilspy/language_servers/omnisharp/runtime_dependencies.json b/src/multilspy/language_servers/omnisharp/runtime_dependencies.json deleted file mode 100644 index 7dfa1fd..0000000 --- a/src/multilspy/language_servers/omnisharp/runtime_dependencies.json +++ /dev/null @@ -1,441 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running OmniSharp. Obtained from https://github.com/dotnet/vscode-csharp/blob/main/package.json", - "runtimeDependencies": [ - { - "id": "OmniSharp", - "description": "OmniSharp for Windows (.NET 4 / x86)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x86-1.39.10.zip", - "installPath": ".omnisharp/1.39.10", - "platforms": [ - "win32" - ], - "architectures": [ - "x86" - ], - "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe", - "platformId": "win-x86", - "isFramework": true, - "integrity": "C81CE2099AD494EF63F9D88FAA70D55A68CF175810F944526FF94AAC7A5109F9", - "dotnet_version": "4", - "binaryName": "OmniSharp.exe" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Windows (.NET 6 / x86)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x86-net6.0-1.39.10.zip", - "installPath": ".omnisharp/1.39.10-net6.0", - "platforms": [ - "win32" - ], - "architectures": [ - "x86" - ], - "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", - "platformId": "win-x86", - "isFramework": false, - "integrity": "B7E62415CFC3DAC2154AC636C5BF0FB4B2C9BBF11B5A1FBF72381DDDED59791E", - "dotnet_version": "6", - "binaryName": "OmniSharp.exe" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Windows (.NET 4 / x64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x64-1.39.10.zip", - "installPath": ".omnisharp/1.39.10", - "platforms": [ - "win32" - ], - "architectures": [ - "x86_64" - ], - "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe", - "platformId": "win-x64", - "isFramework": true, - "integrity": "BE0ED10AACEA17E14B78BD0D887DE5935D4ECA3712192A701F3F2100CA3C8B6E", - "dotnet_version": "4", - "binaryName": "OmniSharp.exe" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Windows (.NET 6 / x64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x64-net6.0-1.39.10.zip", - "installPath": ".omnisharp/1.39.10-net6.0", - "platforms": [ - "win32" - ], - "architectures": [ - "x86_64" - ], - "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", - "platformId": "win-x64", - "isFramework": false, - "integrity": "A73327395E7EF92C1D8E307055463DA412662C03F077ECC743462FD2760BB537", - "dotnet_version": "6", - "binaryName": "OmniSharp.exe" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Windows (.NET 4 / arm64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-arm64-1.39.10.zip", - "installPath": ".omnisharp/1.39.10", - "platforms": [ - "win32" - ], - "architectures": [ - "arm64" - ], - "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe", - "platformId": "win-arm64", - "isFramework": true, - "integrity": "32FA0067B0639F87760CD1A769B16E6A53588C137C4D31661836CA4FB28D3DD6", - "dotnet_version": "4", - "binaryName": "OmniSharp.exe" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Windows (.NET 6 / arm64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-arm64-net6.0-1.39.10.zip", - "installPath": ".omnisharp/1.39.10-net6.0", - "platforms": [ - "win32" - ], - "architectures": [ - "arm64" - ], - "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", - "platformId": "win-arm64", - "isFramework": false, - "integrity": "433F9B360CAA7B4DDD85C604D5C5542C1A718BCF2E71B2BCFC7526E6D41F4E8F", - "dotnet_version": "6", - "binaryName": "OmniSharp.exe" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for OSX (Mono / x64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-1.39.10.zip", - "installPath": ".omnisharp/1.39.10", - "platforms": [ - "darwin" - ], - "architectures": [ - "x86_64", - "arm64" - ], - "binaries": [ - "./mono.osx", - "./run" - ], - "installTestPath": "./.omnisharp/1.39.10/run", - "platformId": "osx", - "isFramework": true, - "integrity": "2CC42F0EC7C30CFA8858501D12ECB6FB685A1FCFB8ECB35698A4B12406551968", - "dotnet_version": "mono" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for OSX (.NET 6 / x64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-x64-net6.0-1.39.10.zip", - "installPath": ".omnisharp/1.39.10-net6.0", - "platforms": [ - "darwin" - ], - "architectures": [ - "x86_64" - ], - "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", - "platformId": "osx-x64", - "isFramework": false, - "integrity": "C9D6E9F2C839A66A7283AE6A9EC545EE049B48EB230D33E91A6322CB67FF9D97", - "dotnet_version": "6" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for OSX (.NET 6 / arm64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-arm64-net6.0-1.39.10.zip", - "installPath": ".omnisharp/1.39.10-net6.0", - "platforms": [ - "darwin" - ], - "architectures": [ - "arm64" - ], - "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", - "platformId": "osx-arm64", - "isFramework": false, - "integrity": "851350F52F83E3BAD5A92D113E4B9882FCD1DEB16AA84FF94B6F2CEE3C70051E", - "dotnet_version": "6" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Linux (Mono / x86)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x86-1.39.10.zip", - "installPath": ".omnisharp/1.39.10", - "platforms": [ - "linux" - ], - "architectures": [ - "x86", - "i686" - ], - "binaries": [ - "./mono.linux-x86", - "./run" - ], - "installTestPath": "./.omnisharp/1.39.10/run", - "platformId": "linux-x86", - "isFramework": true, - "integrity": "474B1CDBAE64CFEC655FB6B0659BCE481023C48274441C72991E67B6E13E56A1", - "dotnet_version": "mono" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Linux (Mono / x64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x64-1.39.10.zip", - "installPath": ".omnisharp/1.39.10", - "platforms": [ - "linux" - ], - "architectures": [ - "x86_64" - ], - "binaries": [ - "./mono.linux-x86_64", - "./run" - ], - "installTestPath": "./.omnisharp/1.39.10/run", - "platformId": "linux-x64", - "isFramework": true, - "integrity": "FB4CAA47343265100349375D79DBCCE1868950CED675CB07FCBE8462EDBCDD37", - "dotnet_version": "mono" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Linux (.NET 6 / x64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x64-net6.0-1.39.10.zip", - "installPath": ".omnisharp/1.39.10-net6.0", - "platforms": [ - "linux" - ], - "architectures": [ - "x86_64" - ], - "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", - "platformId": "linux-x64", - "isFramework": false, - "integrity": "0926D3BEA060BF4373356B2FC0A68C10D0DE1B1150100B551BA5932814CE51E2", - "dotnet_version": "6", - "binaryName": "OmniSharp" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Linux (Mono / arm64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-arm64-1.39.10.zip", - "installPath": ".omnisharp/1.39.10", - "platforms": [ - "linux" - ], - "architectures": [ - "arm64" - ], - "binaries": [ - "./mono.linux-arm64", - "./run" - ], - "installTestPath": "./.omnisharp/1.39.10/run", - "platformId": "linux-arm64", - "isFramework": true, - "integrity": "478F3594DFD0167E9A56E36F0364A86C73F8132A3E7EA916CA1419EFE141D2CC", - "dotnet_version": "mono" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Linux (.NET 6 / arm64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-arm64-net6.0-1.39.10.zip", - "installPath": ".omnisharp/1.39.10-net6.0", - "platforms": [ - "linux" - ], - "architectures": [ - "arm64" - ], - "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", - "platformId": "linux-arm64", - "isFramework": false, - "integrity": "6FB6A572043A74220A92F6C19C7BB0C3743321C7563A815FD2702EF4FA7D688E", - "dotnet_version": "6" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Linux musl (.NET 6 / x64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-musl-x64-net6.0-1.39.10.zip", - "installPath": ".omnisharp/1.39.10-net6.0", - "platforms": [ - "linux-musl" - ], - "architectures": [ - "x86_64" - ], - "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", - "platformId": "linux-musl-x64", - "isFramework": false, - "integrity": "6BFDA3AD11DBB0C6514B86ECC3E1597CC41C6E309B7575F7C599E07D9E2AE610", - "dotnet_version": "6" - }, - { - "id": "OmniSharp", - "description": "OmniSharp for Linux musl (.NET 6 / arm64)", - "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-musl-arm64-net6.0-1.39.10.zip", - "installPath": ".omnisharp/1.39.10-net6.0", - "platforms": [ - "linux-musl" - ], - "architectures": [ - "arm64" - ], - "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", - "platformId": "linux-musl-arm64", - "isFramework": false, - "integrity": "DA63619EA024EB9BBF6DB5A85C6150CAB5C0BD554544A3596ED1B17F926D6875", - "dotnet_version": "6" - }, - { - "id": "RazorOmnisharp", - "description": "Razor Language Server for OmniSharp (Windows / x64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/8d42e62ea4051381c219b3e31bc4eced/razorlanguageserver-win-x64-7.0.0-preview.23363.1.zip", - "installPath": ".razoromnisharp", - "platforms": [ - "win32" - ], - "architectures": [ - "x86_64" - ], - "platformId": "win-x64", - "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" - }, - { - "id": "RazorOmnisharp", - "description": "Razor Language Server for OmniSharp (Windows / x86)", - "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/e440c4f3a4a96334fe177513935fa010/razorlanguageserver-win-x86-7.0.0-preview.23363.1.zip", - "installPath": ".razoromnisharp", - "platforms": [ - "win32" - ], - "architectures": [ - "x86" - ], - "platformId": "win-x86", - "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" - }, - { - "id": "RazorOmnisharp", - "description": "Razor Language Server for OmniSharp (Windows / ARM64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/4ef26e45cf32fe8d51c0e7dd21f1fef6/razorlanguageserver-win-arm64-7.0.0-preview.23363.1.zip", - "installPath": ".razoromnisharp", - "platforms": [ - "win32" - ], - "architectures": [ - "arm64" - ], - "platformId": "win-arm64", - "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" - }, - { - "id": "RazorOmnisharp", - "description": "Razor Language Server for OmniSharp (Linux / x64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/6d4e23a3c7cf0465743950a39515a716/razorlanguageserver-linux-x64-7.0.0-preview.23363.1.zip", - "installPath": ".razoromnisharp", - "platforms": [ - "linux" - ], - "architectures": [ - "x86_64" - ], - "binaries": [ - "./rzls" - ], - "platformId": "linux-x64", - "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" - }, - { - "id": "RazorOmnisharp", - "description": "Razor Language Server for OmniSharp (Linux ARM64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/85deebd44647ebf65724cc291d722283/razorlanguageserver-linux-arm64-7.0.0-preview.23363.1.zip", - "installPath": ".razoromnisharp", - "platforms": [ - "linux" - ], - "architectures": [ - "arm64" - ], - "binaries": [ - "./rzls" - ], - "platformId": "linux-arm64" - }, - { - "id": "RazorOmnisharp", - "description": "Razor Language Server for OmniSharp (Linux musl / x64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/4f0caa94ae182785655efb15eafcef23/razorlanguageserver-linux-musl-x64-7.0.0-preview.23363.1.zip", - "installPath": ".razoromnisharp", - "platforms": [ - "linux-musl" - ], - "architectures": [ - "x86_64" - ], - "binaries": [ - "./rzls" - ], - "platformId": "linux-musl-x64" - }, - { - "id": "RazorOmnisharp", - "description": "Razor Language Server for OmniSharp (Linux musl ARM64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/0a24828206a6f3b4bc743d058ef88ce7/razorlanguageserver-linux-musl-arm64-7.0.0-preview.23363.1.zip", - "installPath": ".razoromnisharp", - "platforms": [ - "linux-musl" - ], - "architectures": [ - "arm64" - ], - "binaries": [ - "./rzls" - ], - "platformId": "linux-musl-arm64" - }, - { - "id": "RazorOmnisharp", - "description": "Razor Language Server for OmniSharp (macOS / x64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/2afcafaf41082989efcc10405abb9314/razorlanguageserver-osx-x64-7.0.0-preview.23363.1.zip", - "installPath": ".razoromnisharp", - "platforms": [ - "darwin" - ], - "architectures": [ - "x86_64" - ], - "binaries": [ - "./rzls" - ], - "platformId": "osx-x64" - }, - { - "id": "RazorOmnisharp", - "description": "Razor Language Server for OmniSharp (macOS ARM64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/8bf2ed2f00d481a5987e3eb5165afddd/razorlanguageserver-osx-arm64-7.0.0-preview.23363.1.zip", - "installPath": ".razoromnisharp", - "platforms": [ - "darwin" - ], - "architectures": [ - "arm64" - ], - "binaries": [ - "./rzls" - ], - "platformId": "osx-arm64" - } - ] -} \ No newline at end of file diff --git a/src/multilspy/language_servers/omnisharp/workspace_did_change_configuration.json b/src/multilspy/language_servers/omnisharp/workspace_did_change_configuration.json deleted file mode 100644 index 148841c..0000000 --- a/src/multilspy/language_servers/omnisharp/workspace_did_change_configuration.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "RoslynExtensionsOptions": { - "EnableDecompilationSupport": false, - "EnableAnalyzersSupport": true, - "EnableImportCompletion": true, - "EnableAsyncCompletion": false, - "DocumentAnalysisTimeoutMs": 30000, - "DiagnosticWorkersThreadCount": 18, - "AnalyzeOpenDocumentsOnly": true, - "InlayHintsOptions": { - "EnableForParameters": false, - "ForLiteralParameters": false, - "ForIndexerParameters": false, - "ForObjectCreationParameters": false, - "ForOtherParameters": false, - "SuppressForParametersThatDifferOnlyBySuffix": false, - "SuppressForParametersThatMatchMethodIntent": false, - "SuppressForParametersThatMatchArgumentName": false, - "EnableForTypes": false, - "ForImplicitVariableTypes": false, - "ForLambdaParameterTypes": false, - "ForImplicitObjectCreation": false - }, - "LocationPaths": null - }, - "FormattingOptions": { - "OrganizeImports": false, - "EnableEditorConfigSupport": true, - "NewLine": "\n", - "UseTabs": false, - "TabSize": 4, - "IndentationSize": 4, - "SpacingAfterMethodDeclarationName": false, - "SeparateImportDirectiveGroups": false, - "SpaceWithinMethodDeclarationParenthesis": false, - "SpaceBetweenEmptyMethodDeclarationParentheses": false, - "SpaceAfterMethodCallName": false, - "SpaceWithinMethodCallParentheses": false, - "SpaceBetweenEmptyMethodCallParentheses": false, - "SpaceAfterControlFlowStatementKeyword": true, - "SpaceWithinExpressionParentheses": false, - "SpaceWithinCastParentheses": false, - "SpaceWithinOtherParentheses": false, - "SpaceAfterCast": false, - "SpaceBeforeOpenSquareBracket": false, - "SpaceBetweenEmptySquareBrackets": false, - "SpaceWithinSquareBrackets": false, - "SpaceAfterColonInBaseTypeDeclaration": true, - "SpaceAfterComma": true, - "SpaceAfterDot": false, - "SpaceAfterSemicolonsInForStatement": true, - "SpaceBeforeColonInBaseTypeDeclaration": true, - "SpaceBeforeComma": false, - "SpaceBeforeDot": false, - "SpaceBeforeSemicolonsInForStatement": false, - "SpacingAroundBinaryOperator": "single", - "IndentBraces": false, - "IndentBlock": true, - "IndentSwitchSection": true, - "IndentSwitchCaseSection": true, - "IndentSwitchCaseSectionWhenBlock": true, - "LabelPositioning": "oneLess", - "WrappingPreserveSingleLine": true, - "WrappingKeepStatementsOnSingleLine": true, - "NewLinesForBracesInTypes": true, - "NewLinesForBracesInMethods": true, - "NewLinesForBracesInProperties": true, - "NewLinesForBracesInAccessors": true, - "NewLinesForBracesInAnonymousMethods": true, - "NewLinesForBracesInControlBlocks": true, - "NewLinesForBracesInAnonymousTypes": true, - "NewLinesForBracesInObjectCollectionArrayInitializers": true, - "NewLinesForBracesInLambdaExpressionBody": true, - "NewLineForElse": true, - "NewLineForCatch": true, - "NewLineForFinally": true, - "NewLineForMembersInObjectInit": true, - "NewLineForMembersInAnonymousTypes": true, - "NewLineForClausesInQuery": true - }, - "FileOptions": { - "SystemExcludeSearchPatterns": [ - "**/node_modules/**/*", - "**/bin/**/*", - "**/obj/**/*", - "**/.git/**/*", - "**/.git", - "**/.svn", - "**/.hg", - "**/CVS", - "**/.DS_Store", - "**/Thumbs.db" - ], - "ExcludeSearchPatterns": [] - }, - "RenameOptions": { - "RenameOverloads": false, - "RenameInStrings": false, - "RenameInComments": false - }, - "ImplementTypeOptions": { - "InsertionBehavior": 0, - "PropertyGenerationBehavior": 0 - }, - "DotNetCliOptions": { - "LocationPaths": null - }, - "Plugins": { - "LocationPaths": null - } -} \ No newline at end of file diff --git a/src/multilspy/language_servers/pyright_language_server/pyright_server.py b/src/multilspy/language_servers/pyright_language_server/pyright_server.py deleted file mode 100644 index 0cb9533..0000000 --- a/src/multilspy/language_servers/pyright_language_server/pyright_server.py +++ /dev/null @@ -1,245 +0,0 @@ -""" -Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. -""" - -import asyncio -import logging -import os -import pathlib -import re -from contextlib import asynccontextmanager -from typing import AsyncIterator - -from overrides import override - -from solidlsp.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.multilspy_config import MultilspyConfig - - -class PyrightServer(LanguageServer): - """ - Provides Python specific instantiation of the LanguageServer class using Pyright. - Contains various configurations and settings specific to Python. - """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - """ - 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", - ) - - # Event to signal when initial workspace analysis is complete - self.analysis_complete = asyncio.Event() - self.found_source_files = False - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the Pyright Language Server. - """ - # Create basic initialization parameters - initialize_params: InitializeParams = { # type: ignore - "processId": os.getpid(), - "rootPath": repository_absolute_path, - "rootUri": pathlib.Path(repository_absolute_path).as_uri(), - "initializationOptions": { - "exclude": [ - "**/__pycache__", - "**/.venv", - "**/.env", - "**/build", - "**/dist", - "**/.pixi", - ], - "reportMissingImports": "error", - }, - "capabilities": { - "workspace": { - "applyEdit": True, - "workspaceEdit": {"documentChanges": True}, - "didChangeConfiguration": {"dynamicRegistration": True}, - "didChangeWatchedFiles": {"dynamicRegistration": True}, - "symbol": { - "dynamicRegistration": True, - "symbolKind": { - "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] - }, - }, - "executeCommand": {"dynamicRegistration": 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, - "preselectSupport": True, - }, - "completionItemKind": { - "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] - }, - }, - "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, - "signatureHelp": { - "dynamicRegistration": True, - "signatureInformation": { - "documentationFormat": ["markdown", "plaintext"], - "parameterInformation": {"labelOffsetSupport": True}, - }, - }, - "definition": {"dynamicRegistration": True}, - "references": {"dynamicRegistration": True}, - "documentHighlight": {"dynamicRegistration": True}, - "documentSymbol": { - "dynamicRegistration": True, - "symbolKind": { - "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] - }, - "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}, - "publishDiagnostics": {"relatedInformation": True}, - }, - }, - "workspaceFolders": [ - {"uri": pathlib.Path(repository_absolute_path).as_uri(), "name": os.path.basename(repository_absolute_path)} - ], - } - - return initialize_params - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["PyrightServer"]: - """ - Starts the Pyright Language Server and waits for initial workspace analysis to complete. - - This prevents zombie processes by ensuring Pyright has finished its initial background - tasks before we consider the server ready. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and workspace analysis is complete - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown cleanly - ``` - """ - - async def execute_client_command_handler(params): - return [] - - async def do_nothing(params): - return - - async def window_log_message(msg): - """ - Monitor Pyright's log messages to detect when initial analysis is complete. - 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) - - # 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) - self.found_source_files = True - self.analysis_complete.set() - self.completions_available.set() - - async def check_experimental_status(params): - """ - 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) - if not self.found_source_files: - self.analysis_complete.set() - self.completions_available.set() - - # Set up notification handlers - 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) - self.server.on_notification("experimental/serverStatus", check_experimental_status) - - async with super().start_server(): - self.logger.log("Starting pyright-langserver server process", logging.INFO) - await 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, - ) - init_response = await self.server.send.initialize(initialize_params) - self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) - - # Verify that the server supports our required features - assert "textDocumentSync" in init_response["capabilities"] - assert "completionProvider" in init_response["capabilities"] - assert "definitionProvider" in init_response["capabilities"] - - # Complete the initialization handshake - self.server.notify.initialized({}) - - # 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) - try: - await asyncio.wait_for(self.analysis_complete.wait(), timeout=1.0) - self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) - except asyncio.TimeoutError: - self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) - # Fallback: assume analysis is complete after timeout - self.analysis_complete.set() - self.completions_available.set() - - yield self \ No newline at end of file diff --git a/src/multilspy/language_servers/rust_analyzer/initialize_params.json b/src/multilspy/language_servers/rust_analyzer/initialize_params.json deleted file mode 100644 index 00368ee..0000000 --- a/src/multilspy/language_servers/rust_analyzer/initialize_params.json +++ /dev/null @@ -1,917 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "clientInfo": { - "name": "Visual Studio Code - Insiders", - "version": "1.82.0-insider" - }, - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "configuration": true, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": true, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": true, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": false - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "window": { - "showMessage": { - "messageActionItem": { - "additionalPropertiesSupport": true - } - }, - "showDocument": { - "support": true - }, - "workDoneProgress": true - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0", - "allowedTags": [ - "ul", - "li", - "p", - "code", - "blockquote", - "ol", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "hr", - "em", - "pre", - "table", - "thead", - "tbody", - "tr", - "th", - "td", - "div", - "del", - "a", - "strong", - "br", - "img", - "span" - ] - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - }, - "experimental": { - "snippetTextEdit": true, - "codeActionGroup": true, - "hoverActions": true, - "serverStatusNotification": true, - "colorDiagnosticOutput": true, - "openServerLogs": true, - "localDocs": true, - "commands": { - "commands": [ - "rust-analyzer.runSingle", - "rust-analyzer.debugSingle", - "rust-analyzer.showReferences", - "rust-analyzer.gotoLocation", - "editor.action.triggerParameterHints" - ] - } - } - }, - "initializationOptions": { - "cargoRunner": null, - "runnables": { - "extraEnv": null, - "problemMatcher": [ - "$rustc" - ], - "command": null, - "extraArgs": [] - }, - "statusBar": { - "clickAction": "openLogs" - }, - "server": { - "path": null, - "extraEnv": null - }, - "trace": { - "server": "verbose", - "extension": false - }, - "debug": { - "engine": "auto", - "sourceFileMap": { - "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" - }, - "openDebugPane": false, - "engineSettings": {} - }, - "restartServerOnConfigChange": false, - "typing": { - "continueCommentsOnNewline": true, - "autoClosingAngleBrackets": { - "enable": false - } - }, - "diagnostics": { - "previewRustcOutput": false, - "useRustcErrorCode": false, - "disabled": [], - "enable": true, - "experimental": { - "enable": false - }, - "remapPrefix": {}, - "warningsAsHint": [], - "warningsAsInfo": [] - }, - "discoverProjectRunner": null, - "showUnlinkedFileNotification": true, - "showDependenciesExplorer": true, - "assist": { - "emitMustUse": false, - "expressionFillDefault": "todo" - }, - "cachePriming": { - "enable": true, - "numThreads": 0 - }, - "cargo": { - "autoreload": true, - "buildScripts": { - "enable": true, - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "overrideCommand": null, - "useRustcWrapper": true - }, - "cfgs": {}, - "extraArgs": [], - "extraEnv": {}, - "features": [], - "noDefaultFeatures": false, - "sysroot": "discover", - "sysrootSrc": null, - "target": null, - "unsetTest": [ - "core" - ] - }, - "checkOnSave": true, - "check": { - "allTargets": true, - "command": "check", - "extraArgs": [], - "extraEnv": {}, - "features": null, - "ignore": [], - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "noDefaultFeatures": null, - "overrideCommand": null, - "targets": null - }, - "completion": { - "autoimport": { - "enable": true - }, - "autoself": { - "enable": true - }, - "callable": { - "snippets": "fill_arguments" - }, - "fullFunctionSignatures": { - "enable": false - }, - "limit": null, - "postfix": { - "enable": true - }, - "privateEditable": { - "enable": false - }, - "snippets": { - "custom": { - "Arc::new": { - "postfix": "arc", - "body": "Arc::new(${receiver})", - "requires": "std::sync::Arc", - "description": "Put the expression into an `Arc`", - "scope": "expr" - }, - "Rc::new": { - "postfix": "rc", - "body": "Rc::new(${receiver})", - "requires": "std::rc::Rc", - "description": "Put the expression into an `Rc`", - "scope": "expr" - }, - "Box::pin": { - "postfix": "pinbox", - "body": "Box::pin(${receiver})", - "requires": "std::boxed::Box", - "description": "Put the expression into a pinned `Box`", - "scope": "expr" - }, - "Ok": { - "postfix": "ok", - "body": "Ok(${receiver})", - "description": "Wrap the expression in a `Result::Ok`", - "scope": "expr" - }, - "Err": { - "postfix": "err", - "body": "Err(${receiver})", - "description": "Wrap the expression in a `Result::Err`", - "scope": "expr" - }, - "Some": { - "postfix": "some", - "body": "Some(${receiver})", - "description": "Wrap the expression in an `Option::Some`", - "scope": "expr" - } - } - } - }, - "files": { - "excludeDirs": [], - "watcher": "client" - }, - "highlightRelated": { - "breakPoints": { - "enable": true - }, - "closureCaptures": { - "enable": true - }, - "exitPoints": { - "enable": true - }, - "references": { - "enable": true - }, - "yieldPoints": { - "enable": true - } - }, - "hover": { - "actions": { - "debug": { - "enable": true - }, - "enable": true, - "gotoTypeDef": { - "enable": true - }, - "implementations": { - "enable": true - }, - "references": { - "enable": false - }, - "run": { - "enable": true - } - }, - "documentation": { - "enable": true, - "keywords": { - "enable": true - } - }, - "links": { - "enable": true - }, - "memoryLayout": { - "alignment": "hexadecimal", - "enable": true, - "niches": false, - "offset": "hexadecimal", - "size": "both" - } - }, - "imports": { - "granularity": { - "enforce": false, - "group": "crate" - }, - "group": { - "enable": true - }, - "merge": { - "glob": true - }, - "preferNoStd": false, - "preferPrelude": false, - "prefix": "plain" - }, - "inlayHints": { - "bindingModeHints": { - "enable": false - }, - "chainingHints": { - "enable": true - }, - "closingBraceHints": { - "enable": true, - "minLines": 25 - }, - "closureCaptureHints": { - "enable": false - }, - "closureReturnTypeHints": { - "enable": "never" - }, - "closureStyle": "impl_fn", - "discriminantHints": { - "enable": "never" - }, - "expressionAdjustmentHints": { - "enable": "never", - "hideOutsideUnsafe": false, - "mode": "prefix" - }, - "lifetimeElisionHints": { - "enable": "never", - "useParameterNames": false - }, - "maxLength": 25, - "parameterHints": { - "enable": true - }, - "reborrowHints": { - "enable": "never" - }, - "renderColons": true, - "typeHints": { - "enable": true, - "hideClosureInitialization": false, - "hideNamedConstructor": false - } - }, - "interpret": { - "tests": false - }, - "joinLines": { - "joinAssignments": true, - "joinElseIf": true, - "removeTrailingComma": true, - "unwrapTrivialBlock": true - }, - "lens": { - "debug": { - "enable": true - }, - "enable": true, - "forceCustomCommands": true, - "implementations": { - "enable": true - }, - "location": "above_name", - "references": { - "adt": { - "enable": false - }, - "enumVariant": { - "enable": false - }, - "method": { - "enable": false - }, - "trait": { - "enable": false - } - }, - "run": { - "enable": true - } - }, - "linkedProjects": [], - "lru": { - "capacity": null, - "query": { - "capacities": {} - } - }, - "notifications": { - "cargoTomlNotFound": true - }, - "numThreads": null, - "procMacro": { - "attributes": { - "enable": true - }, - "enable": true, - "ignored": {}, - "server": null - }, - "references": { - "excludeImports": false - }, - "rust": { - "analyzerTargetDir": null - }, - "rustc": { - "source": null - }, - "rustfmt": { - "extraArgs": [], - "overrideCommand": null, - "rangeFormatting": { - "enable": false - } - }, - "semanticHighlighting": { - "doc": { - "comment": { - "inject": { - "enable": true - } - } - }, - "nonStandardTokens": true, - "operator": { - "enable": true, - "specialization": { - "enable": false - } - }, - "punctuation": { - "enable": false, - "separate": { - "macro": { - "bang": false - } - }, - "specialization": { - "enable": false - } - }, - "strings": { - "enable": true - } - }, - "signatureInfo": { - "detail": "full", - "documentation": { - "enable": true - } - }, - "workspace": { - "symbol": { - "search": { - "kind": "only_types", - "limit": 128, - "scope": "workspace" - } - } - } - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/multilspy/language_servers/rust_analyzer/runtime_dependencies.json b/src/multilspy/language_servers/rust_analyzer/runtime_dependencies.json deleted file mode 100644 index 517d8b8..0000000 --- a/src/multilspy/language_servers/rust_analyzer/runtime_dependencies.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running RustAnalyzer. Obtained from https://github.com/rust-lang/rust-analyzer/releases", - "runtimeDependencies": [ - { - "id": "RustAnalyzer", - "description": "RustAnalyzer for Linux (x64)", - "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-aarch64-apple-darwin.gz", - "platformId": "osx-arm64", - "archiveType": "gz", - "binaryName": "rust_analyzer" - }, - { - "id": "RustAnalyzer", - "description": "RustAnalyzer for Linux (x64)", - "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-unknown-linux-gnu.gz", - "platformId": "linux-x64", - "archiveType": "gz", - "binaryName": "rust_analyzer" - }, - { - "id": "RustAnalyzer", - "description": "RustAnalyzer for Windows (x64)", - "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-pc-windows-msvc.zip", - "platformId": "win-x64", - "archiveType": "zip", - "binaryName": "rust-analyzer.exe" - } - ] -} \ No newline at end of file diff --git a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py b/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py deleted file mode 100644 index 60d7103..0000000 --- a/src/multilspy/language_servers/rust_analyzer/rust_analyzer.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. -""" - -import asyncio -import json -import logging -import os -import stat -import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator - -from overrides import override - -from solidlsp.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_utils import FileUtils -from solidlsp.multilspy_utils import PlatformUtils - - -class RustAnalyzer(LanguageServer): - """ - Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. - """ - - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - """ - Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. - """ - rustanalyzer_executable_path = self.setup_runtime_dependencies(logger, config) - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path), - "rust", - ) - self.server_ready = asyncio.Event() - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - return super().is_ignored_dirname(dirname) or dirname in ["target"] - - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: - """ - Setup runtime dependencies for rust_analyzer. - """ - platform_id = PlatformUtils.get_platform_id() - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: - d = json.load(f) - del d["_description"] - - # assert platform_id.value in [ - # "linux-x64", - # "win-x64", - # ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" - - runtime_dependencies = d["runtimeDependencies"] - runtime_dependencies = [ - dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value - ] - assert len(runtime_dependencies) == 1 - dependency = runtime_dependencies[0] - - rustanalyzer_ls_dir = os.path.join(os.path.dirname(__file__), "static", "RustAnalyzer") - rustanalyzer_executable_path = os.path.join(rustanalyzer_ls_dir, dependency["binaryName"]) - if not os.path.exists(rustanalyzer_ls_dir): - os.makedirs(rustanalyzer_ls_dir) - if dependency["archiveType"] == "gz": - FileUtils.download_and_extract_archive( - logger, dependency["url"], rustanalyzer_executable_path, dependency["archiveType"] - ) - else: - FileUtils.download_and_extract_archive( - logger, dependency["url"], rustanalyzer_ls_dir, dependency["archiveType"] - ) - assert os.path.exists(rustanalyzer_executable_path) - os.chmod(rustanalyzer_executable_path, stat.S_IEXEC) - - return rustanalyzer_executable_path - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the Rust Analyzer Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: - d = json.load(f) - - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["RustAnalyzer"]: - """ - Starts the Rust Analyzer Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - """ - - async def register_capability_handler(params): - assert "registrations" in params - for registration in params["registrations"]: - if registration["method"] == "workspace/executeCommand": - self.initialize_searcher_command_available.set() - self.resolve_main_method_available.set() - return - - async def lang_status_handler(params): - # TODO: Should we wait for - # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} - # Before proceeding? - if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": - self.service_ready_event.set() - - async def execute_client_command_handler(params): - return [] - - async def do_nothing(params): - return - - async def check_experimental_status(params): - if params["quiescent"] == True: - self.server_ready.set() - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - self.server.on_request("client/registerCapability", register_capability_handler) - self.server.on_notification("language/status", lang_status_handler) - 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) - self.server.on_notification("experimental/serverStatus", check_experimental_status) - - async with super().start_server(): - self.logger.log("Starting RustAnalyzer server process", logging.INFO) - await 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, - ) - init_response = await self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "resolveProvider": True, - "triggerCharacters": [":", ".", "'", "("], - "completionItem": {"labelDetailsSupport": True}, - } - self.server.notify.initialized({}) - self.completions_available.set() - - await self.server_ready.wait() - - yield self diff --git a/src/multilspy/language_servers/solargraph/initialize_params.json b/src/multilspy/language_servers/solargraph/initialize_params.json deleted file mode 100644 index 2cbb41f..0000000 --- a/src/multilspy/language_servers/solargraph/initialize_params.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "_description": "This file contains the initialization parameters for the Solargraph Language Server.", - "processId": "$processId", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} diff --git a/src/multilspy/language_servers/solargraph/runtime_dependencies.json b/src/multilspy/language_servers/solargraph/runtime_dependencies.json deleted file mode 100644 index 38cc237..0000000 --- a/src/multilspy/language_servers/solargraph/runtime_dependencies.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "_description": "This file contains URLs and other metadata required for downloading and installing the Solargraph language server.", - "runtimeDependencies": [ - { - "url": "https://rubygems.org/downloads/solargraph-0.51.1.gem", - "installCommand": "gem install solargraph -v 0.51.1", - "binaryName": "solargraph", - "archiveType": "gem" - } - ] -} diff --git a/src/multilspy/language_servers/solargraph/solargraph.py b/src/multilspy/language_servers/solargraph/solargraph.py deleted file mode 100644 index 5f3d135..0000000 --- a/src/multilspy/language_servers/solargraph/solargraph.py +++ /dev/null @@ -1,187 +0,0 @@ -""" -Provides Ruby specific instantiation of the LanguageServer class using Solargraph. -Contains various configurations and settings specific to Ruby. -""" - -import asyncio -import json -import logging -import os -import stat -import subprocess -import pathlib -from contextlib import asynccontextmanager -from typing import AsyncIterator, override - -from solidlsp.multilspy_logger import MultilspyLogger -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.multilspy_config import MultilspyConfig - - -class Solargraph(LanguageServer): - """ - Provides Ruby specific instantiation of the LanguageServer class using Solargraph. - Contains various configurations and settings specific to Ruby. - """ - - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - """ - Creates a Solargraph instance. This class is not meant to be instantiated directly. - Use LanguageServer.create() instead. - """ - solargraph_executable_path = self.setup_runtime_dependencies(logger, config, repository_root_path) - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=f"{solargraph_executable_path} stdio", cwd=repository_root_path), - "ruby", - ) - self.server_ready = asyncio.Event() - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - return super().is_ignored_dirname(dirname) or dirname in ["vendor"] - - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig, repository_root_path: str) -> str: - """ - Setup runtime dependencies for Solargraph. - """ - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: - d = json.load(f) - del d["_description"] - - dependency = d["runtimeDependencies"][0] - - # Check if Ruby is installed - try: - result = subprocess.run(["ruby", "--version"], check=True, capture_output=True, cwd=repository_root_path) - ruby_version = result.stdout.strip() - logger.log(f"Ruby version: {ruby_version}", logging.INFO) - except subprocess.CalledProcessError as e: - raise RuntimeError(f"Error checking for Ruby installation: {e.stderr}") - except FileNotFoundError: - raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") - - # Check if solargraph is installed - try: - result = subprocess.run(["gem", "list", "^solargraph$", "-i"], check=False, capture_output=True, text=True, cwd=repository_root_path) - if result.stdout.strip() == "false": - logger.log("Installing Solargraph...", logging.INFO) - subprocess.run(dependency["installCommand"].split(), check=True, capture_output=True, cwd=repository_root_path) - - # Get the gem executable path directly - result = subprocess.run(["gem", "which", "solargraph"], check=True, capture_output=True, text=True, cwd=repository_root_path) - gem_path = result.stdout.strip() - bin_dir = os.path.join(os.path.dirname(os.path.dirname(gem_path)), "bin") - executable_path = os.path.join(bin_dir, "solargraph") - - if not os.path.exists(executable_path): - raise RuntimeError(f"Solargraph executable not found at {executable_path}") - - # Ensure the executable has the right permissions - os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IEXEC) - - return executable_path - except subprocess.CalledProcessError as e: - raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the Solargraph Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: - d = json.load(f) - - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["Solargraph"]: - """ - Starts the Solargraph Language Server for Ruby, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - """ - - async def register_capability_handler(params): - assert "registrations" in params - for registration in params["registrations"]: - if registration["method"] == "workspace/executeCommand": - self.initialize_searcher_command_available.set() - self.resolve_main_method_available.set() - return - - async def lang_status_handler(params): - # TODO: Should we wait for - # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} - # Before proceeding? - if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": - self.service_ready_event.set() - - async def execute_client_command_handler(params): - return [] - - async def do_nothing(params): - return - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - self.server.on_request("client/registerCapability", register_capability_handler) - self.server.on_notification("language/status", lang_status_handler) - 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) - - async with super().start_server(): - self.logger.log("Starting solargraph server process", logging.INFO) - await 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, - ) - self.logger.log(f"Sending init params: {json.dumps(initialize_params, indent=4)}", logging.INFO) - init_response = await self.server.send.initialize(initialize_params) - self.logger.log(f"Received init response: {init_response}", logging.INFO) - assert init_response["capabilities"]["textDocumentSync"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "resolveProvider": True, - "triggerCharacters": [".", ":", "@"], - } - self.server.notify.initialized({}) - self.completions_available.set() - - self.server_ready.set() - await self.server_ready.wait() - - yield self diff --git a/src/multilspy/language_servers/typescript_language_server/initialize_params.json b/src/multilspy/language_servers/typescript_language_server/initialize_params.json deleted file mode 100644 index 6209ba4..0000000 --- a/src/multilspy/language_servers/typescript_language_server/initialize_params.json +++ /dev/null @@ -1,906 +0,0 @@ -{ - "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", - "processId": "os.getpid()", - "clientInfo": { - "name": "Visual Studio Code - Insiders", - "version": "1.82.0-insider" - }, - "locale": "en", - "rootPath": "$rootPath", - "rootUri": "$rootUri", - "capabilities": { - "workspace": { - "applyEdit": true, - "workspaceEdit": { - "documentChanges": true, - "resourceOperations": [ - "create", - "rename", - "delete" - ], - "failureHandling": "textOnlyTransactional", - "normalizesLineEndings": true, - "changeAnnotationSupport": { - "groupsOnLabel": true - } - }, - "configuration": true, - "didChangeWatchedFiles": { - "dynamicRegistration": true, - "relativePatternSupport": true - }, - "symbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "resolveSupport": { - "properties": [ - "location.range" - ] - } - }, - "codeLens": { - "refreshSupport": true - }, - "executeCommand": { - "dynamicRegistration": true - }, - "didChangeConfiguration": { - "dynamicRegistration": true - }, - "workspaceFolders": true, - "semanticTokens": { - "refreshSupport": true - }, - "fileOperations": { - "dynamicRegistration": true, - "didCreate": true, - "didRename": true, - "didDelete": true, - "willCreate": true, - "willRename": true, - "willDelete": true - }, - "inlineValue": { - "refreshSupport": true - }, - "inlayHint": { - "refreshSupport": true - }, - "diagnostics": { - "refreshSupport": true - } - }, - "textDocument": { - "publishDiagnostics": { - "relatedInformation": true, - "versionSupport": false, - "tagSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "codeDescriptionSupport": true, - "dataSupport": true - }, - "synchronization": { - "dynamicRegistration": true, - "willSave": true, - "willSaveWaitUntil": true, - "didSave": true - }, - "completion": { - "dynamicRegistration": true, - "contextSupport": true, - "completionItem": { - "snippetSupport": true, - "commitCharactersSupport": true, - "documentationFormat": [ - "markdown", - "plaintext" - ], - "deprecatedSupport": true, - "preselectSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "insertReplaceSupport": true, - "resolveSupport": { - "properties": [ - "documentation", - "detail", - "additionalTextEdits" - ] - }, - "insertTextModeSupport": { - "valueSet": [ - 1, - 2 - ] - }, - "labelDetailsSupport": true - }, - "insertTextMode": 2, - "completionItemKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 - ] - }, - "completionList": { - "itemDefaults": [ - "commitCharacters", - "editRange", - "insertTextFormat", - "insertTextMode" - ] - } - }, - "hover": { - "dynamicRegistration": true, - "contentFormat": [ - "markdown", - "plaintext" - ] - }, - "signatureHelp": { - "dynamicRegistration": true, - "signatureInformation": { - "documentationFormat": [ - "markdown", - "plaintext" - ], - "parameterInformation": { - "labelOffsetSupport": true - }, - "activeParameterSupport": true - }, - "contextSupport": true - }, - "definition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "references": { - "dynamicRegistration": true - }, - "documentHighlight": { - "dynamicRegistration": true - }, - "documentSymbol": { - "dynamicRegistration": true, - "symbolKind": { - "valueSet": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26 - ] - }, - "hierarchicalDocumentSymbolSupport": true, - "tagSupport": { - "valueSet": [ - 1 - ] - }, - "labelSupport": true - }, - "codeAction": { - "dynamicRegistration": true, - "isPreferredSupport": true, - "disabledSupport": true, - "dataSupport": true, - "resolveSupport": { - "properties": [ - "edit" - ] - }, - "codeActionLiteralSupport": { - "codeActionKind": { - "valueSet": [ - "", - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports" - ] - } - }, - "honorsChangeAnnotations": false - }, - "codeLens": { - "dynamicRegistration": true - }, - "formatting": { - "dynamicRegistration": true - }, - "rangeFormatting": { - "dynamicRegistration": true - }, - "onTypeFormatting": { - "dynamicRegistration": true - }, - "rename": { - "dynamicRegistration": true, - "prepareSupport": true, - "prepareSupportDefaultBehavior": 1, - "honorsChangeAnnotations": true - }, - "documentLink": { - "dynamicRegistration": true, - "tooltipSupport": true - }, - "typeDefinition": { - "dynamicRegistration": true, - "linkSupport": true - }, - "implementation": { - "dynamicRegistration": true, - "linkSupport": true - }, - "colorProvider": { - "dynamicRegistration": true - }, - "foldingRange": { - "dynamicRegistration": true, - "rangeLimit": 5000, - "lineFoldingOnly": true, - "foldingRangeKind": { - "valueSet": [ - "comment", - "imports", - "region" - ] - }, - "foldingRange": { - "collapsedText": false - } - }, - "declaration": { - "dynamicRegistration": true, - "linkSupport": true - }, - "selectionRange": { - "dynamicRegistration": true - }, - "callHierarchy": { - "dynamicRegistration": true - }, - "semanticTokens": { - "dynamicRegistration": true, - "tokenTypes": [ - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ], - "tokenModifiers": [ - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ], - "formats": [ - "relative" - ], - "requests": { - "range": true, - "full": { - "delta": true - } - }, - "multilineTokenSupport": false, - "overlappingTokenSupport": false, - "serverCancelSupport": true, - "augmentsSyntaxTokens": false - }, - "linkedEditingRange": { - "dynamicRegistration": true - }, - "typeHierarchy": { - "dynamicRegistration": true - }, - "inlineValue": { - "dynamicRegistration": true - }, - "inlayHint": { - "dynamicRegistration": true, - "resolveSupport": { - "properties": [ - "tooltip", - "textEdits", - "label.tooltip", - "label.location", - "label.command" - ] - } - }, - "diagnostic": { - "dynamicRegistration": true, - "relatedDocumentSupport": false - } - }, - "general": { - "staleRequestSupport": { - "cancel": true, - "retryOnContentModified": [ - "textDocument/semanticTokens/full", - "textDocument/semanticTokens/range", - "textDocument/semanticTokens/full/delta" - ] - }, - "regularExpressions": { - "engine": "ECMAScript", - "version": "ES2020" - }, - "markdown": { - "parser": "marked", - "version": "1.1.0", - "allowedTags": [ - "ul", - "li", - "p", - "code", - "blockquote", - "ol", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "hr", - "em", - "pre", - "table", - "thead", - "tbody", - "tr", - "th", - "td", - "div", - "del", - "a", - "strong", - "br", - "img", - "span" - ] - }, - "positionEncodings": [ - "utf-16" - ] - }, - "notebookDocument": { - "synchronization": { - "dynamicRegistration": true, - "executionSummarySupport": true - } - }, - "experimental": { - "snippetTextEdit": true, - "codeActionGroup": true, - "hoverActions": true, - "serverStatusNotification": true, - "colorDiagnosticOutput": true, - "openServerLogs": true, - "localDocs": true, - "commands": { - "commands": [ - "rust-analyzer.runSingle", - "rust-analyzer.debugSingle", - "rust-analyzer.showReferences", - "rust-analyzer.gotoLocation", - "editor.action.triggerParameterHints" - ] - } - } - }, - "initializationOptions": { - "cargoRunner": null, - "runnables": { - "extraEnv": null, - "problemMatcher": [ - "$rustc" - ], - "command": null, - "extraArgs": [] - }, - "statusBar": { - "clickAction": "openLogs" - }, - "server": { - "path": null, - "extraEnv": null - }, - "trace": { - "server": "verbose", - "extension": false - }, - "debug": { - "engine": "auto", - "sourceFileMap": { - "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" - }, - "openDebugPane": false, - "engineSettings": {} - }, - "restartServerOnConfigChange": false, - "typing": { - "continueCommentsOnNewline": true, - "autoClosingAngleBrackets": { - "enable": false - } - }, - "diagnostics": { - "previewRustcOutput": false, - "useRustcErrorCode": false, - "disabled": [], - "enable": true, - "experimental": { - "enable": false - }, - "remapPrefix": {}, - "warningsAsHint": [], - "warningsAsInfo": [] - }, - "discoverProjectRunner": null, - "showUnlinkedFileNotification": true, - "showDependenciesExplorer": true, - "assist": { - "emitMustUse": false, - "expressionFillDefault": "todo" - }, - "cachePriming": { - "enable": true, - "numThreads": 0 - }, - "cargo": { - "autoreload": true, - "buildScripts": { - "enable": true, - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "overrideCommand": null, - "useRustcWrapper": true - }, - "cfgs": {}, - "extraArgs": [], - "extraEnv": {}, - "features": [], - "noDefaultFeatures": false, - "sysroot": "discover", - "sysrootSrc": null, - "target": null, - "unsetTest": [ - "core" - ] - }, - "checkOnSave": true, - "check": { - "allTargets": true, - "command": "check", - "extraArgs": [], - "extraEnv": {}, - "features": null, - "ignore": [], - "invocationLocation": "workspace", - "invocationStrategy": "per_workspace", - "noDefaultFeatures": null, - "overrideCommand": null, - "targets": null - }, - "completion": { - "autoimport": { - "enable": true - }, - "autoself": { - "enable": true - }, - "callable": { - "snippets": "fill_arguments" - }, - "fullFunctionSignatures": { - "enable": false - }, - "limit": null, - "postfix": { - "enable": true - }, - "privateEditable": { - "enable": false - }, - "snippets": { - "custom": { - "Arc::new": { - "postfix": "arc", - "body": "Arc::new(${receiver})", - "requires": "std::sync::Arc", - "description": "Put the expression into an `Arc`", - "scope": "expr" - }, - "Rc::new": { - "postfix": "rc", - "body": "Rc::new(${receiver})", - "requires": "std::rc::Rc", - "description": "Put the expression into an `Rc`", - "scope": "expr" - }, - "Box::pin": { - "postfix": "pinbox", - "body": "Box::pin(${receiver})", - "requires": "std::boxed::Box", - "description": "Put the expression into a pinned `Box`", - "scope": "expr" - }, - "Ok": { - "postfix": "ok", - "body": "Ok(${receiver})", - "description": "Wrap the expression in a `Result::Ok`", - "scope": "expr" - }, - "Err": { - "postfix": "err", - "body": "Err(${receiver})", - "description": "Wrap the expression in a `Result::Err`", - "scope": "expr" - }, - "Some": { - "postfix": "some", - "body": "Some(${receiver})", - "description": "Wrap the expression in an `Option::Some`", - "scope": "expr" - } - } - } - }, - "files": { - "excludeDirs": [], - "watcher": "client" - }, - "highlightRelated": { - "breakPoints": { - "enable": true - }, - "closureCaptures": { - "enable": true - }, - "exitPoints": { - "enable": true - }, - "references": { - "enable": true - }, - "yieldPoints": { - "enable": true - } - }, - "hover": { - "actions": { - "debug": { - "enable": true - }, - "enable": true, - "gotoTypeDef": { - "enable": true - }, - "implementations": { - "enable": true - }, - "references": { - "enable": false - }, - "run": { - "enable": true - } - }, - "documentation": { - "enable": true, - "keywords": { - "enable": true - } - }, - "links": { - "enable": true - }, - "memoryLayout": { - "alignment": "hexadecimal", - "enable": true, - "niches": false, - "offset": "hexadecimal", - "size": "both" - } - }, - "imports": { - "granularity": { - "enforce": false, - "group": "crate" - }, - "group": { - "enable": true - }, - "merge": { - "glob": true - }, - "preferNoStd": false, - "preferPrelude": false, - "prefix": "plain" - }, - "inlayHints": { - "bindingModeHints": { - "enable": false - }, - "chainingHints": { - "enable": true - }, - "closingBraceHints": { - "enable": true, - "minLines": 25 - }, - "closureCaptureHints": { - "enable": false - }, - "closureReturnTypeHints": { - "enable": "never" - }, - "closureStyle": "impl_fn", - "discriminantHints": { - "enable": "never" - }, - "expressionAdjustmentHints": { - "enable": "never", - "hideOutsideUnsafe": false, - "mode": "prefix" - }, - "lifetimeElisionHints": { - "enable": "never", - "useParameterNames": false - }, - "maxLength": 25, - "parameterHints": { - "enable": true - }, - "reborrowHints": { - "enable": "never" - }, - "renderColons": true, - "typeHints": { - "enable": true, - "hideClosureInitialization": false, - "hideNamedConstructor": false - } - }, - "interpret": { - "tests": false - }, - "joinLines": { - "joinAssignments": true, - "joinElseIf": true, - "removeTrailingComma": true, - "unwrapTrivialBlock": true - }, - "lens": { - "debug": { - "enable": true - }, - "enable": true, - "forceCustomCommands": true, - "implementations": { - "enable": true - }, - "location": "above_name", - "references": { - "adt": { - "enable": false - }, - "enumVariant": { - "enable": false - }, - "method": { - "enable": false - }, - "trait": { - "enable": false - } - }, - "run": { - "enable": true - } - }, - "linkedProjects": [], - "lru": { - "capacity": null, - "query": { - "capacities": {} - } - }, - "notifications": { - "cargoTomlNotFound": true - }, - "numThreads": null, - "procMacro": { - "attributes": { - "enable": true - }, - "enable": true, - "ignored": {}, - "server": null - }, - "references": { - "excludeImports": false - }, - "rust": { - "analyzerTargetDir": null - }, - "rustc": { - "source": null - }, - "rustfmt": { - "extraArgs": [], - "overrideCommand": null, - "rangeFormatting": { - "enable": false - } - }, - "semanticHighlighting": { - "doc": { - "comment": { - "inject": { - "enable": true - } - } - }, - "nonStandardTokens": true, - "operator": { - "enable": true, - "specialization": { - "enable": false - } - }, - "punctuation": { - "enable": false, - "separate": { - "macro": { - "bang": false - } - }, - "specialization": { - "enable": false - } - }, - "strings": { - "enable": true - } - }, - "signatureInfo": { - "detail": "full", - "documentation": { - "enable": true - } - }, - "workspace": { - "symbol": { - "search": { - "kind": "only_types", - "limit": 128, - "scope": "workspace" - } - } - } - }, - "trace": "verbose", - "workspaceFolders": [ - { - "uri": "$uri", - "name": "$name" - } - ] -} \ No newline at end of file diff --git a/src/multilspy/language_servers/typescript_language_server/runtime_dependencies.json b/src/multilspy/language_servers/typescript_language_server/runtime_dependencies.json deleted file mode 100644 index 90824a7..0000000 --- a/src/multilspy/language_servers/typescript_language_server/runtime_dependencies.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "_description": "Used to download the runtime dependencies for running typescript-language-server. Obtained from https://github.com/typescript-language-server/typescript-language-server/releases", - "runtimeDependencies": [ - { - "id": "typescript", - "description": "typescript package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", - "command": "npm install --prefix ./ typescript@5.5.4" - }, - { - "id": "typescript-language-server", - "description": "typescript-language-server package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", - "command": "npm install --prefix ./ typescript-language-server@4.3.3" - } - ] -} - diff --git a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py deleted file mode 100644 index edd3b30..0000000 --- a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py +++ /dev/null @@ -1,245 +0,0 @@ -""" -Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. -""" - -import asyncio -import json -import logging -import os -import pathlib -import shutil -import subprocess -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager -from time import sleep - -from overrides import override - -from multilspy.language_server import LanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_utils import PlatformId, PlatformUtils - -# Platform-specific imports -if os.name != 'nt': # Unix-like systems - import pwd -else: - # Dummy pwd module for Windows - class pwd: - @staticmethod - def getpwuid(uid): - return type('obj', (), {'pw_name': os.environ.get('USERNAME', 'unknown')})() - - -# Conditionally import pwd module (Unix-only) -if not PlatformUtils.get_platform_id().value.startswith("win"): - import pwd - - -class TypeScriptLanguageServer(LanguageServer): - """ - Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. - """ - - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - """ - Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. - """ - ts_lsp_executable_path = self.setup_runtime_dependencies(logger, config) - super().__init__( - config, - logger, - repository_root_path, - ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path), - "typescript", - ) - self.server_ready = asyncio.Event() - - @override - def is_ignored_dirname(self, dirname: str) -> bool: - return super().is_ignored_dirname(dirname) or dirname in [ - "node_modules", - "dist", - "build", - "coverage", - ] - - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: - """ - Setup runtime dependencies for TypeScript Language Server. - """ - platform_id = PlatformUtils.get_platform_id() - - valid_platforms = [ - PlatformId.LINUX_x64, - PlatformId.LINUX_arm64, - PlatformId.OSX, - 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 multilspy javascript/typescript at the moment" - - with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: - d = json.load(f) - del d["_description"] - - runtime_dependencies = d.get("runtimeDependencies", []) - tsserver_ls_dir = os.path.join(os.path.dirname(__file__), "static", "ts-lsp") - tsserver_executable_path = os.path.join(tsserver_ls_dir, "typescript-language-server") - - # Verify both node and npm are installed - is_node_installed = shutil.which('node') is not None - assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." - is_npm_installed = shutil.which('npm') is not None - assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - - # Install typescript and typescript-language-server if not already installed - if not os.path.exists(tsserver_ls_dir): - os.makedirs(tsserver_ls_dir, exist_ok=True) - for dependency in runtime_dependencies: - # Windows doesn't support the 'user' parameter and doesn't have pwd module - if PlatformUtils.get_platform_id().value.startswith("win"): - subprocess.run( - dependency["command"], - shell=True, - check=True, - cwd=tsserver_ls_dir, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - else: - # On Unix-like systems, run as non-root user - user = pwd.getpwuid(os.getuid()).pw_name - subprocess.run( - dependency["command"], - shell=True, - check=True, - user=user, - cwd=tsserver_ls_dir, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - - tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") - - assert os.path.exists(tsserver_executable_path), "typescript-language-server executable not found. Please install typescript-language-server and try again." - return f"{tsserver_executable_path} --stdio" - - def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: - """ - Returns the initialize params for the TypeScript Language Server. - """ - with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) as f: - d = json.load(f) - - del d["_description"] - - d["processId"] = os.getpid() - assert d["rootPath"] == "$rootPath" - d["rootPath"] = repository_absolute_path - - assert d["rootUri"] == "$rootUri" - d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["uri"] == "$uri" - d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() - - assert d["workspaceFolders"][0]["name"] == "$name" - d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) - - return d - - @asynccontextmanager - async def start_server(self) -> AsyncIterator["TypeScriptLanguageServer"]: - """ - Starts the TypeScript Language Server, waits for the server to be ready and yields the LanguageServer instance. - - Usage: - ``` - async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests - await lsp.request_definition(...) - await lsp.request_references(...) - # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown - """ - - async def register_capability_handler(params): - assert "registrations" in params - for registration in params["registrations"]: - if registration["method"] == "workspace/executeCommand": - self.initialize_searcher_command_available.set() - # TypeScript doesn't have a direct equivalent to resolve_main_method - # You might want to set a different flag or remove this line - # self.resolve_main_method_available.set() - return - - async def execute_client_command_handler(params): - return [] - - async def do_nothing(params): - return - - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) - - - async def check_experimental_status(params): - """ - Also listen for experimental/serverStatus as a backup signal - """ - if params.get("quiescent") == True: - self.server_ready.set() - self.completions_available.set() - - self.server.on_request("client/registerCapability", register_capability_handler) - 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("experimental/serverStatus", check_experimental_status) - - async with super().start_server(): - self.logger.log("Starting TypeScript server process", logging.INFO) - await 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, - ) - init_response = await self.server.send.initialize(initialize_params) - - # TypeScript-specific capability checks - assert init_response["capabilities"]["textDocumentSync"] == 2 - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": ['.', '"', "'", '/', '@', '<'], - "resolveProvider": True - } - - self.server.notify.initialized({}) - try: - await asyncio.wait_for(self.server_ready.wait(), timeout=1.0) - except asyncio.TimeoutError: - self.logger.log("Timeout waiting for TypeScript server to become ready, proceeding anyway", logging.INFO) - # Fallback: assume server is ready after timeout - self.server_ready.set() - self.completions_available.set() - - yield self - - @override - # For some reason, the LS may need longer to process this, so we just retry - async def _send_references_request(self, relative_file_path: str, line: int, column: int): - # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is - # despite the LS having processed requests already. I don't know what causes this, but sleeping - # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. - # It may be that even this 1sec is not enough in larger TS projects, at some point we should find what - # causes this and solve it. - sleep(1) - return await super()._send_references_request(relative_file_path, line, column) From e60d42211afeafa0f5a1ff16e8e577e3ed25929d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 17:01:36 +0200 Subject: [PATCH 144/174] Update language server instantiation to always use SolidLanguageServer --- src/serena/agent.py | 24 +++++++----------------- src/serena/constants.py | 6 +----- src/serena/mcp.py | 5 ++--- test/conftest.py | 7 +------ 4 files changed, 11 insertions(+), 31 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 1ac3ffb..db96625 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -33,7 +33,6 @@ from sensai.util import logging from sensai.util.logging import LOG_DEFAULT_FORMAT, FallbackHandler from sensai.util.string import ToStringMixin, dict_string -from solidlsp import SolidLanguageServer as SyncLanguageServer from serena import serena_version from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import ( @@ -42,7 +41,6 @@ from serena.constants import ( REPO_ROOT, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME, - USE_SOLID_LSP, ) from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI from serena.prompt_factory import PromptFactory, SerenaPromptFactory @@ -53,6 +51,7 @@ from serena.util.general import load_yaml, save_yaml from serena.util.inspection import determine_programming_language_composition, iter_subclasses from serena.util.shell import execute_shell_command from serena.util.thread import ExecutionResult, execute_with_timeout +from solidlsp import SolidLanguageServer as SyncLanguageServer from solidlsp.ls import SolidLanguageServer from solidlsp.multilspy_config import Language, MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger @@ -678,21 +677,12 @@ def create_ls_for_project( ) ls_logger = MultilspyLogger(log_level=log_level) log.info(f"Creating language server instance for {project_instance.project_root}.") - if USE_SOLID_LSP: - ls = SolidLanguageServer.create( - multilspy_config, - ls_logger, - project_instance.project_root, - timeout=ls_timeout, - ) - return cast(SyncLanguageServer, ls) # TODO: Fix type - else: - return SyncLanguageServer.create( - multilspy_config, - ls_logger, - project_instance.project_root, - timeout=ls_timeout, - ) + return SolidLanguageServer.create( + multilspy_config, + ls_logger, + project_instance.project_root, + timeout=ls_timeout, + ) @click.command() diff --git a/src/serena/constants.py b/src/serena/constants.py index 6a27759..30a9518 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -19,8 +19,4 @@ DEFAULT_MODES = ("interactive", "editing") PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.yml") SELENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml") -USE_SOLID_LSP = True -"""Whether to use Solid LSP instead of multilspy for language server operations. -Multilspy uses asyncio, which creates many problems like deadlocks and requires full process -isolation (and even then some issues remain). -""" +USE_PROCESS_ISOLATION = False diff --git a/src/serena/mcp.py b/src/serena/mcp.py index e2a0bc9..83a2cbf 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -36,7 +36,7 @@ from serena.agent import ( show_fatal_exception_safe, ) from serena.config import RegisteredContext, SerenaAgentContext, SerenaAgentMode -from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, USE_SOLID_LSP +from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, USE_SOLID_LSP, USE_PROCESS_ISOLATION from serena.process_isolated_agent import ( ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent, @@ -587,10 +587,9 @@ def start_mcp_server( project_file = project_file_arg if project_file_arg is not None else project mcp_factory: SerenaMCPFactory - if USE_SOLID_LSP: + if not USE_PROCESS_ISOLATION: mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) else: - # using multilspy requires process isolation to prevent asyncio contamination mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) # Use process isolation by default to prevent asyncio event loop contamination diff --git a/test/conftest.py b/test/conftest.py index 8e086cf..e41a53d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -45,12 +45,7 @@ def create_ls( ignored_paths.extend(spec.patterns) config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) logger = MultilspyLogger(log_level=log_level) - - if USE_SOLID_LSP: - ls = SolidLanguageServer.create(config, logger, repo_path) - return cast(SyncLanguageServer, ls) # TODO: Fix type - else: - return SyncLanguageServer.create(config, logger, repo_path) + return SolidLanguageServer.create(config, logger, repo_path) def create_default_ls(language: Language) -> SyncLanguageServer: From 05a7d438bc66748fd44c8b7100b3cccc7d0287b5 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 17:11:22 +0200 Subject: [PATCH 145/174] Update language server imports to no longer use renames --- src/serena/agent.py | 9 ++-- src/serena/mcp.py | 2 +- src/serena/symbol.py | 6 +-- src/solidlsp/__init__.py | 1 + test/conftest.py | 7 +-- test/multilspy/go/test_go_basic.py | 6 +-- test/multilspy/java/test_java_basic.py | 8 ++-- test/multilspy/php/test_php_basic.py | 14 +++--- test/multilspy/python/test_python_basic.py | 14 +++--- .../test_retrieval_with_ignored_dirs.py | 8 ++-- .../multilspy/python/test_symbol_retrieval.py | 44 +++++++++---------- test/multilspy/rust/test_rust_basic.py | 10 ++--- .../typescript/test_typescript_basic.py | 6 +-- 13 files changed, 66 insertions(+), 69 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index db96625..965dbdf 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -51,8 +51,7 @@ from serena.util.general import load_yaml, save_yaml from serena.util.inspection import determine_programming_language_composition, iter_subclasses from serena.util.shell import execute_shell_command from serena.util.thread import ExecutionResult, execute_with_timeout -from solidlsp import SolidLanguageServer as SyncLanguageServer -from solidlsp.ls import SolidLanguageServer +from solidlsp import SolidLanguageServer from solidlsp.multilspy_config import Language, MultilspyConfig from solidlsp.multilspy_logger import MultilspyLogger from solidlsp.multilspy_types import SymbolKind @@ -640,7 +639,7 @@ def create_ls_for_project( log_level: int = logging.INFO, ls_timeout: float | None = DEFAULT_TOOL_TIMEOUT - 5, trace_lsp_communication: bool = False, -) -> SyncLanguageServer: +) -> SolidLanguageServer: """ Create a language server for a project. Note that you will have to start it before performing any LS operations. @@ -789,7 +788,7 @@ class SerenaAgent: # project-specific instances, which will be initialized upon project activation self._active_project: Project | None = None self._active_project_root: str | None = None - self.language_server: SyncLanguageServer | None = None + self.language_server: SolidLanguageServer | None = None self.symbol_manager: SymbolManager | None = None self.memories_manager: MemoriesManager | None = None self.lines_read: LinesRead | None = None @@ -1135,7 +1134,7 @@ class Component(ABC): self.agent = agent @property - def language_server(self) -> SyncLanguageServer: + def language_server(self) -> SolidLanguageServer: assert self.agent.language_server is not None return self.agent.language_server diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 83a2cbf..8666ef0 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -36,7 +36,7 @@ from serena.agent import ( show_fatal_exception_safe, ) from serena.config import RegisteredContext, SerenaAgentContext, SerenaAgentMode -from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, USE_SOLID_LSP, USE_PROCESS_ISOLATION +from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, USE_PROCESS_ISOLATION from serena.process_isolated_agent import ( ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent, diff --git a/src/serena/symbol.py b/src/serena/symbol.py index ec94db2..e4da274 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Self, Union from sensai.util.string import ToStringMixin -from solidlsp import SolidLanguageServer as SyncLanguageServer +from solidlsp import SolidLanguageServer from solidlsp.ls import ReferenceInSymbol as LSPReferenceInSymbol from solidlsp.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation @@ -488,7 +488,7 @@ class ReferenceInSymbol(ToStringMixin): class SymbolManager: - def __init__(self, lang_server: SyncLanguageServer, agent: Union["SerenaAgent", None] = None) -> None: + def __init__(self, lang_server: SolidLanguageServer, agent: Union["SerenaAgent", None] = None) -> None: """ :param lang_server: the language server to use for symbol retrieval as well as editing operations. :param agent: the agent to use (only needed for marking files as modified). You can pass None if you don't @@ -497,7 +497,7 @@ class SymbolManager: self._lang_server = lang_server self.agent = agent - def set_language_server(self, lang_server: SyncLanguageServer) -> None: + def set_language_server(self, lang_server: SolidLanguageServer) -> None: """ Set the language server to use for symbol retrieval and editing operations. This is useful if you want to change the language server after initializing the SymbolManager. diff --git a/src/solidlsp/__init__.py b/src/solidlsp/__init__.py index fbd65b1..0378e2d 100644 --- a/src/solidlsp/__init__.py +++ b/src/solidlsp/__init__.py @@ -1 +1,2 @@ +# ruff: noqa from .ls import SolidLanguageServer diff --git a/test/conftest.py b/test/conftest.py index e41a53d..1cb81ea 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,12 +1,9 @@ import logging from pathlib import Path -from typing import cast import pytest from sensai.util.logging import configure -from solidlsp import SolidLanguageServer as SyncLanguageServer -from serena.constants import USE_SOLID_LSP from serena.util.file_system import GitignoreParser from solidlsp.ls import SolidLanguageServer from solidlsp.multilspy_config import Language, MultilspyConfig @@ -36,7 +33,7 @@ def create_ls( ignored_paths: list[str] | None = None, trace_lsp_communication: bool = False, log_level: int = logging.INFO, -) -> SyncLanguageServer: +) -> SolidLanguageServer: ignored_paths = ignored_paths or [] if repo_path is None: repo_path = str(get_repo_path(language)) @@ -48,7 +45,7 @@ def create_ls( return SolidLanguageServer.create(config, logger, repo_path) -def create_default_ls(language: Language) -> SyncLanguageServer: +def create_default_ls(language: Language) -> SolidLanguageServer: repo_path = str(get_repo_path(language)) return create_ls(language, repo_path) diff --git a/test/multilspy/go/test_go_basic.py b/test/multilspy/go/test_go_basic.py index e5d82de..82b6960 100644 --- a/test/multilspy/go/test_go_basic.py +++ b/test/multilspy/go/test_go_basic.py @@ -2,7 +2,7 @@ import os import pytest -from solidlsp import SolidLanguageServer as SyncLanguageServer +from solidlsp import SolidLanguageServer from solidlsp.multilspy_config import Language from solidlsp.multilspy_utils import SymbolUtils @@ -10,14 +10,14 @@ from solidlsp.multilspy_utils import SymbolUtils @pytest.mark.go class TestGoLanguageServer: @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) - def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "Helper"), "Helper function not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoStruct"), "DemoStruct not found in symbol tree" @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) - def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: + def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: file_path = os.path.join("main.go") symbols = language_server.request_document_symbols(file_path) helper_symbol = None diff --git a/test/multilspy/java/test_java_basic.py b/test/multilspy/java/test_java_basic.py index 679a7a8..f592373 100644 --- a/test/multilspy/java/test_java_basic.py +++ b/test/multilspy/java/test_java_basic.py @@ -2,7 +2,7 @@ import os import pytest -from solidlsp import SolidLanguageServer as SyncLanguageServer +from solidlsp import SolidLanguageServer from solidlsp.multilspy_config import Language from solidlsp.multilspy_utils import SymbolUtils @@ -10,14 +10,14 @@ from solidlsp.multilspy_utils import SymbolUtils @pytest.mark.java class TestJavaLanguageServer: @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) - def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model class not found in symbol tree" @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) - def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: + def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: # Use correct Maven/Java file paths file_path = os.path.join("src", "main", "java", "test_repo", "Utils.java") refs = language_server.request_references(file_path, 4, 20) @@ -43,7 +43,7 @@ class TestJavaLanguageServer: ), "Main should reference Model (tried all positions in selectionRange)" @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) - def test_overview_methods(self, language_server: SyncLanguageServer) -> None: + def test_overview_methods(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" diff --git a/test/multilspy/php/test_php_basic.py b/test/multilspy/php/test_php_basic.py index ff4bf32..3ae2797 100644 --- a/test/multilspy/php/test_php_basic.py +++ b/test/multilspy/php/test_php_basic.py @@ -2,7 +2,7 @@ from pathlib import Path import pytest -from solidlsp import SolidLanguageServer as SyncLanguageServer +from solidlsp import SolidLanguageServer from solidlsp.multilspy_config import Language @@ -10,7 +10,7 @@ from solidlsp.multilspy_config import Language class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_ls_is_running(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None: """Test that the language server starts and stops successfully.""" # The fixture already handles start and stop assert language_server.is_running() @@ -18,7 +18,7 @@ class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_find_definition_within_file(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None: # In index.php: # Line 9 (1-indexed): $greeting = greet($userName); @@ -41,7 +41,7 @@ class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_find_definition_across_files(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 12, 5) # helperFunction assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}" @@ -53,7 +53,7 @@ class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_find_definition_simple_variable(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_find_definition_simple_variable(self, language_server: SolidLanguageServer, repo_path: Path) -> None: file_path = str(repo_path / "simple_var.php") # In simple_var.php: @@ -74,7 +74,7 @@ class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_find_references_within_file(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None: index_php_path = str(repo_path / "index.php") # In index.php (0-indexed lines): @@ -108,7 +108,7 @@ class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) - def test_find_references_across_files(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: helper_php_path = str(repo_path / "helper.php") # In index.php (0-indexed lines): # Line 13: helperFunction(); // Usage of helperFunction diff --git a/test/multilspy/python/test_python_basic.py b/test/multilspy/python/test_python_basic.py index c5522e5..541f7f5 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/multilspy/python/test_python_basic.py @@ -9,8 +9,8 @@ import os import pytest -from solidlsp import SolidLanguageServer as SyncLanguageServer from serena.text_utils import LineType +from solidlsp import SolidLanguageServer from solidlsp.multilspy_config import Language @@ -19,7 +19,7 @@ class TestLanguageServerBasics: """Test basic functionality of the language server.""" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_user_class(self, language_server: SyncLanguageServer) -> None: + def test_request_references_user_class(self, language_server: SolidLanguageServer) -> None: """Test request_references on the User class.""" # Get references to the User class in models.py file_path = os.path.join("test_repo", "models.py") @@ -34,7 +34,7 @@ class TestLanguageServerBasics: assert len(references) > 1, "User class should be referenced in multiple files (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_item_class(self, language_server: SyncLanguageServer) -> None: + def test_request_references_item_class(self, language_server: SolidLanguageServer) -> None: """Test request_references on the Item class.""" # Get references to the Item class in models.py file_path = os.path.join("test_repo", "models.py") @@ -50,7 +50,7 @@ class TestLanguageServerBasics: assert len(services_references) > 0, "At least one reference should be in services.py (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_function_parameter(self, language_server: SyncLanguageServer) -> None: + def test_request_references_function_parameter(self, language_server: SolidLanguageServer) -> None: """Test request_references on a function parameter.""" # Get references to the id parameter in get_user method file_path = os.path.join("test_repo", "services.py") @@ -65,7 +65,7 @@ class TestLanguageServerBasics: assert len(references) > 0, "id parameter should be referenced within the method (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_create_user_method(self, language_server: SyncLanguageServer) -> None: + def test_request_references_create_user_method(self, language_server: SolidLanguageServer) -> None: # Get references to the create_user method in UserService file_path = os.path.join("test_repo", "services.py") # Line 15 contains the create_user method definition @@ -79,7 +79,7 @@ class TestLanguageServerBasics: assert len(references) > 1, "Should get valid references for create_user (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_retrieve_content_around_line(self, language_server: SyncLanguageServer) -> None: + def test_retrieve_content_around_line(self, language_server: SolidLanguageServer) -> None: """Test retrieve_content_around_line functionality with various scenarios.""" file_path = os.path.join("test_repo", "models.py") @@ -186,7 +186,7 @@ class TestLanguageServerBasics: assert line.match_type == LineType.AFTER_MATCH @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_search_files_for_pattern(self, language_server: SyncLanguageServer) -> None: + def test_search_files_for_pattern(self, language_server: SolidLanguageServer) -> None: """Test search_files_for_pattern with various patterns and glob filters.""" # Test 1: Search for class definitions across all files class_pattern = r"class\s+\w+\s*(?:\([^{]*\)|:)" diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index d0446f9..f100d41 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest -from solidlsp import SolidLanguageServer as SyncLanguageServer +from solidlsp import SolidLanguageServer from solidlsp.multilspy_config import Language from test.conftest import create_ls @@ -12,7 +12,7 @@ pytestmark = pytest.mark.python @pytest.fixture(scope="module") -def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: +def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" ignored_paths = ["scripts", "custom_test"] ls = create_ls(ignored_paths=ignored_paths, language=Language.PYTHON) @@ -24,7 +24,7 @@ def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True) -def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): +def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that request_full_symbol_tree ignores the configured directory.""" root = ls_with_ignored_dirs.request_full_symbol_tree()[0] root_children = root["children"] @@ -33,7 +33,7 @@ def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True) -def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): +def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that find_references ignores the configured directory.""" # Location of Item, which is referenced in scripts definition_file = "test_repo/models.py" diff --git a/test/multilspy/python/test_symbol_retrieval.py b/test/multilspy/python/test_symbol_retrieval.py index 5bca8f6..97f5d44 100644 --- a/test/multilspy/python/test_symbol_retrieval.py +++ b/test/multilspy/python/test_symbol_retrieval.py @@ -10,7 +10,7 @@ import os import pytest -from solidlsp import SolidLanguageServer as SyncLanguageServer +from solidlsp import SolidLanguageServer from solidlsp.multilspy_config import Language from solidlsp.multilspy_types import SymbolKind @@ -21,7 +21,7 @@ class TestLanguageServerSymbols: """Test the language server's symbol-related functionality.""" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_function(self, language_server: SyncLanguageServer) -> None: + def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a function.""" # Test for a position inside the create_user method file_path = os.path.join("test_repo", "services.py") @@ -36,7 +36,7 @@ class TestLanguageServerSymbols: assert containing_symbol["body"].strip().startswith("def create_user(self") @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_references_to_variables(self, language_server: SyncLanguageServer) -> None: + def test_references_to_variables(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a variable.""" file_path = os.path.join("test_repo", "variables.py") # Line 75 contains the field status that is later modified @@ -51,7 +51,7 @@ class TestLanguageServerSymbols: assert "second_dataclass" in ref_names @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_class(self, language_server: SyncLanguageServer) -> None: + def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a class.""" # Test for a position inside the UserService class but outside any method file_path = os.path.join("test_repo", "services.py") @@ -64,7 +64,7 @@ class TestLanguageServerSymbols: assert containing_symbol["kind"] == SymbolKind.Class @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_nested(self, language_server: SyncLanguageServer) -> None: + def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol with nested scopes.""" # Test for a position inside a method which is inside a class file_path = os.path.join("test_repo", "services.py") @@ -90,7 +90,7 @@ class TestLanguageServerSymbols: assert parent_symbol["kind"] == SymbolKind.Class @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_none(self, language_server: SyncLanguageServer) -> None: + def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a position with no containing symbol.""" # Test for a position outside any function/class (e.g., in imports) file_path = os.path.join("test_repo", "services.py") @@ -101,7 +101,7 @@ class TestLanguageServerSymbols: assert containing_symbol is None or containing_symbol == {} @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_function(self, language_server: SyncLanguageServer) -> None: + def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a function.""" # Test referencing symbols for create_user function file_path = os.path.join("test_repo", "services.py") @@ -125,7 +125,7 @@ class TestLanguageServerSymbols: assert "end" in symbol["location"]["range"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_class(self, language_server: SyncLanguageServer) -> None: + def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a class.""" # Test referencing symbols for User class file_path = os.path.join("test_repo", "models.py") @@ -146,7 +146,7 @@ class TestLanguageServerSymbols: assert len(services_references) > 0, "No referencing symbols from services.py for User (selectionRange)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_parameter(self, language_server: SyncLanguageServer) -> None: + def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a function parameter.""" # Test referencing symbols for id parameter in get_user file_path = os.path.join("test_repo", "services.py") @@ -167,7 +167,7 @@ class TestLanguageServerSymbols: assert len(method_refs) > 0, "No referencing symbols within method body for get_user (selectionRange)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_none(self, language_server: SyncLanguageServer) -> None: + def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a position with no symbol.""" # For positions with no symbol, the method might throw an error or return None/empty list # We'll modify our test to handle this by using a try-except block @@ -185,7 +185,7 @@ class TestLanguageServerSymbols: # Tests for request_defining_symbol @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_variable(self, language_server: SyncLanguageServer) -> None: + def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a variable usage.""" # Test finding the definition of a symbol in the create_user method file_path = os.path.join("test_repo", "services.py") @@ -204,7 +204,7 @@ class TestLanguageServerSymbols: assert "services.py" in defining_symbol["location"]["uri"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_imported_class(self, language_server: SyncLanguageServer) -> None: + def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for an imported class.""" # Test finding the definition of the 'User' class used in the UserService.create_user method file_path = os.path.join("test_repo", "services.py") @@ -216,7 +216,7 @@ class TestLanguageServerSymbols: assert defining_symbol.get("name") == "User" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_method_call(self, language_server: SyncLanguageServer) -> None: + def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a method call.""" # Create an example file path for a file that calls UserService.create_user examples_file_path = os.path.join("examples", "user_management.py") @@ -241,7 +241,7 @@ class TestLanguageServerSymbols: warnings.warn("Could not verify method call definition - file structure may differ from expected") @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_none(self, language_server: SyncLanguageServer) -> None: + def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a position with no symbol.""" # Test for a position with no symbol (e.g., whitespace or comment) file_path = os.path.join("test_repo", "services.py") @@ -252,7 +252,7 @@ class TestLanguageServerSymbols: assert defining_symbol is None @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_variable(self, language_server: SyncLanguageServer) -> None: + def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol where the symbol is a variable.""" # Test for a position inside a variable definition file_path = os.path.join("test_repo", "services.py") @@ -265,7 +265,7 @@ class TestLanguageServerSymbols: assert containing_symbol["kind"] == SymbolKind.Variable @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_nested_function(self, language_server: SyncLanguageServer) -> None: + def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a nested function or closure.""" # Use the existing nested.py file which contains nested classes and methods file_path = os.path.join("test_repo", "nested.py") @@ -316,7 +316,7 @@ class TestLanguageServerSymbols: assert defining_symbol.get("kind") == SymbolKind.Function.value @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_symbol_methods_integration(self, language_server: SyncLanguageServer) -> None: + def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None: """Test the integration between different symbol-related methods.""" # This test demonstrates using the various symbol methods together # by finding a symbol and then checking its definition @@ -365,7 +365,7 @@ class TestLanguageServerSymbols: warnings.warn("Could not verify container hierarchy - implementation detail") @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_symbol_tree_structure(self, language_server: SyncLanguageServer) -> None: + def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None: """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file repo_structure = language_server.request_full_symbol_tree() @@ -393,7 +393,7 @@ class TestLanguageServerSymbols: assert user_management_roots == user_management_node["children"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer) -> None: + def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None: """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file examples_package_roots = language_server.request_full_symbol_tree(within_relative_path="examples") @@ -414,7 +414,7 @@ class TestLanguageServerSymbols: assert user_management_roots == user_management_node["children"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_dir_overview(self, language_server: SyncLanguageServer) -> None: + def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None: """Test that request_dir_overview returns correct symbol information for files in a directory.""" # Get overview of the examples directory overview = language_server.request_dir_overview("test_repo") @@ -439,7 +439,7 @@ class TestLanguageServerSymbols: assert symbol in services_symbols @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_document_overview(self, language_server: SyncLanguageServer) -> None: + def test_request_document_overview(self, language_server: SolidLanguageServer) -> None: """Test that request_document_overview returns correct symbol information for a file.""" # Get overview of the user_management.py file overview = language_server.request_document_overview(os.path.join("examples", "user_management.py")) @@ -449,7 +449,7 @@ class TestLanguageServerSymbols: assert {"UserStats", "UserManager", "process_user_data", "main"}.issubset(symbol_names) @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_containing_symbol_of_var_is_file(self, language_server: SyncLanguageServer) -> None: + def test_containing_symbol_of_var_is_file(self, language_server: SolidLanguageServer) -> None: """Test that the containing symbol of a variable is the file itself.""" # Get the containing symbol of a variable in a file file_path = os.path.join("test_repo", "services.py") diff --git a/test/multilspy/rust/test_rust_basic.py b/test/multilspy/rust/test_rust_basic.py index ffe1245..e6cd6f4 100644 --- a/test/multilspy/rust/test_rust_basic.py +++ b/test/multilspy/rust/test_rust_basic.py @@ -2,7 +2,7 @@ import os import pytest -from solidlsp import SolidLanguageServer as SyncLanguageServer +from solidlsp import SolidLanguageServer from solidlsp.multilspy_config import Language from solidlsp.multilspy_utils import SymbolUtils @@ -10,7 +10,7 @@ from solidlsp.multilspy_utils import SymbolUtils @pytest.mark.rust class TestRustLanguageServer: @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_find_references_raw(self, language_server: SyncLanguageServer) -> None: + def test_find_references_raw(self, language_server: SolidLanguageServer) -> None: # Directly test the request_references method for the add function file_path = os.path.join("src", "lib.rs") symbols = language_server.request_document_symbols(file_path) @@ -27,14 +27,14 @@ class TestRustLanguageServer: ), "main.rs should reference add (raw, tried all positions in selectionRange)" @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add function not found in symbol tree" # Add more as needed based on test_repo @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: + def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: # Find references to 'add' defined in lib.rs, should be referenced from main.rs file_path = os.path.join("src", "lib.rs") symbols = language_server.request_document_symbols(file_path) @@ -51,7 +51,7 @@ class TestRustLanguageServer: ), "main.rs should reference add (tried all positions in selectionRange)" @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_overview_methods(self, language_server: SyncLanguageServer) -> None: + def test_overview_methods(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add missing from overview" diff --git a/test/multilspy/typescript/test_typescript_basic.py b/test/multilspy/typescript/test_typescript_basic.py index e135f88..1c6cb5f 100644 --- a/test/multilspy/typescript/test_typescript_basic.py +++ b/test/multilspy/typescript/test_typescript_basic.py @@ -2,7 +2,7 @@ import os import pytest -from solidlsp import SolidLanguageServer as SyncLanguageServer +from solidlsp import SolidLanguageServer from solidlsp.multilspy_config import Language from solidlsp.multilspy_utils import SymbolUtils @@ -10,14 +10,14 @@ from solidlsp.multilspy_utils import SymbolUtils @pytest.mark.typescript class TestTypescriptLanguageServer: @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) - def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoClass"), "DemoClass not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "helperFunction"), "helperFunction not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "printValue"), "printValue method not found in symbol tree" @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) - def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: + def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: file_path = os.path.join("index.ts") symbols = language_server.request_document_symbols(file_path) helper_symbol = None From f182db59e3479f84c17273815a89ea12ec94d65b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 17:40:29 +0200 Subject: [PATCH 146/174] Rename language server modules and classes --- src/serena/agent.py | 10 +- src/serena/symbol.py | 2 +- src/serena/util/inspection.py | 2 +- .../clangd_language_server.py | 10 +- .../dart_language_server.py | 6 +- .../eclipse_jdtls/eclipse_jdtls.py | 16 +- src/solidlsp/language_servers/gopls/gopls.py | 6 +- .../intelephense/intelephense.py | 10 +- .../jedi_language_server/jedi_server.py | 6 +- .../kotlin_language_server.py | 10 +- .../language_servers/omnisharp/omnisharp.py | 14 +- .../pyright_language_server/pyright_server.py | 6 +- .../rust_analyzer/rust_analyzer.py | 10 +- .../language_servers/solargraph/solargraph.py | 8 +- .../typescript_language_server.py | 10 +- src/solidlsp/ls.py | 172 +++--- .../{multilspy_config.py => ls_config.py} | 2 +- ...ltilspy_exceptions.py => ls_exceptions.py} | 2 +- src/solidlsp/ls_handler.py | 8 +- .../{multilspy_logger.py => ls_logger.py} | 2 +- .../{lsp_request.py => ls_request.py} | 2 +- .../{multilspy_types.py => ls_types.py} | 0 .../{multilspy_utils.py => ls_utils.py} | 32 +- src/solidlsp/lsp_protocol_handler/server.py | 517 +----------------- .../{multilspy_settings.py => settings.py} | 2 +- src/solidlsp/type_helpers.py | 28 - test/conftest.py | 8 +- test/multilspy/go/test_go_basic.py | 4 +- test/multilspy/java/test_java_basic.py | 4 +- test/multilspy/php/test_php_basic.py | 2 +- test/multilspy/python/test_python_basic.py | 2 +- .../test_retrieval_with_ignored_dirs.py | 2 +- .../multilspy/python/test_symbol_retrieval.py | 4 +- test/multilspy/rust/test_rust_basic.py | 4 +- .../typescript/test_typescript_basic.py | 4 +- test/serena/test_serena_agent.py | 2 +- test/serena/test_symbol_editing.py | 2 +- 37 files changed, 194 insertions(+), 737 deletions(-) rename src/solidlsp/{multilspy_config.py => ls_config.py} (98%) rename src/solidlsp/{multilspy_exceptions.py => ls_exceptions.py} (87%) rename src/solidlsp/{multilspy_logger.py => ls_logger.py} (98%) rename src/solidlsp/{lsp_request.py => ls_request.py} (99%) rename src/solidlsp/{multilspy_types.py => ls_types.py} (100%) rename src/solidlsp/{multilspy_utils.py => ls_utils.py} (91%) rename src/solidlsp/{multilspy_settings.py => settings.py} (96%) delete mode 100644 src/solidlsp/type_helpers.py diff --git a/src/serena/agent.py b/src/serena/agent.py index 965dbdf..9fea8ec 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -52,9 +52,9 @@ from serena.util.inspection import determine_programming_language_composition, i from serena.util.shell import execute_shell_command from serena.util.thread import ExecutionResult, execute_with_timeout from solidlsp import SolidLanguageServer -from solidlsp.multilspy_config import Language, MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_types import SymbolKind +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_types import SymbolKind if TYPE_CHECKING: from serena.gui_log_viewer import GuiLogViewerHandler @@ -669,12 +669,12 @@ def create_ls_for_project( log.debug(f"Adding {len(spec.patterns)} patterns from {spec.file_path} to the ignored paths.") ignored_paths.extend(spec.patterns) log.debug(f"Using {len(ignored_paths)} ignored paths in total.") - multilspy_config = MultilspyConfig( + multilspy_config = LanguageServerConfig( code_language=project_instance.language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication, ) - ls_logger = MultilspyLogger(log_level=log_level) + ls_logger = LanguageServerLogger(log_level=log_level) log.info(f"Creating language server instance for {project_instance.project_root}.") return SolidLanguageServer.create( multilspy_config, diff --git a/src/serena/symbol.py b/src/serena/symbol.py index e4da274..e039440 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -11,7 +11,7 @@ from sensai.util.string import ToStringMixin from solidlsp import SolidLanguageServer from solidlsp.ls import ReferenceInSymbol as LSPReferenceInSymbol -from solidlsp.multilspy_types import Position, SymbolKind, UnifiedSymbolInformation +from solidlsp.ls_types import Position, SymbolKind, UnifiedSymbolInformation if TYPE_CHECKING: from .agent import SerenaAgent diff --git a/src/serena/util/inspection.py b/src/serena/util/inspection.py index 4847fd1..24c2171 100644 --- a/src/serena/util/inspection.py +++ b/src/serena/util/inspection.py @@ -4,7 +4,7 @@ from collections.abc import Generator from typing import TypeVar from serena.util.file_system import find_all_non_ignored_files -from solidlsp.multilspy_config import Language +from solidlsp.ls_config import Language T = TypeVar("T") diff --git a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py index a61ba13..4572de9 100644 --- a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py @@ -12,9 +12,9 @@ import threading from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils class ClangdLanguageServer(SolidLanguageServer): @@ -24,7 +24,7 @@ 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: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -41,7 +41,7 @@ class ClangdLanguageServer(SolidLanguageServer): self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ Setup runtime dependencies for ClangdLanguageServer. """ diff --git a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py index bf40699..5957558 100644 --- a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py @@ -6,8 +6,8 @@ import stat from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils class DartLanguageServer(SolidLanguageServer): @@ -28,7 +28,7 @@ class DartLanguageServer(SolidLanguageServer): "dart", ) - def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str: + def setup_runtime_dependencies(self, logger: "LanguageServerLogger") -> str: platform_id = PlatformUtils.get_platform_id() with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: diff --git a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py index bd63594..a2453fa 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -18,10 +18,10 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_settings import MultilspySettings -from solidlsp.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.settings import SolidLSPSettings +from solidlsp.ls_utils import FileUtils, PlatformUtils @dataclasses.dataclass @@ -45,7 +45,7 @@ class EclipseJDTLS(SolidLanguageServer): The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a new EclipseJDTLS instance initializing the language server settings appropriately. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. @@ -56,7 +56,7 @@ class EclipseJDTLS(SolidLanguageServer): # ws_dir is the workspace directory for the EclipseJDTLS server ws_dir = str( PurePath( - MultilspySettings.get_language_server_directory(), + SolidLSPSettings.get_language_server_directory(), "EclipseJDTLS", "workspaces", uuid.uuid4().hex, @@ -64,7 +64,7 @@ class EclipseJDTLS(SolidLanguageServer): ) # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces - shared_cache_location = str(PurePath(MultilspySettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex")) + shared_cache_location = str(PurePath(SolidLSPSettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex")) jre_path = self.runtime_dependency_paths.jre_path lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path @@ -153,7 +153,7 @@ class EclipseJDTLS(SolidLanguageServer): "lib", # General ] - def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> RuntimeDependencyPaths: + def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> RuntimeDependencyPaths: """ Setup runtime dependencies for EclipseJDTLS. """ diff --git a/src/solidlsp/language_servers/gopls/gopls.py b/src/solidlsp/language_servers/gopls/gopls.py index cd7f6e0..32ae5e5 100644 --- a/src/solidlsp/language_servers/gopls/gopls.py +++ b/src/solidlsp/language_servers/gopls/gopls.py @@ -10,8 +10,8 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger class Gopls(SolidLanguageServer): @@ -71,7 +71,7 @@ class Gopls(SolidLanguageServer): return True - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): self.setup_runtime_dependency() super().__init__( diff --git a/src/solidlsp/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense/intelephense.py index 7e7e8db..a02f9d1 100644 --- a/src/solidlsp/language_servers/intelephense/intelephense.py +++ b/src/solidlsp/language_servers/intelephense/intelephense.py @@ -15,9 +15,9 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_utils import PlatformId, PlatformUtils +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import PlatformId, PlatformUtils class Intelephense(SolidLanguageServer): @@ -33,7 +33,7 @@ class Intelephense(SolidLanguageServer): # - cache: commonly used for caching return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ Setup runtime dependencies for Intelephense. """ @@ -97,7 +97,7 @@ class Intelephense(SolidLanguageServer): return f"{intelephense_executable_path} --stdio" - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): # Setup runtime dependencies before initializing intelephense_cmd = self.setup_runtime_dependencies(logger, config) diff --git a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py index 9ec7554..a0ea203 100644 --- a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py +++ b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py @@ -12,8 +12,8 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger class JediServer(SolidLanguageServer): @@ -21,7 +21,7 @@ class JediServer(SolidLanguageServer): Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ diff --git a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py index cf4022c..3031535 100644 --- a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py @@ -12,9 +12,9 @@ import stat from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils @dataclasses.dataclass @@ -33,7 +33,7 @@ class KotlinLanguageServer(SolidLanguageServer): Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a Kotlin Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -54,7 +54,7 @@ class KotlinLanguageServer(SolidLanguageServer): "kotlin", ) - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> KotlinRuntimeDependencyPaths: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> KotlinRuntimeDependencyPaths: """ Setup runtime dependencies for Kotlin Language Server. """ diff --git a/src/solidlsp/language_servers/omnisharp/omnisharp.py b/src/solidlsp/language_servers/omnisharp/omnisharp.py index b8b33ea..0a0216c 100644 --- a/src/solidlsp/language_servers/omnisharp/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp/omnisharp.py @@ -15,10 +15,10 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_exceptions import MultilspyException -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils def breadth_first_file_scan(root) -> Iterable[str]: @@ -59,7 +59,7 @@ class OmniSharp(SolidLanguageServer): Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -68,7 +68,7 @@ class OmniSharp(SolidLanguageServer): slnfilename = find_least_depth_sln_file(repository_root_path) if slnfilename is None: logger.log("No *.sln file found in repository", logging.ERROR) - raise MultilspyException("No SLN file found in repository") + raise LanguageServerException("No SLN file found in repository") cmd = " ".join( [ @@ -136,7 +136,7 @@ class OmniSharp(SolidLanguageServer): return d - def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> tuple[str, str]: + def setupRuntimeDependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> tuple[str, str]: """ Setup runtime dependencies for OmniSharp. """ diff --git a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py index b7ddd07..64dbd57 100644 --- a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py @@ -13,8 +13,8 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger class PyrightServer(SolidLanguageServer): @@ -23,7 +23,7 @@ class PyrightServer(SolidLanguageServer): Contains various configurations and settings specific to Python. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a PyrightServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. diff --git a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py index d88cc59..a21768e 100644 --- a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py @@ -14,9 +14,9 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils class RustAnalyzer(SolidLanguageServer): @@ -24,7 +24,7 @@ class RustAnalyzer(SolidLanguageServer): Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -45,7 +45,7 @@ class RustAnalyzer(SolidLanguageServer): def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["target"] - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ Setup runtime dependencies for rust_analyzer. """ diff --git a/src/solidlsp/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph/solargraph.py index f733e30..07cc1b3 100644 --- a/src/solidlsp/language_servers/solargraph/solargraph.py +++ b/src/solidlsp/language_servers/solargraph/solargraph.py @@ -15,8 +15,8 @@ from typing import override from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger class Solargraph(SolidLanguageServer): @@ -25,7 +25,7 @@ class Solargraph(SolidLanguageServer): Contains various configurations and settings specific to Ruby. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a Solargraph instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. @@ -47,7 +47,7 @@ class Solargraph(SolidLanguageServer): def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["vendor"] - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig, repository_root_path: str) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig, repository_root_path: str) -> str: """ Setup runtime dependencies for Solargraph. """ diff --git a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py index 3fe2d86..dd1d230 100644 --- a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py @@ -16,9 +16,9 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo -from solidlsp.multilspy_config import MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_utils import PlatformId, PlatformUtils +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import PlatformId, PlatformUtils # Platform-specific imports if os.name != "nt": # Unix-like systems @@ -41,7 +41,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. """ - def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): """ Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ @@ -65,7 +65,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): "coverage", ] - def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: """ Setup runtime dependencies for TypeScript Language Server. """ diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 610c60e..426d471 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -19,7 +19,7 @@ import pathspec import tqdm from serena.text_utils import MatchedConsecutiveLines, search_files -from solidlsp import multilspy_types +from solidlsp import ls_types from solidlsp.ls_handler import SolidLanguageServerHandler from solidlsp.lsp_protocol_handler import lsp_types from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes @@ -30,19 +30,19 @@ from solidlsp.lsp_protocol_handler.server import ( ProcessLaunchInfo, StringDict, ) -from solidlsp.multilspy_config import Language, MultilspyConfig -from solidlsp.multilspy_exceptions import MultilspyException -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_utils import FileUtils, PathUtils, TextUtils +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PathUtils, TextUtils -GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, multilspy_types.UnifiedSymbolInformation] +GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, ls_types.UnifiedSymbolInformation] @dataclasses.dataclass(kw_only=True) class ReferenceInSymbol: """A symbol retrieved when requesting reference to a symbol, together with the location of the reference""" - symbol: multilspy_types.UnifiedSymbolInformation + symbol: ls_types.UnifiedSymbolInformation line: int character: int @@ -90,7 +90,7 @@ class SolidLanguageServer(ABC): @classmethod def create( - cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, timeout: float | None = None + cls, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, timeout: float | None = None ) -> "SolidLanguageServer": """ Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. @@ -178,12 +178,12 @@ class SolidLanguageServer(ABC): else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) - raise MultilspyException(f"Language {config.code_language} is not supported") + raise LanguageServerException(f"Language {config.code_language} is not supported") def __init__( self, - config: MultilspyConfig, - logger: MultilspyLogger, + config: LanguageServerConfig, + logger: LanguageServerLogger, repository_root_path: str, process_launch_info: ProcessLaunchInfo, language_id: str, @@ -214,7 +214,7 @@ class SolidLanguageServer(ABC): # load cache first to prevent any racing conditions due to asyncio stuff self._document_symbols_cache: dict[ - str, tuple[str, tuple[list[multilspy_types.UnifiedSymbolInformation], list[multilspy_types.UnifiedSymbolInformation]]] + str, tuple[str, tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]] ] = {} """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" self._cache_lock = threading.Lock() @@ -368,7 +368,7 @@ class SolidLanguageServer(ABC): "open_file called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -411,7 +411,7 @@ class SolidLanguageServer(ABC): def insert_text_at_position( self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str - ) -> multilspy_types.Position: + ) -> ls_types.Position: """ Insert text at the given line and column in the given file and return the updated cursor position after inserting the text. @@ -426,7 +426,7 @@ class SolidLanguageServer(ABC): "insert_text_at_position called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -456,13 +456,13 @@ class SolidLanguageServer(ABC): ], } ) - return multilspy_types.Position(line=new_l, character=new_c) + return ls_types.Position(line=new_l, character=new_c) def delete_text_between_positions( self, relative_file_path: str, - start: multilspy_types.Position, - end: multilspy_types.Position, + start: ls_types.Position, + end: ls_types.Position, ) -> str: """ Delete text between the given start and end positions in the given file and return the deleted text. @@ -472,7 +472,7 @@ class SolidLanguageServer(ABC): "insert_text_at_position called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -500,7 +500,7 @@ class SolidLanguageServer(ABC): def _send_definition_request(self, definition_params: DefinitionParams) -> Definition | list[LocationLink] | None: return self.server.send.definition(definition_params) - def request_definition(self, relative_file_path: str, line: int, column: int) -> list[multilspy_types.Location]: + def request_definition(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]: """ Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server for the symbol at the given line and column in the given file. Wait for the response and return the result. @@ -516,7 +516,7 @@ class SolidLanguageServer(ABC): "find_function_definition called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") with self.open_file(relative_file_path): # sending request to the language server and waiting for response @@ -534,29 +534,29 @@ class SolidLanguageServer(ABC): ) response = self._send_definition_request(definition_params) - ret: list[multilspy_types.Location] = [] + ret: list[ls_types.Location] = [] if isinstance(response, list): # response is either of type Location[] or LocationLink[] for item in response: assert isinstance(item, dict) if LSPConstants.URI in item and LSPConstants.RANGE in item: - new_item: multilspy_types.Location = {} + new_item: ls_types.Location = {} new_item.update(item) new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) - ret.append(multilspy_types.Location(new_item)) + ret.append(ls_types.Location(new_item)) elif ( LSPConstants.ORIGIN_SELECTION_RANGE in item and LSPConstants.TARGET_URI in item and LSPConstants.TARGET_RANGE in item and LSPConstants.TARGET_SELECTION_RANGE in item ): - new_item: multilspy_types.Location = {} + new_item: ls_types.Location = {} new_item["uri"] = item[LSPConstants.TARGET_URI] new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) new_item["range"] = item[LSPConstants.TARGET_SELECTION_RANGE] - ret.append(multilspy_types.Location(**new_item)) + ret.append(ls_types.Location(**new_item)) else: assert False, f"Unexpected response from Language Server: {item}" elif isinstance(response, dict): @@ -564,11 +564,11 @@ class SolidLanguageServer(ABC): assert LSPConstants.URI in response assert LSPConstants.RANGE in response - new_item: multilspy_types.Location = {} + new_item: ls_types.Location = {} new_item.update(response) new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) - ret.append(multilspy_types.Location(**new_item)) + ret.append(ls_types.Location(**new_item)) elif response is None: # Some language servers return None when they cannot find a definition # This is expected for certain symbol types like generics or types with incomplete information @@ -591,7 +591,7 @@ class SolidLanguageServer(ABC): } ) - def request_references(self, relative_file_path: str, line: int, column: int) -> list[multilspy_types.Location]: + def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]: """ Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server to find references to the symbol at the given line and column in the given file. Wait for the response and return the result. @@ -608,7 +608,7 @@ class SolidLanguageServer(ABC): "request_references called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") with self.open_file(relative_file_path): try: @@ -624,7 +624,7 @@ class SolidLanguageServer(ABC): if response is None: return [] - ret: list[multilspy_types.Location] = [] + ret: list[ls_types.Location] = [] assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}" for item in response: assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}" @@ -637,11 +637,11 @@ class SolidLanguageServer(ABC): self.logger.log(f"Ignoring reference in {rel_path} since it should be ignored", logging.DEBUG) continue - new_item: multilspy_types.Location = {} + new_item: ls_types.Location = {} new_item.update(item) new_item["absolutePath"] = str(abs_path) new_item["relativePath"] = str(rel_path) - ret.append(multilspy_types.Location(**new_item)) + ret.append(ls_types.Location(**new_item)) return ret @@ -697,7 +697,7 @@ class SolidLanguageServer(ABC): def request_completions( self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False - ) -> list[multilspy_types.CompletionItem]: + ) -> list[ls_types.CompletionItem]: """ Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server to find completions at the given line and column in the given file. Wait for the response and return the result. @@ -737,7 +737,7 @@ class SolidLanguageServer(ABC): # TODO: Handle the case when the completion is a keyword items = [item for item in response if item["kind"] != LSPTypes.CompletionItemKind.Keyword] - completions_list: list[multilspy_types.CompletionItem] = [] + completions_list: list[ls_types.CompletionItem] = [] for item in items: assert "insertText" in item or "textEdit" in item @@ -776,14 +776,14 @@ class SolidLanguageServer(ABC): else: assert False - completion_item = multilspy_types.CompletionItem(**completion_item) + completion_item = ls_types.CompletionItem(**completion_item) completions_list.append(completion_item) return [json.loads(json_repr) for json_repr in set(json.dumps(item, sort_keys=True) for item in completions_list)] def request_document_symbols( self, relative_file_path: str, include_body: bool = False - ) -> tuple[list[multilspy_types.UnifiedSymbolInformation], list[multilspy_types.UnifiedSymbolInformation]]: + ) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]: """ Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server to find symbols in the given file. Wait for the response and return the result. @@ -823,14 +823,14 @@ class SolidLanguageServer(ABC): ) def turn_item_into_symbol_with_children(item: GenericDocumentSymbol): - item = cast(multilspy_types.UnifiedSymbolInformation, item) + item = cast(ls_types.UnifiedSymbolInformation, item) absolute_path = os.path.join(self.repository_root_path, relative_file_path) # handle missing entries in location if "location" not in item: uri = pathlib.Path(absolute_path).as_uri() assert "range" in item - tree_location = multilspy_types.Location( + tree_location = ls_types.Location( uri=uri, range=item["range"], absolutePath=absolute_path, @@ -855,9 +855,9 @@ class SolidLanguageServer(ABC): child["parent"] = item item[LSPConstants.CHILDREN] = children - flat_all_symbol_list: list[multilspy_types.UnifiedSymbolInformation] = [] + flat_all_symbol_list: list[ls_types.UnifiedSymbolInformation] = [] assert isinstance(response, list), f"Unexpected response from Language Server: {response}" - root_nodes: list[multilspy_types.UnifiedSymbolInformation] = [] + root_nodes: list[ls_types.UnifiedSymbolInformation] = [] for root_item in response: if "range" not in root_item and "location" not in root_item: if root_item["kind"] in [SymbolKind.File, SymbolKind.Module]: @@ -867,7 +867,7 @@ class SolidLanguageServer(ABC): # so we cast and rename the var after the mutating call to turn_item_into_symbol_with_children # which turned and item into a "symbol" turn_item_into_symbol_with_children(root_item) - root_symbol = cast(multilspy_types.UnifiedSymbolInformation, root_item) + root_symbol = cast(ls_types.UnifiedSymbolInformation, root_item) root_symbol["parent"] = None root_nodes.append(root_symbol) @@ -878,9 +878,9 @@ class SolidLanguageServer(ABC): if LSPConstants.CHILDREN in root_symbol: # TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well - def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> list[multilspy_types.UnifiedSymbolInformation]: - node = cast(multilspy_types.UnifiedSymbolInformation, node) - l: list[multilspy_types.UnifiedSymbolInformation] = [] + def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> list[ls_types.UnifiedSymbolInformation]: + node = cast(ls_types.UnifiedSymbolInformation, node) + l: list[ls_types.UnifiedSymbolInformation] = [] turn_item_into_symbol_with_children(node) assert LSPConstants.CHILDREN in node children = node[LSPConstants.CHILDREN] @@ -891,7 +891,7 @@ class SolidLanguageServer(ABC): flat_all_symbol_list.extend(visit_tree_nodes_and_build_tree_repr(root_symbol)) else: - flat_all_symbol_list.append(multilspy_types.UnifiedSymbolInformation(**root_symbol)) + flat_all_symbol_list.append(ls_types.UnifiedSymbolInformation(**root_symbol)) result = flat_all_symbol_list, root_nodes self.logger.log(f"Caching document symbols for {relative_file_path}", logging.DEBUG) @@ -902,7 +902,7 @@ class SolidLanguageServer(ABC): def request_full_symbol_tree( self, within_relative_path: str | None = None, include_body: bool = False - ) -> list[multilspy_types.UnifiedSymbolInformation]: + ) -> list[ls_types.UnifiedSymbolInformation]: """ Will go through all files in the project or within a relative path and build a tree of symbols. Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search. @@ -937,7 +937,7 @@ class SolidLanguageServer(ABC): return root_nodes # Helper function to recursively process directories - def process_directory(rel_dir_path: str) -> list[multilspy_types.UnifiedSymbolInformation]: + def process_directory(rel_dir_path: str) -> list[ls_types.UnifiedSymbolInformation]: abs_dir_path = self.repository_root_path if rel_dir_path == "." else os.path.join(self.repository_root_path, rel_dir_path) abs_dir_path = os.path.realpath(abs_dir_path) @@ -952,10 +952,10 @@ class SolidLanguageServer(ABC): return [] # Create package symbol for directory - package_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore + package_symbol = ls_types.UnifiedSymbolInformation( # type: ignore name=os.path.basename(abs_dir_path), - kind=multilspy_types.SymbolKind.Package, - location=multilspy_types.Location( + kind=ls_types.SymbolKind.Package, + location=ls_types.Location( uri=str(pathlib.Path(abs_dir_path).as_uri()), range={"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}, absolutePath=str(abs_dir_path), @@ -985,12 +985,12 @@ class SolidLanguageServer(ABC): file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)) with self.open_file(file_rel_path) as file_data: fileRange = self._get_range_from_file_content(file_data.contents) - file_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore + file_symbol = ls_types.UnifiedSymbolInformation( # type: ignore name=os.path.splitext(contained_dir_or_file_name)[0], - kind=multilspy_types.SymbolKind.File, + kind=ls_types.SymbolKind.File, range=fileRange, selectionRange=fileRange, - location=multilspy_types.Location( + location=ls_types.Location( uri=str(pathlib.Path(contained_dir_or_file_abs_path).as_uri()), range=fileRange, absolutePath=str(contained_dir_or_file_abs_path), @@ -1006,7 +1006,7 @@ class SolidLanguageServer(ABC): package_symbol["children"].append(file_symbol) # TODO: Not sure if this is actually still needed given recent changes to relative path handling - def fix_relative_path(nodes: list[multilspy_types.UnifiedSymbolInformation]): + def fix_relative_path(nodes: list[ls_types.UnifiedSymbolInformation]): for node in nodes: if "location" in node and "relativePath" in node["location"]: path = Path(node["location"]["relativePath"]) @@ -1028,18 +1028,18 @@ class SolidLanguageServer(ABC): return process_directory(start_rel_path) @staticmethod - def _get_range_from_file_content(file_content: str) -> multilspy_types.Range: + def _get_range_from_file_content(file_content: str) -> ls_types.Range: """ Get the range for the given file. """ lines = file_content.split("\n") end_line = len(lines) end_column = len(lines[-1]) - return multilspy_types.Range( - start=multilspy_types.Position(line=0, character=0), end=multilspy_types.Position(line=end_line, character=end_column) + return ls_types.Range( + start=ls_types.Position(line=0, character=0), end=ls_types.Position(line=end_line, character=end_column) ) - def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: + def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, ls_types.SymbolKind, int, int]]]: """ An overview of the given directory. @@ -1048,11 +1048,11 @@ class SolidLanguageServer(ABC): """ symbol_tree = self.request_full_symbol_tree(relative_dir_path) # Initialize result dictionary - result: dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]] = defaultdict(list) + result: dict[str, list[tuple[str, ls_types.SymbolKind, int, int]]] = defaultdict(list) # Helper function to process a symbol and its children - def process_symbol(symbol: multilspy_types.UnifiedSymbolInformation): - if symbol["kind"] == multilspy_types.SymbolKind.File: + def process_symbol(symbol: ls_types.UnifiedSymbolInformation): + if symbol["kind"] == ls_types.SymbolKind.File: # For file symbols, process their children (top-level symbols) for child in symbol["children"]: assert "location" in child @@ -1075,7 +1075,7 @@ class SolidLanguageServer(ABC): process_symbol(root) return result - def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: + def request_document_overview(self, relative_file_path: str) -> list[tuple[str, ls_types.SymbolKind, int, int]]: """ An overview of the given file. Returns the list of tuples (name, kind, line, column) of all top-level symbols in the file. @@ -1091,7 +1091,7 @@ class SolidLanguageServer(ABC): raise KeyError(f"Could not process symbol of name {root.get('name', 'unknown')} in {relative_file_path=}") from e return result - def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: + def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, ls_types.SymbolKind, int, int]]]: """ An overview of all symbols in the given file or directory. @@ -1108,7 +1108,7 @@ class SolidLanguageServer(ABC): else: return self.request_dir_overview(within_relative_path) - def request_hover(self, relative_file_path: str, line: int, column: int) -> multilspy_types.Hover | None: + def request_hover(self, relative_file_path: str, line: int, column: int) -> ls_types.Hover | None: """ Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server to find the hover information at the given line and column in the given file. Wait for the response and return the result. @@ -1135,12 +1135,12 @@ class SolidLanguageServer(ABC): assert isinstance(response, dict) - return multilspy_types.Hover(**response) + return ls_types.Hover(**response) # ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA -------------------- def retrieve_symbol_body( - self, symbol: multilspy_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation + self, symbol: ls_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation ) -> str: """ Load the body of the given symbol. If the body is already contained in the symbol, just return it. @@ -1169,7 +1169,7 @@ class SolidLanguageServer(ABC): "request_parsed_files called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") rel_file_paths = [] for root, dirs, files in os.walk(self.repository_root_path): dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d))] @@ -1244,7 +1244,7 @@ class SolidLanguageServer(ABC): "request_referencing_symbols called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") # First, get all references to the symbol references = self.request_references(relative_file_path, line, column) @@ -1281,7 +1281,7 @@ class SolidLanguageServer(ABC): containing_symbol_name = ref_text.split(".")[0] all_symbols, _ = self.request_document_symbols(ref_path) for symbol in all_symbols: - if symbol["name"] == containing_symbol_name and symbol["kind"] == multilspy_types.SymbolKind.Variable: + if symbol["name"] == containing_symbol_name and symbol["kind"] == ls_types.SymbolKind.Variable: containing_symbol = copy(symbol) containing_symbol["location"] = ref containing_symbol["range"] = ref["range"] @@ -1294,7 +1294,7 @@ class SolidLanguageServer(ABC): logging.WARNING, ) fileRange = self._get_range_from_file_content(file_data.contents) - location = multilspy_types.Location( + location = ls_types.Location( uri=str(pathlib.Path(os.path.join(self.repository_root_path, ref_path)).as_uri()), range=fileRange, absolutePath=str(os.path.join(self.repository_root_path, ref_path)), @@ -1307,8 +1307,8 @@ class SolidLanguageServer(ABC): else: body = "" - containing_symbol = multilspy_types.UnifiedSymbolInformation( - kind=multilspy_types.SymbolKind.File, + containing_symbol = ls_types.UnifiedSymbolInformation( + kind=ls_types.SymbolKind.File, range=fileRange, selectionRange=fileRange, location=location, @@ -1316,7 +1316,7 @@ class SolidLanguageServer(ABC): children=[], body=body, ) - if containing_symbol is None or (not include_file_symbols and containing_symbol["kind"] == multilspy_types.SymbolKind.File): + if containing_symbol is None or (not include_file_symbols and containing_symbol["kind"] == ls_types.SymbolKind.File): continue assert "location" in containing_symbol @@ -1363,7 +1363,7 @@ class SolidLanguageServer(ABC): column: int | None = None, strict: bool = False, include_body: bool = False, - ) -> multilspy_types.UnifiedSymbolInformation | None: + ) -> ls_types.UnifiedSymbolInformation | None: """ Finds the first symbol containing the position for the given file. For Python, container symbols are considered to be those with kinds corresponding to @@ -1409,7 +1409,7 @@ class SolidLanguageServer(ABC): for symbol in symbols: if "location" not in symbol: range = symbol["range"] - location = multilspy_types.Location( + location = ls_types.Location( uri=f"file:/{absolute_file_path}", range=range, absolutePath=absolute_file_path, @@ -1424,9 +1424,9 @@ class SolidLanguageServer(ABC): location["uri"] = Path(absolute_file_path).as_uri() # Allowed container kinds, currently only for Python - container_symbol_kinds = {multilspy_types.SymbolKind.Method, multilspy_types.SymbolKind.Function, multilspy_types.SymbolKind.Class} + container_symbol_kinds = {ls_types.SymbolKind.Method, ls_types.SymbolKind.Function, ls_types.SymbolKind.Class} - def is_position_in_range(line: int, range_d: multilspy_types.Range) -> bool: + def is_position_in_range(line: int, range_d: ls_types.Range) -> bool: start = range_d["start"] end = range_d["end"] @@ -1447,7 +1447,7 @@ class SolidLanguageServer(ABC): for s in symbols if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"] ] - var_containers = [s for s in symbols if s["kind"] == multilspy_types.SymbolKind.Variable] + var_containers = [s for s in symbols if s["kind"] == ls_types.SymbolKind.Variable] candidate_containers.extend(var_containers) if not candidate_containers: @@ -1471,8 +1471,8 @@ class SolidLanguageServer(ABC): return None def request_container_of_symbol( - self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False - ) -> multilspy_types.UnifiedSymbolInformation | None: + self, symbol: ls_types.UnifiedSymbolInformation, include_body: bool = False + ) -> ls_types.UnifiedSymbolInformation | None: """ Finds the container of the given symbol if there is one. If the parent attribute is present, the parent is returned without further searching. @@ -1498,7 +1498,7 @@ class SolidLanguageServer(ABC): line: int, column: int, include_body: bool = False, - ) -> multilspy_types.UnifiedSymbolInformation | None: + ) -> ls_types.UnifiedSymbolInformation | None: """ Finds the symbol that defines the symbol at the given location. @@ -1516,7 +1516,7 @@ class SolidLanguageServer(ABC): "request_defining_symbol called before Language Server started", logging.ERROR, ) - raise MultilspyException("Language Server not started") + raise LanguageServerException("Language Server not started") # Get the definition location(s) definitions = self.request_definition(relative_file_path, line, column) @@ -1596,7 +1596,7 @@ class SolidLanguageServer(ABC): logging.ERROR, ) - def request_workspace_symbol(self, query: str) -> list[multilspy_types.UnifiedSymbolInformation] | None: + def request_workspace_symbol(self, query: str) -> list[ls_types.UnifiedSymbolInformation] | None: """ Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server to find symbols across the whole workspace. Wait for the response and return the result. @@ -1611,7 +1611,7 @@ class SolidLanguageServer(ABC): assert isinstance(response, list) - ret: list[multilspy_types.UnifiedSymbolInformation] = [] + ret: list[ls_types.UnifiedSymbolInformation] = [] for item in response: assert isinstance(item, dict) @@ -1619,7 +1619,7 @@ class SolidLanguageServer(ABC): assert LSPConstants.KIND in item assert LSPConstants.LOCATION in item - ret.append(multilspy_types.UnifiedSymbolInformation(**item)) + ret.append(ls_types.UnifiedSymbolInformation(**item)) return ret diff --git a/src/solidlsp/multilspy_config.py b/src/solidlsp/ls_config.py similarity index 98% rename from src/solidlsp/multilspy_config.py rename to src/solidlsp/ls_config.py index f676e52..27deaf1 100644 --- a/src/solidlsp/multilspy_config.py +++ b/src/solidlsp/ls_config.py @@ -73,7 +73,7 @@ class Language(str, Enum): @dataclass -class MultilspyConfig: +class LanguageServerConfig: """ Configuration parameters """ diff --git a/src/solidlsp/multilspy_exceptions.py b/src/solidlsp/ls_exceptions.py similarity index 87% rename from src/solidlsp/multilspy_exceptions.py rename to src/solidlsp/ls_exceptions.py index 1dc12ed..0484da6 100644 --- a/src/solidlsp/multilspy_exceptions.py +++ b/src/solidlsp/ls_exceptions.py @@ -3,7 +3,7 @@ This module contains the exceptions raised by the Multilspy framework. """ -class MultilspyException(Exception): +class LanguageServerException(Exception): """ Exceptions raised by the Multilspy framework. """ diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index e4c8b68..6de9ef7 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -28,8 +28,8 @@ from solidlsp.lsp_protocol_handler.server import ( make_request, make_response, ) -from solidlsp.lsp_request import SolidLspRequest -from solidlsp.multilspy_exceptions import MultilspyException +from solidlsp.ls_request import LanguageServerRequest +from solidlsp.ls_exceptions import LanguageServerException log = logging.getLogger(__name__) @@ -109,7 +109,7 @@ class SolidLanguageServerHandler: logger: An optional function that takes two strings (source and destination) and a payload dictionary, and logs the communication between the client and the server. """ - self.send = SolidLspRequest(self.send_request) + self.send = LanguageServerRequest(self.send_request) self.notify = LspNotification(self.send_notification) self.process_launch_info = process_launch_info @@ -422,7 +422,7 @@ class SolidLanguageServerHandler: self._log("Processing result") if result.is_error(): - raise MultilspyException( + raise LanguageServerException( f"Could not process request {method} with params:\n{params}.\n Language server error: {result.error}" ) from result.error diff --git a/src/solidlsp/multilspy_logger.py b/src/solidlsp/ls_logger.py similarity index 98% rename from src/solidlsp/multilspy_logger.py rename to src/solidlsp/ls_logger.py index 993aa3b..6bdf12c 100644 --- a/src/solidlsp/multilspy_logger.py +++ b/src/solidlsp/ls_logger.py @@ -22,7 +22,7 @@ class LogLine(BaseModel): message: str -class MultilspyLogger: +class LanguageServerLogger: """ Logger class """ diff --git a/src/solidlsp/lsp_request.py b/src/solidlsp/ls_request.py similarity index 99% rename from src/solidlsp/lsp_request.py rename to src/solidlsp/ls_request.py index 11fc67b..209a3f0 100644 --- a/src/solidlsp/lsp_request.py +++ b/src/solidlsp/ls_request.py @@ -3,7 +3,7 @@ from typing import Union from solidlsp.lsp_protocol_handler import lsp_types -class SolidLspRequest: +class LanguageServerRequest: def __init__(self, send_request): self.send_request = send_request diff --git a/src/solidlsp/multilspy_types.py b/src/solidlsp/ls_types.py similarity index 100% rename from src/solidlsp/multilspy_types.py rename to src/solidlsp/ls_types.py diff --git a/src/solidlsp/multilspy_utils.py b/src/solidlsp/ls_utils.py similarity index 91% rename from src/solidlsp/multilspy_utils.py rename to src/solidlsp/ls_utils.py index e1b71c2..535d342 100644 --- a/src/solidlsp/multilspy_utils.py +++ b/src/solidlsp/ls_utils.py @@ -14,9 +14,9 @@ from pathlib import Path, PurePath import requests -from solidlsp.multilspy_exceptions import MultilspyException -from solidlsp.multilspy_logger import MultilspyLogger -from solidlsp.multilspy_types import UnifiedSymbolInformation +from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_types import UnifiedSymbolInformation class TextUtils: @@ -147,22 +147,22 @@ class FileUtils: """ @staticmethod - def read_file(logger: MultilspyLogger, file_path: str) -> str: + def read_file(logger: LanguageServerLogger, file_path: str) -> str: """ Reads the file at the given path and returns the contents as a string. """ if not os.path.exists(file_path): logger.log(f"File read '{file_path}' failed: File does not exist.", logging.ERROR) - raise MultilspyException(f"File read '{file_path}' failed: File does not exist.") + raise LanguageServerException(f"File read '{file_path}' failed: File does not exist.") try: with open(file_path, encoding="utf-8") as inp_file: return inp_file.read() except Exception as exc: logger.log(f"File read '{file_path}' failed to read with encoding 'utf-8': {exc}", logging.ERROR) - raise MultilspyException("File read failed.") from None + raise LanguageServerException("File read failed.") from None @staticmethod - def download_file(logger: MultilspyLogger, url: str, target_path: str) -> None: + def download_file(logger: LanguageServerLogger, url: str, target_path: str) -> None: """ Downloads the file from the given URL to the given {target_path} """ @@ -170,15 +170,15 @@ class FileUtils: response = requests.get(url, stream=True, timeout=60) if response.status_code != 200: logger.log(f"Error downloading file '{url}': {response.status_code} {response.text}", logging.ERROR) - raise MultilspyException("Error downloading file.") + raise LanguageServerException("Error downloading file.") with open(target_path, "wb") as f: shutil.copyfileobj(response.raw, f) except Exception as exc: logger.log(f"Error downloading file '{url}': {exc}", logging.ERROR) - raise MultilspyException("Error downloading file.") from None + raise LanguageServerException("Error downloading file.") from None @staticmethod - def download_and_extract_archive(logger: MultilspyLogger, url: str, target_path: str, archive_type: str) -> None: + def download_and_extract_archive(logger: LanguageServerLogger, url: str, target_path: str, archive_type: str) -> None: """ Downloads the archive from the given URL having format {archive_type} and extracts it to the given {target_path} """ @@ -203,10 +203,10 @@ class FileUtils: shutil.copyfileobj(f_in, f_out) else: logger.log(f"Unknown archive type '{archive_type}' for extraction", logging.ERROR) - raise MultilspyException(f"Unknown archive type '{archive_type}'") + raise LanguageServerException(f"Unknown archive type '{archive_type}'") except Exception as exc: logger.log(f"Error extracting archive '{tmp_file_name}' obtained from '{url}': {exc}", logging.ERROR) - raise MultilspyException("Error extracting archive.") from exc + raise LanguageServerException("Error extracting archive.") from exc finally: for tmp_file_name in tmp_files: if os.path.exists(tmp_file_name): @@ -268,7 +268,7 @@ class PlatformUtils: platform_id += "-" + libc return PlatformId(platform_id) else: - raise MultilspyException(f"Unknown platform: {system=}, {machine=}, {bitness=}") + raise LanguageServerException(f"Unknown platform: {system=}, {machine=}, {bitness=}") @staticmethod def _determine_windows_machine_type(): @@ -324,7 +324,7 @@ class PlatformUtils: available_version_cmd_output.append(version_cmd_output) if not available_version_cmd_output: - raise MultilspyException("dotnet not found on the system") + raise LanguageServerException("dotnet not found on the system") # Check for supported versions in order of preference (latest first) for version_cmd_output in available_version_cmd_output: @@ -341,7 +341,7 @@ class PlatformUtils: return DotnetVersion.V4 # If no supported version found, raise exception with all available versions - raise MultilspyException( + raise LanguageServerException( f"No supported dotnet version found. Available versions: {', '.join(available_version_cmd_output)}. Supported versions: 4, 6, 7, 8" ) except (FileNotFoundError, subprocess.CalledProcessError): @@ -349,7 +349,7 @@ class PlatformUtils: result = subprocess.run(["mono", "--version"], capture_output=True, check=True) return DotnetVersion.VMONO except (FileNotFoundError, subprocess.CalledProcessError): - raise MultilspyException("dotnet or mono not found on the system") + raise LanguageServerException("dotnet or mono not found on the system") class SymbolUtils: diff --git a/src/solidlsp/lsp_protocol_handler/server.py b/src/solidlsp/lsp_protocol_handler/server.py index c628d43..0a001f4 100644 --- a/src/solidlsp/lsp_protocol_handler/server.py +++ b/src/solidlsp/lsp_protocol_handler/server.py @@ -39,7 +39,7 @@ from typing import Any, Union import psutil -from ..multilspy_exceptions import MultilspyException +from ..ls_exceptions import LanguageServerException from .lsp_requests import LspNotification, LspRequest from .lsp_types import ErrorCodes @@ -118,23 +118,6 @@ class MessageType: log = 4 -class Request: - def __init__(self) -> None: - self.cv = asyncio.Condition() - self.result: PayloadLike | None = None - self.error: Error | None = None - - async def on_result(self, params: PayloadLike) -> None: - self.result = params - async with self.cv: - self.cv.notify() - - async def on_error(self, err: Error) -> None: - self.error = err - async with self.cv: - self.cv.notify() - - def content_length(line: bytes) -> int | None: if line.startswith(b"Content-Length: "): _, value = line.split(b"Content-Length: ") @@ -145,501 +128,3 @@ def content_length(line: bytes) -> int | None: raise ValueError(f"Invalid Content-Length header: {value}") return None - -class LanguageServerHandler: - """ - This class provides the implementation of Python client for the Language Server Protocol. - A class that launches the language server and communicates with it - using the Language Server Protocol (LSP). - - It provides methods for sending requests, responses, and notifications to the server - and for registering handlers for requests and notifications from the server. - - Uses JSON-RPC 2.0 for communication with the server over stdin/stdout. - - Attributes: - send: A LspRequest object that can be used to send requests to the server and - await for the responses. - notify: A LspNotification object that can be used to send notifications to the server. - cmd: A string that represents the command to launch the language server process. - process: A subprocess.Popen object that represents the language server process. - _received_shutdown: A boolean flag that indicates whether the client has received - a shutdown request from the server. - request_id: An integer that represents the next available request id for the client. - _response_handlers: A dictionary that maps request ids to Request objects that - store the results or errors of the requests. - on_request_handlers: A dictionary that maps method names to callback functions - that handle requests from the server. - on_notification_handlers: A dictionary that maps method names to callback functions - that handle notifications from the server. - logger: An optional function that takes two strings (source and destination) and - a payload dictionary, and logs the communication between the client and the server. - tasks: A dictionary that maps task ids to asyncio.Task objects that represent - the asynchronous tasks created by the handler. - task_counter: An integer that represents the next available task id for the handler. - loop: An asyncio.AbstractEventLoop object that represents the event loop used by the handler. - start_independent_lsp_process: An optional boolean flag that indicates whether to start the - language server process in an independent process group. Default is `True`. Setting it to - `False` means that the language server process will be in the same process group as the - the current process, and any SIGINT and SIGTERM signals will be sent to both processes. - - """ - - def __init__( - self, - process_launch_info: ProcessLaunchInfo, - logger: Callable[[str, str, StringDict | str], None] | None = None, - start_independent_lsp_process=True, - ) -> None: - """ - Params: - cmd: A string that represents the command to launch the language server process. - logger: An optional function that takes two strings (source and destination) and - a payload dictionary, and logs the communication between the client and the server. - """ - self.send = LspRequest(self.send_request) - self.notify = LspNotification(self.send_notification) - - self.process_launch_info = process_launch_info - self.process = None - self._received_shutdown = False - - self.request_id = 1 - self._response_handlers: dict[Any, Request] = {} - self.on_request_handlers = {} - self.on_notification_handlers = {} - self.logger = logger - self.tasks = {} - self.task_counter = 0 - self.loop = None - self.start_independent_lsp_process = start_independent_lsp_process - - # Add thread locks for shared resources to prevent race conditions - self._stdin_lock = threading.Lock() - self._request_id_lock = threading.Lock() - self._response_handlers_lock = threading.Lock() - self._tasks_lock = threading.Lock() - - def is_running(self) -> bool: - """ - Checks if the language server process is currently running. - """ - return self.process is not None and self.process.returncode is None - - async def start(self) -> None: - """ - Starts the language server process and creates a task to continuously read from its stdout to handle communications - from the server to the client - """ - child_proc_env = os.environ.copy() - child_proc_env.update(self.process_launch_info.env) - - log.info("Starting language server process via command: %s", self.process_launch_info.cmd) - self.process = await asyncio.create_subprocess_shell( - self.process_launch_info.cmd, - stdout=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - env=child_proc_env, - cwd=self.process_launch_info.cwd, - start_new_session=self.start_independent_lsp_process, - ) - - # Check if process terminated immediately - if self.process.returncode is not None: - log.error("Language server has already terminated/could not be started") - # Process has already terminated - stderr_data = await self.process.stderr.read() - error_message = stderr_data.decode("utf-8", errors="replace") - raise RuntimeError(f"Process terminated immediately with code {self.process.returncode}. Error: {error_message}") - - self.loop = asyncio.get_event_loop() - - # Use lock to prevent race conditions on tasks and task_counter during startup - with self._tasks_lock: - self.tasks[self.task_counter] = self.loop.create_task(self.run_forever()) - self.task_counter += 1 - self.tasks[self.task_counter] = self.loop.create_task(self.run_forever_stderr()) - self.task_counter += 1 - - async def stop(self) -> None: - """ - Sends the terminate signal to the language server process and waits for it to exit, with a timeout, killing it if necessary - """ - # First cancel all tasks - await self._cancel_pending_tasks() - - process = self.process - self.process = None - - if not process: - return - - # Clean up the process - await self._cleanup_process(process) - - async def _cancel_pending_tasks(self): - """Cancel all pending tasks and wait for them to complete or timeout.""" - pending_tasks = [] - - # Use lock to safely access tasks dictionary - with self._tasks_lock: - for task in self.tasks.values(): - if not task.done(): - task.cancel() - pending_tasks.append(task) - - if pending_tasks: - try: - await asyncio.wait_for(asyncio.gather(*pending_tasks, return_exceptions=True), timeout=5.0) - except (TimeoutError, Exception): - pass - - # Clear tasks dictionary under lock - with self._tasks_lock: - self.tasks = {} - - async def _cleanup_process(self, process): - """Clean up a process: close stdin, terminate/kill process, close stdout/stderr.""" - # Close stdin first to prevent deadlocks - # See: https://bugs.python.org/issue35539 - self._safely_close_pipe(process.stdin) - - # Terminate/kill the process if it's still running - if process.returncode is None: - await self._terminate_or_kill_process(process) - - # Close stdout and stderr pipes after process has exited - # This is essential to prevent "I/O operation on closed pipe" errors and - # "Event loop is closed" errors during garbage collection - # See: https://bugs.python.org/issue41320 and https://github.com/python/cpython/issues/88050 - self._safely_close_pipe(process.stdout) - self._safely_close_pipe(process.stderr) - - # Small delay to ensure OS has released file handles - await asyncio.sleep(0.5) - - def _safely_close_pipe(self, pipe): - """Safely close a pipe, ignoring any exceptions.""" - if pipe: - try: - pipe.close() - except Exception: - pass - - async def _terminate_or_kill_process(self, process): - """Try to terminate the process gracefully, then forcefully if necessary.""" - # First try to terminate the process tree gracefully - self._signal_process_tree(process, terminate=True) - - # Wait for the process to exit (with timeout) - try: - await asyncio.wait_for(process.wait(), timeout=10) - except (TimeoutError, Exception): - # If termination failed, forcefully kill the process tree - self._signal_process_tree(process, terminate=False) - try: - # Give it one more chance to exit - await asyncio.wait_for(process.wait(), timeout=2) - except Exception: - pass - - def _signal_process_tree(self, process, terminate=True): - """Send signal (terminate or kill) to the process and all its children.""" - signal_method = "terminate" if terminate else "kill" - - # Try to get the parent process - parent = None - try: - parent = psutil.Process(process.pid) - except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): - pass - - # If we have the parent process and it's running, signal the entire tree - if parent and parent.is_running(): - # Signal children first - for child in parent.children(recursive=True): - try: - getattr(child, signal_method)() - except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): - pass - - # Then signal the parent - try: - getattr(parent, signal_method)() - except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): - pass - else: - # Fall back to direct process signaling - try: - getattr(process, signal_method)() - except Exception: - pass - - async def shutdown(self) -> None: - """ - Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit - """ - self._log("Sending shutdown request to server") - await self.send.shutdown() - self._log("Received shutdown response from server") - self._received_shutdown = True - self._log("Sending exit notification to server") - self.notify.exit() - self._log("Sent exit notification to server") - if self.process and self.process.stdout: - self.process.stdout.set_exception(StopLoopException()) - # This yields the control to the event loop to allow the exception to be handled - # in the run_forever and run_forever_stderr methods - await asyncio.sleep(0) - - def _log(self, message: str | StringDict) -> None: - """ - Create a log message - """ - if self.logger is not None: - self.logger("client", "logger", message) - - async def run_forever(self) -> bool: - """ - Continuously read from the language server process stdout and handle the messages - invoking the registered response and notification handlers - """ - try: - while self.process and self.process.stdout and not self.process.stdout.at_eof(): - line = await self.process.stdout.readline() - if not line: - continue - try: - num_bytes = content_length(line) - except ValueError: - continue - if num_bytes is None: - continue - while line and line.strip(): - line = await self.process.stdout.readline() - if not line: - continue - body = await self.process.stdout.readexactly(num_bytes) - - # Use lock to prevent race conditions on tasks and task_counter - with self._tasks_lock: - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._handle_body(body)) - self.task_counter += 1 - except (BrokenPipeError, ConnectionResetError, StopLoopException): - pass - return self._received_shutdown - - async def run_forever_stderr(self) -> None: - """ - Continuously read from the language server process stderr and log the messages - """ - try: - while self.process and self.process.stderr and not self.process.stderr.at_eof(): - line = await self.process.stderr.readline() - if not line: - continue - self._log("LSP stderr: " + line.decode(ENCODING, errors="replace")) - except (BrokenPipeError, ConnectionResetError, StopLoopException): - pass - - async def _handle_body(self, body: bytes) -> None: - """ - Parse the body text received from the language server process and invoke the appropriate handler - """ - try: - await self._receive_payload(json.loads(body)) - except OSError as ex: - self._log(f"malformed {ENCODING}: {ex}") - except UnicodeDecodeError as ex: - self._log(f"malformed {ENCODING}: {ex}") - except json.JSONDecodeError as ex: - self._log(f"malformed JSON: {ex}") - - async def _receive_payload(self, payload: StringDict) -> None: - """ - Determine if the payload received from server is for a request, response, or notification and invoke the appropriate handler - """ - if self.logger: - self.logger("server", "client", payload) - try: - if "method" in payload: - if "id" in payload: - await self._request_handler(payload) - else: - await self._notification_handler(payload) - elif "id" in payload: - await self._response_handler(payload) - else: - self._log(f"Unknown payload type: {payload}") - except Exception as err: - self._log(f"Error handling server payload: {err}") - - def send_notification(self, method: str, params: dict | None = None) -> None: - """ - Send notification pertaining to the given method to the server with the given parameters - """ - self._send_payload_sync(make_notification(method, params)) - - def send_response(self, request_id: Any, params: PayloadLike) -> None: - """ - Send response to the given request id to the server with the given parameters - """ - # Use lock to prevent race conditions on tasks and task_counter - with self._tasks_lock: - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._send_payload(make_response(request_id, params))) - self.task_counter += 1 - - def send_error_response(self, request_id: Any, err: Error) -> None: - """ - Send error response to the given request id to the server with the given error - """ - # Use lock to prevent race conditions on tasks and task_counter - with self._tasks_lock: - self.tasks[self.task_counter] = asyncio.get_event_loop().create_task(self._send_payload(make_error_response(request_id, err))) - self.task_counter += 1 - - async def send_request(self, method: str, params: dict | None = None) -> PayloadLike: - """ - Send request to the server, register the request id, and wait for the response - """ - request = Request() - - # Use lock to prevent race conditions on request_id and _response_handlers - with self._request_id_lock: - request_id = self.request_id - self.request_id += 1 - - with self._response_handlers_lock: - self._response_handlers[request_id] = request - - async with request.cv: - await self._send_payload(make_request(method, request_id, params)) - self._log(f"Waiting for asyncio condition for request {method} with params:\n{params}") - await request.cv.wait() - self._log("Finished waiting, processing result") - if isinstance(request.error, Error): - raise MultilspyException( - f"Could not process request {method} with params:\n{params}.\n Language server error: {request.error}" - ) from request.error - self._log(f"Returning non-error result, which is:\n{request.result}") - return request.result - - def _send_payload_sync(self, payload: StringDict) -> None: - """ - Send the payload to the server by writing to its stdin synchronously - """ - if not self.process or not self.process.stdin: - return - msg = create_message(payload) - if self.logger: - self.logger("client", "server", payload) - - # Use lock to prevent concurrent writes to stdin that cause buffer corruption - with self._stdin_lock: - try: - self.process.stdin.writelines(msg) - except (BrokenPipeError, ConnectionResetError, OSError) as e: - # Log the error but don't raise to prevent cascading failures - if self.logger: - self.logger("client", "logger", f"Failed to write to stdin: {e}") - return - - async def _send_payload(self, payload: StringDict) -> None: - """ - Send the payload to the server by writing to its stdin asynchronously. - """ - if not self.process or not self.process.stdin: - return - self._log(payload) - msg = create_message(payload) - - # Use lock to prevent concurrent writes to stdin that cause buffer corruption - with self._stdin_lock: - try: - self.process.stdin.writelines(msg) - await self.process.stdin.drain() - except (BrokenPipeError, ConnectionResetError, OSError) as e: - # Log the error but don't raise to prevent cascading failures - if self.logger: - self.logger("client", "logger", f"Failed to write to stdin: {e}") - return - - def on_request(self, method: str, cb) -> None: - """ - Register the callback function to handle requests from the server to the client for the given method - """ - self.on_request_handlers[method] = cb - - def on_notification(self, method: str, cb) -> None: - """ - Register the callback function to handle notifications from the server to the client for the given method - """ - self.on_notification_handlers[method] = cb - - async def _response_handler(self, response: StringDict) -> None: - """ - Handle the response received from the server for a request, using the id to determine the request - """ - with self._response_handlers_lock: - request = self._response_handlers.pop(response["id"]) - - if "result" in response and "error" not in response: - await request.on_result(response["result"]) - elif "result" not in response and "error" in response: - await request.on_error(Error.from_lsp(response["error"])) - else: - await request.on_error(Error(ErrorCodes.InvalidRequest, "")) - - async def _request_handler(self, response: StringDict) -> None: - """ - Handle the request received from the server: call the appropriate callback function and return the result - """ - method = response.get("method", "") - params = response.get("params") - request_id = response.get("id") - handler = self.on_request_handlers.get(method) - if not handler: - self.send_error_response( - request_id, - Error( - ErrorCodes.MethodNotFound, - f"method '{method}' not handled on client.", - ), - ) - return - try: - self.send_response(request_id, await handler(params)) - except Error as ex: - self.send_error_response(request_id, ex) - except Exception as ex: - self.send_error_response(request_id, Error(ErrorCodes.InternalError, str(ex))) - - async def _notification_handler(self, response: StringDict) -> None: - """ - Handle the notification received from the server: call the appropriate callback function - """ - method = response.get("method", "") - params = response.get("params") - handler = self.on_notification_handlers.get(method) - if not handler: - self._log(f"unhandled {method}") - return - try: - await handler(params) - except asyncio.CancelledError: - return - except Exception as ex: - if (not self._received_shutdown) and self.logger: - self.logger( - "client", - "logger", - str( - { - "type": MessageType.error, - "message": str(ex), - "method": method, - "params": params, - } - ), - ) diff --git a/src/solidlsp/multilspy_settings.py b/src/solidlsp/settings.py similarity index 96% rename from src/solidlsp/multilspy_settings.py rename to src/solidlsp/settings.py index b404030..166be83 100644 --- a/src/solidlsp/multilspy_settings.py +++ b/src/solidlsp/settings.py @@ -6,7 +6,7 @@ import os import pathlib -class MultilspySettings: +class SolidLSPSettings: """ Provides the various settings for multilspy. """ diff --git a/src/solidlsp/type_helpers.py b/src/solidlsp/type_helpers.py deleted file mode 100644 index 53a10de..0000000 --- a/src/solidlsp/type_helpers.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -This module provides type-helpers used across multilspy implementation -""" - -import inspect -from collections.abc import Callable -from typing import TypeVar - -R = TypeVar("R", bound=object) - - -def ensure_all_methods_implemented( - source_cls: type[object], -) -> Callable[[type[R]], type[R]]: - """ - A decorator to ensure that all methods of source_cls class are implemented in the decorated class. - """ - - def check_all_methods_implemented(target_cls: R) -> R: - for name, _ in inspect.getmembers(source_cls, inspect.isfunction): - if name.startswith("_"): - continue - if name not in target_cls.__dict__ or not callable(target_cls.__dict__[name]): - raise NotImplementedError(f"{name} is not implemented in {target_cls}") - - return target_cls - - return check_all_methods_implemented diff --git a/test/conftest.py b/test/conftest.py index 1cb81ea..6632f3a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -6,8 +6,8 @@ from sensai.util.logging import configure from serena.util.file_system import GitignoreParser from solidlsp.ls import SolidLanguageServer -from solidlsp.multilspy_config import Language, MultilspyConfig -from solidlsp.multilspy_logger import MultilspyLogger +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger configure(level=logging.DEBUG) @@ -40,8 +40,8 @@ def create_ls( gitignore_parser = GitignoreParser(str(repo_path)) for spec in gitignore_parser.get_ignore_specs(): ignored_paths.extend(spec.patterns) - config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) - logger = MultilspyLogger(log_level=log_level) + config = LanguageServerConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) + logger = LanguageServerLogger(log_level=log_level) return SolidLanguageServer.create(config, logger, repo_path) diff --git a/test/multilspy/go/test_go_basic.py b/test/multilspy/go/test_go_basic.py index 82b6960..5632315 100644 --- a/test/multilspy/go/test_go_basic.py +++ b/test/multilspy/go/test_go_basic.py @@ -3,8 +3,8 @@ import os import pytest from solidlsp import SolidLanguageServer -from solidlsp.multilspy_config import Language -from solidlsp.multilspy_utils import SymbolUtils +from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils @pytest.mark.go diff --git a/test/multilspy/java/test_java_basic.py b/test/multilspy/java/test_java_basic.py index f592373..d74a9fe 100644 --- a/test/multilspy/java/test_java_basic.py +++ b/test/multilspy/java/test_java_basic.py @@ -3,8 +3,8 @@ import os import pytest from solidlsp import SolidLanguageServer -from solidlsp.multilspy_config import Language -from solidlsp.multilspy_utils import SymbolUtils +from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils @pytest.mark.java diff --git a/test/multilspy/php/test_php_basic.py b/test/multilspy/php/test_php_basic.py index 3ae2797..391c565 100644 --- a/test/multilspy/php/test_php_basic.py +++ b/test/multilspy/php/test_php_basic.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest from solidlsp import SolidLanguageServer -from solidlsp.multilspy_config import Language +from solidlsp.ls_config import Language @pytest.mark.php diff --git a/test/multilspy/python/test_python_basic.py b/test/multilspy/python/test_python_basic.py index 541f7f5..74737c4 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/multilspy/python/test_python_basic.py @@ -11,7 +11,7 @@ import pytest from serena.text_utils import LineType from solidlsp import SolidLanguageServer -from solidlsp.multilspy_config import Language +from solidlsp.ls_config import Language @pytest.mark.python diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index f100d41..a3d7d6a 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest from solidlsp import SolidLanguageServer -from solidlsp.multilspy_config import Language +from solidlsp.ls_config import Language from test.conftest import create_ls # This mark will be applied to all tests in this module diff --git a/test/multilspy/python/test_symbol_retrieval.py b/test/multilspy/python/test_symbol_retrieval.py index 97f5d44..c19bf8b 100644 --- a/test/multilspy/python/test_symbol_retrieval.py +++ b/test/multilspy/python/test_symbol_retrieval.py @@ -11,8 +11,8 @@ import os import pytest from solidlsp import SolidLanguageServer -from solidlsp.multilspy_config import Language -from solidlsp.multilspy_types import SymbolKind +from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind pytestmark = pytest.mark.python diff --git a/test/multilspy/rust/test_rust_basic.py b/test/multilspy/rust/test_rust_basic.py index e6cd6f4..d0576de 100644 --- a/test/multilspy/rust/test_rust_basic.py +++ b/test/multilspy/rust/test_rust_basic.py @@ -3,8 +3,8 @@ import os import pytest from solidlsp import SolidLanguageServer -from solidlsp.multilspy_config import Language -from solidlsp.multilspy_utils import SymbolUtils +from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils @pytest.mark.rust diff --git a/test/multilspy/typescript/test_typescript_basic.py b/test/multilspy/typescript/test_typescript_basic.py index 1c6cb5f..1410c5c 100644 --- a/test/multilspy/typescript/test_typescript_basic.py +++ b/test/multilspy/typescript/test_typescript_basic.py @@ -3,8 +3,8 @@ import os import pytest from solidlsp import SolidLanguageServer -from solidlsp.multilspy_config import Language -from solidlsp.multilspy_utils import SymbolUtils +from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils @pytest.mark.typescript diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index a31ccd4..4e29785 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -7,7 +7,7 @@ import pytest from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase from serena.process_isolated_agent import ProcessIsolatedSerenaAgent -from solidlsp.multilspy_config import Language +from solidlsp.ls_config import Language from test.conftest import get_repo_path diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index bf6e2a3..86fdc7c 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -12,7 +12,7 @@ from typing import Literal import pytest from serena.symbol import CodeDiff -from solidlsp.multilspy_config import Language +from solidlsp.ls_config import Language from src.serena.symbol import SymbolManager from test.conftest import create_ls, get_repo_path From a96e5bb476e3a062e4c78193b32507c070dc58ee Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 18:04:59 +0200 Subject: [PATCH 147/174] Update change log [skip ci] --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51a8fc8..f2eabb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. * **Reduce the use of asyncio to a minimum**, improving stability and reducing the need for workarounds - * Switch to newly developed fully synchronous LSP library `solidlsp` (derived from `multilspy`) + * Switch to newly developed fully synchronous LSP library `solidlsp` (derived from `multilspy`), + removing our fork of `multilspy` (src/multilspy) * Switch from fastapi (which uses asyncio) to Flask in the Serena dashboard * The MCP server is the only asynchronous component now, which resolves cross-component loop contamination, such that process isolation is no longer required. From 4e6ad37e26fd2f6087d28807914a2f28bdafadc5 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 23 Jun 2025 20:33:04 +0200 Subject: [PATCH 148/174] Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2eabb3..575952d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Status of the `main` branch. Changes prior to the next official version change w Fixes: * Fix `ExecuteShellCommandTool` and `GetCurrentConfigTool` hanging on Windows * Fix project activation by name via `--project` not working (was broken in previous release) +* Improve handling of indentation and newlines in symbolic editing tools +* Fix that `insert_after_symbol` was failing for insertions at the end of a file that did not end with a newline # 2025-06-20 From b951331764808d47a3d5993e8364b19100e8ec7e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 23 Jun 2025 20:40:52 +0200 Subject: [PATCH 149/174] Formatting, removing mentions of multilspy in pyproject --- pyproject.toml | 6 +++--- .../clangd_language_server.py | 4 ++-- .../dart_language_server.py | 2 +- .../eclipse_jdtls/eclipse_jdtls.py | 6 +++--- src/solidlsp/language_servers/gopls/gopls.py | 4 ++-- .../intelephense/intelephense.py | 4 ++-- .../jedi_language_server/jedi_server.py | 4 ++-- .../kotlin_language_server.py | 4 ++-- .../language_servers/omnisharp/omnisharp.py | 4 ++-- .../pyright_language_server/pyright_server.py | 4 ++-- .../rust_analyzer/rust_analyzer.py | 4 ++-- .../language_servers/solargraph/solargraph.py | 4 ++-- .../typescript_language_server.py | 4 ++-- src/solidlsp/ls.py | 20 +++++++------------ src/solidlsp/ls_handler.py | 4 ++-- src/solidlsp/ls_utils.py | 4 ++-- src/solidlsp/lsp_protocol_handler/server.py | 8 -------- 17 files changed, 38 insertions(+), 52 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d3ba68a..4ac950b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ google = [ Homepage = "https://github.com/oraios/serena" [tool.hatch.build.targets.wheel] -packages = ["src/serena", "src/multilspy", "src/interprompt", "src/solidlsp"] +packages = ["src/serena", "src/interprompt", "src/solidlsp"] [tool.black] line-length = 140 @@ -82,7 +82,7 @@ target-version = [ ] exclude = ''' /( - src/multilspy + src/solidlsp/language_servers/.*/static, )/ ''' @@ -138,7 +138,7 @@ type-check = [ target-version = "py311" line-length = 140 exclude = [ - "src/multilspy", + "src/solidlsp/language_servers/**/static", ] [tool.ruff.format] diff --git a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py index 4572de9..893c5cc 100644 --- a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py @@ -10,11 +10,11 @@ import stat import threading from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo class ClangdLanguageServer(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py index 5957558..58d7101 100644 --- a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py @@ -5,9 +5,9 @@ import pathlib import stat from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo class DartLanguageServer(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py index a2453fa..7453bf1 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -16,12 +16,12 @@ from pathlib import PurePath from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger -from solidlsp.settings import SolidLSPSettings 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 @dataclasses.dataclass diff --git a/src/solidlsp/language_servers/gopls/gopls.py b/src/solidlsp/language_servers/gopls/gopls.py index 32ae5e5..c3fadd7 100644 --- a/src/solidlsp/language_servers/gopls/gopls.py +++ b/src/solidlsp/language_servers/gopls/gopls.py @@ -8,10 +8,10 @@ import threading from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo 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 class Gopls(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense/intelephense.py index a02f9d1..19eaa8b 100644 --- a/src/solidlsp/language_servers/intelephense/intelephense.py +++ b/src/solidlsp/language_servers/intelephense/intelephense.py @@ -13,11 +13,11 @@ from time import sleep from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo 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 DefinitionParams, InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo class Intelephense(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py index a0ea203..467d3ca 100644 --- a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py +++ b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py @@ -10,10 +10,10 @@ import pathlib from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo 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 class JediServer(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py index 3031535..6ddab2c 100644 --- a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py @@ -10,11 +10,11 @@ import pathlib import stat from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @dataclasses.dataclass diff --git a/src/solidlsp/language_servers/omnisharp/omnisharp.py b/src/solidlsp/language_servers/omnisharp/omnisharp.py index 0a0216c..e66540c 100644 --- a/src/solidlsp/language_servers/omnisharp/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp/omnisharp.py @@ -13,12 +13,12 @@ from collections.abc import Iterable from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_exceptions import LanguageServerException 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 def breadth_first_file_scan(root) -> Iterable[str]: diff --git a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py index 64dbd57..893db94 100644 --- a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py @@ -11,10 +11,10 @@ import threading from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo 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 class PyrightServer(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py index a21768e..1f951e5 100644 --- a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py @@ -12,11 +12,11 @@ import threading from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo class RustAnalyzer(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph/solargraph.py index 07cc1b3..116e04b 100644 --- a/src/solidlsp/language_servers/solargraph/solargraph.py +++ b/src/solidlsp/language_servers/solargraph/solargraph.py @@ -13,10 +13,10 @@ import threading from typing import override from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo 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 class Solargraph(SolidLanguageServer): diff --git a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py index dd1d230..f220406 100644 --- a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py @@ -14,11 +14,11 @@ from time import sleep from overrides import override from solidlsp.ls import SolidLanguageServer -from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams -from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo 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 InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo # Platform-specific imports if os.name != "nt": # Unix-like systems diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 426d471..0d4dcaa 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -20,7 +20,11 @@ import tqdm from serena.text_utils import MatchedConsecutiveLines, search_files from solidlsp import ls_types +from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_exceptions import LanguageServerException from solidlsp.ls_handler import SolidLanguageServerHandler +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PathUtils, TextUtils from solidlsp.lsp_protocol_handler import lsp_types from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants @@ -30,10 +34,6 @@ from solidlsp.lsp_protocol_handler.server import ( ProcessLaunchInfo, StringDict, ) -from solidlsp.ls_config import Language, LanguageServerConfig -from solidlsp.ls_exceptions import LanguageServerException -from solidlsp.ls_logger import LanguageServerLogger -from solidlsp.ls_utils import FileUtils, PathUtils, TextUtils GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, ls_types.UnifiedSymbolInformation] @@ -409,9 +409,7 @@ class SolidLanguageServer(ABC): ) del self.open_file_buffers[uri] - def insert_text_at_position( - self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str - ) -> ls_types.Position: + def insert_text_at_position(self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str) -> ls_types.Position: """ Insert text at the given line and column in the given file and return the updated cursor position after inserting the text. @@ -1035,9 +1033,7 @@ class SolidLanguageServer(ABC): lines = file_content.split("\n") end_line = len(lines) end_column = len(lines[-1]) - return ls_types.Range( - start=ls_types.Position(line=0, character=0), end=ls_types.Position(line=end_line, character=end_column) - ) + return ls_types.Range(start=ls_types.Position(line=0, character=0), end=ls_types.Position(line=end_line, character=end_column)) def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, ls_types.SymbolKind, int, int]]]: """ @@ -1139,9 +1135,7 @@ class SolidLanguageServer(ABC): # ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA -------------------- - def retrieve_symbol_body( - self, symbol: ls_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation - ) -> str: + def retrieve_symbol_body(self, symbol: ls_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation) -> str: """ Load the body of the given symbol. If the body is already contained in the symbol, just return it. """ diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 6de9ef7..458a023 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -12,6 +12,8 @@ from typing import Any import psutil +from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_request import LanguageServerRequest from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification from solidlsp.lsp_protocol_handler.lsp_types import ErrorCodes from solidlsp.lsp_protocol_handler.server import ( @@ -28,8 +30,6 @@ from solidlsp.lsp_protocol_handler.server import ( make_request, make_response, ) -from solidlsp.ls_request import LanguageServerRequest -from solidlsp.ls_exceptions import LanguageServerException log = logging.getLogger(__name__) diff --git a/src/solidlsp/ls_utils.py b/src/solidlsp/ls_utils.py index 39a1d91..7aa610e 100644 --- a/src/solidlsp/ls_utils.py +++ b/src/solidlsp/ls_utils.py @@ -54,7 +54,7 @@ class TextUtils: idx = 0 while line > 0: if idx >= len(text): - raise InvalidTextLocationError() + raise InvalidTextLocationError if text[idx] == "\n": line -= 1 idx += 1 @@ -95,7 +95,7 @@ class TextUtils: """ try: change_index = TextUtils.get_index_from_line_col(text, line, col) - except InvalidTextLocationError as e: + except InvalidTextLocationError: num_lines_in_text = text.count("\n") + 1 max_line = num_lines_in_text - 1 if line == max_line + 1 and col == 0: # trying to insert at new line after full text diff --git a/src/solidlsp/lsp_protocol_handler/server.py b/src/solidlsp/lsp_protocol_handler/server.py index 0a001f4..5045312 100644 --- a/src/solidlsp/lsp_protocol_handler/server.py +++ b/src/solidlsp/lsp_protocol_handler/server.py @@ -28,19 +28,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import asyncio import dataclasses import json import logging import os -import threading -from collections.abc import Callable from typing import Any, Union -import psutil - -from ..ls_exceptions import LanguageServerException -from .lsp_requests import LspNotification, LspRequest from .lsp_types import ErrorCodes StringDict = dict[str, Any] @@ -127,4 +120,3 @@ def content_length(line: bytes) -> int | None: except ValueError: raise ValueError(f"Invalid Content-Length header: {value}") return None - From dfc841857e34f8c5098db397166754fc2a11ff2e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 23 Jun 2025 20:44:42 +0200 Subject: [PATCH 150/174] Typo --- src/serena/symbol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 5f5fbf1..0f48bb5 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -767,7 +767,7 @@ class SymbolManager: col = 0 line = pos["line"] + 1 # make sure a suitable number of leading empty lines is used (at least 0/1 depending on the symbol type, - # otherweise as many as the caller wanted to insert) + # otherwise as many as the caller wanted to insert) original_leading_newlines = self._count_leading_newlines(body) body = body.lstrip("\r\n") min_empty_lines = 0 From 719a2c55a872296836a8b3bc422c5656da17ab74 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 20:47:27 +0200 Subject: [PATCH 151/174] Update references to multilspy and update READMEs --- README.md | 17 ++++++++--------- src/README.md | 2 +- src/solidlsp/ls_config.py | 2 +- src/solidlsp/settings.py | 6 +----- .../{multilspy => solidlsp}/go/test_go_basic.py | 0 .../java/test_java_basic.py | 0 .../php/test_php_basic.py | 0 .../python/test_python_basic.py | 0 .../python/test_retrieval_with_ignored_dirs.py | 0 .../python/test_symbol_retrieval.py | 0 .../rust/test_rust_basic.py | 0 .../typescript/test_typescript_basic.py | 0 12 files changed, 11 insertions(+), 16 deletions(-) rename test/{multilspy => solidlsp}/go/test_go_basic.py (100%) rename test/{multilspy => solidlsp}/java/test_java_basic.py (100%) rename test/{multilspy => solidlsp}/php/test_php_basic.py (100%) rename test/{multilspy => solidlsp}/python/test_python_basic.py (100%) rename test/{multilspy => solidlsp}/python/test_retrieval_with_ignored_dirs.py (100%) rename test/{multilspy => solidlsp}/python/test_symbol_retrieval.py (100%) rename test/{multilspy => solidlsp}/rust/test_rust_basic.py (100%) rename test/{multilspy => solidlsp}/typescript/test_typescript_basic.py (100%) diff --git a/README.md b/README.md index d67000a..b3907da 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ With Serena, we provide * direct, out-of-the-box support for: * Python * TypeScript/Javascript - * PhP + * PHP * Go (need to install go and gopls first) * Rust * C/C++ @@ -80,8 +80,8 @@ With Serena, we provide * Kotlin (untested) * Dart (untested) - These languages are supported by the language server library [multilspy](https://github.com/microsoft/multilspy), which Serena uses under the hood. - But we did not explicitly test whether the support for these languages actually works. + These languages are supported by the language server library, but + we did not explicitly test whether the support for these languages actually works flawlessly. Further languages can, in principle, easily be supported by providing a shallow adapter for a new language server implementation. @@ -786,17 +786,16 @@ larger codebases. We built Serena on top of multiple existing open-source technologies, the most important ones being: 1. [multilspy](https://github.com/microsoft/multilspy). - A beautifully designed wrapper around language servers following the LSP. It - was not easily extendable with the symbolic - logic that Serena required, so instead of incorporating it as dependency, we - copied the source code - and adapted it to our needs. + A library which wraps language server implementations and adapts them for interaction via Python + and which provided the basis for our library Solid-LSP (src/solidlsp). + Solid-LSP provides pure synchronous LSP calls and extends the original library with the symbolic logic + that Serena required. 2. [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk) 3. [Agno](https://github.com/agno-agi/agno) and the associated [agent-ui](https://github.com/agno-agi/agent-ui), which we use to allow Serena to work with any model, beyond the ones supporting the MCP. -4. All the language servers that we use through multilspy. +4. All the language servers that we use through Solid-LSP. Without these projects, Serena would not have been possible (or would have been significantly more difficult to build). diff --git a/src/README.md b/src/README.md index fc5a9c9..4198627 100644 --- a/src/README.md +++ b/src/README.md @@ -1,4 +1,4 @@ Serena uses (modified) versions of other libraries/packages: - * [multilspy](https://github.com/oraios/multilspy) (for language server protocol support); original repo: microsoft/multilspy + * solidlsp (our fork of [microsoft/multilspy](https://github.com/microsoft/multilspy) for fully synchronous language server communication) * [interprompt](https://github.com/oraios/interprompt) (our prompt templating library) diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 27deaf1..07b68f5 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -1,5 +1,5 @@ """ -Configuration parameters for Multilspy. +Configuration objects for language servers """ import fnmatch diff --git a/src/solidlsp/settings.py b/src/solidlsp/settings.py index 166be83..38b3010 100644 --- a/src/solidlsp/settings.py +++ b/src/solidlsp/settings.py @@ -1,5 +1,5 @@ """ -Defines the settings for multilspy. +Defines settings for Solid-LSP """ import os @@ -7,10 +7,6 @@ import pathlib class SolidLSPSettings: - """ - Provides the various settings for multilspy. - """ - @staticmethod def get_language_server_directory() -> str: """Returns the directory for language servers""" diff --git a/test/multilspy/go/test_go_basic.py b/test/solidlsp/go/test_go_basic.py similarity index 100% rename from test/multilspy/go/test_go_basic.py rename to test/solidlsp/go/test_go_basic.py diff --git a/test/multilspy/java/test_java_basic.py b/test/solidlsp/java/test_java_basic.py similarity index 100% rename from test/multilspy/java/test_java_basic.py rename to test/solidlsp/java/test_java_basic.py diff --git a/test/multilspy/php/test_php_basic.py b/test/solidlsp/php/test_php_basic.py similarity index 100% rename from test/multilspy/php/test_php_basic.py rename to test/solidlsp/php/test_php_basic.py diff --git a/test/multilspy/python/test_python_basic.py b/test/solidlsp/python/test_python_basic.py similarity index 100% rename from test/multilspy/python/test_python_basic.py rename to test/solidlsp/python/test_python_basic.py diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/solidlsp/python/test_retrieval_with_ignored_dirs.py similarity index 100% rename from test/multilspy/python/test_retrieval_with_ignored_dirs.py rename to test/solidlsp/python/test_retrieval_with_ignored_dirs.py diff --git a/test/multilspy/python/test_symbol_retrieval.py b/test/solidlsp/python/test_symbol_retrieval.py similarity index 100% rename from test/multilspy/python/test_symbol_retrieval.py rename to test/solidlsp/python/test_symbol_retrieval.py diff --git a/test/multilspy/rust/test_rust_basic.py b/test/solidlsp/rust/test_rust_basic.py similarity index 100% rename from test/multilspy/rust/test_rust_basic.py rename to test/solidlsp/rust/test_rust_basic.py diff --git a/test/multilspy/typescript/test_typescript_basic.py b/test/solidlsp/typescript/test_typescript_basic.py similarity index 100% rename from test/multilspy/typescript/test_typescript_basic.py rename to test/solidlsp/typescript/test_typescript_basic.py From ba74c059700e6d2641a877110d00d61f6ffcbb3d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 23 Jun 2025 21:00:20 +0200 Subject: [PATCH 152/174] Removed javascript from languages (just ts is enough), enhanced ts file matching --- src/solidlsp/ls.py | 2 +- src/solidlsp/ls_config.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 0d4dcaa..cf5f3bb 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -144,7 +144,7 @@ class SolidLanguageServer(ABC): return OmniSharp(config, logger, repository_root_path) - elif config.code_language in [Language.TYPESCRIPT, Language.JAVASCRIPT]: + elif config.code_language == Language.TYPESCRIPT: from solidlsp.language_servers.typescript_language_server.typescript_language_server import ( TypeScriptLanguageServer, ) diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 27deaf1..abe9e27 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -32,7 +32,6 @@ class Language(str, Enum): JAVA = "java" KOTLIN = "kotlin" TYPESCRIPT = "typescript" - JAVASCRIPT = "javascript" GO = "go" RUBY = "ruby" DART = "dart" @@ -49,9 +48,13 @@ class Language(str, Enum): case self.JAVA: return FilenameMatcher("*.java") case self.TYPESCRIPT: - return FilenameMatcher("*.ts", "*.js", "*.jsx", "*.tsx") - case self.JAVASCRIPT: - return FilenameMatcher("*.js", "*.jsx") + # see https://github.com/oraios/serena/issues/204 + path_patterns = [] + for prefix in ["c", "m", ""]: + for postfix in ["x", ""]: + for base_pattern in ["ts", "js"]: + path_patterns.append(f"*.{prefix}{base_pattern}{postfix}") + return FilenameMatcher(*path_patterns) case self.CSHARP: return FilenameMatcher("*.cs") case self.RUST: From 769fc798509fa67d77d9ffa7142f49ee1ecd5961 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 23 Jun 2025 21:05:05 +0200 Subject: [PATCH 153/174] Backwards compat restored for activating javascript projects [ci skip] --- src/serena/agent.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index f2c0941..0ff552e 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -177,12 +177,18 @@ class ProjectConfig(ToStringMixin): """ Create a ProjectConfig instance from a configuration dictionary """ + language_str = data["language"].lower() + project_name = data["project_name"] + # backwards compatibility + if language_str == "javascript": + log.warning(f"Found deprecated project language `javascript` in project {project_name}, please change to `typescript`") + language_str = "typescript" try: - language = Language(data["language"].lower()) + language = Language(language_str) except ValueError as e: raise ValueError(f"Invalid language: {data['language']}.\nValid languages are: {[l.value for l in Language]}") from e return cls( - project_name=data["project_name"], + project_name=project_name, language=language, ignored_paths=data.get("ignored_paths", []), excluded_tools=set(data.get("excluded_tools", [])), From 449188693ee4f82264daf9029f840b1b6cf4a3bd Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 21:15:32 +0200 Subject: [PATCH 154/174] Consider timeout for LS requests --- src/solidlsp/ls.py | 45 ++++++++++++++++++++------------------ src/solidlsp/ls_handler.py | 21 +++++++++--------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 0d4dcaa..22be2de 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -101,85 +101,83 @@ class SolidLanguageServer(ABC): :param repository_root_path: The root path of the repository. :param config: The Multilspy configuration. :param logger: The logger to use. + :param timeout: the timeout for requests to the language server. If None, no timeout will be used. :return LanguageServer: A language specific LanguageServer instance. """ - # TODO: Apply timeout + ls: SolidLanguageServer + if config.code_language == Language.PYTHON: from solidlsp.language_servers.pyright_language_server.pyright_server import ( PyrightServer, ) - return PyrightServer(config, logger, repository_root_path) - # It used to be jedi, but pyright is a bit faster, and also more actively maintained - # Keeping the previous code for reference - # from multilspy.language_servers.jedi_language_server.jedi_server import ( - # JediServer, - # ) - - # return JediServer(config, logger, repository_root_path) + ls = PyrightServer(config, logger, repository_root_path) elif config.code_language == Language.JAVA: from solidlsp.language_servers.eclipse_jdtls.eclipse_jdtls import ( EclipseJDTLS, ) - return EclipseJDTLS(config, logger, repository_root_path) + ls = EclipseJDTLS(config, logger, repository_root_path) elif config.code_language == Language.KOTLIN: from solidlsp.language_servers.kotlin_language_server.kotlin_language_server import ( KotlinLanguageServer, ) - return KotlinLanguageServer(config, logger, repository_root_path) + ls = KotlinLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.RUST: from solidlsp.language_servers.rust_analyzer.rust_analyzer import ( RustAnalyzer, ) - return RustAnalyzer(config, logger, repository_root_path) + ls = RustAnalyzer(config, logger, repository_root_path) elif config.code_language == Language.CSHARP: from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp - return OmniSharp(config, logger, repository_root_path) + ls = OmniSharp(config, logger, repository_root_path) elif config.code_language in [Language.TYPESCRIPT, Language.JAVASCRIPT]: from solidlsp.language_servers.typescript_language_server.typescript_language_server import ( TypeScriptLanguageServer, ) - return TypeScriptLanguageServer(config, logger, repository_root_path) + ls = TypeScriptLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.GO: from solidlsp.language_servers.gopls.gopls import Gopls - return Gopls(config, logger, repository_root_path) + ls = Gopls(config, logger, repository_root_path) elif config.code_language == Language.RUBY: from solidlsp.language_servers.solargraph.solargraph import Solargraph - return Solargraph(config, logger, repository_root_path) + ls = Solargraph(config, logger, repository_root_path) elif config.code_language == Language.DART: from solidlsp.language_servers.dart_language_server.dart_language_server import DartLanguageServer - return DartLanguageServer(config, logger, repository_root_path) + ls = DartLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.CPP: from solidlsp.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer - return ClangdLanguageServer(config, logger, repository_root_path) + ls = ClangdLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.PHP: from solidlsp.language_servers.intelephense.intelephense import Intelephense - return Intelephense(config, logger, repository_root_path) + ls = Intelephense(config, logger, repository_root_path) else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) raise LanguageServerException(f"Language {config.code_language} is not supported") + ls.set_request_timeout(timeout) + return ls + def __init__( self, config: LanguageServerConfig, @@ -255,6 +253,13 @@ class SolidLanguageServer(ABC): self._ignore_spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, processed_patterns) self._server_context = None + self._request_timeout: float | None = None + + def set_request_timeout(self, timeout: float | None) -> None: + """ + :param timeout: the timeout, in seconds, for requests to the language server. + """ + self.server.set_request_timeout(timeout) def get_ignore_spec(self) -> pathspec.PathSpec: """Returns the pathspec matcher for the paths that were configured to be ignored through @@ -1133,8 +1138,6 @@ class SolidLanguageServer(ABC): return ls_types.Hover(**response) - # ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA -------------------- - def retrieve_symbol_body(self, symbol: ls_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation) -> str: """ Load the body of the given symbol. If the body is already contained in the symbol, just return it. diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 458a023..f367a1f 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -53,9 +53,8 @@ class Request: def on_error(self, err: Error) -> None: self._result_queue.put(Request.Result(error=err)) - def get_result(self) -> Result: - # TODO could add timeout - return self._result_queue.get() + def get_result(self, timeout: float | None = None) -> Result: + return self._result_queue.get(timeout=timeout) class SolidLanguageServerHandler: @@ -102,13 +101,8 @@ class SolidLanguageServerHandler: process_launch_info: ProcessLaunchInfo, logger: Callable[[str, str, StringDict | str], None] | None = None, start_independent_lsp_process=True, + request_timeout: float | None = None, ) -> None: - """ - Params: - cmd: A string that represents the command to launch the language server process. - logger: An optional function that takes two strings (source and destination) and - a payload dictionary, and logs the communication between the client and the server. - """ self.send = LanguageServerRequest(self.send_request) self.notify = LspNotification(self.send_notification) @@ -125,6 +119,7 @@ class SolidLanguageServerHandler: self.task_counter = 0 self.loop = None self.start_independent_lsp_process = start_independent_lsp_process + self._request_timeout = request_timeout # Add thread locks for shared resources to prevent race conditions self._stdin_lock = threading.Lock() @@ -132,6 +127,12 @@ class SolidLanguageServerHandler: self._response_handlers_lock = threading.Lock() self._tasks_lock = threading.Lock() + def set_request_timeout(self, timeout: float | None) -> None: + """ + :param timeout: the timeout, in seconds, for all requests sent to the language server. + """ + self._request_timeout = timeout + def is_running(self) -> bool: """ Checks if the language server process is currently running. @@ -418,7 +419,7 @@ class SolidLanguageServerHandler: self._send_payload(make_request(method, request_id, params)) self._log(f"Waiting for response to request {method} with params:\n{params}") - result = request.get_result() + result = request.get_result(timeout=self._request_timeout) self._log("Processing result") if result.is_error(): From 42a8796353d7a06e253ddfc04e18a4f195c45ce9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 23 Jun 2025 21:22:59 +0200 Subject: [PATCH 155/174] search_for_pattern: better default and description of `restrict_search_to_code_files` param --- src/serena/agent.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 0ff552e..114d3b0 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -2131,7 +2131,7 @@ class SearchForPatternTool(Tool): context_lines_after: int = 0, paths_include_glob: str | None = None, paths_exclude_glob: str | None = None, - only_in_code_files: bool = True, + restrict_search_to_code_files: bool = False, max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH, ) -> str: """ @@ -2144,15 +2144,22 @@ class SearchForPatternTool(Tool): :param context_lines_after: Number of lines of context to include after each match :param paths_include_glob: optional glob pattern specifying files to include in the search; if not provided, search globally. :param paths_exclude_glob: optional glob pattern specifying files to exclude from the search (takes precedence over paths_include_glob). - :param only_in_code_files: whether to search only in code files or in the entire code base. - The explicitly ignored files (from serena config and gitignore) are never searched. :param max_answer_chars: if the output is longer than this number of characters, no content will be returned. Don't adjust unless there is really no other way to get the content required for the task. Instead, if the output is too long, you should make a stricter query. + :param restrict_search_to_code_files: whether to restrict the search to only those files where + analyzed code symbols can be found. Otherwise, will search all non-ignored files. + Set this to True if your search is only meant to discover code that can be manipulated with symbolic tools. + For example, for finding classes or methods from a name pattern. + Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files, + which is why it is the default. :return: A JSON object mapping file paths to lists of matched consecutive lines (with context, if requested). """ - if only_in_code_files: + # this was previously a kwarg and was true by default + # However, the LLM doesn't really know which files are taken into account by the language server + # and which onees + if restrict_search_to_code_files: matches = self.language_server.search_files_for_pattern( pattern=pattern, context_lines_before=context_lines_before, @@ -2165,6 +2172,7 @@ class SearchForPatternTool(Tool): project_root = self.get_project_root() rel_paths_to_search = [] for root, dirs, files in os.walk(project_root): + # don't explore ignored dirs dirs[:] = [d for d in dirs if not self.agent.path_is_gitignored(os.path.join(root, d))] for file in files: file_path = os.path.join(root, file) From ac4ef9911e3f8f6ef2bd126721f5a442b7d1472d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 23 Jun 2025 21:26:38 +0200 Subject: [PATCH 156/174] changelog [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 575952d..00d5023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Status of the `main` branch. Changes prior to the next official version change w * The MCP server is the only asynchronous component now, which resolves cross-component loop contamination, such that process isolation is no longer required. Neither are non-graceful shutdowns on Windows. +* Better default and description for restricting the search in `search_for_pattern` Fixes: * Fix `ExecuteShellCommandTool` and `GetCurrentConfigTool` hanging on Windows From 1de0a186ce69c873b3c2775cf974d9e58e373e6f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 23 Jun 2025 21:29:20 +0200 Subject: [PATCH 157/174] Bumped cache version [ci skip] --- src/solidlsp/ls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index cf5f3bb..e9df135 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1533,7 +1533,7 @@ class SolidLanguageServer(ABC): """ The path to the cache file for the document symbols. """ - return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v20-05-25.pkl" + return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v23-06-25.pkl" def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None: """Will go through the entire repository and "index" all files, meaning save their symbols to the cache. From bffea2172c8daefa75677e7916dc30ce2423554b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 21:40:02 +0200 Subject: [PATCH 158/174] Re-add multilspy folders to exclusions as unversioned files (*/static) remain unless explicitly deleted --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4ac950b..21d2fa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ target-version = [ ] exclude = ''' /( - src/solidlsp/language_servers/.*/static, + src/solidlsp/language_servers/.*/static|src/multilspy )/ ''' @@ -139,6 +139,7 @@ target-version = "py311" line-length = 140 exclude = [ "src/solidlsp/language_servers/**/static", + "src/multilspy", ] [tool.ruff.format] From 757e4bb79bd9b83c8131bad9fc20643cc5c65c0f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 23 Jun 2025 21:46:37 +0200 Subject: [PATCH 159/174] Memories [ci skip] --- .../current_solid_lsp_architecture.md | 123 +++++++++++++ .../deadlock_analysis_mcp_symbolic_tools.md | 63 ------- .serena/memories/language_servers.md | 16 -- .../memories/multilspy_historical_context.md | 73 ++++++++ .../process_isolation_current_status.md | 126 +++++++++++++ .serena/memories/project_structure.md | 16 -- .serena/memories/serena_project_structure.md | 173 ++++++++++++++++++ .serena/memories/solid-lsp-architecture.md | 161 ---------------- .../memories/supported_language_servers.md | 97 ++++++++++ 9 files changed, 592 insertions(+), 256 deletions(-) create mode 100644 .serena/memories/current_solid_lsp_architecture.md delete mode 100644 .serena/memories/deadlock_analysis_mcp_symbolic_tools.md delete mode 100644 .serena/memories/language_servers.md create mode 100644 .serena/memories/multilspy_historical_context.md create mode 100644 .serena/memories/process_isolation_current_status.md delete mode 100644 .serena/memories/project_structure.md create mode 100644 .serena/memories/serena_project_structure.md delete mode 100644 .serena/memories/solid-lsp-architecture.md create mode 100644 .serena/memories/supported_language_servers.md diff --git a/.serena/memories/current_solid_lsp_architecture.md b/.serena/memories/current_solid_lsp_architecture.md new file mode 100644 index 0000000..90a60ad --- /dev/null +++ b/.serena/memories/current_solid_lsp_architecture.md @@ -0,0 +1,123 @@ +# Current Architecture: Solid-LSP Implementation + +## Overview + +Solid-LSP (`src/solidlsp/`) is Serena's current language server integration layer, designed as a **simplified, deadlock-resistant** replacement for the problematic multilspy architecture. It enables **single-process operation** with MCP servers while maintaining all semantic code analysis capabilities. + +## Current System Status + +- **Default Implementation**: Solid-LSP is the only language server implementation (multilspy completely removed) +- **Process Isolation**: **Disabled by default** (`USE_PROCESS_ISOLATION = False`) +- **MCP Integration**: Runs safely in the same process as MCP server +- **Performance**: Lower latency and resource usage than process-isolated multilspy + +## Core Architecture + +### Primary Components + +#### 1. **`SolidLanguageServer`** (`src/solidlsp/ls.py`) +- **1,634 lines**: Main language server interface +- **Enhanced Methods**: Same semantic capabilities as multilspy but with improved async handling +- **Lifecycle Control**: `start()`, `stop()`, `is_running()`, `language_server()` property +- **Clean Async Patterns**: No coroutine leakage between async contexts + +#### 2. **`SolidLanguageServerHandler`** (`src/solidlsp/ls_handler.py`) +- **512 lines**: Simplified LSP protocol handler +- **Direct Process Management**: More straightforward than multilspy's complex orchestration +- **Resource Management**: Better cleanup and process termination + +#### 3. **Protocol Layer** (`src/solidlsp/lsp_protocol_handler/`) +- **Simplified Protocol**: Direct LSP communication without excessive abstraction +- **Types and Constants**: LSP type definitions and protocol constants +- **Request Handling**: Streamlined request/response pattern + +### Language Server Support + +Solid-LSP maintains **identical language support** to multilspy: + +#### Primary Languages (Directly Supported) +- **Python**: Pyright Language Server (`src/solidlsp/language_servers/pyright_language_server/`) +- **Java**: Eclipse JDTLS (`src/solidlsp/language_servers/eclipse_jdtls/`) +- **TypeScript/JavaScript**: TypeScript Language Server (`src/solidlsp/language_servers/typescript_language_server/`) + +#### Additional Languages (Full Support) +- **C#**: OmniSharp (`src/solidlsp/language_servers/omnisharp/`) +- **Rust**: Rust-Analyzer (`src/solidlsp/language_servers/rust_analyzer/`) +- **Go**: Gopls (`src/solidlsp/language_servers/gopls/`) +- **Ruby**: Solargraph (`src/solidlsp/language_servers/solargraph/`) +- **C++**: Clangd (`src/solidlsp/language_servers/clangd_language_server/`) +- **Dart**: Dart Language Server (`src/solidlsp/language_servers/dart_language_server/`) +- **PHP**: Intelephense (`src/solidlsp/language_servers/intelephense/`) +- **Kotlin**: Kotlin Language Server (`src/solidlsp/language_servers/kotlin_language_server/`) + +## Key Architectural Improvements + +### 1. **Simplified Async Patterns** +- **Clean Boundaries**: Proper async context management prevents MCP server contamination +- **No Coroutine Leakage**: Eliminates the unawaited coroutine warnings from multilspy +- **Direct Operations**: Fewer abstraction layers where async context could be corrupted + +### 2. **Single Process Safety** +- **Safe MCP Integration**: Designed to work within MCP server process without deadlocks +- **Eliminated IPC Overhead**: Direct method calls instead of inter-process communication +- **Resource Efficiency**: Lower memory usage and faster startup times + +### 3. **Enhanced Process Control** +- **Direct Process Management**: `_start_server_process()`, `_start_server()` methods +- **Better State Management**: `_server_context` attribute for enhanced control +- **Improved Cleanup**: More reliable resource management and process termination + +## Integration Points + +### MCP Server Integration +Current implementation in `src/serena/mcp.py:590`: +```python +if not USE_PROCESS_ISOLATION: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) +else: + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) +``` + +### Agent Integration +Language server creation in `src/serena/agent.py:642-689`: +```python +def create_ls_for_project(...) -> SolidLanguageServer: + # Creates LanguageServerConfig with project settings + # Returns SolidLanguageServer.create() instance +``` + +## Configuration and Settings + +### Core Configuration +- **`USE_PROCESS_ISOLATION = False`**: Process isolation disabled by default +- **Language Detection**: Automatic based on project composition +- **Timeout Settings**: Configurable language server timeouts +- **LSP Communication Tracing**: Optional debugging support + +### Project-Level Settings +- **Ignored Paths**: Respects project configuration and gitignore +- **Language Selection**: Automatic or manual language server selection +- **Timeout Configuration**: Per-project timeout settings + +## Performance Benefits + +### vs. Multilspy + Process Isolation +1. **Lower Latency**: Direct method calls instead of IPC +2. **Reduced Memory**: Single process instead of multiple processes +3. **Faster Startup**: No process creation overhead +4. **Simpler Debugging**: All components in same process with unified stack traces + +### Stability Improvements +1. **No Asyncio Deadlocks**: Clean async boundaries prevent contamination +2. **Reliable Operations**: `find_symbol` and other tools work consistently +3. **Resource Management**: Better cleanup prevents resource leaks +4. **Error Handling**: Simplified error propagation and handling + +## Current Operational State + +- **Production Ready**: Default implementation for all Serena deployments +- **Fully Tested**: All semantic tools (`find_symbol`, `replace_symbol_body`, etc.) working reliably +- **MCP Compatible**: Stable operation with Claude Desktop and other MCP clients +- **Cross-Platform**: Works on all supported operating systems + +This architecture represents the successful resolution of the multilspy asyncio contamination issues while maintaining full semantic code analysis capabilities and improving overall performance. \ No newline at end of file diff --git a/.serena/memories/deadlock_analysis_mcp_symbolic_tools.md b/.serena/memories/deadlock_analysis_mcp_symbolic_tools.md deleted file mode 100644 index fa3fc20..0000000 --- a/.serena/memories/deadlock_analysis_mcp_symbolic_tools.md +++ /dev/null @@ -1,63 +0,0 @@ -# Deadlock Analysis: MCP Symbolic Tools Timeout Issue - -## Problem Summary -Intermittent deadlocks occur when using symbolic tools (particularly `find_symbol`) through MCP clients. Successful calls are followed by calls that hang for 240 seconds before timing out. Once a timeout occurs, the language server calls will no longer respond. - -## ROOT CAUSE DISCOVERED: Asyncio Event Loop Interference - -### Critical Evidence from MCP Server Logs -The smoking gun was found in the MCP server logs showing **unawaited coroutines**: - -``` -INFO mcp.server.lowlevel.server:_handle_message:524 - Warning: RuntimeWarning: coroutine 'LanguageServer.request_full_symbol_tree' was never awaited -``` - -This warning is being logged by the **MCP server's `_handle_message` method**, not SerenaAgent code, indicating coroutines are being created in the MCP server's asyncio context but never properly awaited. - -### The Real Issue: Dual Asyncio Context Contamination - -**Architecture Problem:** -1. **MCP Server**: Runs its own asyncio event loop to handle incoming requests -2. **SerenaAgent**: Creates its own asyncio event loop in separate thread for language serve, and even a second loop for the dashboard if so configured -3. **Conflict**: When MCP server calls SerenaAgent tools, we have **two or three asyncio contexts interacting** - -**Deadlock Mechanism:** -``` -MCP Server (asyncio loop A) - → handles tool request - → calls SerenaAgent.find_symbol() - → SerenaAgent uses asyncio.run_coroutine_threadsafe() - → Creates coroutine in language server loop (loop B) - → BUT: Coroutine gets leaked into MCP's context and never awaited - → Dangling coroutines accumulate in MCP server's loop - → Eventually causes resource exhaustion/event loop blocking - → Language server appears to "hang" but it's actually MCP loop contamination -``` - -### Discarded Hypotheses -- **Not a timeout issue**: Language server actually works fine in isolation -- **Not abandoned threads**: The threading mechanism works correctly -- **Not LSP deadlock**: The TypeScript language server itself isn't hanging - -The actual issue is **asyncio context bleeding** between MCP server and SerenaAgent. - -### Why We Can't Reproduce Outside MCP -- **Direct script calls**: No MCP server, no dual asyncio context -- **Single event loop**: Only SerenaAgent's language server loop exists -- **No async interference**: Clean, isolated execution -- **MCP-specific**: Requires the exact async context interaction pattern - -## Technical Details - -### Key Files and Locations -- `src/multilspy/language_server.py:1870` - `run_coroutine_threadsafe` creates coroutines that leak to MCP context -- MCP server `_handle_message` - Where unawaited coroutine warnings appear - -## Solution -**Process Isolation** is the fundamental fix: -1. **Separate processes**: MCP server, SerenaAgent and Dashboard in different processes -2. **IPC communication**: Replace direct method calls with inter-process communication -3. **Clean async boundaries**: Each process manages its own asyncio context -4. **No coroutine leakage**: Complete isolation prevents context contamination - -This explains why the deadlock is **MCP-specific** and doesn't occur in direct tool execution - it's fundamentally about asyncio context contamination between the MCP server and SerenaAgent. diff --git a/.serena/memories/language_servers.md b/.serena/memories/language_servers.md deleted file mode 100644 index f6ea4d0..0000000 --- a/.serena/memories/language_servers.md +++ /dev/null @@ -1,16 +0,0 @@ -# Available Language Servers - -Serena supports multiple programming languages through language servers. The language servers currently available in the project are: - -## Directly Supported (Out of the Box) -- **Python**: Using Pyright Language Server (previously used Jedi) -- **Java**: Using Eclipse JDTLS (Note: startup is slow, especially initial startup) -- **TypeScript/JavaScript**: Using TypeScript Language Server - -## Indirectly Supported (May Require Manual Setup) -- **Ruby**: Using Solargraph -- **Go**: Using Gopls -- **C#**: Using OmniSharp -- **Rust**: Using Rust-Analyzer - -These language servers utilize the Language Server Protocol (LSP) to provide semantic code analysis capabilities. The system can be extended to support additional languages by providing adapters for new language server implementations. \ No newline at end of file diff --git a/.serena/memories/multilspy_historical_context.md b/.serena/memories/multilspy_historical_context.md new file mode 100644 index 0000000..7f73a58 --- /dev/null +++ b/.serena/memories/multilspy_historical_context.md @@ -0,0 +1,73 @@ +# Historical Context: Multilspy Era and Migration to Solid-LSP + +## What multilspy Was + +Multilspy was Serena's original language server integration layer that provided semantic code analysis through Language Server Protocol (LSP). It was located in `src/multilspy/` and included: + +### Core Components (No Longer Present) +- **`multilspy.LanguageServer`**: Main language server interface with async methods like `request_full_symbol_tree()` +- **`LanguageServerHandler`**: Complex async task management and protocol handling +- **`lsp_protocol_handler`**: Separate protocol abstraction layer +- **Process Management**: Complex threading and async orchestration + +### Supported Languages +Multilspy supported the same languages now supported by solid-lsp: +- Python (Pyright), Java (Eclipse JDTLS), TypeScript/JavaScript, C#, Rust, Go, Ruby, C++, Dart, PHP, Kotlin + +## Why We Moved Away from Multilspy + +### 1. **Asyncio Contamination Issues** +- **Root Problem**: MCP server runs its own asyncio event loop, while multilspy created additional asyncio loops +- **Coroutine Leakage**: `multilspy.LanguageServer.request_full_symbol_tree()` coroutines leaked into MCP server context but were never awaited +- **Evidence**: MCP server logs showed: `RuntimeWarning: coroutine 'LanguageServer.request_full_symbol_tree' was never awaited` +- **Result**: Accumulated unawaited coroutines caused resource exhaustion and apparent "hangs" + +### 2. **Dual Event Loop Interference** +- **MCP Server**: Asyncio loop A for handling requests +- **SerenaAgent**: Asyncio loop B for language servers + loop C for dashboard +- **Conflict**: Multiple asyncio contexts caused contamination and deadlocks + +### 3. **Complex Architecture** +- **Over-abstraction**: Multiple layers of protocol handling increased complexity +- **Threading Issues**: Complex async task management made debugging difficult +- **Resource Overhead**: Separate protocol handler processes added overhead + +### 4. **MCP-Specific Problems** +- **Only in MCP environments**: Issues didn't occur in direct script execution +- **Mandatory Process Isolation**: Required separate processes to prevent asyncio contamination +- **Performance Penalty**: IPC overhead was necessary for stability + +## The Process Isolation Workaround + +Before solid-lsp, the **only way** to use multilspy with MCP was through process isolation: + +```python +# Old architecture (when multilspy was still present) +if USE_SOLID_LSP: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) +else: + # multilspy required process isolation to prevent asyncio contamination + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) +``` + +### Why Process Isolation Was Mandatory for Multilspy +- **Complete Separation**: MCP server, SerenaAgent, and language servers in different processes +- **IPC Overhead**: All communication through inter-process communication +- **Resource Cost**: Higher memory usage and startup time +- **Debugging Complexity**: Distributed architecture made troubleshooting harder + +## Timeline of Changes + +1. **Pre-solid-lsp**: Multilspy was the only option, required process isolation for MCP +2. **Solid-lsp Introduction**: New implementation designed to eliminate asyncio issues +3. **Current State**: Multilspy completely removed, solid-lsp is the only implementation +4. **Process Isolation**: Now disabled by default (`USE_PROCESS_ISOLATION = False`) since it's no longer needed + +## Key Lessons Learned + +- **Asyncio Complexity**: Multiple event loops in the same application are extremely difficult to manage correctly +- **MCP Integration Challenges**: MCP server's async nature requires careful consideration of async boundaries +- **Simplicity Benefits**: Removing abstraction layers often improves stability and performance +- **Process Isolation Trade-offs**: While effective for isolation, the performance cost is significant when not necessary + +This migration represents a successful architectural evolution from a complex, problematic system to a simpler, more reliable one. \ No newline at end of file diff --git a/.serena/memories/process_isolation_current_status.md b/.serena/memories/process_isolation_current_status.md new file mode 100644 index 0000000..67d9490 --- /dev/null +++ b/.serena/memories/process_isolation_current_status.md @@ -0,0 +1,126 @@ +# Process Isolation: Current Status and Architecture + +## Current Status: Disabled by Default + +Process isolation is **disabled by default** in the current Serena implementation: +- **Configuration**: `USE_PROCESS_ISOLATION = False` in `src/serena/constants.py:22` +- **Reason**: No longer needed with solid-lsp architecture +- **Default Operation**: Single process mode with MCP server, agent, and language servers in same process + +## Why Process Isolation Is No Longer Needed + +### Historical Context +Process isolation was **mandatory** when using multilspy because: +1. **Asyncio Contamination**: Multilspy leaked coroutines into MCP server's event loop +2. **Event Loop Conflicts**: Multiple asyncio contexts caused deadlocks +3. **Only Solution**: Complete process separation was the only way to prevent issues + +### Current Architecture Benefits +With solid-lsp, process isolation became **unnecessary** because: +1. **Clean Async Boundaries**: No coroutine leakage between MCP server and language server contexts +2. **Single Process Safety**: Solid-lsp designed to work safely within MCP server process +3. **Performance Gains**: Direct method calls instead of expensive IPC + +## Architecture Components (Still Present) + +The process isolation infrastructure remains **available but unused** by default: + +### Core Components (`src/serena/process_isolated_agent.py`) + +#### 1. **ProcessIsolatedSerenaAgent** (lines 392-550) +- **Purpose**: Wrapper that manages isolated agent process +- **Communication**: Uses `multiprocessing.Pipe()` for bidirectional communication +- **Process Management**: Creates and manages `SerenaAgentWorker` subprocess +- **Status**: **Available but not used by default** + +#### 2. **SerenaAgentWorker** (lines 173-389) +- **Purpose**: Worker process hosting actual SerenaAgent +- **Event Loop**: Polling loop checking for requests every 500ms +- **Request Handling**: INITIALIZE, TOOL_CALL, SHUTDOWN, etc. +- **Status**: **Available but not used by default** + +#### 3. **ProcessIsolatedDashboard** (lines 133-170) +- **Purpose**: Runs web dashboard in separate process +- **Integration**: Async web server with port management +- **Status**: **Available but not used by default** + +### Global Synchronization (lines 25-28) +```python +_global_log_queue: multiprocessing.Queue = multiprocessing.Queue() +_dashboard_ready_event = multiprocessing.Event() +_dashboard_port_value = multiprocessing.Value("i", 0) +global_shutdown_event = multiprocessing.Event() +``` +**Status**: **Available but not actively used** + +## Current MCP Factory Selection + +In `src/serena/mcp.py:590`: +```python +if not USE_PROCESS_ISOLATION: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) +else: + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) +``` + +### Default Flow +1. **`USE_PROCESS_ISOLATION = False`** → **`SerenaMCPFactorySingleProcess`** +2. **Single Process**: MCP server, SerenaAgent, and solid-lsp in same process +3. **Direct Communication**: No IPC overhead, direct method calls + +### Fallback Option +If `USE_PROCESS_ISOLATION = True`: +1. **`SerenaMCPFactoryWithProcessIsolation`** would be used +2. **Separate Processes**: MCP server and SerenaAgent in different processes +3. **IPC Communication**: Higher latency but complete isolation + +## Benefits of Current Single Process Architecture + +### 1. **Performance Improvements** +- **Lower Latency**: Direct method calls vs IPC communication +- **Reduced Memory**: No duplicate process memory footprints +- **Faster Startup**: No process creation and initialization overhead + +### 2. **Operational Simplicity** +- **Unified Logging**: All components log to same destination +- **Simpler Debugging**: Single process, unified stack traces +- **Resource Management**: Simpler cleanup and shutdown procedures + +### 3. **Stability Benefits** +- **No IPC Failures**: Eliminated inter-process communication failure modes +- **Consistent State**: No synchronization issues between processes +- **Reliable Shutdown**: No orphaned processes or cleanup complexity + +## When Process Isolation Might Still Be Useful + +While not needed by default, process isolation could still be beneficial for: + +### 1. **Fault Isolation** +- **Agent Crashes**: Prevent agent failures from affecting MCP server +- **Memory Protection**: Isolate memory leaks to specific processes +- **Recovery**: Restart failed components without full system restart + +### 2. **Resource Management** +- **Memory Limits**: Constrain memory usage of specific components +- **CPU Isolation**: Prevent CPU-intensive operations from blocking MCP server +- **Security**: Additional process boundaries for security-sensitive environments + +### 3. **Debugging and Development** +- **Component Isolation**: Debug specific components in isolation +- **Performance Analysis**: Measure resource usage per component +- **Development Safety**: Prevent development errors from affecting stable components + +## Configuration Management + +### Enabling Process Isolation +To re-enable process isolation: +1. **Set**: `USE_PROCESS_ISOLATION = True` in `src/serena/constants.py` +2. **Result**: Automatic fallback to `SerenaMCPFactoryWithProcessIsolation` +3. **Trade-off**: Higher resource usage but complete component isolation + +### Current Recommendation +- **Default**: Keep `USE_PROCESS_ISOLATION = False` for optimal performance +- **Special Cases**: Enable only when specific isolation requirements exist +- **Testing**: Both modes should be tested to ensure compatibility + +The current architecture successfully eliminated the need for process isolation while maintaining the capability as a fallback option for specialized use cases. \ No newline at end of file diff --git a/.serena/memories/project_structure.md b/.serena/memories/project_structure.md deleted file mode 100644 index c0e7712..0000000 --- a/.serena/memories/project_structure.md +++ /dev/null @@ -1,16 +0,0 @@ -# Project Structure - -The Serena codebase is organized as follows: - -- **src/serena/**: Main package with core functionality - - **llm/**: LLM integration - - **util/**: Utilities - - Key files: mcp.py, agno.py, agent.py -- **src/multilspy/**: Language server integration - - **language_servers/**: Implementations for different languages - - Python (pyright/jedi) - - Java (Eclipse JDTLS) - - TypeScript/JavaScript - - C#, Rust, Go, Ruby, C++ -- **test/**: Tests -- **scripts/**: Utility scripts \ No newline at end of file diff --git a/.serena/memories/serena_project_structure.md b/.serena/memories/serena_project_structure.md new file mode 100644 index 0000000..4475b0b --- /dev/null +++ b/.serena/memories/serena_project_structure.md @@ -0,0 +1,173 @@ +# Serena Project Structure Overview + +## Top-Level Organization + +Serena is organized into several key directories, each serving distinct architectural purposes: + +``` +serena/ +├── src/ # Main source code +│ ├── interprompt/ # Template and prompt management system +│ ├── serena/ # Core Serena functionality +│ └── solidlsp/ # Language server integration layer +├── tests/ # Test suites +├── docs/ # Documentation +├── prompts/ # Prompt templates and configurations +├── contexts/ # Context definitions (desktop-app, agent, ide-assistant) +├── modes/ # Mode definitions (planning, editing, interactive, one-shot) +└── pyproject.toml # Project configuration and dependencies +``` + +## Core Source Structure (`src/`) + +### 1. **Serena Core (`src/serena/`)** +The heart of the Serena system containing: + +#### **Agent and Core Logic** +- **`agent.py`**: Main `SerenaAgent` class, tool implementations, project management +- **`mcp.py`**: Model Context Protocol server implementation and MCP factories +- **`config.py`**: Configuration system (contexts, modes, registered configurations) +- **`constants.py`**: System constants including `USE_PROCESS_ISOLATION = False` + +#### **Specialized Components** +- **`dashboard.py`**: Web dashboard for monitoring and logging (`MemoryLogHandler`, `SerenaDashboardAPI`) +- **`symbol.py`**: Symbol management (`SymbolLocation`, `Symbol`, `SymbolManager`) +- **`text_utils.py`**: Text processing utilities for search and file operations +- **`gui_log_viewer.py`**: GUI log window implementation + +#### **Integration Layers** +- **`agno.py`**: Agno framework integration (`SerenaAgnoToolkit`, `SerenaAgnoAgentProvider`) +- **`process_isolated_agent.py`**: Process isolation infrastructure (available but unused by default) +- **`prompt_factory.py`**: Prompt generation and management + +#### **Utility Modules (`src/serena/util/`)** +- **`file_system.py`**: File scanning, gitignore parsing (`GitignoreParser`, `scan_directory`) +- **`git.py`**: Git operations and status checking +- **`shell.py`**: Shell command execution (`execute_shell_command`) +- **`thread.py`**: Threading utilities with timeout support +- **`inspection.py`**: Code inspection and language detection utilities + +### 2. **Solid-LSP (`src/solidlsp/`)** +Current language server integration layer: + +#### **Core Language Server Components** +- **`ls.py`**: Main `SolidLanguageServer` class (1,634 lines) +- **`ls_handler.py`**: `SolidLanguageServerHandler` for protocol management (512 lines) +- **`ls_request.py`**: Request handling (`LanguageServerRequest`) +- **`ls_types.py`**: LSP type definitions +- **`ls_utils.py`**: Utility classes (`TextUtils`, `PathUtils`, `FileUtils`, `SymbolUtils`) + +#### **Protocol Handling (`src/solidlsp/lsp_protocol_handler/`)** +- **`server.py`**: LSP protocol server implementation +- **`lsp_requests.py`**: LSP request and notification classes +- **`lsp_types.py`**: Complete LSP type system definitions +- **`lsp_constants.py`**: LSP protocol constants + +#### **Language Server Implementations (`src/solidlsp/language_servers/`)** +Each language has its own subdirectory with specific implementations: +- **`pyright_language_server/`**: Python support via Pyright +- **`eclipse_jdtls/`**: Java support via Eclipse JDTLS +- **`typescript_language_server/`**: TypeScript/JavaScript support +- **`omnisharp/`**: C# support via OmniSharp +- **`rust_analyzer/`**: Rust support via Rust-Analyzer +- **`gopls/`**: Go support via Gopls +- **`solargraph/`**: Ruby support via Solargraph +- **`clangd_language_server/`**: C++ support via Clangd +- **`dart_language_server/`**: Dart support +- **`intelephense/`**: PHP support via Intelephense +- **`kotlin_language_server/`**: Kotlin support + +### 3. **Interprompt (`src/interprompt/`)** +Template and prompt management system: + +- **`multilang_prompt.py`**: Multi-language prompt templates (`MultiLangPromptTemplate`) +- **`jinja_template.py`**: Jinja2 template integration (`JinjaTemplate`) +- **`prompt_factory.py`**: Prompt factory base classes +- **`util/class_decorators.py`**: Utility decorators like `@singleton` + +## Configuration and Templates + +### **Prompt Templates (`prompts/`)** +Organized by context and functionality: +- **Context-specific prompts**: Different prompt sets for different execution contexts +- **Tool-specific prompts**: Specialized prompts for specific tools and operations +- **Multi-language support**: Prompts available in multiple languages when applicable + +### **Contexts (`contexts/`)** +Execution environment definitions: +- **`desktop-app.yml`**: Desktop application context +- **`agent.yml`**: Agent-specific context +- **`ide-assistant.yml`**: IDE assistant context +- **Custom contexts**: Support for user-defined contexts + +### **Modes (`modes/`)** +Behavior mode definitions: +- **`planning.yml`**: Planning mode behavior +- **`editing.yml`**: Editing mode behavior +- **`interactive.yml`**: Interactive mode behavior +- **`one-shot.yml`**: One-shot execution mode + +## Key Architectural Patterns + +### 1. **Four-Layer Configuration Hierarchy** +1. **Global**: `serena_config.yml` +2. **CLI Arguments**: Runtime overrides +3. **Project**: `.serena/project.yml` +4. **Active Modes**: Runtime behavior modification + +### 2. **Tool System Architecture** +- **Base Classes**: `ToolInterface`, `Tool` in `agent.py` +- **Marker Interfaces**: `ToolMarkerCanEdit`, `ToolMarkerDoesNotRequireActiveProject` +- **Tool Registry**: `ToolRegistry` with automatic tool discovery via `_iter_tool_classes()` +- **Tool Categories**: Semantic tools, file operations, project management, meta-tools + +### 3. **Integration Patterns** +- **MCP Server**: Primary integration via `SerenaMCPFactory` classes +- **Agno Agent**: Model-agnostic integration via `SerenaAgnoToolkit` +- **Process Isolation**: Optional isolation via `ProcessIsolatedSerenaAgent` + +### 4. **Memory Management** +- **Project Memories**: `.serena/memories/` directory for project-specific information +- **Memory Tools**: `WriteMemoryTool`, `ReadMemoryTool`, `ListMemoriesTool` +- **Memory Managers**: `MemoriesManager`, `MemoriesManagerMDFilesInProject` + +## Dependencies and Build System + +### **Development Tools** +- **`uv`**: Dependency management and virtual environment +- **`poe`**: Task orchestration (defined in `pyproject.toml`) +- **Essential Commands**: `uv run poe lint`, `uv run poe format`, `uv run poe type-check`, `uv run poe test` + +### **Key Dependencies** +- **Language Server Protocol**: LSP implementations for each supported language +- **MCP (Model Context Protocol)**: For integration with Claude Desktop and other MCP clients +- **Agno Framework**: For model-agnostic agent implementations +- **AsyncIO**: For concurrent operations (carefully managed to avoid contamination) + +## Project State Management + +### **Project Configuration** +- **Global Config**: `serena_config.yml` in user home directory +- **Project Config**: `.serena/project.yml` in project root +- **Auto-generation**: Automatic creation of default configurations when missing + +### **Language Detection** +- **Automatic**: Based on file composition analysis +- **Manual Override**: Via project configuration +- **Multi-language**: Support for projects with multiple languages + +## Runtime Architecture + +### **Default Operation Mode** +- **Single Process**: MCP server, agent, and language servers in same process +- **Direct Communication**: No IPC overhead +- **Solid-LSP**: Only language server implementation +- **Process Isolation**: Disabled by default (`USE_PROCESS_ISOLATION = False`) + +### **Semantic Tool Integration** +- **Symbol-based Operations**: `find_symbol`, `replace_symbol_body`, `insert_after_symbol` +- **Regex-based Operations**: `replace_regex` for fine-grained edits +- **File Operations**: `read_file`, `create_text_file`, `list_dir` +- **Project Operations**: `search_for_pattern`, `get_symbols_overview` + +This structure reflects Serena's evolution from a complex, multi-process system to a streamlined, single-process architecture that maintains all semantic capabilities while improving performance and reliability. \ No newline at end of file diff --git a/.serena/memories/solid-lsp-architecture.md b/.serena/memories/solid-lsp-architecture.md deleted file mode 100644 index 4a334cf..0000000 --- a/.serena/memories/solid-lsp-architecture.md +++ /dev/null @@ -1,161 +0,0 @@ -# Solid-LSP: Deadlock-Free Language Server Architecture - -## Overview - -Solid-LSP (`src/solidlsp/`) is a **simplified, deadlock-resistant** reimplementation of the language server communication layer, designed to replace the problematic `multilspy` architecture that suffered from asyncio contamination issues when used with MCP servers. - -## The Problem Solid-LSP Solves - -### Root Cause: Asyncio Context Contamination -The original `multilspy` implementation created **asyncio deadlocks** in MCP environments due to: - -1. **Dual Event Loop Interference**: MCP server runs its own asyncio event loop, while SerenaAgent creates separate asyncio loops for language servers and dashboard -2. **Coroutine Leakage**: When MCP server calls SerenaAgent tools, coroutines created by `multilspy.LanguageServer.request_full_symbol_tree()` were leaked into the MCP server's asyncio context but never awaited -3. **Resource Exhaustion**: Accumulated unawaited coroutines in the MCP server's event loop eventually caused blocking and apparent "hangs" -4. **MCP-Specific Issue**: Only occurred when using MCP clients, not in direct script execution - -### Evidence from Logs -``` -INFO mcp.server.lowlevel.server:_handle_message:524 - Warning: RuntimeWarning: coroutine 'LanguageServer.request_full_symbol_tree' was never awaited -``` - -## Solid-LSP Architecture - -### Core Design Principles - -1. **Simplified Protocol Handling**: Eliminates the complex `lsp_protocol_handler` layer from `multilspy` -2. **Direct Async Management**: More explicit control over async operations without complex threading abstractions -3. **Single Process Operation**: Designed to work directly in the MCP server process without requiring process isolation -4. **Clean Asyncio Boundaries**: Prevents coroutine leakage between async contexts - -### Key Architectural Differences from multilspy - -#### 1. Simplified Handler Architecture -- **multilspy**: Complex `LanguageServerHandler` with extensive async task management and event loops -- **solid-lsp**: Streamlined `SolidLanguageServerHandler` with direct process management - -#### 2. Protocol Simplification -- **No separate protocol handler package**: Direct LSP communication without abstraction layers -- **Reduced complexity**: Fewer components and dependencies -- **Same language server support**: Identical `language_servers/` implementations for all supported languages - -#### 3. Enhanced Process Control -**solid-lsp** provides additional methods for finer process control: -- `_start_server_process()`: Direct process control -- `_start_server()`: Server initialization -- `_server_context` attribute: Enhanced server state management - -## Process Isolation: No Longer Required - -### Key Benefit: Single Process Operation - -With solid-lsp, **process isolation is no longer needed** and is **disabled by default**: - -```python -# src/serena/mcp.py:581-585 -if USE_SOLID_LSP: - mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) -else: - # using multilspy requires process isolation to prevent asyncio contamination - mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) -``` - -### Benefits of Single Process Operation -1. **Lower Resource Usage**: No separate processes for SerenaAgent and language servers -2. **Reduced Latency**: Direct method calls instead of inter-process communication -3. **Simplified Architecture**: No IPC overhead or process management complexity -4. **Better Performance**: Eliminated serialization/deserialization overhead -5. **Easier Debugging**: All components in same process, simplified stack traces - -### Why multilspy Required Process Isolation -- **Asyncio Contamination**: multilspy's async patterns leaked coroutines into MCP server context -- **Mandatory Isolation**: Process boundaries were the only way to prevent deadlocks -- **Performance Penalty**: IPC overhead was necessary for stability - -### How solid-lsp Enables Single Process -- **Clean Async Patterns**: No coroutine leakage between MCP server and language server contexts -- **Direct Integration**: Can safely run in same process as MCP server -- **Stable Operation**: No more timeout issues or hanging operations - -## Supported Language Servers - -Solid-LSP maintains compatibility with all language servers from `multilspy`: - -### Primary Languages -- **Python**: Pyright Language Server (`src/solidlsp/language_servers/pyright_language_server/`) -- **Java**: Eclipse JDTLS (`src/solidlsp/language_servers/eclipse_jdtls/`) -- **TypeScript/JavaScript**: TypeScript Language Server (`src/solidlsp/language_servers/typescript_language_server/`) - -### Additional Languages -- **C#**: OmniSharp (`src/solidlsp/language_servers/omnisharp/`) -- **Rust**: Rust-Analyzer (`src/solidlsp/language_servers/rust_analyzer/`) -- **Go**: Gopls (`src/solidlsp/language_servers/gopls/`) -- **Ruby**: Solargraph (`src/solidlsp/language_servers/solargraph/`) -- **C++**: Clangd (`src/solidlsp/language_servers/clangd_language_server/`) -- **Dart**: Dart Language Server (`src/solidlsp/language_servers/dart_language_server/`) -- **PHP**: Intelephense (`src/solidlsp/language_servers/intelephense/`) -- **Kotlin**: Kotlin Language Server (`src/solidlsp/language_servers/kotlin_language_server/`) - -## Implementation Details - -### Core Classes - -#### `SolidLanguageServer` (`src/solidlsp/ls.py`) -- **1,634 lines**: Main language server interface -- **Enhanced methods**: Same semantic capabilities as `multilspy.LanguageServer` but with improved async handling -- **Lifecycle control**: `start()`, `stop()`, `is_running()`, `language_server()` property (similar to multilspy's `SyncLanguageServer`) - -#### `SolidLanguageServerHandler` (`src/solidlsp/ls_handler.py`) -- **512 lines**: Simplified LSP protocol handler -- **Direct process management**: More straightforward than `multilspy`'s complex async task orchestration -- **Improved cleanup**: Better resource management and process termination - -#### `SolidLspRequest` (`src/solidlsp/lsp_request.py`) -- **Simplified request handling**: Direct LSP request/response without complex protocol abstractions - -### Configuration Integration - -Solid-LSP is controlled by the `USE_SOLID_LSP` constant in `src/serena/constants.py`: - -```python -USE_SOLID_LSP = True # Default: enabled -``` - -This allows: -- **Default Operation**: solid-lsp enabled by default for stability -- **Fallback Capability**: Can switch back to multilspy + process isolation if needed -- **A/B Testing**: Easy comparison between implementations - -## Deadlock Prevention - -### How Solid-LSP Prevents Deadlocks - -1. **Simplified Async Patterns**: Reduces complexity that led to coroutine leakage -2. **Clean Boundaries**: Proper async context management prevents MCP server contamination -3. **Direct Operations**: Eliminates abstraction layers where async context could be corrupted -4. **Single Process Safety**: Designed to work safely within MCP server process - -### Verification - -The solid-lsp approach provides: -- **No more timeout issues**: Clean async boundaries prevent MCP server contamination -- **Stable MCP operation**: Language server calls no longer hang after first timeout -- **Resource efficiency**: Lower memory usage without separate processes -- **Reliable semantic tools**: `find_symbol` and other tools work consistently -- **Better Performance**: Direct method calls instead of IPC - -## Migration Strategy - -### Current Status -- **Default**: Solid-LSP is the default implementation (`USE_SOLID_LSP = True`) -- **Stable**: Resolves known deadlock issues in MCP environments -- **Compatible**: Supports all existing language servers and semantic operations -- **Performant**: Single process operation with lower overhead - -### Rollback Path -If issues arise, can fallback to multilspy with process isolation: -1. **Disable solid-lsp**: Set `USE_SOLID_LSP = False` -2. **Automatic fallback**: System automatically uses multilspy + process isolation -3. **Performance trade-off**: Higher resource usage but guaranteed isolation - -Solid-LSP represents an **architectural evolution** that eliminates the need for process isolation while maintaining all semantic capabilities and providing **better performance and stability** for MCP server deployments. \ No newline at end of file diff --git a/.serena/memories/supported_language_servers.md b/.serena/memories/supported_language_servers.md new file mode 100644 index 0000000..e145528 --- /dev/null +++ b/.serena/memories/supported_language_servers.md @@ -0,0 +1,97 @@ +# Supported Language Servers in Solid-LSP + +Serena currently supports multiple programming languages through language servers via the solid-lsp architecture located in `src/solidlsp/language_servers/`. + +## Primary Languages (Directly Supported) + +### Python +- **Language Server**: Pyright Language Server +- **Location**: `src/solidlsp/language_servers/pyright_language_server/` +- **Features**: Full semantic analysis, type checking, symbol navigation +- **Previously**: Also supported Jedi (now deprecated in favor of Pyright) + +### Java +- **Language Server**: Eclipse JDTLS (Java Development Tools Language Server) +- **Location**: `src/solidlsp/language_servers/eclipse_jdtls/` +- **Features**: Full Java language support, Maven/Gradle integration +- **Note**: Startup can be slow, especially on initial launch + +### TypeScript/JavaScript +- **Language Server**: TypeScript Language Server +- **Location**: `src/solidlsp/language_servers/typescript_language_server/` +- **Features**: TypeScript and JavaScript support, type checking, IntelliSense + +## Additional Supported Languages + +### C# +- **Language Server**: OmniSharp +- **Location**: `src/solidlsp/language_servers/omnisharp/` +- **Features**: Full C# language support, .NET integration + +### Rust +- **Language Server**: Rust-Analyzer +- **Location**: `src/solidlsp/language_servers/rust_analyzer/` +- **Features**: Rust language support, cargo integration + +### Go +- **Language Server**: Gopls +- **Location**: `src/solidlsp/language_servers/gopls/` +- **Features**: Go language support, module system integration + +### Ruby +- **Language Server**: Solargraph +- **Location**: `src/solidlsp/language_servers/solargraph/` +- **Features**: Ruby language support, gem integration + +### C++ +- **Language Server**: Clangd +- **Location**: `src/solidlsp/language_servers/clangd_language_server/` +- **Features**: C++ language support, CMake integration + +### Dart +- **Language Server**: Dart Language Server +- **Location**: `src/solidlsp/language_servers/dart_language_server/` +- **Features**: Dart and Flutter support + +### PHP +- **Language Server**: Intelephense +- **Location**: `src/solidlsp/language_servers/intelephense/` +- **Features**: PHP language support, Composer integration + +### Kotlin +- **Language Server**: Kotlin Language Server +- **Location**: `src/solidlsp/language_servers/kotlin_language_server/` +- **Features**: Kotlin language support, Gradle integration + +## Language Server Protocol (LSP) Integration + +All language servers utilize the Language Server Protocol (LSP) to provide: +- **Semantic Analysis**: Symbol definitions, references, and relationships +- **Code Navigation**: Go-to-definition, find-references, symbol search +- **Error Detection**: Syntax and semantic error reporting +- **Code Completion**: IntelliSense-style code completion +- **Documentation**: Hover information and signature help + +## Architecture Integration + +### Solid-LSP Framework +- **Unified Interface**: All language servers use the same `SolidLanguageServer` interface +- **Configuration**: `LanguageServerConfig` handles language-specific settings +- **Lifecycle Management**: Start, stop, and restart capabilities for all servers +- **Process Management**: Direct process control for each language server + +### Project Integration +- **Automatic Detection**: Language selection based on project file composition +- **Configuration**: Project-specific language server settings via `.serena/project.yml` +- **Ignored Paths**: Respects gitignore and custom ignore patterns +- **Timeout Settings**: Configurable per-language server timeouts + +## Adding New Language Servers + +The architecture supports adding new language servers by: +1. **Creating Language Server Class**: Implement language-specific server in `src/solidlsp/language_servers/` +2. **Configuration**: Add language detection and configuration rules +3. **Integration**: Register with the solid-lsp framework +4. **Testing**: Ensure semantic tools work correctly with the new language server + +The system is designed to be extensible while maintaining consistent behavior across all supported languages. \ No newline at end of file From 540b6786767dc5dcc78256eb73ae4e32b047048d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 23 Jun 2025 21:58:25 +0200 Subject: [PATCH 160/174] project template, removed javascript [ci skip] --- src/serena/resources/project.template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index 2e1e1c3..868e2e3 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -1,4 +1,4 @@ -# language of the project (csharp, python, rust, java, typescript, javascript, go, cpp, or ruby) +# language of the project (csharp, python, rust, java, typescript (also for js projects), go, cpp, or ruby) # Special requirements: # * csharp: Requires the presence of a .sln file in the project folder. language: python From d3b1f9bb90b4d2de98413940fd1888733554cd4a Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 22:08:14 +0200 Subject: [PATCH 161/174] Explain language choice in more detail --- src/serena/resources/project.template.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index 868e2e3..edef951 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -1,4 +1,6 @@ -# language of the project (csharp, python, rust, java, typescript (also for js projects), go, cpp, or ruby) +# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) +# * For C, use cpp +# * For JavaScript, use typescript # Special requirements: # * csharp: Requires the presence of a .sln file in the project folder. language: python From 404f026bbd799c166eb7e7803bf72f2c1acb7ef9 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 23 Jun 2025 22:48:22 +0200 Subject: [PATCH 162/174] Use a synchronized task queue for agent tasks (thread pool with 1 worker), which we also use to start the language server in the background --- src/serena/agent.py | 126 +++++++++++++++++++++++++------------------- src/serena/mcp.py | 2 + 2 files changed, 73 insertions(+), 55 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 114d3b0..ebb2266 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -14,6 +14,7 @@ import webbrowser from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Callable, Generator, Iterable, Sequence +from concurrent.futures import Future, ThreadPoolExecutor from copy import copy, deepcopy from dataclasses import asdict, dataclass, field from fnmatch import fnmatch @@ -30,7 +31,7 @@ from overrides import override from pathspec import PathSpec from ruamel.yaml.comments import CommentedMap from sensai.util import logging -from sensai.util.logging import LOG_DEFAULT_FORMAT, FallbackHandler +from sensai.util.logging import LOG_DEFAULT_FORMAT, FallbackHandler, LogTime from sensai.util.string import ToStringMixin, dict_string from serena import serena_version @@ -50,7 +51,6 @@ from serena.util.file_system import GitignoreParser, match_path, scan_directory from serena.util.general import load_yaml, save_yaml from serena.util.inspection import determine_programming_language_composition, iter_subclasses from serena.util.shell import execute_shell_command -from serena.util.thread import ExecutionResult, execute_with_timeout from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger @@ -787,6 +787,10 @@ class SerenaAgent: log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})") log.info("Available projects: {}".format(", ".join(self.serena_config.project_names))) + # create executor for starting the language server and running tools in another thread + # This executor is used to achieve linear task execution, so it is important to use a single-threaded executor. + self._task_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="SerenaAgentExecutor") + # Initialize the prompt factory self.prompt_factory = SerenaPromptFactory() self._project_activation_callback = project_activation_callback @@ -949,21 +953,34 @@ class SerenaAgent: log.info(f"Active tools after all exclusions ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}") + def issue_task(self, task: Callable[[], Any]) -> Future: + """ + Issue a task to the executor for asynchronous execution. + :param task: the task to execute + :return: a Future object representing the execution of the task + """ + return self._task_executor.submit(task) + def _activate_project(self, project: Project) -> None: log.info(f"Activating {project.project_name} at {project.project_root}") self._active_project = project self._update_active_tools() - # start the language server - self.reset_language_server() - assert self.language_server is not None - self.ignore_spec = self.language_server.get_ignore_spec() + def init_language_server() -> None: + # start the language server + with LogTime("Language server initialization", logger=log): + self.reset_language_server() + assert self.language_server is not None + self.ignore_spec = self.language_server.get_ignore_spec() - # initialize project-specific instances - log.debug(f"Initializing symbol and memories manager for {project.project_name} at {project.project_root}") - self.symbol_manager = SymbolManager(self.language_server, self) - self.memories_manager = MemoriesManagerMDFilesInProject(project.project_root) - self.lines_read = LinesRead() + # initialize project-specific instances (some of which depend on the language server) + log.debug(f"Initializing symbol and memories manager for {project.project_name} at {project.project_root}") + self.symbol_manager = SymbolManager(self.language_server, self) + self.memories_manager = MemoriesManagerMDFilesInProject(project.project_root) + self.lines_read = LinesRead() + + # initialize the language server in the background + self.issue_task(init_language_server) if self._project_activation_callback is not None: self._project_activation_callback() @@ -1323,57 +1340,56 @@ class Tool(Component, ToolInterface): """ Applies the tool with the given arguments """ - apply_fn = self.get_apply_fn() - try: - if not self.is_active(): - return f"Error: Tool '{self.get_name_from_cls()}' is not active. Active tools: {self.agent.get_active_tool_names()}" - except Exception as e: - return f"RuntimeError while checking if tool {self.get_name_from_cls()} is active: {e}" + def task() -> str: + apply_fn = self.get_apply_fn() - if log_call: - self._log_tool_application(inspect.currentframe()) - try: - # check whether the tool requires an active project and language server - if not isinstance(self, ToolMarkerDoesNotRequireActiveProject): - if self.agent._active_project is None: - return ( - "Error: No active project. Ask to user to select a project from this list: " - + f"{self.agent.serena_config.project_names}" - ) - if not self.agent.is_language_server_running(): - log.info("Language server is not running. Starting it ...") - self.agent.reset_language_server() + try: + if not self.is_active(): + return f"Error: Tool '{self.get_name_from_cls()}' is not active. Active tools: {self.agent.get_active_tool_names()}" + except Exception as e: + return f"RuntimeError while checking if tool {self.get_name_from_cls()} is active: {e}" - # apply the actual tool with a timeout - execution_fn = lambda: apply_fn(**kwargs) - execution_result = execute_with_timeout(execution_fn, self.agent.serena_config.tool_timeout, self.get_name_from_cls()) - if execution_result.status == ExecutionResult.Status.SUCCESS: - result = cast(str, execution_result.result_value) - else: - assert execution_result.exception is not None - raise execution_result.exception + if log_call: + self._log_tool_application(inspect.currentframe()) + try: + # check whether the tool requires an active project and language server + if not isinstance(self, ToolMarkerDoesNotRequireActiveProject): + if self.agent._active_project is None: + return ( + "Error: No active project. Ask to user to select a project from this list: " + + f"{self.agent.serena_config.project_names}" + ) + if not self.agent.is_language_server_running(): + log.info("Language server is not running. Starting it ...") + self.agent.reset_language_server() - except Exception as e: - if not catch_exceptions: - raise - msg = f"Error executing tool: {e}\n{traceback.format_exc()}" - log.error( - f"Error executing tool: {e}. " - f"Consider restarting the language server to solve this (especially, if it's a timeout of a symbolic operation)", - exc_info=e, - ) - result = msg + # apply the actual tool + result = apply_fn(**kwargs) - if log_call: - log.info(f"Result: {result}") + except Exception as e: + if not catch_exceptions: + raise + msg = f"Error executing tool: {e}\n{traceback.format_exc()}" + log.error( + f"Error executing tool: {e}. " + f"Consider restarting the language server to solve this (especially, if it's a timeout of a symbolic operation)", + exc_info=e, + ) + result = msg - try: - self.language_server.save_cache() - except Exception as e: - log.error(f"Error saving language server cache: {e}") + if log_call: + log.info(f"Result: {result}") - return result + try: + self.language_server.save_cache() + except Exception as e: + log.error(f"Error saving language server cache: {e}") + + return result + + future = self.agent.issue_task(task) + return future.result(timeout=self.agent.serena_config.tool_timeout) class RestartLanguageServerTool(Tool): diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 8666ef0..db74203 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -220,6 +220,7 @@ class SerenaMCPFactorySingleProcess(SerenaMCPFactory): @asynccontextmanager async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: self._set_mcp_tools(mcp_server) + log.info("MCP server lifetime setup complete") yield @@ -427,6 +428,7 @@ class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): # Start monitoring task monitor_task = asyncio.create_task(monitor_global_shutdown()) + log.info("MCP server lifetime setup complete") try: yield except (KeyboardInterrupt, SystemExit): From e3d021a4328a0581338ebc99e579dfb324704a76 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 24 Jun 2025 12:50:01 +0200 Subject: [PATCH 163/174] Improve logging to show thread name --- src/serena/agent.py | 8 +++----- src/serena/constants.py | 2 ++ src/serena/dashboard.py | 4 ++-- src/solidlsp/ls_handler.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index ebb2266..407b18c 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -31,7 +31,7 @@ from overrides import override from pathspec import PathSpec from ruamel.yaml.comments import CommentedMap from sensai.util import logging -from sensai.util.logging import LOG_DEFAULT_FORMAT, FallbackHandler, LogTime +from sensai.util.logging import FallbackHandler, LogTime from sensai.util.string import ToStringMixin, dict_string from serena import serena_version @@ -41,6 +41,7 @@ from serena.constants import ( PROJECT_TEMPLATE_FILE, REPO_ROOT, SELENA_CONFIG_TEMPLATE_FILE, + SERENA_LOG_FORMAT, SERENA_MANAGED_DIR_NAME, ) from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI @@ -60,7 +61,6 @@ if TYPE_CHECKING: from serena.gui_log_viewer import GuiLogViewerHandler log = logging.getLogger(__name__) -LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" TTool = TypeVar("TTool", bound="Tool") SUCCESS_RESULT = "OK" DEFAULT_TOOL_TIMEOUT: float = 240 @@ -751,8 +751,6 @@ class SerenaAgent: if Logger.root.level > serena_log_level: log.info(f"Changing the root logger level to {serena_log_level}") Logger.root.setLevel(serena_log_level) - # Override existing configuration (needed to configure multilspy logger as desired) - logging.basicConfig(format=LOG_DEFAULT_FORMAT, level=serena_log_level, force=True) # open GUI log window if enabled self._gui_log_handler: Union["GuiLogViewerHandler", None] = None # noqa @@ -765,7 +763,7 @@ class SerenaAgent: from serena.gui_log_viewer import GuiLogViewer, GuiLogViewerHandler self._gui_log_handler = GuiLogViewerHandler( - GuiLogViewer("dashboard", title="Serena Logs"), level=serena_log_level, format_string=LOG_FORMAT + GuiLogViewer("dashboard", title="Serena Logs"), level=serena_log_level, format_string=SERENA_LOG_FORMAT ) Logger.root.addHandler(self._gui_log_handler) diff --git a/src/serena/constants.py b/src/serena/constants.py index 30a9518..368a743 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -20,3 +20,5 @@ PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.y SELENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml") USE_PROCESS_ISOLATION = False + +SERENA_LOG_FORMAT = "%(levelname)-5s %(asctime)-15s [%(threadName)s] %(name)s:%(funcName)s:%(lineno)d - %(message)s" diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index eb86e74..4cd98b6 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -9,7 +9,7 @@ from flask import Flask, Response, request, send_from_directory from pydantic import BaseModel from sensai.util import logging -from serena.constants import SERENA_DASHBOARD_DIR +from serena.constants import SERENA_DASHBOARD_DIR, SERENA_LOG_FORMAT log = logging.getLogger(__name__) @@ -20,7 +20,7 @@ logging.getLogger("werkzeug").setLevel(logging.WARNING) class MemoryLogHandler(logging.Handler): def __init__(self, level: int = logging.NOTSET) -> None: super().__init__(level=level) - self.setFormatter(logging.Formatter(logging.LOG_DEFAULT_FORMAT)) + self.setFormatter(logging.Formatter(SERENA_LOG_FORMAT)) self._log_buffer = LogBuffer() self._log_queue: queue.Queue[str] = queue.Queue() self._stop_event = threading.Event() diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index f367a1f..65a5931 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -170,12 +170,12 @@ class SolidLanguageServerHandler: # start threads to read stdout and stderr of the process threading.Thread( target=self.run_forever, - name="LSP stdout reader", + name="LSP-stdout-reader", daemon=True, ).start() threading.Thread( target=self.run_forever_stderr, - name="LSP stderr reader", + name="LSP-stderr-reader", daemon=True, ).start() From 6c9746ec76fff6a4b51320a0ff988c7bdf873cd8 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 24 Jun 2025 13:02:22 +0200 Subject: [PATCH 164/174] Log task scheduling and execution explicitly, adding task identifiers Lock task scheduling (just in case we call it from multiple threads in the future) --- src/serena/agent.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 407b18c..43538ba 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -9,6 +9,7 @@ import platform import re import shutil import sys +import threading import traceback import webbrowser from abc import ABC, abstractmethod @@ -788,6 +789,8 @@ class SerenaAgent: # create executor for starting the language server and running tools in another thread # This executor is used to achieve linear task execution, so it is important to use a single-threaded executor. self._task_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="SerenaAgentExecutor") + self._task_executor_lock = threading.Lock() + self._task_executor_task_index = 1 # Initialize the prompt factory self.prompt_factory = SerenaPromptFactory() @@ -957,7 +960,16 @@ class SerenaAgent: :param task: the task to execute :return: a Future object representing the execution of the task """ - return self._task_executor.submit(task) + with self._task_executor_lock: + task_name = f"Task-{self._task_executor_task_index}" + self._task_executor_task_index += 1 + + def task_execution_wrapper() -> Any: + with LogTime(task_name, logger=log): + return task() + + log.info(f"Scheduling {task_name} ({task.__name__})") + return self._task_executor.submit(task_execution_wrapper) def _activate_project(self, project: Project) -> None: log.info(f"Activating {project.project_name} at {project.project_root}") From 76558edd44dee2d3e040c5d53349a2fb6ae58a97 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 24 Jun 2025 13:10:45 +0200 Subject: [PATCH 165/174] Initialize project-specific instances that don't require the LS (such as the memory manager) immediately --- src/serena/agent.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 43538ba..af5b0b1 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -976,6 +976,13 @@ class SerenaAgent: self._active_project = project self._update_active_tools() + # initialize project-specific instances which do not depend on the language server + self.memories_manager = MemoriesManagerMDFilesInProject(project.project_root) + self.lines_read = LinesRead() + + # reset project-specific instances that depend on the language server + self.symbol_manager = None + def init_language_server() -> None: # start the language server with LogTime("Language server initialization", logger=log): @@ -983,11 +990,9 @@ class SerenaAgent: assert self.language_server is not None self.ignore_spec = self.language_server.get_ignore_spec() - # initialize project-specific instances (some of which depend on the language server) + # initialize project-specific instances which depend on the language server log.debug(f"Initializing symbol and memories manager for {project.project_name} at {project.project_root}") self.symbol_manager = SymbolManager(self.language_server, self) - self.memories_manager = MemoriesManagerMDFilesInProject(project.project_root) - self.lines_read = LinesRead() # initialize the language server in the background self.issue_task(init_language_server) From e1d664b25d7bfb181a4ad52858cc31413ec98760 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 24 Jun 2025 13:23:26 +0200 Subject: [PATCH 166/174] Assign meaningful names to tasks --- src/serena/agent.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index af5b0b1..9b7192d 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -954,21 +954,22 @@ class SerenaAgent: log.info(f"Active tools after all exclusions ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}") - def issue_task(self, task: Callable[[], Any]) -> Future: + def issue_task(self, task: Callable[[], Any], name: str | None = None) -> Future: """ Issue a task to the executor for asynchronous execution. :param task: the task to execute + :param name: the name of the task for logging purposes; if None, use the task function's name :return: a Future object representing the execution of the task """ with self._task_executor_lock: - task_name = f"Task-{self._task_executor_task_index}" + task_name = f"Task-{self._task_executor_task_index}[{name or task.__name__}]" self._task_executor_task_index += 1 def task_execution_wrapper() -> Any: with LogTime(task_name, logger=log): return task() - log.info(f"Scheduling {task_name} ({task.__name__})") + log.info(f"Scheduling {task_name}") return self._task_executor.submit(task_execution_wrapper) def _activate_project(self, project: Project) -> None: @@ -1403,7 +1404,7 @@ class Tool(Component, ToolInterface): return result - future = self.agent.issue_task(task) + future = self.agent.issue_task(task, name=self.__class__.__name__) return future.result(timeout=self.agent.serena_config.tool_timeout) From 2b68790bce8386f5c540f5f22361802d6c8d9652 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 24 Jun 2025 13:33:15 +0200 Subject: [PATCH 167/174] Add method execute_task for synchronous execution and apply it in demo script --- scripts/demo_run_tools.py | 4 ++-- src/serena/agent.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index 23726f1..8822db7 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -20,10 +20,10 @@ class InMemorySerenaConfig(SerenaConfigBase): if __name__ == "__main__": - # project_path = str(Path("test") / "resources" / "repos" / "python" / "test_repo") agent = SerenaAgent(project=REPO_ROOT) # apply a tool find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) print("Finding the symbol 'SyncLanguageServer'\n") - pprint(json.loads(find_refs_tool.apply(name_path="SyncLanguageServer", relative_path="src/multilspy/language_server.py"))) + result = agent.execute_task(lambda: find_refs_tool.apply(name_path="SolidLanguageServer", relative_path="src/solidlsp/ls.py")) + pprint(json.loads(result)) diff --git a/src/serena/agent.py b/src/serena/agent.py index 9b7192d..7da88e1 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -63,6 +63,7 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) TTool = TypeVar("TTool", bound="Tool") +T = TypeVar("T") SUCCESS_RESULT = "OK" DEFAULT_TOOL_TIMEOUT: float = 240 @@ -957,6 +958,8 @@ class SerenaAgent: def issue_task(self, task: Callable[[], Any], name: str | None = None) -> Future: """ Issue a task to the executor for asynchronous execution. + It is ensured that tasks are executed in the order they are issued, one after another. + :param task: the task to execute :param name: the name of the task for logging purposes; if None, use the task function's name :return: a Future object representing the execution of the task @@ -972,6 +975,17 @@ class SerenaAgent: log.info(f"Scheduling {task_name}") return self._task_executor.submit(task_execution_wrapper) + def execute_task(self, task: Callable[[], T]) -> T: + """ + Executes the given task synchronously via the agent's task executor. + This is useful for tasks that need to be executed immediately and whose results are needed right away. + + :param task: the task to execute + :return: the result of the task execution + """ + future = self.issue_task(task) + return future.result() + def _activate_project(self, project: Project) -> None: log.info(f"Activating {project.project_name} at {project.project_root}") self._active_project = project From 1a055cf2bb2502b0bf65b09c79d2a47f2ff33b75 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 24 Jun 2025 13:48:34 +0200 Subject: [PATCH 168/174] Do not log error determining Serena git commit id (not possible when using uvx) --- src/serena/util/git.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/serena/util/git.py b/src/serena/util/git.py index b46d17e..865ae59 100644 --- a/src/serena/util/git.py +++ b/src/serena/util/git.py @@ -16,6 +16,5 @@ def get_git_status() -> GitStatus | None: return GitStatus( commit=commit_hash, has_unstaged_changes=unstaged, has_staged_uncommitted_changes=staged, has_untracked_files=untracked ) - except Exception as e: - log.error("Error determining Git status", exc_info=e) + except: return None From edc5112fea707c5db65945f2a009eb206b2270b3 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 24 Jun 2025 15:05:42 +0200 Subject: [PATCH 169/174] Skip not found files in request_parsed_files with a warning Instead of raising an error --- src/solidlsp/ls.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 420f315..d7adfd8 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1172,8 +1172,14 @@ class SolidLanguageServer(ABC): dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d))] for file in files: rel_file_path = os.path.relpath(os.path.join(root, file), start=self.repository_root_path) - if not self.is_ignored_path(rel_file_path): - rel_file_paths.append(rel_file_path) + try: + if not self.is_ignored_path(rel_file_path): + rel_file_paths.append(rel_file_path) + except FileNotFoundError: + self.logger.log( + f"File {rel_file_path} not found (possibly due it being a symlink), skipping it in request_parsed_files", + logging.WARNING, + ) return rel_file_paths def search_files_for_pattern( From b1eba4563e7f892c96e2b8e97ca871984b4d2e08 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 24 Jun 2025 16:14:00 +0200 Subject: [PATCH 170/174] LS.request_parsed_file: Enable followlinks in os.walk --- src/solidlsp/ls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index d7adfd8..162b52a 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1168,7 +1168,7 @@ class SolidLanguageServer(ABC): ) raise LanguageServerException("Language Server not started") rel_file_paths = [] - for root, dirs, files in os.walk(self.repository_root_path): + for root, dirs, files in os.walk(self.repository_root_path, followlinks=True): dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d))] for file in files: rel_file_path = os.path.relpath(os.path.join(root, file), start=self.repository_root_path) From 4fd281b7bc1219c2c36ec2332e1602fd8c4ba061 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 24 Jun 2025 22:28:11 +0200 Subject: [PATCH 171/174] Readme: removed mentions of goose --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b3907da..225931f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,6 @@ Serena can be integrated with an LLM in several ways: * Claude Code and Claude Desktop, * IDEs like VSCode, Cursor or IntelliJ, * Extensions like Cline or Roo Code - * Goose (for a nice CLI experience) * and many others, including [the ChatGPT app soon](https://x.com/OpenAIDevs/status/1904957755829481737) * by using **Agno – the model-agnostic agent framework**. Serena's Agno-based agent allows you to turn virtually any LLM into a coding agent, whether it's provided by Google, OpenAI or Anthropic (with a paid API key) @@ -173,7 +172,7 @@ We thus built Serena with the prospect of being able to cancel most other subscr Serena can be used in various ways, below you will find instructions for selected integrations. - If you just want to turn Claude into a free-to-use coding agent, we recommend using Serena through [Claude Code](#claude-code) or [Claude Desktop](#claude-desktop). -- If you want to use Gemini or any other model, and you want a GUI experience, you can use [Agno](#agno-agent). On macOS, you can also use the GUI of [goose](#goose). +- If you want to use Gemini or any other model, and you want a GUI experience, you can use [Agno](#agno-agent) or one of the many other GUIs that support MCP servers. - If you want to use Serena integrated in your IDE, see the section on [other MCP clients](#other-mcp-clients---cline-roo-code-cursor-windsurf-etc). Serena is managed by `uv`, so you will need to [install it](https://docs.astral.sh/uv/getting-started/installation/)). @@ -429,8 +428,7 @@ e.g., for one of the following reasons: Agno is a model-agnostic agent framework that allows you to turn Serena into an agent (independent of the MCP technology) with a large number of underlying LLMs. Agno is currently -the simplest way of running Serena in a chat GUI with an LLM of your choice -(unless you are using a Mac, then you might prefer goose, which requires almost no setup). +the simplest way of running Serena in a chat GUI with an LLM of your choice. While Agno is not yet entirely stable, we chose it, because it comes with its own open-source UI, making it easy to directly use the agent using a chat interface. With Agno, Serena is turned into an agent @@ -714,9 +712,7 @@ platform and from client to client. We recommend always using absolute paths, as errors. The language server is running in a separate sub-process and is called with asyncio – sometimes a client may make it crash. If you have Serena's log window enabled, and it disappears, you'll know what happened. -Some clients (like goose) may not properly terminate MCP servers, -look out for hanging python processes and terminate them -manually, if needed. +Some clients may not properly terminate MCP servers, look out for hanging python processes and terminate them manually, if needed. ## Comparison with Other Coding Agents From 13d2051bab6be61a27562a1dec345394d9406ed5 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Wed, 25 Jun 2025 18:14:55 +0200 Subject: [PATCH 172/174] CONTRIBUTING.md, updated LS adding instructions [ci skip] --- CONTRIBUTING.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ced77bd..3e75556 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,21 +55,18 @@ An example script for running tools is provided in [scripts/demo_run_tools.py](s ## Adding a New Supported Language Serena interacts with code through language servers which are included in -the `multilspy` package. It is rather easy to include a new supported language +the `solidlsp` package. It is rather easy to include a new supported language if an LSP implementation for it exists. You just need to: -1. create a new subclass of `LanguageServer` +1. create a new subclass of `SolidLanguageServer` 2. add a new value to the `Language` enum -3. make a new `elif` case in the `LanguageServer.create` method -4. write minor tests +3. make a new `elif` case in the `SolidLanguageServer.create` method +4. add a test repo of the new language to `test/resources/repos//test_repo` + and new tests in `test/solidlsp/`. Similar to existing tests for other languages +5. also add a new case to the parameterized tests in `test/serena/test_serena_agent` The subclasses are typically easy to write, have a look at the [PyrightLanguageServer](src/multilspy/language_servers/pyright_language_server/pyright_server.py) for an example, or at any other implementation to see how non-python dependencies for language servers are handled there. There are also some tips from the multilspy admin [here](https://github.com/microsoft/multilspy/issues/5). - -âš ī¸ Important: The LSP allows for lot of optional fields and symbols, so the language servers may differ -in some details, even if they follow the LSP. Therefore you should include some code of the new -language in `test/resources` and add tests for symbolic read operations on that code. Have a look -at `test/multilspy/test_symbol_retrieval.py` for an example of such tests for the python LS. From 92d3b80918b70431fc927bed307bd37928d7ac09 Mon Sep 17 00:00:00 2001 From: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> Date: Thu, 26 Jun 2025 22:20:30 +0200 Subject: [PATCH 173/174] Update README.md - missing /sse in link [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 225931f..8e315b2 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ you would run this command from the Serena directory, uv run serena-mcp-server --transport sse --port 9121 ``` -and then configure your client to connect to `http://localhost:9121`. +and then configure your client to connect to `http://localhost/sse:9121`. #### Command-Line Arguments From 65a64d75f3df2ece8955445b87d324cf381e29fa Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 27 Jun 2025 13:29:40 +0200 Subject: [PATCH 174/174] Update change log --- CHANGELOG.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d5023..1f5d4b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,16 +6,27 @@ Status of the `main` branch. Changes prior to the next official version change w * Switch to newly developed fully synchronous LSP library `solidlsp` (derived from `multilspy`), removing our fork of `multilspy` (src/multilspy) * Switch from fastapi (which uses asyncio) to Flask in the Serena dashboard - * The MCP server is the only asynchronous component now, which resolves cross-component loop contamination, + * The MCP server is the only asyncio-based component now, which resolves cross-component loop contamination, such that process isolation is no longer required. Neither are non-graceful shutdowns on Windows. * Better default and description for restricting the search in `search_for_pattern` +* **Improved editing tools**: The editing logic was simplified and improved, making it more robust. + * The "minimal indentation" logic was removed, because LLMs did not understand it. + * The logic for the insertion of empty lines was improved (mostly controlled by the LLM now) +* Add a task queue for the agent, which is executed in a separate and thread and + * allows the language server to be initialized in the background, making the MCP server respond to requests + immediately upon startup, + * ensures that all tool executions are fully synchronized (executed linearly). Fixes: * Fix `ExecuteShellCommandTool` and `GetCurrentConfigTool` hanging on Windows * Fix project activation by name via `--project` not working (was broken in previous release) * Improve handling of indentation and newlines in symbolic editing tools -* Fix that `insert_after_symbol` was failing for insertions at the end of a file that did not end with a newline +* Fix `InsertAfterSymbolTool` failing for insertions at the end of a file that did not end with a newline +* Fix `InsertBeforeSymbolTool` inserting in the wrong place in the absence of empty lines above the reference symbol +* Fix `ReplaceSymbolBodyTool` changing whitespace before/after the symbol +* Fix repository indexing not following links and catch exceptions during indexing, allowing indexing + to continue even if unexpected errors occur for individual files. # 2025-06-20