Merge remote-tracking branch 'origin/main' into pywebview

Conflicts:
	src/serena/resources/serena_config.template.yml
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-03-30 19:44:58 +02:00
commit d96cda3ee1
65 files changed
+3962 -1540

No files matched your search

+3
View File
@@ -223,6 +223,9 @@ tmp/
# Exception: Don't ignore Elixir test repository lib directory (contains source code)
!/test/resources/repos/elixir/test_repo/lib
#Cpp
/test/resources/repos/cpp/test_repo/.ccls-cache
# Exception: Don't ignore Nix test repository lib directory (contains source code)
!/test/resources/repos/nix/test_repo/lib
+33 -17
View File
@@ -8,10 +8,10 @@ project_name: "serena"
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# powershell python python_jedi r rego
# ruby ruby_solargraph rust scala swift
# terraform toml typescript typescript_vts vue
# yaml zig
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
@@ -20,8 +20,8 @@ project_name: "serena"
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# - csharp: Requires the presence of a .sln file in the project folder.
# - pascal: Requires Free Pascal Compiler (fpc) and optionally Lazarus.
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
@@ -32,17 +32,19 @@ languages:
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# list of additional paths to ignore in all projects
# same syntax as gitignore, so you can use * and **
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# list of tool names to exclude.
# This extends the existing exclusions (e.g. from the global configuration)
#
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
@@ -83,11 +85,10 @@ read_only: false
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: |
@@ -106,11 +107,11 @@ initial_prompt: |
Comments:
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
clearly defines *what* it is. Any details then follow in subsequent sentences.
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: utf-8
# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
@@ -147,6 +148,21 @@ language_backend:
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []
# line ending convention for file writes: unset (use global setting, "lf", "crlf", or "native" (platform default)
# If not undefined, overrides the global setting in serena_config.yml
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# list of regex patterns for memories to completely ignore.
# Matching memories will not appear in list_memories or activate_project output
# and cannot be accessed via read_memory or write_memory.
# To access ignored memory files, use the read_file tool on the raw file path.
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
ls_specific_settings: {}
+3
View File
@@ -147,6 +147,9 @@ you would run
and then configure your client to connect to `http://localhost:9121/mcp`.
By default, only connections from localhost are allowed; pass the `--host <listen_address>` option to configure
the listen address and allow remote connections if needed (but be aware of the security implications of doing so).
**When to use.** Note that Serena is a stateful MCP server, and only one coding project can be active at a time.
Therefore, starting a single Serena instance and connecting it to multiple clients is only
appropriate if all clients will be working on the same project.
+419 -17
View File
@@ -226,16 +226,10 @@ ls_specific_settings:
# language-server-specific keys
```
:::{attention}
Most settings are currently undocumented. Please refer to the
[source code of the respective language server](https://github.com/oraios/serena/tree/main/src/solidlsp/language_servers)
implementation to determine supported settings.
:::
(override-ls-path)=
#### Overriding the Language Server Path
Some language servers, particularly those that use a single core path for the language server (e.g. the main executable),
Most of Serena's language servers, particularly those that use a single core path for the language server (e.g. the main executable),
support overriding that path via the `ls_path` setting.
Therefore, if you have installed the language server yourself and want to use your installation
instead of Serena's managed installation, you can set the `ls_path` setting as follows:
@@ -246,9 +240,87 @@ ls_specific_settings:
ls_path: "/path/to/language-server"
```
This is supported by all language servers deriving their dependency provider from `LanguageServerDependencyProviderSinglePath`.
Currently, this includes the following languages: `bash`, `clojure`, `cpp`, `kotlin`, `markdown`, `php`, `php_phpactor`, `python`, `rust`, `toml`, `typescript`, `yaml`.
We will add support for more languages over time.
This is supported by all language servers deriving their dependency provider from `LanguageServerDependencyProviderSinglePath`,
and by some additional wrappers that explicitly expose `ls_path`.
Common examples include: `ansible`, `bash`, `clojure`, `cpp`, `cpp_ccls`, `hlsl`, `kotlin`, `lean4`, `luau`, `markdown`, `php`,
`php_phpactor`, `python`, `rust`, `solidity`, `systemverilog`, `toml`, `typescript`, and `yaml`.
If a language server supports `ls_path`, setting it bypasses Serena's managed download or install for that server.
In that case, any server-specific version or registry settings only apply when `ls_path` is not set.
#### AL
Serena uses the AL language server bundled in the Microsoft Dynamics 365 Business Central VS Code extension.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `al_extension_version` | `18.0.2242655` | Override the AL VS Code extension version Serena downloads from the VS Code Marketplace. |
#### Ansible
Serena uses `@ansible/ansible-language-server` for the `ansible` language key.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed install | Override the `ansible-language-server` executable path. |
| `ansible_language_server_version` | `1.2.3` | Override the npm package version Serena installs when `ls_path` is not set. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
| `ansible_path` | `"ansible"` | Path to the `ansible` executable forwarded to the language server. |
| `ansible_settings` | `null` | Full Ansible LS settings dict, deep-merged on top of Serena's defaults. |
| `lint_enabled` | `false` | Enable `ansible-lint` integration. |
| `lint_path` | `"ansible-lint"` | Path to the `ansible-lint` executable. |
| `python_interpreter_path` | `"python3"` | Python interpreter path forwarded to the language server. |
| `python_activation_script` | `""` | Virtualenv activation script forwarded to the language server. |
#### Bash
Serena uses `bash-language-server` for Bash support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed install | Override the `bash-language-server` executable path. |
| `bash_language_server_version` | `5.6.0` | Override the npm package version Serena installs when `ls_path` is not set. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
#### Clojure
Serena uses `clojure-lsp` for Clojure support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed download | Override the `clojure-lsp` executable path. |
| `clojure_lsp_version` | `2026.02.20-16.08.58` | Override the `clojure-lsp` release version Serena downloads when `ls_path` is not set. |
#### C/C++ (`clangd`)
Serena uses `clangd` for the `cpp` language key.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed download | Override the `clangd` executable path. |
| `compile_commands_dir` | `.serena` | Directory where Serena writes a transformed `compile_commands.json` if the project's original database uses relative `directory` entries. |
| `clangd_version` | `19.1.2` | Override the `clangd` version Serena downloads when `ls_path` is not set. |
#### C/C++ via `ccls`
Serena uses the `cpp_ccls` language key for `ccls`.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | `ccls` from PATH | Override the `ccls` executable path. Serena does not manage `ccls` downloads or installs. |
#### C# (Roslyn Language Server)
@@ -266,12 +338,14 @@ Automatic download is supported for: Windows (x64, ARM64), macOS (x64, ARM64), L
**Configuration:**
The `runtime_dependencies` setting allows you to override the download URLs for the Roslyn Language Server. This is useful if you need to use a private package mirror or a specific version.
For the common case of changing only the package version, use `csharp_language_server_version`.
Example configuration to override the language server download URL:
```yaml
ls_specific_settings:
csharp:
csharp_language_server_version: "5.5.0-2.26078.4"
runtime_dependencies:
- id: "CSharpLanguageServer"
platform_id: "linux-x64" # or win-x64, win-arm64, osx-x64, osx-arm64, linux-arm64
@@ -294,6 +368,59 @@ Notes:
- The language server package is a `.nupkg` file (ZIP format) downloaded from NuGet.org by default.
- If you have .NET 10+ already installed, Serena will use your system installation.
#### C# (`OmniSharp`)
Serena uses the `csharp_omnisharp` language key for OmniSharp.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `omnisharp_version` | `1.39.10` | Override the OmniSharp version Serena downloads. |
| `razor_omnisharp_version` | `7.0.0-preview.23363.1` | Override the Razor OmniSharp plugin version Serena downloads. |
#### Dart
Serena uses the Dart SDK's built-in language server for Dart support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `dart_sdk_version` | `3.7.1` | Override the Dart SDK version Serena downloads. |
#### Elixir
Serena uses [Expert](https://github.com/elixir-lang/expert) for Elixir support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `expert_version` | `v0.1.0-rc.6` | Override the Expert version Serena downloads when it does not use an `expert` executable already found in PATH. |
#### Elm
Serena uses `@elm-tooling/elm-language-server` for Elm support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `elm_language_server_version` | `2.8.0` | Override the npm package version Serena installs when no system `elm-language-server` is found. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
#### F#
Serena uses FsAutoComplete (Ionide LSP) for F# support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `fsautocomplete_version` | `0.83.0` | Override the FsAutoComplete version Serena installs as a .NET tool. |
#### Go (`gopls`)
Serena forwards `ls_specific_settings.go.gopls_settings` to `gopls` as LSP `initializationOptions` when the Go language server is started.
@@ -318,6 +445,45 @@ Notes:
- `GOFLAGS` (from the environment you start Serena in) may also affect the Go build context. Prefer `buildFlags` for tags.
- Build context changes are only picked up when `gopls` starts. After changing `gopls_settings` (or relevant env vars like `GOFLAGS`), restart the Serena process (or server) that hosts the Go language server, or use your client's "Restart language server" action if it causes `gopls` to restart.
#### Groovy
Serena uses a user-provided Groovy Language Server JAR for Groovy support. If `ls_java_home_path` is not set, Serena downloads
a bundled Java runtime for launching that JAR.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_jar_path` | required | Path to the Groovy Language Server JAR |
| `ls_java_home_path` | `null` | Path to a Java installation to use instead of Serena's managed runtime |
| `ls_jar_options` | `""` | Additional options passed when launching the Groovy LS JAR |
| `vscode_java_version` | `1.42.0-561` | Override the bundled Java runtime bundle version Serena downloads by default |
Note:
- When overriding `vscode_java_version`, Serena still assumes that the downloaded runtime bundle keeps the same internal
directory layout and file names as the bundled default version.
Example:
```yaml
ls_specific_settings:
groovy:
ls_jar_path: "/path/to/groovy-language-server-all.jar"
vscode_java_version: "1.42.0-561"
```
#### HLSL
Serena uses `shader-language-server` for the `hlsl` language key.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed install or build | Override the `shader-language-server` executable path. |
| `version` | `1.3.0` | Override the bundled version Serena downloads, or builds from source on macOS, when `ls_path` is not set. |
#### Java (`eclipse.jdt.ls`)
The following settings are supported for the Java language server:
@@ -329,6 +495,17 @@ The following settings are supported for the Java language server:
| `gradle_wrapper_enabled` | `false` | Use the project's Gradle wrapper (`gradlew`) instead of the bundled Gradle distribution. Enable this for projects with custom plugins or repositories. |
| `gradle_java_home` | `null` | Path to the JDK used by Gradle. When unset, Gradle uses the bundled JRE. |
| `use_system_java_home` | `false` | Use the system's `JAVA_HOME` environment variable for JDTLS itself. Enable this if your project requires a specific JDK vendor or version for Gradle's JDK checks. |
| `gradle_version` | `8.14.2` | Override the Gradle distribution version Serena downloads by default. |
| `vscode_java_version` | `1.42.0-561` | Override the bundled `vscode-java` runtime bundle version Serena downloads by default. |
| `intellicode_version` | `1.2.30` | Override the IntelliCode VSIX version Serena downloads by default. |
| `jdtls_xmx` | `3G` | Maximum heap size for the JDTLS server JVM. |
| `jdtls_xms` | `100m` | Initial heap size for the JDTLS server JVM. |
| `intellicode_xmx` | `1G` | Maximum heap size for the IntelliCode embedded JVM. |
| `intellicode_xms` | `100m` | Initial heap size for the IntelliCode embedded JVM. |
Note:
- When overriding `vscode_java_version`, Serena still assumes that the downloaded runtime bundle keeps the same internal
directory layout and file names as the bundled default version.
Example for a project with custom Gradle plugins and JDK requirements:
@@ -343,21 +520,44 @@ ls_specific_settings:
Serena uses [JetBrains' Kotlin Language Server](https://github.com/Kotlin/kotlin-lsp) for Kotlin support.
**Runtime Requirements:**
Supported settings:
- Java 21 or higher is required. If not found, Serena automatically downloads an appropriate JRE.
- The Kotlin Language Server is automatically downloaded from JetBrains' CDN.
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed download | Override the Kotlin Language Server executable path. |
| `kotlin_lsp_version` | `261.13587.0` | Override the Kotlin Language Server version Serena downloads when `ls_path` is not set. |
| `jvm_options` | `-Xmx2G` | Value assigned to `JAVA_TOOL_OPTIONS` for the Kotlin LS process. Set to `""` to disable JVM options entirely. |
**Configuration:**
Example:
```yaml
ls_specific_settings:
kotlin:
ls_path: "/path/to/kotlin-lsp.sh" # Override the Kotlin Language Server executable
kotlin_lsp_version: "261.13587.0" # Override the Kotlin Language Server version
jvm_options: "-Xmx4G -XX:+UseG1GC" # JVM options (default: -Xmx2G). Set to "" to disable.
kotlin_lsp_version: "261.13587.0"
jvm_options: "-Xmx4G -XX:+UseG1GC"
```
#### Lean 4
Serena uses `lean --server` for Lean 4 support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | `lean` from PATH | Override the `lean` executable path. Serena does not manage Lean downloads. |
#### Lua
Serena uses `lua-language-server` for Lua support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `lua_language_server_version` | `3.15.0` | Override the bundled `lua-language-server` version Serena downloads when it cannot use an existing installation from PATH or common install locations. |
#### Luau
Serena uses [`luau-lsp`](https://github.com/JohnnyMorganz/luau-lsp) for Luau support.
@@ -373,6 +573,7 @@ Serena uses [`luau-lsp`](https://github.com/JohnnyMorganz/luau-lsp) for Luau sup
ls_specific_settings:
luau:
ls_path: "/path/to/luau-lsp" # Optional: override the language server executable
luau_lsp_version: "1.63.0" # Optional: override the bundled luau-lsp version
platform: "roblox" # "roblox" (default) or "standard"
roblox_security_level: "PluginSecurity" # Roblox only: None, PluginSecurity, LocalUserSecurity, RobloxScriptSecurity
```
@@ -381,6 +582,29 @@ Notes:
- In `roblox` mode, Serena downloads Roblox definitions and Roblox API docs and passes them to `luau-lsp`.
- In `standard` mode, Serena skips Roblox definitions and only downloads the standard Luau docs bundle.
#### Markdown
Serena uses [Marksman](https://github.com/artempyanykh/marksman) for the `markdown` language key.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed download | Override the `marksman` executable path. |
| `marksman_version` | `2024-12-18` | Override the Marksman release tag Serena downloads when `ls_path` is not set. |
#### MATLAB
Serena uses the official MathWorks MATLAB language server from the VS Code extension.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `matlab_path` | auto-detected | Path to the MATLAB installation. This overrides `MATLAB_PATH` and auto-detection, but not Serena's managed extension download. |
| `matlab_extension_version` | `1.3.9` | Override the MathWorks VS Code extension version Serena downloads. |
#### Pascal (`pasls`)
Serena uses [pasls](https://github.com/genericptr/pascal-language-server) (Pascal Language Server) for Pascal/Free Pascal support.
@@ -405,6 +629,7 @@ Configure pasls via `ls_specific_settings.pascal` in `serena_config.yml`:
| Setting | Description |
| ---------------- | --------------------------------------------------------------------------- |
| `pasls_version` | Override the pinned pasls version Serena downloads by default |
| `pp` | Path to FPC compiler driver (must be `fpc` or `fpc.exe`, not `ppc386.exe`) |
| `fpcdir` | Path to FPC source directory |
| `lazarusdir` | Path to Lazarus directory (required for LCL projects) |
@@ -426,6 +651,183 @@ Notes:
- Use the FPC compiler driver (`fpc`/`fpc.exe`), not backend compilers like `ppc386.exe`.
- These settings are passed as environment variables to the pasls process.
#### PHP (`Intelephense`)
Serena uses Intelephense for the `php` language key.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed install | Override the `intelephense` executable path. |
| `intelephense_version` | `1.14.4` | Override the npm package version Serena installs when `ls_path` is not set. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
| `ignore_vendor` | `true` | Ignore directories named `vendor` while indexing the project. |
| `maxFileSize` | unset | Forwarded as `intelephense.files.maxSize` in `initializationOptions`. |
| `maxMemory` | unset | Forwarded as `intelephense.maxMemory` in `initializationOptions`. |
#### PHP (`Phpactor`)
Serena uses the `php_phpactor` language key for Phpactor.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed download | Override the Phpactor PHAR path. |
| `phpactor_version` | `2025.12.21.1` | Override the Phpactor PHAR version Serena downloads when `ls_path` is not set. |
| `ignore_vendor` | `true` | Ignore directories named `vendor` while indexing the project. |
#### PowerShell
Serena uses PowerShell Editor Services for PowerShell support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `pses_version` | `4.4.0` | Override the PowerShell Editor Services version Serena downloads. Serena still requires `pwsh` to be available locally. |
#### Python
Serena uses Pyright for the `python` language key.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | current Python executable | Override the Python interpreter Serena uses to run `-m pyright.langserver`. |
Note:
- There is currently no separate `python_ty` language key in Serena's current SolidLSP implementation.
#### Ruby
Serena uses Shopify's `ruby-lsp` for Ruby support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ruby_lsp_version` | `0.26.8` | Override the `ruby-lsp` gem version Serena installs when no project-local or global `ruby-lsp` is already available. |
#### Rust
Serena uses `rust-analyzer` for Rust support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | auto-detected | Override the `rust-analyzer` executable path. Without `ls_path`, Serena prefers `rustup which rust-analyzer`, then `rustup component add rust-analyzer`, then PATH/common install locations. |
#### Scala
Serena uses Metals for Scala support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `metals_version` | `1.6.4` | Override the Metals version Serena bootstraps. |
| `client_name` | `Serena` | Client identifier sent to Metals. |
| `on_stale_lock` | `auto-clean` | How Serena handles stale Metals H2 database locks. Supported values: `auto-clean`, `warn`, `fail`. |
| `log_multi_instance_notice` | `true` | Log a notice when another Metals instance is detected. |
#### Solidity
Serena uses `@nomicfoundation/solidity-language-server` for Solidity support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed install | Override the Solidity language server executable path. |
| `solidity_language_server_version` | `0.8.4` | Override the npm package version Serena installs when `ls_path` is not set. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
#### SystemVerilog
Serena uses `verible-verilog-ls` for SystemVerilog support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | system PATH or managed download | Override the `verible-verilog-ls` executable path. |
| `verible_version` | `v0.0-4051-g9fdb4057` | Override the Verible release Serena downloads when `ls_path` is not set and no system installation is found. |
#### Terraform
Serena uses `terraform-ls` for Terraform support.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `terraform_ls_version` | `0.36.5` | Override the `terraform-ls` version Serena downloads. Terraform itself must still be installed and available in PATH. |
#### TOML
Serena uses [Taplo](https://github.com/tamasfe/taplo) for the `toml` language key.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed download | Override the `taplo` executable path. |
| `taplo_version` | `0.10.0` | Override the Taplo version Serena downloads when `ls_path` is not set. |
#### TypeScript
Serena uses `typescript-language-server` for the `typescript` language key.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed install | Override the `typescript-language-server` executable path. |
| `typescript_version` | `5.9.3` | Override the bundled `typescript` npm package version Serena installs when `ls_path` is not set. |
| `typescript_language_server_version` | `5.1.3` | Override the bundled `typescript-language-server` npm package version Serena installs when `ls_path` is not set. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
#### TypeScript via `vtsls`
The actual configuration key for vtsls is `typescript_vts`, not `vts`.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `vtsls_version` | `0.2.9` | Override the `@vtsls/language-server` npm package version Serena installs. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
#### Vue
Serena uses `@vue/language-server` (Volar) for the `vue` language key, together with a companion TypeScript language server.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `vue_language_server_version` | `3.1.5` | Override the bundled `@vue/language-server` npm package version Serena installs. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. If unset on `vue`, Serena falls back to `ls_specific_settings.typescript.npm_registry`. |
Notes:
- `typescript_version` and `typescript_language_server_version` are read from `ls_specific_settings.typescript`, not from `ls_specific_settings.vue`.
#### YAML
Serena uses `yaml-language-server` for the `yaml` language key.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed install | Override the `yaml-language-server` executable path. |
| `yaml_language_server_version` | `1.19.2` | Override the npm package version Serena installs when `ls_path` is not set. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
### Custom Prompts
All of Serena's prompts can be fully customized.
+165 -9
View File
@@ -1,12 +1,168 @@
# Security Considerations
As fundamental abilities for a coding agent, Serena contains tools for executing shell commands and modifying files.
Therefore, if the respective tool calls are not monitored or restricted (and execution takes place in a sensitive environment),
there is a risk of unintended consequences.
Security is important to us, and we take this topic seriously.
Therefore, to reduce the risk of unintended consequences when using Serena, it is recommended to
* back up your work regularly (e.g. use a version control system like Git),
* monitor tool executions carefully (e.g. via your MCP client, provided that it supports it),
* consider enabling read-only mode for your project (set `read_only: True` in project.yml) if you only want to analyze code without modifying it,
* restrict the set of allowed tools via the [configuration](050_configuration),
* use a sandboxed environment for running Serena (e.g. by [using Docker](docker)).
## Serena's Assumptions
The current security model for Serena assumes:
- the local machine is trusted,
- the MCP client (i.e. the LLM) is trusted,
- the code repository being worked on is trusted,
- user configuration is trusted,
- package manager configuration (e.g. npm) for downloading additional dependencies (i.e. language servers when using Serena with the LSP backend) is trusted.
Serena contains tools for executing shell commands and modifying files.
As such tools are, however, an essential part of coding agent workflows, they typically need to be made available.
Therefore, the only way to *fully* protect against unintended consequences is to use a [sandboxed environment](sandboxing) for running Serena.
## General Recommendations for Risk Reduction
To reduce the risk of unintended consequences, we recommend that you:
- back up your work regularly (keep the project being worked on under version control),
- restrict the set of allowed tools via the [configuration](050_configuration),
- do not expose [Serena's network services](network-security) to untrusted networks.
If you do not fully the trust the client/the LLM, we additionally recommend to monitor tool executions carefully
(provided that your MCP client supports this).
(sandboxing)=
## Sandboxing
Sandboxing is the most effective way to mitigate risks when using coding agents.
[Running Serena inside a docker container](docker) which only exposes the necessary files and tools to the agent is a good way to achieve this.
While setting up a sandboxed environment may require some initial effort, we highly recommend it for all security-conscious users.
(network-security)=
## Network Security
Serena includes several network services:
- the Serena MCP server itself (when run in [HTTP or SSE mode](streamable-http) instead of stdio mode)
- the Serena Dashboard web server
- the Serena JetBrains Plugin server, which runs within the JetBrains IDE (when using the JetBrains language backend)
- the Serena Project Server (only started explicitly for [project querying](query-projects))
By default, these services accept connections from localhost only, which is a secure default for most users
(given our assumption that the local machine is trusted; see above).
These services can be reconfigured to listen on other addresses, but doing so may have security implications.
If you need to allow connections from other machines, we recommend that you set up a secure networking environment
(e.g. using a VPN or SSH tunnels) and ensure that only trusted machines can connect to these services.
## Supply Chain Security
Serena has two language backends with different security characteristics:
- the JetBrains-based variant, which integrates with a running JetBrains IDE, and
- the language-server-based variant (the free variant), which can automatically acquire language server dependencies on demand.
While we can assume that JetBrains IDEs installed by the user do not pose a security risk,
language server dependencies (if not handled with care) could.
For convenience, Serena downloads or installs certain language server dependencies on demand.
We treat this path as security-sensitive and have hardened it accordingly.
The most important supply chain protections are:
- exact version pinning,
- hash verification,
- host restriction,
- and isolated Serena-managed installation directories.
### Auto-Downloaded Language Server Dependencies
For language servers that are auto-installed by downloading archives, binaries, VSIX packages, NuGet packages, or other release artifacts, Serena uses a hardened shared download path with the following protections:
- **Pinned versions by default**: default downloads use exact versions instead of floating `latest` or nightly channels.
- **Integrity verification**: downloaded artifacts are checked against pinned SHA256 hashes stored in Serena's source code.
- **Host allowlists**: download URLs are restricted to the expected hosts for a given dependency.
- **Safe extraction**: archive extraction validates paths to prevent path traversal and zip-slip style attacks.
- **Managed install locations**: dependencies are installed into Serena-managed directories instead of into the project repository.
In practice, this means that a downloaded artifact must match all of the following:
- the expected version,
- the expected host,
- the expected SHA256 checksum,
- and the expected extraction layout.
If any of these checks fail, Serena aborts the installation instead of continuing.
### npm-Based Language Servers
Some language servers are distributed primarily through npm. For those, Serena currently uses pinned package versions and installs them into Serena-managed directories.
By default, Serena uses the **user's normal npm configuration**. We do **not** force a registry override unless one is explicitly configured. If needed, both the package version and the registry can be overridden through `ls_specific_settings`.
For npm-based installs, Serena's current security posture is based on these rules:
- **Exact package versions are pinned by default**.
- **The install location is isolated from the project** and lives in Serena-managed language-server directories.
- **The user's npm configuration is trusted by default**.
- **Repository and user configuration are assumed to be trusted**.
This means Serena protects well against accidental version drift, but npm installs still rely on the npm ecosystem and package-manager execution model. In particular, Serena does **not** currently use lockfile-based `npm ci` installs for bundled language-server dependencies.
### `uvx` and Python Dependency Pinning
Some parts of Serena rely on `uv` / `uvx`.
One important detail is that `uvx` ignores the lockfile when installing directly from a Git repository. Because of that, we pin Serena's Python dependencies exactly in `pyproject.toml` so that installations from Git still resolve to exact dependency versions rather than floating ranges.
For the `ty` Python language server, Serena also uses an exact pinned version when invoking it through `uvx`.
```{dropdown} What Serena Downloads by Default for Language Servers
:open:
Only the language servers listed below download or install additional dependencies automatically by default when the required dependency is missing. Everything else either relies on a system-installed server or on tooling you install separately.
### Release Artifacts, Archives, or VSIX Packages
- **AL**: the pinned Microsoft AL VS Code extension (`ms-dynamics-smb.al`) from the VS Code Marketplace.
- **C/C++ (`clangd`)**: pinned `clangd` release archives on supported platforms.
- **C# (Roslyn LS)**: pinned Roslyn language-server NuGet package for the current platform.
- **Clojure**: pinned `clojure-lsp` release artifact.
- **Dart**: pinned Dart SDK archive that contains the language server.
- **Elixir (`expert`)**: pinned Expert release binary, if not already available locally.
- **Groovy**: pinned `vscode-java` runtime bundle used to provide Java for the Groovy LS setup.
- **HLSL / shader-language-server**: pinned GitHub release artifacts on supported prebuilt platforms.
- **Java (`eclipse.jdt.ls`)**: pinned Gradle distribution, pinned `vscode-java` extension bundle, and pinned IntelliCode VSIX.
- **Kotlin**: pinned Kotlin LSP archive.
- **Lua**: pinned `lua-language-server` release archive.
- **Luau**: pinned `luau-lsp` release archive. In Roblox or standard-doc modes it may also download Luau/Roblox docs and type-definition files.
- **Markdown (`marksman`)**: pinned Marksman release binary.
- **MATLAB**: the pinned MathWorks MATLAB VS Code extension from the VS Code Marketplace.
- **OmniSharp (legacy C# backend)**: pinned OmniSharp and Razor plugin archives.
- **Pascal**: pinned Pascal language-server release artifact.
- **PHP (`phpactor`)**: pinned `phpactor.phar`.
- **PowerShell**: pinned PowerShell Editor Services archive.
- **SystemVerilog (`verible`)**: pinned Verible release archive on supported platforms.
- **TOML (`taplo`)**: pinned Taplo release artifact.
- **Terraform**: pinned `terraform-ls` release archive. The Terraform CLI itself must still already be installed.
### npm Package Installs
- **Ansible**: `@ansible/ansible-language-server`
- **Bash**: `bash-language-server`
- **Elm**: `@elm-tooling/elm-language-server`
- **PHP (`intelephense`)**: `intelephense`
- **Solidity**: `@nomicfoundation/solidity-language-server`
- **TypeScript**: `typescript` and `typescript-language-server`
- **Vue**: `@vue/language-server`, plus `typescript` and `typescript-language-server`
- **VTSLS**: `@vtsls/language-server`
- **YAML**: `yaml-language-server`
All of the above are installed with exact pinned package versions by default, into Serena-managed directories.
### Other Package-Manager Based Installs
- **F#**: installs pinned `fsautocomplete` via `dotnet tool install`.
- **Ruby (`ruby-lsp`)**: if not already available through Bundler or as a global executable, Serena installs a pinned `ruby-lsp` gem.
- **Python (`ty`)**: launched through `uvx` / `uv x` using an exact pinned `ty` version.
- **HLSL on macOS**: if no prebuilt binary is used, Serena builds `shader_language_server` from a pinned version using Cargo.
### No Automatic Download by Serena
- **Python (`pyright`)**: Serena uses the locally available Python environment and starts `pyright.langserver` from there.
- **Go (`gopls`)**, **Rust (`rust-analyzer`)**, and several other system-tool based integrations expect the language server to be available locally and do not download it automatically.
```
+4 -3
View File
@@ -14,13 +14,13 @@ classifiers = [
"Programming Language :: Python :: 3.11",
]
dependencies = [
"requests==2.32.5",
"requests==2.33.0",
"pyright==1.1.403",
"fortls==3.2.2",
"overrides==7.7.0",
"python-dotenv==1.2.1",
"mcp==1.26.0",
"flask==3.1.3", # bumped from 3.1.1 for CVE fix (also fixes werkzeug alert)
"flask==3.1.3", # bumped from 3.1.1 for CVE fix (also fixes werkzeug alert)
"sensai-utils==1.5.0",
"pydantic==2.12.5",
"types-pyyaml==6.0.12.20250516",
@@ -46,6 +46,7 @@ dependencies = [
"python-multipart==0.0.22",
"filelock==3.25.2",
"cryptography==46.0.5",
"regex==2026.2.28",
]
[tool.uv.sources]
@@ -166,7 +167,7 @@ doc-build = ["doc-clean", "doc-generate-files", "_sphinx_build"]
[tool.ruff]
target-version = "py311"
line-length = 140
exclude = ["src/solidlsp/language_servers/**/static", "src/multilspy"]
exclude = ["src/solidlsp/language_servers/**/static", "src/multilspy", "test/resources/repos"]
[tool.ruff.format]
quote-style = "double"
+191
View File
@@ -0,0 +1,191 @@
"""
Demonstrates the progressive shortening of tool results when max_answer_chars is exceeded.
It exercises all tools that use _limit_length with shortened_results,
printing the full result, then progressively tighter max_answer_chars
to show the successive shortening stages. Both LSP and JetBrains backends
are tested (JB is skipped if no IDE is running).
"""
import json
from pprint import pprint
from serena.agent import SerenaAgent
from serena.config.serena_config import LanguageBackend, SerenaConfig
from serena.constants import REPO_ROOT
from serena.tools import (
FindReferencingSymbolsTool,
FindSymbolTool,
GetSymbolsOverviewTool,
JetBrainsFindReferencingSymbolsTool,
JetBrainsFindSymbolTool,
JetBrainsGetSymbolsOverviewTool,
SearchForPatternTool,
)
SEPARATOR = "=" * 80
# symbol with many references across multiple files, good for testing shortening
REF_SYMBOL = "_limit_length"
REF_FILE = "src/serena/tools/tools_base.py"
# file with many symbols, good for testing overview shortening
OVERVIEW_FILE = "src/serena/tools/tools_base.py"
def run_with_shrinking(agent: SerenaAgent, label: str, fn, char_limits: list[int]) -> None:
"""Run a tool call at several max_answer_chars limits and print results."""
for limit in char_limits:
tag = f"{label} (max_answer_chars={limit})"
print(f"\n{SEPARATOR}")
print(tag)
print(SEPARATOR)
result = agent.execute_task(lambda lim=limit: fn(lim))
n = len(result)
print(f"[length={n}]")
try:
pprint(json.loads(result), width=200)
except (json.JSONDecodeError, ValueError):
print(result)
def run_lsp_tools(agent: SerenaAgent) -> None:
print("\n\n### LSP BACKEND ###\n")
# LSP: FindReferencingSymbolsTool — three shortening stages:
# 1. refs without context lines 2. per-file counts 3. total summary
lsp_refs = agent.get_tool(FindReferencingSymbolsTool)
run_with_shrinking(
agent,
"LSP FindReferencingSymbolsTool",
lambda lim: lsp_refs.apply(REF_SYMBOL, REF_FILE, max_answer_chars=lim),
char_limits=[50000, 3000, 500, 200],
)
# LSP: FindSymbolTool — one shortening stage: names with kind only
lsp_find = agent.get_tool(FindSymbolTool)
run_with_shrinking(
agent,
"LSP FindSymbolTool (depth=1)",
lambda lim: lsp_find.apply("Tool", relative_path=REF_FILE, depth=1, max_answer_chars=lim),
char_limits=[50000, 200],
)
# LSP: FindSymbolTool with max_matches exceeded — tests the early-return shortened path
print(f"\n{SEPARATOR}")
print("LSP FindSymbolTool (max_matches=1, broad search)")
print(SEPARATOR)
result = agent.execute_task(lambda: lsp_find.apply("apply", max_matches=1))
print(f"[length={len(result)}]")
print(result)
# LSP: GetSymbolsOverviewTool — two shortening stages for depth>0:
# 1. depth-0 overview 2. counts by kind
# one stage for depth==0: counts by kind
lsp_overview = agent.get_tool(GetSymbolsOverviewTool)
run_with_shrinking(
agent,
"LSP GetSymbolsOverviewTool (depth=1)",
lambda lim: lsp_overview.apply(OVERVIEW_FILE, depth=1, max_answer_chars=lim),
char_limits=[50000, 500, 200],
)
run_with_shrinking(
agent,
"LSP GetSymbolsOverviewTool (depth=0)",
lambda lim: lsp_overview.apply(OVERVIEW_FILE, depth=0, max_answer_chars=lim),
char_limits=[50000, 200],
)
def run_backend_independent_tools(agent: SerenaAgent) -> None:
print("\n\n### BACKEND-INDEPENDENT TOOLS ###\n")
# SearchForPatternTool — three shortening stages:
# 1. match lines per file (no context) 2. match counts per file 3. total summary
search_tool = agent.get_tool(SearchForPatternTool)
run_with_shrinking(
agent,
"SearchForPatternTool (with context)",
lambda lim: search_tool.apply(
"_limit_length",
context_lines_before=1,
context_lines_after=1,
relative_path="src/serena/tools",
max_answer_chars=lim,
),
char_limits=[50000, 1000, 200],
)
def run_jb_tools(agent: SerenaAgent) -> None:
print("\n\n### JETBRAINS BACKEND ###\n")
# JB: FindReferencingSymbolsTool — two shortening stages:
# 1. per-file counts 2. total summary
jb_refs = agent.get_tool(JetBrainsFindReferencingSymbolsTool)
run_with_shrinking(
agent,
"JB FindReferencingSymbolsTool",
lambda lim: jb_refs.apply(REF_SYMBOL, REF_FILE, max_answer_chars=lim),
char_limits=[50000, 500, 200],
)
# JB: FindSymbolTool — one shortening stage: names with kind only
jb_find = agent.get_tool(JetBrainsFindSymbolTool)
run_with_shrinking(
agent,
"JB FindSymbolTool (depth=1)",
lambda lim: jb_find.apply("Tool", relative_path=REF_FILE, depth=1, max_answer_chars=lim),
char_limits=[50000, 200],
)
# JB: FindSymbolTool with max_matches exceeded — tests the early-return shortened path
print(f"\n{SEPARATOR}")
print("JB FindSymbolTool (max_matches=1, broad search)")
print(SEPARATOR)
result = agent.execute_task(lambda: jb_find.apply("apply", max_matches=1))
print(f"[length={len(result)}]")
print(result)
# JB: GetSymbolsOverviewTool — two shortening stages for depth>0:
# 1. depth-0 overview 2. counts by type
# two stages for depth==0: grouped symbols, then counts by type
jb_overview = agent.get_tool(JetBrainsGetSymbolsOverviewTool)
run_with_shrinking(
agent,
"JB GetSymbolsOverviewTool (depth=1)",
lambda lim: jb_overview.apply(OVERVIEW_FILE, depth=1, max_answer_chars=lim),
char_limits=[50000, 500, 200],
)
run_with_shrinking(
agent,
"JB GetSymbolsOverviewTool (depth=0)",
lambda lim: jb_overview.apply(OVERVIEW_FILE, depth=0, max_answer_chars=lim),
char_limits=[50000, 200],
)
def make_agent(backend: LanguageBackend) -> SerenaAgent:
config = SerenaConfig.from_config_file()
config.web_dashboard = False
config.language_backend = backend
return SerenaAgent(project=REPO_ROOT, serena_config=config)
if __name__ == "__main__":
# LSP backend
lsp_agent = make_agent(LanguageBackend.LSP)
try:
run_lsp_tools(lsp_agent)
run_backend_independent_tools(lsp_agent)
finally:
lsp_agent.shutdown()
# JetBrains backend (requires a running IDE)
try:
jb_agent = make_agent(LanguageBackend.JETBRAINS)
try:
run_jb_tools(jb_agent)
finally:
jb_agent.shutdown()
except Exception as e:
print(f"\nJetBrains backend not available, skipping: {e}")
+6 -2
View File
@@ -5,6 +5,7 @@ import os
import shutil
import subprocess
import sys
import time
from collections.abc import Iterator, Sequence
from logging import Logger
from pathlib import Path
@@ -187,7 +188,7 @@ class TopLevelCommands(AutoRegisteringGroup):
@click.option(
"--host",
type=str,
default="0.0.0.0",
default="127.0.0.1",
show_default=True,
help="Listen address for the MCP server (when using corresponding transport).",
)
@@ -721,6 +722,7 @@ class ProjectCommands(AutoRegisteringGroup):
collected_exceptions: list[Exception] = []
files_failed = []
language_file_counts: dict[Language, int] = collections.defaultdict(lambda: 0)
last_save_time = time.monotonic()
for i, f in enumerate(tqdm(files, desc="Indexing")):
try:
ls = ls_mgr.get_language_server(f)
@@ -730,8 +732,10 @@ class ProjectCommands(AutoRegisteringGroup):
log.error(f"Failed to index {f}, continuing.")
collected_exceptions.append(e)
files_failed.append(f)
if (i + 1) % 10 == 0:
now = time.monotonic()
if now - last_save_time >= 30:
ls_mgr.save_all_caches()
last_save_time = now
reported_language_file_counts = {k.value: v for k, v in language_file_counts.items()}
click.echo(f"Indexed files per language: {dict_string(reported_language_file_counts, brackets=None)}")
ls_mgr.save_all_caches()
+2 -2
View File
@@ -565,7 +565,7 @@ class ProjectConfig(SharedConfig):
# transfer missing comments from the template file
template_config, _ = self._load_yaml_dict(PROJECT_TEMPLATE_FILE, self.YAML_COMMENT_NORMALISATION)
transfer_missing_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION)
transfer_missing_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION, force_update_all=True)
# save project.yml
save_yaml(config_path, config_with_comments)
@@ -1047,7 +1047,7 @@ class SerenaConfig(SharedConfig):
# For some keys, we force updates, because old comments are problematic/misleading.
normalise_yaml_comments(commented_yaml, YamlCommentNormalisation.LEADING_WITH_CONVERSION_FROM_TRAILING)
template_yaml = load_yaml(SERENA_CONFIG_TEMPLATE_FILE, comment_normalisation=YamlCommentNormalisation.LEADING)
transfer_missing_yaml_comments(template_yaml, commented_yaml, YamlCommentNormalisation.LEADING, forced_update_keys=["projects"])
transfer_missing_yaml_comments(template_yaml, commented_yaml, YamlCommentNormalisation.LEADING, force_update_all=True)
save_yaml(self.config_file_path, commented_yaml)
+3 -1
View File
@@ -374,6 +374,8 @@ class SerenaDashboardAPI:
return post_installation_news_ids
with open(news_snippet_id_file, encoding="utf-8") as f:
last_read_news_id = int(f.read().strip())
if last_read_news_id == "20262103":
last_read_news_id = "20260321" # fix originally misnamed file
return [news_id for news_id in post_installation_news_ids if news_id > last_read_news_id]
try:
@@ -553,7 +555,7 @@ class SerenaDashboardAPI:
from solidlsp.ls_config import Language
def run() -> ResponseAvailableLanguages:
all_languages = [lang.value for lang in Language.iter_all(include_experimental=False)]
all_languages = [lang.value for lang in Language.iter_all(include_experimental=True)]
# Filter out already added languages for the active project
project = self._agent.get_active_project()
+1 -1
View File
@@ -270,7 +270,7 @@ class SerenaMCPFactory:
def create_mcp_server(
self,
host: str = "0.0.0.0",
host: str = "127.0.0.1",
port: int = 8000,
modes: Sequence[str] = (),
language_backend: LanguageBackend | None = None,
@@ -0,0 +1,14 @@
<div class="news-item">
<h3>Progressive Shortening and General Improvements in Tool Responses</h3>
<p class="date">March 21, 2026</p>
<p>
Tools that may deliver too long results now output informative shorter
messages instead of just stating "result is too long". The mechanism uses
progressive shortening so that the contained information is optimized
for the length limit.
</p>
<p>
In addition, several tool responses have been optimized without losing
content, making Serena even more token efficient in general.
</p>
</div>
@@ -0,0 +1,13 @@
<div class="news-item">
<h3>Querying of External Projects</h3>
<p class="date">March 30, 2026</p>
<p>
We added a new tool for querying external projects that are
known to Serena: From the current project, use Serena's tools on another project to retrieve information.
This is useful in situations where the current project builds on or is otherwise related
to other projects.
</p>
<p>
Find out more about this feature in our <a href="https://oraios.github.io/serena/02-usage/040_workflow.html#reading-from-external-projects">documentation</a>.
</p>
</div>
+72 -31
View File
@@ -1,3 +1,4 @@
import copy
import json
import logging
import os
@@ -409,8 +410,23 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
string representation of the symbol kind (name attribute of the `SymbolKind` enum item)
"""
children: NotRequired[list["LanguageServerSymbol.OutputDict"]]
content_around_reference: NotRequired[str]
"""set by :class:`FindReferencingSymbolsTool` when including surrounding code lines"""
reference_line: NotRequired[int]
"""line number of the reference, set by :class:`FindReferencingSymbolsTool`"""
OutputDictKey = Literal["name", "name_path", "relative_path", "location", "body_location", "body", "kind", "children"]
OutputDictKey = Literal[
"name",
"name_path",
"relative_path",
"location",
"body_location",
"body",
"kind",
"children",
"content_around_reference",
"reference_line",
]
def to_dict(
self,
@@ -423,6 +439,8 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
body: bool = False,
body_location: bool = False,
children_body: bool = False,
children_name_path: bool | None = None,
children_name: bool | None = None,
relative_path: bool = False,
child_inclusion_predicate: Callable[[Self], bool] | None = None,
) -> OutputDict:
@@ -439,6 +457,8 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
Note that the body of the children is part of the body of the parent symbol,
so there is usually no need to set this to True unless you want process the output
and pass the children without passing the parent body to the LM.
:param children_name_path: whether to include the name path of the children; if None, defaults to the value of `name_path`
:param children_name: whether to include the name of the children; if None, defaults to the value of `name`
:param relative_path: whether to include the relative path of the symbol.
If `location` is True, this defines whether to include the path in the location entry.
If `location` is False, this defines whether to include the relative path as a top-level entry.
@@ -449,6 +469,11 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
"""
result: LanguageServerSymbol.OutputDict = {}
if children_name_path is None:
children_name_path = name_path
if children_name is None:
children_name = name
if name_path:
result["name_path"] = self.get_name_path()
if name:
@@ -479,8 +504,8 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
continue
children.append(
c.to_dict(
name_path=name_path,
name=name,
name_path=children_name_path,
name=children_name,
kind=kind,
location=location,
body_location=body_location,
@@ -941,35 +966,42 @@ class SymbolDictGrouper(Generic[TSymbolDict], ABC):
self._group_children_keys = group_children_keys
self._collapse_singleton = collapse_singleton
def _group_by(self, l: list[dict], keys: list[str], children_keys: list[str]) -> dict[str, Any]:
assert len(keys) > 0, "keys must not be empty"
# group by the first key
grouped: dict[str, Any] = {}
for item in l:
key_value = item.pop(keys[0], "unknown")
if key_value not in grouped:
grouped[key_value] = []
grouped[key_value].append(item)
if len(keys) > 1:
def _group_by(self, l: list[dict], keys: list[str], children_keys: list[str], is_children: bool) -> dict[str, Any] | list[Any]:
"""
:param l: the list of symbol dictionaries to group
:param keys: the keys to group by
:param children_keys: the keys to group the children by
:param is_children: whether this is a children grouping operation
:return: the (grouped) symbols
"""
if len(keys) > 0:
# group by the first key
grouped: dict[str, Any] = {}
for item in l:
key_value = item.pop(keys[0], "unknown")
if key_value not in grouped:
grouped[key_value] = []
grouped[key_value].append(item)
# continue grouping by the remaining keys
for k, group in grouped.items():
grouped[k] = self._group_by(group, keys[1:], children_keys)
grouped[k] = self._group_by(group, keys[1:], children_keys, is_children=is_children)
return grouped
else:
# grouping is complete; now group the children if necessary
if children_keys:
for k, group in grouped.items():
for item in group:
if self._children_key in item:
children = item[self._children_key]
item[self._children_key] = self._group_by(children, children_keys, children_keys)
# post-process final group items
grouped = {k: [self._transform_item(i) for i in v] for k, v in grouped.items()}
return grouped
for item in l:
if self._children_key in item:
children = item[self._children_key]
item[self._children_key] = self._group_by(children, children_keys, children_keys, is_children=True)
# post-process final items
return [self._transform_item(item, is_children) for item in l]
def _transform_item(self, item: dict) -> dict:
def _transform_item(self, item: dict, is_child: bool) -> dict:
"""
Post-processes a final group item (which has been regrouped, i.e. some keys may have been removed),
collapsing singleton items (and items containing only a single non-children key)
:param item: the item to post-process
:param is_child: whether the item is a child item
"""
if self._collapse_singleton:
if len(item) == 1:
@@ -985,12 +1017,15 @@ class SymbolDictGrouper(Generic[TSymbolDict], ABC):
return new_item
return item
def group(self, symbols: list[TSymbolDict]) -> GroupedSymbolDict:
def group(self, symbols: list[TSymbolDict]) -> GroupedSymbolDict | list:
"""
:param symbols: the symbols to group
:return: dictionary with the symbols grouped as defined at construction
:return: dictionary with the symbols grouped as defined at construction if at least one key was used for grouping,
otherwise the list of symbols (potentially transformed)
"""
return self._group_by(symbols, self._group_keys, self._group_children_keys) # type: ignore
# avoid side effects by working on a deep-copy
symbols_copy = copy.deepcopy(symbols)
return self._group_by(symbols_copy, self._group_keys, self._group_children_keys, is_children=False) # type: ignore
class LanguageServerSymbolDictGrouper(SymbolDictGrouper[LanguageServerSymbol.OutputDict]):
@@ -1011,16 +1046,22 @@ class JetBrainsSymbolDictGrouper(SymbolDictGrouper[jb.SymbolDTO]):
collapse_singleton: bool = False,
map_name_path_to_name: bool = False,
) -> None:
"""
:param group_keys: keys to group main symbols by
:param group_children_keys: keys to group child symbols by
:param collapse_singleton: whether to collapse singleton symbol dictionaries
:param map_name_path_to_name: whether to transform the "name_path" key of child symbols to the bare "name"
"""
super().__init__(jb.SymbolDTO, "children", group_keys, group_children_keys, collapse_singleton)
self._map_name_path_to_name = map_name_path_to_name
def _transform_item(self, item: dict) -> dict:
if self._map_name_path_to_name:
def _transform_item(self, item: dict, is_child: bool) -> dict:
if self._map_name_path_to_name and is_child:
# {"name_path: "Class/myMethod"} -> {"name: "myMethod"}
new_item = dict(item)
if "name_path" in item:
name_path = new_item.pop("name_path")
new_item["name"] = name_path.split("/")[-1]
return super()._transform_item(new_item)
return super()._transform_item(new_item, is_child)
else:
return super()._transform_item(item)
return super()._transform_item(item, is_child)
+24 -1
View File
@@ -404,6 +404,8 @@ class SearchForPatternTool(Tool):
matches = search_files(
rel_paths_to_search,
substring_pattern,
context_lines_before=context_lines_before,
context_lines_after=context_lines_after,
file_reader=self.project.read_file,
root_path=self.get_project_root(),
paths_include_glob=paths_include_glob,
@@ -414,5 +416,26 @@ class SearchForPatternTool(Tool):
for match in matches:
assert match.source_file_path is not None
file_to_matches[match.source_file_path].append(match.to_display_string())
# capture lightweight match data for shortening before serialization
match_lines_by_file: dict[str, list[int]] = defaultdict(list)
for match in matches:
assert match.source_file_path is not None
match_lines_by_file[match.source_file_path].append(match.matched_lines[0].line_number)
# shortened result closures, from least to most aggressive shortening
def make_lines_only() -> str:
"""Match locations without surrounding context"""
return f"Match lines per file:\n{self._to_json(match_lines_by_file)}"
def make_per_file_counts() -> str:
counts = {path: len(lines) for path, lines in match_lines_by_file.items()}
return f"Match counts per file:\n{self._to_json(counts)}"
def make_summary() -> str:
return f"Found {len(matches)} matches in {len(match_lines_by_file)} files."
result = self._to_json(file_to_matches)
return self._limit_length(result, max_answer_chars)
return self._limit_length(
result, max_answer_chars, shortened_result_factories=[make_lines_only, make_per_file_counts, make_summary]
)
+85 -21
View File
@@ -1,8 +1,10 @@
import logging
from collections import Counter
from typing import Any, Literal
import serena.jetbrains.jetbrains_types as jb
from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient
from serena.jetbrains.jetbrains_types import SymbolDTO
from serena.symbol import JetBrainsSymbolDictGrouper
from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicRead
@@ -14,6 +16,11 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
Performs a global (or local) search for symbols using the JetBrains backend
"""
# groups top-level symbols only; children are grouped separately by _group_children_by_type
symbol_dict_grouper = JetBrainsSymbolDictGrouper(
["relative_path", "type"], ["type"], collapse_singleton=True, map_name_path_to_name=True
)
def apply(
self,
name_path_pattern: str,
@@ -22,6 +29,7 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
include_body: bool = False,
include_info: bool = False,
search_deps: bool = False,
max_matches: int = -1,
max_answer_chars: int = -1,
) -> str:
"""
@@ -46,21 +54,21 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
:param name_path_pattern: the name path matching pattern (see above)
:param depth: depth up to which descendants shall be retrieved (e.g. use 1 to also retrieve immediate children;
for the case where the symbol is a class, this will return its methods).
Default 0.
:param relative_path: Optional. Restrict search to this file or directory. If None, searches entire codebase.
If a directory is passed, the search will be restricted to the files in that directory.
If a file is passed, the search will be restricted to that file.
If you have some knowledge about the codebase, you should use this parameter, as it will significantly
speed up the search as well as reduce the number of results.
Ignored if `include_body=True`. Default 0.
:param relative_path: Optional. Restrict search to this file or directory. If not specified, searches entire codebase.
:param include_body: If True, include the symbol's source code. Use judiciously.
:param include_info: whether to include additional info (hover-like, typically including docstring and signature),
about the symbol (ignored if include_body is True).
Default False; info is never included for child symbols and is not included when body is requested.
about the symbol.
Default False; info is never included for child symbols or if include_body is True.
:param search_deps: If True, also search in project dependencies (e.g., libraries).
:param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned.
-1 means the default value from the config will be used.
:return: JSON string: a list of symbols (with locations) matching the name.
:param max_matches: Maximum number of permitted matches. If exceeded, a shortened result is returned
which allows refining the search. -1 (default) means no limit. Set to 1 if you search for a single symbol.
:param max_answer_chars: max characters for the result (-1 for default). If exceeded, no content/a shortened result is returned.
:return: symbols matching the name.
"""
if include_body:
depth = 0 # ignore user-specified depth if body is requested
if relative_path == ".":
relative_path = None
with JetBrainsPluginClient.from_project(self.project) as client:
@@ -75,7 +83,7 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
# If no additional information is requested, we still include the quick info (type signature)
include_documentation = False
include_quick_info = True
response_dict = client.find_symbol(
symbol_collection_response = client.find_symbol(
name_path=name_path_pattern,
relative_path=relative_path,
depth=depth,
@@ -84,8 +92,23 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
include_quick_info=include_quick_info,
search_deps=search_deps,
)
result = self._to_json(response_dict)
return self._limit_length(result, max_answer_chars)
symbols = symbol_collection_response["symbols"]
def create_shortened_result() -> str:
"""Shortened results containing symbol types and identifiers (path + name_path) only, without children"""
dicts: list[SymbolDTO] = [
{"name_path": s["name_path"], "type": s["type"], "relative_path": s["relative_path"]} for s in symbols
]
grouped = self.symbol_dict_grouper.group(dicts)
return f"Names with paths:\n{self._to_json(grouped)}"
n_matches = len(symbols)
if 0 < max_matches < n_matches:
return f"Matched {n_matches}>{max_matches=} symbols.\n" + create_shortened_result()
grouped_symbols = self.symbol_dict_grouper.group(symbols)
result = self._to_json(grouped_symbols)
return self._limit_length(result, max_answer_chars, shortened_result_factories=[create_shortened_result])
class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
@@ -109,8 +132,7 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark
:param name_path: name path of the symbol for which to find references; matching logic as described in find symbol tool.
:param relative_path: the relative path to the file containing the symbol for which to find references.
Note that here you can't pass a directory but must pass a file.
:param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned. -1 means the
default value from the config will be used.
:param max_answer_chars: max characters for the result (-1 for default). If exceeded, no content/a shortened result is returned.
:return: a list of JSON objects with the symbols referencing the requested symbol
"""
with JetBrainsPluginClient.from_project(self.project) as client:
@@ -120,9 +142,24 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark
include_quick_info=False,
)
symbol_dicts = response_dict["symbols"]
# capture file paths before grouping, which mutates the dicts
ref_paths = [s.get("relative_path", "unknown") for s in symbol_dicts]
result = self.symbol_dict_grouper.group(symbol_dicts)
def create_shortened_result_counts_per_file() -> str:
return f"Reference counts per file:\n{self._to_json(Counter(ref_paths))}"
def create_shortened_result_num_results() -> str:
return f"Found {len(ref_paths)} references."
result_json = self._to_json(result)
return self._limit_length(result_json, max_answer_chars)
return self._limit_length(
result_json,
max_answer_chars,
shortened_result_factories=[create_shortened_result_counts_per_file, create_shortened_result_num_results],
)
class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
@@ -149,8 +186,7 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp
:param relative_path: the relative path to the file to get the overview of
:param depth: depth up to which descendants shall be retrieved (e.g., use 1 to also retrieve immediate children).
:param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned.
-1 means the default value from the config will be used.
:param max_answer_chars: max characters for the result (-1 for default). If exceeded, no content/a shortened result is returned.
:param include_file_documentation: whether to include the file's docstring. Default False.
:return: a JSON object containing the symbols grouped by kind in a compact format.
"""
@@ -158,16 +194,44 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp
symbol_overview = client.get_symbols_overview(
relative_path=relative_path, depth=depth, include_file_documentation=include_file_documentation
)
if self.USE_COMPACT_FORMAT:
symbols = symbol_overview["symbols"]
result: dict[str, Any] = {"symbols": self.symbol_dict_grouper.group(symbols)}
grouped_symbols = self.symbol_dict_grouper.group(symbols)
shortened_result_factories = []
# create full result
result: dict[str, Any] = {"symbols": grouped_symbols}
documentation = symbol_overview.pop("documentation", None)
if documentation:
result["docstring"] = documentation
shortened_result_factories.append(lambda: self._to_json(grouped_symbols)) # shortened result without docstring
json_result = self._to_json(result)
if depth > 0:
def create_short_result_depth_0() -> str:
depth_0_symbols = [d.copy() for d in symbols]
for d in depth_0_symbols:
d.pop("children", None)
compact_depth_0_result = self.symbol_dict_grouper.group(depth_0_symbols)
return "Depth 0 overview:\n" + self._to_json(compact_depth_0_result)
shortened_result_factories.append(create_short_result_depth_0)
def create_short_result_type_counts() -> str:
type_names = [d.get("type", "unknown") for d in symbols]
return f"Symbol counts by type:\n{self._to_json(Counter(type_names))}"
shortened_result_factories.append(create_short_result_type_counts)
else:
# this path is currently abandoned, consider introducing shortened results if ever needed
shortened_result_factories = None
json_result = self._to_json(symbol_overview)
return self._limit_length(json_result, max_answer_chars)
return self._limit_length(json_result, max_answer_chars, shortened_result_factories=shortened_result_factories)
class JetBrainsTypeHierarchyTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
+100 -9
View File
@@ -2,7 +2,9 @@
Language server-related tools
"""
import copy
import os
from collections import Counter, defaultdict
from collections.abc import Sequence
from serena.symbol import LanguageServerSymbol, LanguageServerSymbolDictGrouper
@@ -49,9 +51,32 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead):
:return: a JSON object containing symbols grouped by kind in a compact format.
"""
result = self.get_symbol_overview(relative_path, depth=depth)
# capture kind names and depth-0 snapshots before grouping, which mutates the dicts
kind_names = [d.get("kind", "unknown") for d in result]
if depth > 0:
depth_0_result = [d.copy() for d in result]
for d in depth_0_result:
d.pop("children", None)
compact_result = self.symbol_dict_grouper.group(result)
result_json_str = self._to_json(compact_result)
return self._limit_length(result_json_str, max_answer_chars)
# shortened result closures
def make_kind_counts() -> str:
return f"Symbol counts by kind:\n{self._to_json(Counter(kind_names))}"
if depth == 0:
shortened_results = [make_kind_counts]
else:
def make_depth_0_result() -> str:
compact_depth_0_result = self.symbol_dict_grouper.group(depth_0_result)
return "Depth 0 overview:\n" + self._to_json(compact_depth_0_result)
shortened_results = [make_depth_0_result, make_kind_counts]
return self._limit_length(result_json_str, max_answer_chars, shortened_result_factories=shortened_results)
def get_symbol_overview(self, relative_path: str, depth: int = 0) -> list[LanguageServerSymbol.OutputDict]:
"""
@@ -99,6 +124,10 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
Performs a global (or local) search using the language server backend.
"""
# group children by kind, keeping just the name (the parent's name_path makes it unambiguous);
# we don't group the top-level result list because many tests rely on it being a flat list of symbol dicts
symbol_dict_grouper = LanguageServerSymbolDictGrouper([], ["kind"], collapse_singleton=True)
# noinspection PyDefaultArgument
def apply(
self,
@@ -110,6 +139,7 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
include_kinds: list[int] = [], # noqa: B006
exclude_kinds: list[int] = [], # noqa: B006
substring_matching: bool = False,
max_matches: int = -1,
max_answer_chars: int = -1,
) -> str:
"""
@@ -131,7 +161,8 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
:param name_path_pattern: the name path matching pattern (see above)
:param depth: depth up to which descendants shall be retrieved (e.g. use 1 to also retrieve immediate children;
for the case where the symbol is a class, this will return its methods). Default 0.
for the case where the symbol is a class, this will return its methods).
Ignored if `include_body=True`. Default 0.
:param relative_path: Optional. Restrict search to this file or directory. If None, searches entire codebase.
If a directory is passed, the search will be restricted to the files in that directory.
If a file is passed, the search will be restricted to that file.
@@ -147,10 +178,15 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
If not provided, no kinds are excluded.
:param substring_matching: If True, use substring matching for the last element of the pattern, such that
"Foo/get" would match "Foo/getValue" and "Foo/getData".
:param max_matches: Maximum number of permitted matches. If exceeded, a shortened result is returned
which allows refining the search. -1 (default) means no limit. Set to 1 if you search for a single symbol.
:param max_answer_chars: Max characters for the JSON result. If exceeded, no content is returned.
-1 means the default value from the config will be used.
:return: a list of symbols (with locations) matching the name.
:return: symbols (with locations) matching the name.
"""
if include_body:
depth = 0 # ignore user-specified depth if include_body is True
assert max_matches != 0, "max_matches must be > 0 or equal to -1."
parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None
parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None
symbol_retriever = self.create_language_server_symbol_retriever()
@@ -161,15 +197,43 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
substring_matching=substring_matching,
within_relative_path=relative_path,
)
symbol_dicts = [dict(s.to_dict(kind=True, relative_path=True, body_location=True, depth=depth, body=include_body)) for s in symbols]
n_matches = len(symbols)
def create_short_result_relative_path_to_name_paths() -> str:
relative_path_to_name_paths: defaultdict[str, list[str]] = defaultdict(list)
for s in symbols:
relative_path_to_name_paths[s.location.relative_path or "unknown"].append(s.get_name_path())
return f"Shortened result:\n{self._to_json(relative_path_to_name_paths)}"
if 0 < max_matches < n_matches:
return f"Matched {n_matches}>{max_matches=} symbols.\n" + create_short_result_relative_path_to_name_paths()
symbol_dicts = [
s.to_dict(
kind=True,
name_path=True,
name=False,
relative_path=True,
body_location=True,
depth=depth,
body=include_body,
children_name=True,
children_name_path=False,
)
for s in symbols
]
if not include_body and include_info:
info_by_symbol = symbol_retriever.request_info_for_symbol_batch(symbols)
for s, s_dict in zip(symbols, symbol_dicts, strict=True):
if symbol_info := info_by_symbol.get(s):
s_dict["info"] = symbol_info
s_dict.pop("name", None) # name is included in the info
result = self._to_json(symbol_dicts)
return self._limit_length(result, max_answer_chars)
# In python 3.15 we could specify extra_items=True in the TypedDict definition,
# https://peps.python.org/pep-0728/
# If we ever upgrade to 3.15, we can remove the type: ignore[typeddict-unknown-key]
s_dict["info"] = symbol_info # type: ignore[typeddict-unknown-key]
grouped_symbol_dicts = self.symbol_dict_grouper.group(symbol_dicts)
result = self._to_json(grouped_symbol_dicts)
return self._limit_length(result, max_answer_chars, shortened_result_factories=[create_short_result_relative_path_to_name_paths])
class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
@@ -226,10 +290,37 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
ref_dict["content_around_reference"] = content_around_ref.to_display_string()
reference_dicts.append(ref_dict)
# capture lightweight reference data before grouping, which mutates the dicts
ref_summaries = []
for ref, d in zip(references_in_symbols, reference_dicts, strict=True):
ref_summaries.append(
{
"name_path": d.get("name_path"),
"kind": d.get("kind"),
"relative_path": d.get("relative_path"),
"reference_line": ref.line,
}
)
result = self.symbol_dict_grouper.group(reference_dicts) # type: ignore
# shortened result closures, from least to most aggressive shortening
def make_refs_without_context() -> str:
"""References with name_path and reference line, without surrounding code lines"""
grouped = self.symbol_dict_grouper.group(copy.deepcopy(ref_summaries)) # type: ignore
return f"References without surrounding lines:\n{self._to_json(grouped)}"
def make_per_file_counts() -> str:
counts = Counter(str(r["relative_path"]) for r in ref_summaries)
return f"Reference counts per file:\n{self._to_json(counts)}"
def make_summary() -> str:
return f"Found {len(ref_summaries)} references."
shortened_results = [make_refs_without_context, make_per_file_counts, make_summary]
result_json = self._to_json(result)
return self._limit_length(result_json, max_answer_chars)
return self._limit_length(result_json, max_answer_chars, shortened_result_factories=shortened_results)
class ReplaceSymbolBodyTool(Tool, ToolMarkerSymbolicEdit):
+25 -5
View File
@@ -1,7 +1,7 @@
import inspect
import json
from abc import ABC
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from types import TracebackType
from typing import TYPE_CHECKING, Any, Protocol, Self, TypeVar, cast
@@ -221,16 +221,36 @@ class Tool(Component):
params[param] = value
log.info(f"{self.get_name_from_cls()}: {dict_string(params)}")
def _limit_length(self, result: str, max_answer_chars: int) -> str:
def _limit_length(
self,
result: str,
max_answer_chars: int,
shortened_result_factories: list[Callable[[], str]] | None = None,
) -> str:
"""Limit the length of the result string, optionally trying progressively shorter versions.
:param result: the full result string
:param max_answer_chars: maximum allowed characters. -1 means use the default from config.
:param shortened_result_factories: optional list of closures, each producing a progressively shorter
version of the result. They are tried in order until one fits within ``max_answer_chars``.
:return: the result string, potentially replaced by a shortened version
"""
if max_answer_chars == -1:
max_answer_chars = self.agent.serena_config.default_max_tool_answer_chars
if max_answer_chars <= 0:
raise ValueError(f"Must be positive or the default (-1), got: {max_answer_chars=}")
if (n_chars := len(result)) > max_answer_chars:
result = (
f"The answer is too long ({n_chars} characters). "
+ "Please try a more specific tool query or raise the max_answer_chars parameter."
too_long_msg = (
f"The answer is too long ({n_chars} characters). " + "You can adjust your query or raise the max_answer_chars parameter."
)
if shortened_result_factories is not None:
# try each shortening closure in order;
for make_shorter in shortened_result_factories:
shortened = make_shorter()
candidate = f"{too_long_msg}\n{shortened}"
if len(candidate) <= max_answer_chars:
return candidate
result = too_long_msg
return result
def is_active(self) -> bool:
+32
View File
@@ -523,3 +523,35 @@ class ContentReplacer:
"Please revise the expression to be more specific or enable allow_multiple_occurrences if this is expected."
)
return updated_content
@dataclass
class TextCoords:
line: int
"""
0-based line number
"""
col: int
"""
0-based column number
"""
def find_text_coordinates(content: str, regex: str) -> TextCoords | None:
"""
Finds the line and column number of the first match of a regex pattern in the given content.
:param content: the text content to search through
:param regex: the regular expression pattern to search for; it must match part of a single line,
and contain exactly one group that captures the position of interest (e.g., the exact variable name to find the coordinates of)
:return: a tuple of (line_number, column_number) for the first match found, both 0-based, or None if no match is found
"""
pattern = re.compile(regex)
for line_number, line in enumerate(content.splitlines()):
match = pattern.search(line)
if match:
if len(match.groups()) != 1:
raise ValueError(f"Regex must contain exactly one group to capture the position, but found {len(match.groups())} groups.")
column_number = match.start(1)
return TextCoords(line_number, column_number)
return None
+12 -4
View File
@@ -205,13 +205,14 @@ def yaml_comment_entry_is_empty(comment_entry: Any) -> bool:
def transfer_missing_yaml_comments_by_index(
source: CommentedMap, target: CommentedMap, indices: list[int], forced_update_keys: Sequence[str] = ()
source: CommentedMap, target: CommentedMap, indices: list[int], forced_update_keys: Sequence[str] = (), force_update_all: bool = False
) -> None:
"""
:param source: the source, from which to transfer missing comments
:param target: the target map, whose comments will be updated
:param indices: list of comment indices to transfer
:param forced_update_keys: keys for which comments are always transferred, even if present in target
:param force_update_all: if True, comments are transferred for all keys, even if present in target
"""
for key in target.keys():
if key in source:
@@ -224,14 +225,18 @@ def transfer_missing_yaml_comments_by_index(
target_comment = [None] * 4
target.ca.items[key] = target_comment
# transfer comments at specified indices
is_forced_update = force_update_all or key in forced_update_keys
for index in indices:
is_forced_update = key in forced_update_keys
if is_forced_update or yaml_comment_entry_is_empty(target_comment[index]):
target_comment[index] = source_comment[index]
def transfer_missing_yaml_comments(
source: CommentedMap, target: CommentedMap, comment_normalisation: YamlCommentNormalisation, forced_update_keys: Sequence[str] = ()
source: CommentedMap,
target: CommentedMap,
comment_normalisation: YamlCommentNormalisation,
forced_update_keys: Sequence[str] = (),
force_update_all: bool = False,
) -> None:
"""
Transfers missing comments from source to target YAML.
@@ -240,11 +245,14 @@ def transfer_missing_yaml_comments(
:param target: the target map, whose comments will be updated.
:param comment_normalisation: the comment normalisation to assume; if NONE, no comments are transferred
:param forced_update_keys: keys for which comments are always transferred, even if present in target
:param force_update_all: if True, comments are transferred for all keys, even if present in target
"""
match comment_normalisation:
case YamlCommentNormalisation.NONE:
pass
case YamlCommentNormalisation.LEADING | YamlCommentNormalisation.LEADING_WITH_CONVERSION_FROM_TRAILING:
transfer_missing_yaml_comments_by_index(source, target, [ITEM_COMMENT_INDEX_BEFORE], forced_update_keys=forced_update_keys)
transfer_missing_yaml_comments_by_index(
source, target, [ITEM_COMMENT_INDEX_BEFORE], forced_update_keys=forced_update_keys, force_update_all=force_update_all
)
case _:
raise ValueError(f"Unhandled comment normalisation: {comment_normalisation}")
@@ -1,4 +1,9 @@
"""AL Language Server implementation for Microsoft Dynamics 365 Business Central."""
"""AL Language Server implementation for Microsoft Dynamics 365 Business Central.
You can pass the following entries in ``ls_specific_settings["al"]``:
- al_extension_version: Override the pinned AL VS Code extension version
downloaded by Serena (default: the bundled Serena version).
"""
import logging
import os
@@ -7,23 +12,29 @@ import platform
import re
import stat
import time
import zipfile
from pathlib import Path
import requests
from overrides import override
from solidlsp import ls_types
from solidlsp.language_servers.common import quote_windows_path
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, RawDocumentSymbol, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
AL_EXTENSION_VERSION = "18.0.2242655"
AL_EXTENSION_URL = (
f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/vsextensions/al/{AL_EXTENSION_VERSION}/vspackage"
)
AL_EXTENSION_SHA256 = "3971995e61a59dc4fcce4a65053072a67991ed624a16635c4f2911f12564b2b9"
AL_EXTENSION_ALLOWED_HOSTS = ("marketplace.visualstudio.com",)
class ALLanguageServer(SolidLanguageServer):
"""
@@ -38,6 +49,7 @@ class ALLanguageServer(SolidLanguageServer):
- Special initialization sequence required by AL Language Server
- Custom AL-specific LSP commands (al/gotodefinition, al/setActiveWorkspace)
- File opening requirement before symbol retrieval
- `al_extension_version` to override the bundled AL VS Code extension version
"""
# Regex pattern to match AL object names like:
@@ -141,46 +153,14 @@ class ALLanguageServer(SolidLanguageServer):
"""
try:
log.info(f"Downloading AL extension from {url}")
# Create target directory for the extension
os.makedirs(target_dir, exist_ok=True)
# Download with proper headers to mimic VS Code marketplace client
# These headers are required for the marketplace to serve the VSIX file
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/octet-stream, application/vsix, */*",
}
response = requests.get(url, headers=headers, stream=True, timeout=300)
response.raise_for_status()
# Save to temporary VSIX file (will be deleted after extraction)
temp_file = os.path.join(target_dir, "al_extension_temp.vsix")
total_size = int(response.headers.get("content-length", 0))
log.info(f"Downloading {total_size / 1024 / 1024:.1f} MB...")
with open(temp_file, "wb") as f:
downloaded = 0
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0: # Log progress every 10MB
progress = (downloaded / total_size) * 100
log.info(f"Download progress: {progress:.1f}%")
log.info("Download complete, extracting...")
# Extract VSIX file (VSIX files are just ZIP archives with a different extension)
# This will extract the extension folder containing the language server binaries
with zipfile.ZipFile(temp_file, "r") as zip_ref:
zip_ref.extractall(target_dir)
# Clean up temp file
os.remove(temp_file)
FileUtils.download_and_extract_archive_verified(
url,
target_dir,
"zip",
expected_sha256=AL_EXTENSION_SHA256 if url == AL_EXTENSION_URL else None,
allowed_hosts=AL_EXTENSION_ALLOWED_HOSTS,
)
log.info("AL extension extracted successfully")
return True
@@ -276,14 +256,16 @@ class ALLanguageServer(SolidLanguageServer):
"""
al_extension_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "al-extension")
al_settings = solidlsp_settings.get_ls_specific_settings(Language.AL)
al_extension_version = al_settings.get("al_extension_version", AL_EXTENSION_VERSION)
al_extension_url = (
"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/"
f"vsextensions/al/{al_extension_version}/vspackage"
)
# AL extension version - using latest stable version
AL_VERSION = "latest"
url = f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/vsextensions/al/{AL_VERSION}/vspackage"
log.info(f"Downloading AL extension from: {al_extension_url}")
log.info(f"Downloading AL extension from: {url}")
if cls._download_al_extension(url, al_extension_dir):
if cls._download_al_extension(al_extension_url, al_extension_dir):
extension_path = os.path.join(al_extension_dir, "extension")
if os.path.exists(extension_path):
log.info("AL extension downloaded and installed successfully")
@@ -1025,35 +1007,25 @@ class ALLanguageServer(SolidLanguageServer):
# Get symbols from parent implementation
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
# Normalize names by stripping AL object metadata, storing originals for hover
def normalize_name(symbol: UnifiedSymbolInformation) -> None:
original_name = symbol["name"]
normalized_name = self._extract_al_display_name(original_name)
# Store original name if it was normalized (for hover injection)
# Only store if we have valid position data to avoid false matches at (0, 0)
if original_name != normalized_name:
sel_range = symbol.get("selectionRange")
if sel_range:
start = sel_range.get("start")
if start and "line" in start and "character" in start:
line = start["line"]
char = start["character"]
self._al_original_names[(relative_file_path, line, char)] = original_name
symbol["name"] = normalized_name
# Process children recursively
if symbol.get("children"):
for child in symbol["children"]:
normalize_name(child)
# Apply to all root symbols
for sym in document_symbols.root_symbols:
normalize_name(sym)
return document_symbols
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
original_name = symbol["name"]
normalized_name = self._extract_al_display_name(original_name)
# Store original name if it was normalized (for hover injection)
# Only store if we have valid position data to avoid false matches at (0, 0)
if original_name != normalized_name:
sel_range = symbol.get("selectionRange")
if sel_range:
start = sel_range.get("start") # type: ignore
if start and "line" in start and "character" in start:
line = start["line"]
char = start["character"]
self._al_original_names[(relative_file_path, line, char)] = original_name
return normalized_name
@override
def request_hover(
self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None
@@ -12,7 +12,7 @@ from typing import Any, ClassVar
from overrides import override
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
@@ -162,13 +162,19 @@ class AnsibleLanguageServer(SolidLanguageServer):
assert is_node_installed, "node is not installed or isn't in PATH. Please install Node.js 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."
ansible_language_server_version = self._custom_settings.get("ansible_language_server_version", "1.2.3")
npm_registry = self._custom_settings.get("npm_registry")
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="ansible-language-server",
description="Ansible Language Server (@ansible/ansible-language-server)",
command="npm install --prefix ./ @ansible/ansible-language-server@1.2.3",
command=build_npm_install_command(
"@ansible/ansible-language-server",
ansible_language_server_version,
npm_registry,
),
platform_id="any",
),
]
@@ -9,7 +9,7 @@ import pathlib
import shutil
import threading
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
from solidlsp.ls import (
DocumentSymbols,
LanguageServerDependencyProvider,
@@ -58,13 +58,15 @@ class BashLanguageServer(SolidLanguageServer):
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."
bash_language_server_version = self._custom_settings.get("bash_language_server_version", "5.6.0")
npm_registry = self._custom_settings.get("npm_registry")
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="bash-language-server",
description="bash-language-server package",
command="npm install --prefix ./ bash-language-server@5.6.0",
command=build_npm_install_command("bash-language-server", bash_language_server_version, npm_registry),
platform_id="any",
),
]
@@ -14,12 +14,20 @@ from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
CLANGD_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
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.
Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example.
You can pass the following entries in ``ls_specific_settings["cpp"]``:
- compile_commands_dir: Directory where Serena writes its transformed
``compile_commands.json`` if needed.
- clangd_version: Override the pinned Clangd version downloaded by Serena
(default: the bundled Serena version).
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
@@ -125,39 +133,50 @@ class ClangdLanguageServer(SolidLanguageServer):
"""
import shutil
clangd_version = self._custom_settings.get("clangd_version", "19.1.2")
default_version = clangd_version == "19.1.2"
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="Clangd",
description="Clangd for Linux (x64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip",
url=f"https://github.com/clangd/clangd/releases/download/{clangd_version}/clangd-linux-{clangd_version}.zip",
platform_id="linux-x64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd",
binary_name=f"clangd_{clangd_version}/bin/clangd",
sha256="7c09614eff857d590e4502ef516f035ff94cfb8b795de14ece5afbc53a206caf" if default_version else None,
allowed_hosts=CLANGD_ALLOWED_HOSTS,
),
RuntimeDependency(
id="Clangd",
description="Clangd for Windows (x64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip",
url=f"https://github.com/clangd/clangd/releases/download/{clangd_version}/clangd-windows-{clangd_version}.zip",
platform_id="win-x64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd.exe",
binary_name=f"clangd_{clangd_version}/bin/clangd.exe",
sha256="5b6ceb0f85d63fa0c2c9aab31c29bebd41dc11da1f160ef21bc2fea93270a20d" if default_version else None,
allowed_hosts=CLANGD_ALLOWED_HOSTS,
),
RuntimeDependency(
id="Clangd",
description="Clangd for macOS (x64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
url=f"https://github.com/clangd/clangd/releases/download/{clangd_version}/clangd-mac-{clangd_version}.zip",
platform_id="osx-x64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd",
binary_name=f"clangd_{clangd_version}/bin/clangd",
sha256="d3b329b3f58602c57ca6501d255147af1bccad3691b1cb0c12c258fcd2da1be3" if default_version else None,
allowed_hosts=CLANGD_ALLOWED_HOSTS,
),
RuntimeDependency(
id="Clangd",
description="Clangd for macOS (Arm64)",
url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
url=f"https://github.com/clangd/clangd/releases/download/{clangd_version}/clangd-mac-{clangd_version}.zip",
platform_id="osx-arm64",
archive_type="zip",
binary_name="clangd_19.1.2/bin/clangd",
binary_name=f"clangd_{clangd_version}/bin/clangd",
sha256="d3b329b3f58602c57ca6501d255147af1bccad3691b1cb0c12c258fcd2da1be3" if default_version else None,
allowed_hosts=CLANGD_ALLOWED_HOSTS,
),
]
)
+70 -42
View File
@@ -19,6 +19,13 @@ from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
CLOJURE_LSP_VERSION = "2026.02.20-16.08.58"
CLOJURE_LSP_ALLOWED_HOSTS = (
"github.com",
"release-assets.githubusercontent.com",
"objects.githubusercontent.com",
)
def run_command(cmd: list, capture_output: bool = True) -> subprocess.CompletedProcess:
return subprocess.run(
@@ -42,49 +49,69 @@ def verify_clojure_cli() -> None:
class ClojureLSP(SolidLanguageServer):
"""
Provides a clojure-lsp specific instantiation of the LanguageServer class. Contains various configurations and settings specific to clojure.
Provides a clojure-lsp specific instantiation of the LanguageServer class.
You can pass the following entries in ``ls_specific_settings["clojure"]``:
- clojure_lsp_version: Override the pinned clojure-lsp version downloaded
by Serena (default: the bundled Serena version).
"""
clojure_lsp_releases = "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download"
runtime_dependencies = RuntimeDependencyCollection(
[
RuntimeDependency(
id="clojure-lsp",
url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip",
platform_id="osx-arm64",
archive_type="zip",
binary_name="clojure-lsp",
),
RuntimeDependency(
id="clojure-lsp",
url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-amd64.zip",
platform_id="osx-x64",
archive_type="zip",
binary_name="clojure-lsp",
),
RuntimeDependency(
id="clojure-lsp",
url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip",
platform_id="linux-arm64",
archive_type="zip",
binary_name="clojure-lsp",
),
RuntimeDependency(
id="clojure-lsp",
url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip",
platform_id="linux-x64",
archive_type="zip",
binary_name="clojure-lsp",
),
RuntimeDependency(
id="clojure-lsp",
url=f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip",
platform_id="win-x64",
archive_type="zip",
binary_name="clojure-lsp.exe",
),
]
)
CLOJURE_LSP_VERSION = CLOJURE_LSP_VERSION
CLOJURE_LSP_ALLOWED_HOSTS = CLOJURE_LSP_ALLOWED_HOSTS
@classmethod
def _runtime_dependencies(cls, version: str) -> RuntimeDependencyCollection:
clojure_lsp_releases = f"https://github.com/clojure-lsp/clojure-lsp/releases/download/{version}"
default_version = version == cls.CLOJURE_LSP_VERSION
return RuntimeDependencyCollection(
[
RuntimeDependency(
id="clojure-lsp",
url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip",
platform_id="osx-arm64",
archive_type="zip",
binary_name="clojure-lsp",
sha256="a14d4db074f665378214e2dc888472e186c228dfa065c777b0534bfda5571669" if default_version else None,
allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS,
),
RuntimeDependency(
id="clojure-lsp",
url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-amd64.zip",
platform_id="osx-x64",
archive_type="zip",
binary_name="clojure-lsp",
sha256="5507434c27104ab816e096d3336d8191641de8a65b57d76afb585d07167a3cf2" if default_version else None,
allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS,
),
RuntimeDependency(
id="clojure-lsp",
url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip",
platform_id="linux-arm64",
archive_type="zip",
binary_name="clojure-lsp",
sha256="f8f09fa07dd4b6743b5c57270ccf1ee5cdbc5fca09dbca8b6a3b22705b5da4e1" if default_version else None,
allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS,
),
RuntimeDependency(
id="clojure-lsp",
url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip",
platform_id="linux-x64",
archive_type="zip",
binary_name="clojure-lsp",
sha256="52e8bf4fd4cf171df0a3077c8bb5a3bf598d4c621e94b4876dab943a61267309" if default_version else None,
allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS,
),
RuntimeDependency(
id="clojure-lsp",
url=f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip",
platform_id="win-x64",
archive_type="zip",
binary_name="clojure-lsp.exe",
sha256="817b1271288817c954fb9e595278b1f25003827ce31f8785f253dc4ac911041f" if default_version else None,
allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS,
),
]
)
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
@@ -109,7 +136,8 @@ class ClojureLSP(SolidLanguageServer):
def _get_or_install_core_dependency(self) -> str:
"""Setup runtime dependencies for clojure-lsp and return the path to the executable."""
verify_clojure_cli()
deps = ClojureLSP.runtime_dependencies
clojure_lsp_version = self._custom_settings.get("clojure_lsp_version", ClojureLSP.CLOJURE_LSP_VERSION)
deps = ClojureLSP._runtime_dependencies(clojure_lsp_version)
dependency = deps.get_single_dep_for_current_platform()
clojurelsp_executable_path = deps.binary_path(self._ls_resources_dir)
+25 -2
View File
@@ -21,6 +21,8 @@ class RuntimeDependency:
id: str
platform_id: str | None = None
url: str | None = None
sha256: str | None = None
allowed_hosts: tuple[str, ...] | list[str] | None = None
archive_type: str | None = None
binary_name: str | None = None
command: str | list[str] | None = None
@@ -136,9 +138,30 @@ class RuntimeDependencyCollection:
if dep.archive_type in ("gz", "binary") and dep.binary_name:
dest = os.path.join(target_dir, dep.binary_name)
FileUtils.download_and_extract_archive(dep.url, dest, dep.archive_type)
FileUtils.download_and_extract_archive_verified(
dep.url,
dest,
dep.archive_type,
expected_sha256=dep.sha256,
allowed_hosts=dep.allowed_hosts,
)
else:
FileUtils.download_and_extract_archive(dep.url, target_dir, dep.archive_type or "zip")
FileUtils.download_and_extract_archive_verified(
dep.url,
target_dir,
dep.archive_type or "zip",
expected_sha256=dep.sha256,
allowed_hosts=dep.allowed_hosts,
)
def build_npm_install_command(package_name: str, version: str, registry: str | None = None) -> list[str]:
"""Build a pinned npm install command for a package in a Serena-managed install directory."""
command = ["npm", "install", "--prefix", "./"]
if registry:
command.extend(["--registry", registry])
command.append(f"{package_name}@{version}")
return command
def quote_windows_path(path: str) -> str:
@@ -7,97 +7,134 @@ import os
import platform
import shutil
import threading
import urllib.request
from collections.abc import Iterable
from collections.abc import Hashable, Iterable
from dataclasses import replace
from pathlib import Path
from typing import Any, cast
from overrides import override
from serena.util.dotnet import DotNETUtil
from solidlsp.ls import DocumentSymbols, LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls import (
LanguageServerDependencyProvider,
LSPFileBuffer,
RawDocumentSymbol,
SolidLanguageServer,
)
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_types import Hover, UnifiedSymbolInformation
from solidlsp.ls_utils import PathUtils
from solidlsp.ls_types import Hover
from solidlsp.ls_utils import FileUtils, PathUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
from solidlsp.settings import SolidLSPSettings
from solidlsp.util.zip import SafeZipExtractor
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
NUGET_ALLOWED_HOSTS = ("www.nuget.org", "nuget.org", "globalcdn.nuget.org")
DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION = "5.5.0-2.26078.4"
_RUNTIME_DEPENDENCIES = [
RuntimeDependency(
id="CSharpLanguageServer",
description="Roslyn Language Server for Windows (x64)",
package_name="roslyn-language-server.win-x64",
package_version="5.5.0-2.26078.4",
url="https://www.nuget.org/api/v2/package/roslyn-language-server.win-x64/5.5.0-2.26078.4",
package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION,
url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.win-x64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}",
platform_id="win-x64",
archive_type="nupkg",
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
extract_path="tools/net10.0/win-x64",
sha256="7f3d4119e75305399e6faa81a68240b33c48b94ad523a904594abd00db95572a",
allowed_hosts=NUGET_ALLOWED_HOSTS,
),
RuntimeDependency(
id="CSharpLanguageServer",
description="Roslyn Language Server for Windows (ARM64)",
package_name="roslyn-language-server.win-arm64",
package_version="5.5.0-2.26078.4",
url="https://www.nuget.org/api/v2/package/roslyn-language-server.win-arm64/5.5.0-2.26078.4",
package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION,
url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.win-arm64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}",
platform_id="win-arm64",
archive_type="nupkg",
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
extract_path="tools/net10.0/win-arm64",
sha256="0fe3381c4340a7494a5242c3d0c8be1af6ef0802de8b458f947cebca76fd26bc",
allowed_hosts=NUGET_ALLOWED_HOSTS,
),
RuntimeDependency(
id="CSharpLanguageServer",
description="Roslyn Language Server for macOS (x64)",
package_name="roslyn-language-server.osx-x64",
package_version="5.5.0-2.26078.4",
url="https://www.nuget.org/api/v2/package/roslyn-language-server.osx-x64/5.5.0-2.26078.4",
package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION,
url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.osx-x64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}",
platform_id="osx-x64",
archive_type="nupkg",
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
extract_path="tools/net10.0/osx-x64",
sha256="c8de61a88c65150e12f561a2659f70b59d27a7465865136a1de950d2ef826c6d",
allowed_hosts=NUGET_ALLOWED_HOSTS,
),
RuntimeDependency(
id="CSharpLanguageServer",
description="Roslyn Language Server for macOS (ARM64)",
package_name="roslyn-language-server.osx-arm64",
package_version="5.5.0-2.26078.4",
url="https://www.nuget.org/api/v2/package/roslyn-language-server.osx-arm64/5.5.0-2.26078.4",
package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION,
url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.osx-arm64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}",
platform_id="osx-arm64",
archive_type="nupkg",
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
extract_path="tools/net10.0/osx-arm64",
sha256="995207c14e01dafa71e84080a7eb1f045a697b0c3bb468077bb3809b69bdf456",
allowed_hosts=NUGET_ALLOWED_HOSTS,
),
RuntimeDependency(
id="CSharpLanguageServer",
description="Roslyn Language Server for Linux (x64)",
package_name="roslyn-language-server.linux-x64",
package_version="5.5.0-2.26078.4",
url="https://www.nuget.org/api/v2/package/roslyn-language-server.linux-x64/5.5.0-2.26078.4",
package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION,
url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.linux-x64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}",
platform_id="linux-x64",
archive_type="nupkg",
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
extract_path="tools/net10.0/linux-x64",
sha256="1aad25de456d637a1eee993ca0d569a1b78d711744ccb36410a3a20250a48aa6",
allowed_hosts=NUGET_ALLOWED_HOSTS,
),
RuntimeDependency(
id="CSharpLanguageServer",
description="Roslyn Language Server for Linux (ARM64)",
package_name="roslyn-language-server.linux-arm64",
package_version="5.5.0-2.26078.4",
url="https://www.nuget.org/api/v2/package/roslyn-language-server.linux-arm64/5.5.0-2.26078.4",
package_version=DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION,
url=f"https://www.nuget.org/api/v2/package/roslyn-language-server.linux-arm64/{DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION}",
platform_id="linux-arm64",
archive_type="nupkg",
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
extract_path="tools/net10.0/linux-arm64",
sha256="a7dd49bbc0e25d0e2968ae31ec5f3c774373866db51f3500fcea0ac320e2bbc1",
allowed_hosts=NUGET_ALLOWED_HOSTS,
),
]
def _runtime_dependencies_for_version(version: str) -> list[RuntimeDependency]:
"""Return the Roslyn LS runtime dependencies for the configured version."""
default_version = version == DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION
result: list[RuntimeDependency] = []
for dependency in _RUNTIME_DEPENDENCIES:
assert dependency.package_version is not None
assert dependency.url is not None
result.append(
replace(
dependency,
package_version=version,
url=dependency.url.replace(dependency.package_version, version),
sha256=dependency.sha256 if default_version else None,
)
)
return result
def breadth_first_file_scan(root_dir: str) -> Iterable[str]:
"""
Perform a breadth-first scan of files in the given directory.
@@ -151,6 +188,9 @@ class CSharpLanguageServer(SolidLanguageServer):
This is a list of dicts, each containing at least the "id" key, and optionally "platform_id" to uniquely
identify the dependency to override.
You can also set `csharp_language_server_version` in ``ls_specific_settings["csharp"]`` to override
the pinned Roslyn Language Server package version Serena downloads by default.
Example - Override Roslyn Language Server URL:
```
{
@@ -183,28 +223,6 @@ class CSharpLanguageServer(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]
@override
def request_document_symbols(self, relative_file_path: str, file_buffer: Any = None) -> DocumentSymbols:
"""
Override to normalize Roslyn symbol names and cache originals.
Roslyn 5.5.0+ returns symbol names with type annotations:
- Properties: "Name : string"
- Methods: "Add(int, int) : int"
This method:
1. Normalizes names to base form ("Name", "Add")
2. Caches original names for rich information display
3. Populates LSP spec's 'detail' field with type/signature info
"""
symbols = super().request_document_symbols(relative_file_path, file_buffer)
# Normalize all symbols recursively
for symbol in symbols.iter_symbols():
self._normalize_symbol_name(symbol, relative_file_path)
return symbols
@override
def request_hover(self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None) -> Hover | None:
"""
@@ -230,12 +248,20 @@ class CSharpLanguageServer(SolidLanguageServer):
return hover
def _normalize_symbol_name(self, symbol: UnifiedSymbolInformation, relative_file_path: str) -> None:
"""
Normalize a single symbol's name and cache the original.
Processes children recursively.
"""
original_name = symbol.get("name", "")
def _document_symbols_cache_fingerprint(self) -> Hashable | None:
normalize_symbol_name_version = 1
return normalize_symbol_name_version
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
# Roslyn 5.5.0+ returns symbol names with type annotations:
# - Properties: "Name : string"
# - Methods: "Add(int, int) : int"
#
# This method:
# 1. Normalizes names to base form ("Name", "Add")
# 2. Caches original names for rich information display
# 3. Populates LSP spec's 'detail' field with type/signature info
original_name = symbol["name"]
# Extract base name and type/signature info
normalized_name, type_info = self._extract_base_name_and_type(original_name)
@@ -244,24 +270,18 @@ class CSharpLanguageServer(SolidLanguageServer):
if original_name != normalized_name:
sel_range = symbol.get("selectionRange")
if sel_range:
start = sel_range.get("start")
start = sel_range.get("start") # type: ignore
if start and "line" in start and "character" in start:
line = start["line"]
char = start["character"]
cache_key = (relative_file_path, line, char)
self._original_symbol_names[cache_key] = original_name
# Populate LSP spec's 'detail' field with type/signature information
# Populate 'detail' field with type/signature information (for UnifiedSymbolInformation)
if type_info and "detail" not in symbol:
symbol["detail"] = type_info
symbol["detail"] = type_info # type: ignore
# Update the symbol name
symbol["name"] = normalized_name
# Process children recursively
children = symbol.get("children", [])
for child in children:
self._normalize_symbol_name(child, relative_file_path)
return normalized_name
@staticmethod
def _extract_base_name_and_type(roslyn_name: str) -> tuple[str, str]:
@@ -347,9 +367,12 @@ class CSharpLanguageServer(SolidLanguageServer):
filtered_overrides.append(dep_override)
log.debug("Resolving runtime dependencies")
csharp_language_server_version = self._custom_settings.get(
"csharp_language_server_version", DEFAULT_CSHARP_LANGUAGE_SERVER_VERSION
)
runtime_dependencies = RuntimeDependencyCollection(
_RUNTIME_DEPENDENCIES,
_runtime_dependencies_for_version(csharp_language_server_version),
overrides=filtered_overrides,
)
@@ -433,27 +456,19 @@ class CSharpLanguageServer(SolidLanguageServer):
if url is None:
raise SolidLSPException(f"No URL specified for package {package_name} version {package_version}")
# Create temporary directory for package download
temp_dir = Path(self._ls_resources_dir) / "temp_downloads"
temp_dir.mkdir(parents=True, exist_ok=True)
try:
log.debug(f"Downloading package from: {url}")
# Download the .nupkg file
nupkg_file = temp_dir / f"{package_name}.{package_version}.nupkg"
urllib.request.urlretrieve(url, nupkg_file)
# Extract the .nupkg file (it's just a zip file)
package_extract_dir = temp_dir / f"{package_name}.{package_version}"
package_extract_dir.mkdir(exist_ok=True)
# Use SafeZipExtractor to handle long paths and skip errors
extractor = SafeZipExtractor(archive_path=nupkg_file, extract_dir=package_extract_dir, verbose=False)
extractor.extract_all()
# Clean up the nupkg file
nupkg_file.unlink()
FileUtils.download_and_extract_archive_verified(
url,
str(package_extract_dir),
"zip",
expected_sha256=dependency.sha256,
allowed_hosts=dependency.allowed_hosts,
)
log.info(f"Successfully downloaded and extracted {package_name} version {package_version} from NuGet.org")
return package_extract_dir
@@ -7,16 +7,22 @@ from solidlsp.ls import SolidLanguageServer
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from ..ls_config import LanguageServerConfig
from ..ls_config import Language, LanguageServerConfig
from ..lsp_protocol_handler.lsp_types import InitializeParams
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
DART_ALLOWED_HOSTS = ("storage.googleapis.com",)
class DartLanguageServer(SolidLanguageServer):
"""
Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart.
You can pass the following entries in ``ls_specific_settings["dart"]``:
- dart_sdk_version: Override the pinned Dart SDK version downloaded by Serena
(default: the bundled Serena version).
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
@@ -30,47 +36,59 @@ class DartLanguageServer(SolidLanguageServer):
@classmethod
def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str:
dart_settings = solidlsp_settings.get_ls_specific_settings(Language.DART)
dart_sdk_version = dart_settings.get("dart_sdk_version", "3.7.1")
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
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",
url=f"https://storage.googleapis.com/dart-archive/channels/stable/release/{dart_sdk_version}/sdk/dartsdk-linux-x64-release.zip",
platform_id="linux-x64",
archive_type="zip",
binary_name="dart-sdk/bin/dart",
sha256="2813959e7d9650334015b927cc533f5beadfbf7fa48248beec471f8942a0ee71" if dart_sdk_version == "3.7.1" else None,
allowed_hosts=DART_ALLOWED_HOSTS,
),
RuntimeDependency(
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",
url=f"https://storage.googleapis.com/dart-archive/channels/stable/release/{dart_sdk_version}/sdk/dartsdk-windows-x64-release.zip",
platform_id="win-x64",
archive_type="zip",
binary_name="dart-sdk/bin/dart.exe",
sha256="f56c03122e17abe5be1429eee0a975fb8ed511b6731ec90c6475992d3dee4ea5" if dart_sdk_version == "3.7.1" else None,
allowed_hosts=DART_ALLOWED_HOSTS,
),
RuntimeDependency(
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",
url=f"https://storage.googleapis.com/dart-archive/channels/stable/release/{dart_sdk_version}/sdk/dartsdk-windows-arm64-release.zip",
platform_id="win-arm64",
archive_type="zip",
binary_name="dart-sdk/bin/dart.exe",
sha256="fada411c6538d0ac24c35d6360767241f1298f64cbc5e88716387d54757a105a" if dart_sdk_version == "3.7.1" else None,
allowed_hosts=DART_ALLOWED_HOSTS,
),
RuntimeDependency(
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",
url=f"https://storage.googleapis.com/dart-archive/channels/stable/release/{dart_sdk_version}/sdk/dartsdk-macos-x64-release.zip",
platform_id="osx-x64",
archive_type="zip",
binary_name="dart-sdk/bin/dart",
sha256="a2765917b6ae49d1ac119553df9584989f9c441a46e8f18c129ba52489658d2e" if dart_sdk_version == "3.7.1" else None,
allowed_hosts=DART_ALLOWED_HOSTS,
),
RuntimeDependency(
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",
url=f"https://storage.googleapis.com/dart-archive/channels/stable/release/{dart_sdk_version}/sdk/dartsdk-macos-arm64-release.zip",
platform_id="osx-arm64",
archive_type="zip",
binary_name="dart-sdk/bin/dart",
sha256="f57c25163092bac818f8ca6250a0d8b2c56344c6a075a1bd7c60da7ac28b32a4" if dart_sdk_version == "3.7.1" else None,
allowed_hosts=DART_ALLOWED_HOSTS,
),
]
)
+108 -31
View File
@@ -25,6 +25,23 @@ from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
GRADLE_ALLOWED_HOSTS = ("services.gradle.org", "github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
GRADLE_SHA256 = "7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999"
VSCODE_JAVA_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
VSCODE_JAVA_SHA256_BY_PLATFORM = {
"osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933",
"osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f",
"linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1",
"linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9",
"win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3",
}
INTELLICODE_ALLOWED_HOSTS = (
"visualstudioexptteam.gallery.vsassets.io",
"marketplace.visualstudio.com",
"download.visualstudio.microsoft.com",
)
INTELLICODE_SHA256 = "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251"
@dataclasses.dataclass
class RuntimeDependencyPaths:
@@ -52,6 +69,13 @@ class EclipseJDTLS(SolidLanguageServer):
- gradle_wrapper_enabled: Whether to use the project's Gradle wrapper (default: false)
- gradle_java_home: Path to JDK for Gradle (default: null, uses bundled JRE)
- use_system_java_home: Whether to use the system's JAVA_HOME for JDTLS itself (default: false)
- jdtls_xmx: Maximum heap size for the JDTLS server JVM (default: "3G")
- jdtls_xms: Initial heap size for the JDTLS server JVM (default: "100m")
- intellicode_xmx: Maximum heap size for the IntelliCode embedded JVM (default: "1G")
- intellicode_xms: Initial heap size for the IntelliCode embedded JVM (default: "100m")
- gradle_version: Override the pinned Gradle distribution version downloaded by Serena
- vscode_java_version: Override the pinned vscode-java runtime bundle version downloaded by Serena
- intellicode_version: Override the pinned IntelliCode VSIX version downloaded by Serena
Example configuration in ~/.serena/serena_config.yml:
```yaml
@@ -64,6 +88,13 @@ class EclipseJDTLS(SolidLanguageServer):
gradle_wrapper_enabled: true # set to true for projects with custom plugins/repositories
gradle_java_home: "/path/to/jdk" # set to override Gradle's JDK
use_system_java_home: true # set to true to use system JAVA_HOME for JDTLS
jdtls_xmx: "3G" # maximum heap size for the JDTLS server JVM
jdtls_xms: "100m" # initial heap size for the JDTLS server JVM
intellicode_xmx: "1G" # maximum heap size for the IntelliCode embedded JVM
intellicode_xms: "100m" # initial heap size for the IntelliCode embedded JVM
gradle_version: "8.14.2"
vscode_java_version: "1.42.0-561"
intellicode_version: "1.2.30"
```
"""
@@ -115,33 +146,48 @@ class EclipseJDTLS(SolidLanguageServer):
super().__init__(custom_settings, ls_resources_dir)
self._solidlsp_settings = solidlsp_settings
self._repository_root_path = repository_root_path
self.runtime_dependency_paths = self._setup_runtime_dependencies(ls_resources_dir)
self.runtime_dependency_paths = self._setup_runtime_dependencies(ls_resources_dir, custom_settings)
@classmethod
def _setup_runtime_dependencies(cls, ls_resources_dir: str) -> RuntimeDependencyPaths:
@staticmethod
def _setup_runtime_dependencies(
ls_resources_dir: str, custom_settings: SolidLSPSettings.CustomLSSettings
) -> RuntimeDependencyPaths:
"""
Setup runtime dependencies for EclipseJDTLS and return the paths.
"""
platformId = PlatformUtils.get_platform_id()
gradle_version = custom_settings.get("gradle_version", "8.14.2")
vscode_java_version = custom_settings.get("vscode_java_version", "1.42.0-561")
vscode_java_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}"
intellicode_version = custom_settings.get("intellicode_version", "1.2.30")
default_gradle_version = gradle_version == "8.14.2"
default_vscode_java_version = vscode_java_version == "1.42.0-561"
default_intellicode_version = intellicode_version == "1.2.30"
runtime_dependencies = {
runtime_dependencies: dict[str, dict[str, dict[str, object]]] = {
"gradle": {
"platform-agnostic": {
"url": "https://services.gradle.org/distributions/gradle-8.14.2-bin.zip",
"url": f"https://services.gradle.org/distributions/gradle-{gradle_version}-bin.zip",
"archiveType": "zip",
"relative_extraction_path": ".",
"sha256": GRADLE_SHA256 if default_gradle_version else None,
"allowed_hosts": GRADLE_ALLOWED_HOSTS,
}
},
"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",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["osx-arm64"] if default_vscode_java_version else None,
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
},
"osx-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["osx-arm64"] if default_vscode_java_version else None,
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"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",
@@ -149,9 +195,11 @@ class EclipseJDTLS(SolidLanguageServer):
"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",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-x64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["osx-x64"] if default_vscode_java_version else None,
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"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",
@@ -159,9 +207,11 @@ class EclipseJDTLS(SolidLanguageServer):
"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",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["linux-arm64"] if default_vscode_java_version else None,
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": "extension/jre/21.0.7-linux-aarch64",
"jre_path": "extension/jre/21.0.7-linux-aarch64/bin/java",
"lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
@@ -169,9 +219,11 @@ class EclipseJDTLS(SolidLanguageServer):
"jdtls_readonly_config_path": "extension/server/config_linux_arm",
},
"linux-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-x64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["linux-x64"] if default_vscode_java_version else None,
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"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",
@@ -179,9 +231,11 @@ class EclipseJDTLS(SolidLanguageServer):
"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",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-win32-x64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": "vscode-java",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["win-x64"] if default_vscode_java_version else None,
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"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",
@@ -191,10 +245,12 @@ class EclipseJDTLS(SolidLanguageServer):
},
"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",
"url": f"https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/{intellicode_version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage",
"alternate_url": f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/{intellicode_version}/vspackage",
"archiveType": "zip",
"relative_extraction_path": "intellicode",
"sha256": INTELLICODE_SHA256 if default_intellicode_version else None,
"allowed_hosts": INTELLICODE_ALLOWED_HOSTS,
"intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar",
"intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members",
}
@@ -204,27 +260,30 @@ class EclipseJDTLS(SolidLanguageServer):
gradle_path = str(
PurePath(
ls_resources_dir,
"gradle-8.14.2",
f"gradle-{gradle_version}",
)
)
if not os.path.exists(gradle_path):
FileUtils.download_and_extract_archive(
runtime_dependencies["gradle"]["platform-agnostic"]["url"],
gradle_dependency = runtime_dependencies["gradle"]["platform-agnostic"]
FileUtils.download_and_extract_archive_verified(
cast(str, gradle_dependency["url"]),
str(PurePath(gradle_path).parent),
runtime_dependencies["gradle"]["platform-agnostic"]["archiveType"],
cast(str, gradle_dependency["archiveType"]),
expected_sha256=cast(str | None, gradle_dependency["sha256"]),
allowed_hosts=cast(tuple[str, ...], gradle_dependency["allowed_hosts"]),
)
assert os.path.exists(gradle_path)
dependency = runtime_dependencies["vscode-java"][platformId.value]
vscode_java_path = str(PurePath(ls_resources_dir, dependency["relative_extraction_path"]))
vscode_java_path = str(PurePath(ls_resources_dir, cast(str, 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"]))
jre_home_path = str(PurePath(vscode_java_path, cast(str, dependency["jre_home_path"])))
jre_path = str(PurePath(vscode_java_path, cast(str, dependency["jre_path"])))
lombok_jar_path = str(PurePath(vscode_java_path, cast(str, dependency["lombok_jar_path"])))
jdtls_launcher_jar_path = str(PurePath(vscode_java_path, cast(str, dependency["jdtls_launcher_jar_path"])))
jdtls_readonly_config_path = str(PurePath(vscode_java_path, cast(str, dependency["jdtls_readonly_config_path"])))
if not all(
[
os.path.exists(vscode_java_path),
@@ -235,7 +294,13 @@ class EclipseJDTLS(SolidLanguageServer):
os.path.exists(jdtls_readonly_config_path),
]
):
FileUtils.download_and_extract_archive(dependency["url"], vscode_java_path, dependency["archiveType"])
FileUtils.download_and_extract_archive_verified(
cast(str, dependency["url"]),
vscode_java_path,
cast(str, dependency["archiveType"]),
expected_sha256=cast(str | None, dependency["sha256"]),
allowed_hosts=cast(tuple[str, ...], dependency["allowed_hosts"]),
)
os.chmod(jre_path, 0o755)
@@ -247,10 +312,10 @@ class EclipseJDTLS(SolidLanguageServer):
assert os.path.exists(jdtls_readonly_config_path)
dependency = runtime_dependencies["intellicode"]["platform-agnostic"]
intellicode_directory_path = str(PurePath(ls_resources_dir, dependency["relative_extraction_path"]))
intellicode_directory_path = str(PurePath(ls_resources_dir, cast(str, 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"]))
intellicode_jar_path = str(PurePath(intellicode_directory_path, cast(str, dependency["intellicode_jar_path"])))
intellisense_members_path = str(PurePath(intellicode_directory_path, cast(str, dependency["intellisense_members_path"])))
if not all(
[
os.path.exists(intellicode_directory_path),
@@ -258,7 +323,13 @@ class EclipseJDTLS(SolidLanguageServer):
os.path.exists(intellisense_members_path),
]
):
FileUtils.download_and_extract_archive(dependency["url"], intellicode_directory_path, dependency["archiveType"])
FileUtils.download_and_extract_archive_verified(
cast(str, dependency["url"]),
intellicode_directory_path,
cast(str, dependency["archiveType"]),
expected_sha256=cast(str | None, dependency["sha256"]),
allowed_hosts=cast(tuple[str, ...], dependency["allowed_hosts"]),
)
assert os.path.exists(intellicode_directory_path)
assert os.path.exists(intellicode_jar_path)
@@ -298,6 +369,8 @@ class EclipseJDTLS(SolidLanguageServer):
lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path
jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path
jdtls_xmx = self._custom_settings.get("jdtls_xmx", "3G")
jdtls_xms = self._custom_settings.get("jdtls_xms", "100m")
data_dir = str(PurePath(ws_dir, "data_dir"))
jdtls_config_path = str(PurePath(ws_dir, "config_path"))
@@ -336,8 +409,8 @@ class EclipseJDTLS(SolidLanguageServer):
"-XX:AdaptiveSizePolicyWeight=90",
"-Dsun.zip.disableMemoryMapping=true",
"-Djava.lsp.joinOnCompletion=true",
"-Xmx3G",
"-Xms100m",
f"-Xmx{jdtls_xmx}",
f"-Xms{jdtls_xms}",
"-Xlog:disable",
"-Dlog.level=ALL",
f"-javaagent:{lombok_jar_path}",
@@ -421,6 +494,10 @@ class EclipseJDTLS(SolidLanguageServer):
gradle_user_home = None
log.info(f"Gradle user home not found at default location ({default_gradle_home}), will use JDTLS defaults")
# IntelliCode JVM settings (used in vmargs for the embedded JVM)
intellicode_xmx = self._custom_settings.get("intellicode_xmx", "1G")
intellicode_xms = self._custom_settings.get("intellicode_xms", "100m")
# Gradle wrapper: default to False to preserve existing behaviour
gradle_wrapper_enabled = self._custom_settings.get("gradle_wrapper_enabled", False)
log.info(
@@ -617,7 +694,7 @@ class EclipseJDTLS(SolidLanguageServer):
"jdt": {
"ls": {
"java": {"home": None},
"vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -Xlog:disable",
"vmargs": f"-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx{intellicode_xmx} -Xms{intellicode_xms} -Xlog:disable",
"lombokSupport": {"enabled": True},
"protobufSupport": {"enabled": True},
"androidSupport": {"enabled": True},
@@ -9,7 +9,7 @@ from typing import Any, cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -19,10 +19,21 @@ from ..common import RuntimeDependency
log = logging.getLogger(__name__)
EXPERT_VERSION = "v0.1.0-rc.6"
EXPERT_ALLOWED_HOSTS = (
"github.com",
"release-assets.githubusercontent.com",
"objects.githubusercontent.com",
)
class ElixirTools(SolidLanguageServer):
"""
Provides Elixir specific instantiation of the LanguageServer class using Expert, the official Elixir language server.
You can pass the following entries in ``ls_specific_settings["elixir"]``:
- expert_version: Override the pinned Expert version downloaded by Serena
(default: the bundled Serena version).
"""
@override
@@ -66,6 +77,8 @@ class ElixirTools(SolidLanguageServer):
Setup runtime dependencies for Expert.
Downloads the Expert binary for the current platform and returns the path to the executable.
"""
elixir_settings = solidlsp_settings.get_ls_specific_settings(Language.ELIXIR)
expert_version = elixir_settings.get("expert_version", EXPERT_VERSION)
# Check if Elixir is available first
elixir_version = cls._get_elixir_version()
if not elixir_version:
@@ -91,63 +104,62 @@ class ElixirTools(SolidLanguageServer):
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 Expert at the moment"
expert_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "expert")
EXPERT_VERSION = "nightly"
# Define runtime dependencies inline
runtime_deps = {
PlatformId.LINUX_x64: RuntimeDependency(
id="expert_linux_amd64",
platform_id="linux-x64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_linux_amd64",
url=f"https://github.com/elixir-lang/expert/releases/download/{expert_version}/expert_linux_amd64",
archive_type="binary",
binary_name="expert_linux_amd64",
extract_path="expert",
sha256="643a492ff972246668b0ca356a84c3d0a0f5feeae0ab5dc1b9a126876ed460e4" if expert_version == EXPERT_VERSION else None,
allowed_hosts=EXPERT_ALLOWED_HOSTS,
),
PlatformId.LINUX_arm64: RuntimeDependency(
id="expert_linux_arm64",
platform_id="linux-arm64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_linux_arm64",
url=f"https://github.com/elixir-lang/expert/releases/download/{expert_version}/expert_linux_arm64",
archive_type="binary",
binary_name="expert_linux_arm64",
extract_path="expert",
sha256="d8b830bdaa8991d7ebf255dacbb3674f3ea335c87d0bfba4b7f907ded4a8f014" if expert_version == EXPERT_VERSION else None,
allowed_hosts=EXPERT_ALLOWED_HOSTS,
),
PlatformId.OSX_x64: RuntimeDependency(
id="expert_darwin_amd64",
platform_id="osx-x64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_darwin_amd64",
url=f"https://github.com/elixir-lang/expert/releases/download/{expert_version}/expert_darwin_amd64",
archive_type="binary",
binary_name="expert_darwin_amd64",
extract_path="expert",
sha256="964f316f1633090b33aab392b6b85fb778c5fb3c0db862671424458da34b1d4d" if expert_version == EXPERT_VERSION else None,
allowed_hosts=EXPERT_ALLOWED_HOSTS,
),
PlatformId.OSX_arm64: RuntimeDependency(
id="expert_darwin_arm64",
platform_id="osx-arm64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_darwin_arm64",
url=f"https://github.com/elixir-lang/expert/releases/download/{expert_version}/expert_darwin_arm64",
archive_type="binary",
binary_name="expert_darwin_arm64",
extract_path="expert",
sha256="5fb5be151baedd635d99835cf3f9986afc9af6ae7b07bd001a1962f4298e45da" if expert_version == EXPERT_VERSION else None,
allowed_hosts=EXPERT_ALLOWED_HOSTS,
),
PlatformId.WIN_x64: RuntimeDependency(
id="expert_windows_amd64",
platform_id="win-x64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_windows_amd64.exe",
url=f"https://github.com/elixir-lang/expert/releases/download/{expert_version}/expert_windows_amd64.exe",
archive_type="binary",
binary_name="expert_windows_amd64.exe",
extract_path="expert.exe",
),
PlatformId.WIN_arm64: RuntimeDependency(
id="expert_windows_arm64",
platform_id="win-arm64",
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_windows_arm64.exe",
archive_type="binary",
binary_name="expert_windows_arm64.exe",
extract_path="expert.exe",
sha256="babee77d2653679021600b99c68d984d4463290cb221e0fc0d1093b3afdeb3b0" if expert_version == EXPERT_VERSION else None,
allowed_hosts=EXPERT_ALLOWED_HOSTS,
),
}
@@ -161,7 +173,12 @@ class ElixirTools(SolidLanguageServer):
if not os.path.exists(executable_path):
log.info(f"Downloading Expert binary from {dependency.url}")
assert dependency.url is not None
FileUtils.download_file(dependency.url, binary_path)
FileUtils.download_file_verified(
dependency.url,
binary_path,
expected_sha256=dependency.sha256,
allowed_hosts=dependency.allowed_hosts,
)
# Make the binary executable on Unix-like systems
if not platform_id.value.startswith("win"):
@@ -173,6 +190,10 @@ class ElixirTools(SolidLanguageServer):
os.remove(executable_path)
os.symlink(os.path.basename(binary_path), executable_path)
# normalizing the executable name on Windows
if binary_path != executable_path and platform_id.value.startswith("win"):
shutil.copy2(binary_path, executable_path)
assert os.path.exists(executable_path), f"Expert executable not found at {executable_path}"
log.info(f"Expert binary ready at: {executable_path}")
@@ -12,12 +12,12 @@ from overrides import override
from sensai.util.logging import LogTime
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
log = logging.getLogger(__name__)
@@ -76,13 +76,16 @@ class ElmLanguageServer(SolidLanguageServer):
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."
elm_config = solidlsp_settings.get_ls_specific_settings(Language.ELM)
elm_language_server_version = elm_config.get("elm_language_server_version", "2.8.0")
npm_registry = elm_config.get("npm_registry")
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="elm-language-server",
description="@elm-tooling/elm-language-server package",
command=["npm", "install", "--prefix", "./", "@elm-tooling/elm-language-server@2.8.0"],
command=build_npm_install_command("@elm-tooling/elm-language-server", elm_language_server_version, npm_registry),
platform_id="any",
),
]
@@ -14,7 +14,7 @@ from overrides import override
from serena.util.dotnet import DotNETUtil
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -22,11 +22,17 @@ from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
FSAUTOCOMPLETE_VERSION = "0.83.0"
class FSharpLanguageServer(SolidLanguageServer):
"""
Provides F# specific instantiation of the LanguageServer class using Ionide LSP (FsAutoComplete).
Contains various configurations and settings specific to F# development.
You can pass the following entries in ``ls_specific_settings["fsharp"]``:
- fsautocomplete_version: Override the pinned FsAutoComplete version
installed by Serena (default: the bundled Serena version).
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
@@ -62,6 +68,8 @@ class FSharpLanguageServer(SolidLanguageServer):
"""
Setup runtime dependencies for F# Language Server and return the command to start the server.
"""
fsharp_settings = solidlsp_settings.get_ls_specific_settings(Language.FSHARP)
fsautocomplete_version = fsharp_settings.get("fsautocomplete_version", FSAUTOCOMPLETE_VERSION)
dotnet_exe = DotNETUtil("8.0", allow_higher_version=True).get_dotnet_path_or_raise()
RuntimeDependencyCollection(
@@ -69,7 +77,7 @@ class FSharpLanguageServer(SolidLanguageServer):
RuntimeDependency(
id="fsautocomplete",
description="FsAutoComplete (Ionide F# Language Server)",
command="dotnet tool install --tool-path ./ fsautocomplete",
command=f"dotnet tool install --tool-path ./ fsautocomplete --version {fsautocomplete_version}",
platform_id="any",
),
]
@@ -94,7 +102,7 @@ class FSharpLanguageServer(SolidLanguageServer):
import subprocess
result = subprocess.run(
[dotnet_exe, "tool", "install", "--tool-path", fsharp_ls_dir, "fsautocomplete"],
[dotnet_exe, "tool", "install", "--tool-path", fsharp_ls_dir, "fsautocomplete", "--version", fsautocomplete_version],
cwd=fsharp_ls_dir,
capture_output=True,
text=True,
@@ -17,6 +17,15 @@ from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
VSCODE_JAVA_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
VSCODE_JAVA_SHA256_BY_PLATFORM = {
"win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3",
"linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9",
"linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1",
"osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f",
"osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933",
}
@dataclasses.dataclass
class GroovyRuntimeDependencyPaths:
@@ -34,6 +43,13 @@ class GroovyLanguageServer(SolidLanguageServer):
"""
Provides Groovy specific instantiation of the LanguageServer class.
Contains various configurations and settings specific to Groovy.
You can pass the following entries in ``ls_specific_settings["groovy"]``:
- ls_jar_path: Path to the Groovy Language Server JAR.
- ls_java_home_path: Optional Java home to use instead of Serena's managed JRE.
- ls_jar_options: Additional JVM/JAR options passed to the Groovy LS.
- vscode_java_version: Override the pinned vscode-java runtime bundle version
downloaded by Serena when it manages Java itself (default: the bundled Serena version).
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
@@ -76,6 +92,9 @@ class GroovyLanguageServer(SolidLanguageServer):
Setup runtime dependencies for Groovy Language Server and return paths.
"""
platform_id = PlatformUtils.get_platform_id()
groovy_settings = solidlsp_settings.get_ls_specific_settings(Language.GROOVY)
vscode_java_version = groovy_settings.get("vscode_java_version", "1.42.0-561")
vscode_java_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}"
# Verify platform support
assert platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-"), (
@@ -105,39 +124,52 @@ class GroovyLanguageServer(SolidLanguageServer):
runtime_dependencies = {
"java": {
"win-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-win32-x64-{vscode_java_version}.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",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["win-x64"] if vscode_java_version == "1.42.0-561" else None,
},
"linux-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-x64-{vscode_java_version}.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",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["linux-x64"] if vscode_java_version == "1.42.0-561" else None,
},
"linux-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"java_home_path": "extension/jre/21.0.7-linux-aarch64",
"java_path": "extension/jre/21.0.7-linux-aarch64/bin/java",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["linux-arm64"] if vscode_java_version == "1.42.0-561" else None,
},
"osx-x64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-x64-{vscode_java_version}.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",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["osx-x64"] if vscode_java_version == "1.42.0-561" else None,
},
"osx-arm64": {
"url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"java_home_path": "extension/jre/21.0.7-macosx-aarch64",
"java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java",
"sha256": VSCODE_JAVA_SHA256_BY_PLATFORM["osx-arm64"] if vscode_java_version == "1.42.0-561" else None,
},
},
}
java_dependency = runtime_dependencies["java"][platform_id.value]
java_home_relative_path = java_dependency["java_home_path"]
java_relative_path = java_dependency["java_path"]
java_download_url = java_dependency["url"]
java_archive_type = java_dependency["archiveType"]
assert java_home_relative_path is not None
assert java_relative_path is not None
assert java_download_url is not None
assert java_archive_type is not None
static_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "groovy_language_server")
os.makedirs(static_dir, exist_ok=True)
@@ -145,12 +177,18 @@ class GroovyLanguageServer(SolidLanguageServer):
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"])
java_home_path = os.path.join(java_dir, java_home_relative_path)
java_path = os.path.join(java_dir, java_relative_path)
if not os.path.exists(java_path):
log.info(f"Downloading Java for {platform_id.value}...")
FileUtils.download_and_extract_archive(java_dependency["url"], java_dir, java_dependency["archiveType"])
FileUtils.download_and_extract_archive_verified(
java_download_url,
java_dir,
java_archive_type,
expected_sha256=java_dependency["sha256"],
allowed_hosts=VSCODE_JAVA_ALLOWED_HOSTS,
)
if not platform_id.value.startswith("win-"):
os.chmod(java_path, 0o755)
@@ -24,12 +24,22 @@ log = logging.getLogger(__name__)
# GitHub release version to download when not installed locally
_DEFAULT_VERSION = "1.3.0"
_GITHUB_RELEASE_BASE = "https://github.com/antaalt/shader-sense/releases/download"
_HLSL_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
_HLSL_SHA256_BY_ASSET = {
"shader-language-server-x86_64-pc-windows-msvc.zip": "a945b000c296cdeebb9ee2d4452cec2a0f26544dd076bb08bfdcade2278296a6",
"shader-language-server-x86_64-unknown-linux-gnu.zip": "8c0a7b36f51cc58593762db3592ae13e21ca3cb982b2526cfaaf7c82e92ca089",
"shader-language-server-aarch64-pc-windows-msvc.zip": "cdbd7b41e71cf6040d5cdb7e211ba4b76671a404ee0f7add281d72d3ab8dfa65",
}
class HlslLanguageServer(SolidLanguageServer):
"""
Shader language server using shader-language-server.
Supports .hlsl, .hlsli, .fx, .fxh, .cginc, .compute, .shader, .glsl, .vert, .frag, .geom, .tesc, .tese, .comp, .wgsl files.
You can pass the following entries in ``ls_specific_settings["hlsl"]``:
- version: Override the pinned shader-language-server version downloaded
or built by Serena (default: the bundled Serena version).
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
@@ -53,7 +63,7 @@ class HlslLanguageServer(SolidLanguageServer):
base_url = f"{_GITHUB_RELEASE_BASE}/{tag}"
# macOS has no pre-built binaries; build from source via cargo install
cargo_install_cmd = f"cargo install shader_language_server --version {version} --root ."
cargo_install_cmd = ["cargo", "install", "shader_language_server", "--version", version, "--root", "."]
deps = RuntimeDependencyCollection(
[
@@ -64,6 +74,10 @@ class HlslLanguageServer(SolidLanguageServer):
platform_id="win-x64",
archive_type="zip",
binary_name="shader-language-server.exe",
sha256=_HLSL_SHA256_BY_ASSET["shader-language-server-x86_64-pc-windows-msvc.zip"]
if version == _DEFAULT_VERSION
else None,
allowed_hosts=_HLSL_ALLOWED_HOSTS,
),
RuntimeDependency(
id="shader-language-server",
@@ -72,6 +86,10 @@ class HlslLanguageServer(SolidLanguageServer):
platform_id="linux-x64",
archive_type="zip",
binary_name="shader-language-server",
sha256=_HLSL_SHA256_BY_ASSET["shader-language-server-x86_64-unknown-linux-gnu.zip"]
if version == _DEFAULT_VERSION
else None,
allowed_hosts=_HLSL_ALLOWED_HOSTS,
),
RuntimeDependency(
id="shader-language-server",
@@ -80,6 +98,10 @@ class HlslLanguageServer(SolidLanguageServer):
platform_id="win-arm64",
archive_type="zip",
binary_name="shader-language-server.exe",
sha256=_HLSL_SHA256_BY_ASSET["shader-language-server-aarch64-pc-windows-msvc.zip"]
if version == _DEFAULT_VERSION
else None,
allowed_hosts=_HLSL_ALLOWED_HOSTS,
),
RuntimeDependency(
id="shader-language-server",
@@ -17,7 +17,7 @@ from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams
from solidlsp.settings import SolidLSPSettings
from ..lsp_protocol_handler import lsp_types
from .common import RuntimeDependency, RuntimeDependencyCollection
from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
log = logging.getLogger(__name__)
@@ -59,6 +59,8 @@ class Intelephense(SolidLanguageServer):
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."
intelephense_version = self._custom_settings.get("intelephense_version", "1.14.4")
npm_registry = self._custom_settings.get("npm_registry")
# Install intelephense if not already installed
intelephense_ls_dir = os.path.join(self._ls_resources_dir, "php-lsp")
@@ -69,7 +71,7 @@ class Intelephense(SolidLanguageServer):
[
RuntimeDependency(
id="intelephense",
command="npm install --prefix ./ intelephense@1.14.4",
command=build_npm_install_command("intelephense", intelephense_version, npm_registry),
platform_id="any",
)
]
@@ -43,6 +43,14 @@ DEFAULT_KOTLIN_JVM_OPTIONS = "-Xmx2G"
# Default Kotlin Language Server version (can be overridden via ls_specific_settings)
DEFAULT_KOTLIN_LSP_VERSION = "261.13587.0"
KOTLIN_LSP_ALLOWED_HOSTS = ("download-cdn.jetbrains.com",)
KOTLIN_LSP_SHA256_BY_SUFFIX = {
"win-x64": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad",
"linux-x64": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec",
"linux-aarch64": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651",
"mac-x64": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3",
"mac-aarch64": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c",
}
# Platform-specific Kotlin LSP download suffixes
PLATFORM_KOTLIN_SUFFIX = {
@@ -111,8 +119,17 @@ class KotlinLanguageServer(SolidLanguageServer):
if not os.path.exists(kotlin_script):
kotlin_lsp_version = self._custom_settings.get("kotlin_lsp_version", DEFAULT_KOTLIN_LSP_VERSION)
kotlin_url = f"https://download-cdn.jetbrains.com/kotlin-lsp/{kotlin_lsp_version}/kotlin-lsp-{kotlin_lsp_version}-{kotlin_suffix}.zip"
expected_sha256 = None
if kotlin_lsp_version == DEFAULT_KOTLIN_LSP_VERSION:
expected_sha256 = KOTLIN_LSP_SHA256_BY_SUFFIX[kotlin_suffix]
log.info("Downloading Kotlin Language Server...")
FileUtils.download_and_extract_archive(kotlin_url, static_dir, "zip")
FileUtils.download_and_extract_archive_verified(
kotlin_url,
static_dir,
"zip",
expected_sha256=expected_sha256,
allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS,
)
if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"):
os.chmod(
+45 -38
View File
@@ -1,5 +1,9 @@
"""
Provides Lua specific instantiation of the LanguageServer class using lua-language-server.
You can pass the following entries in ``ls_specific_settings["lua"]``:
- lua_language_server_version: Override the pinned lua-language-server version
downloaded by Serena (default: the bundled Serena version).
"""
import logging
@@ -7,21 +11,29 @@ import os
import pathlib
import platform
import shutil
import tarfile
import zipfile
from pathlib import Path
import requests
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
LUA_LS_VERSION = "3.15.0"
LUA_LS_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
LUA_LS_SHA256_BY_ASSET = {
"lua-language-server-3.15.0-linux-x64.tar.gz": "4877b874c52fb7587707898da9026cc3a6c854d9bbab115ef49ac4e6a1b88007",
"lua-language-server-3.15.0-linux-arm64.tar.gz": "7dff8edfed4f34cf6325ff384791287d95f9a8dd9615a5279c7c6af81cf8c45d",
"lua-language-server-3.15.0-darwin-x64.tar.gz": "01d28a31e264434e51662814a68f584af068393caecfa158c4df5f7fdc3ca2f7",
"lua-language-server-3.15.0-darwin-arm64.tar.gz": "050f5f493f65112afc116e31281a9f73918546782d3696485dc052724838f58b",
"lua-language-server-3.15.0-win32-x64.zip": "76a10c05e8c947a448f00a61acead4240484cd1e2e8c66d54401c67d99b77535",
}
class LuaLanguageServer(SolidLanguageServer):
"""
@@ -37,7 +49,7 @@ class LuaLanguageServer(SolidLanguageServer):
return super().is_ignored_dirname(dirname) or dirname in [".luarocks", "lua_modules", "node_modules", "build", "dist", ".cache"]
@staticmethod
def _get_lua_ls_path() -> str | None:
def _get_lua_ls_path(solidlsp_settings: SolidLSPSettings | None = None) -> str | None:
"""Get the path to lua-language-server executable."""
# First check if it's in PATH
lua_ls = shutil.which("lua-language-server")
@@ -48,17 +60,24 @@ class LuaLanguageServer(SolidLanguageServer):
home = Path.home()
possible_paths = [
home / ".local" / "bin" / "lua-language-server",
home / ".serena" / "language_servers" / "lua" / "bin" / "lua-language-server",
Path("/usr/local/bin/lua-language-server"),
Path("/opt/lua-language-server/bin/lua-language-server"),
]
if solidlsp_settings is not None:
ls_resource_dir = Path(LuaLanguageServer.ls_resources_dir(solidlsp_settings)) / "lua"
possible_paths.extend(
[
ls_resource_dir / "bin" / "lua-language-server",
ls_resource_dir / "bin" / "lua-language-server.exe",
]
)
# Add Windows-specific paths
if platform.system() == "Windows":
possible_paths.extend(
[
home / "AppData" / "Local" / "lua-language-server" / "bin" / "lua-language-server.exe",
home / ".serena" / "language_servers" / "lua" / "bin" / "lua-language-server.exe",
]
)
@@ -69,11 +88,12 @@ class LuaLanguageServer(SolidLanguageServer):
return None
@staticmethod
def _download_lua_ls() -> str:
def _download_lua_ls(solidlsp_settings: SolidLSPSettings) -> str:
"""Download and install lua-language-server if not present."""
lua_settings = solidlsp_settings.get_ls_specific_settings(Language.LUA)
lua_ls_version = lua_settings.get("lua_language_server_version", LUA_LS_VERSION)
system = platform.system()
machine = platform.machine().lower()
lua_ls_version = "3.15.0"
# Map platform and architecture to download URL
if system == "Linux":
@@ -100,31 +120,18 @@ class LuaLanguageServer(SolidLanguageServer):
download_url = f"https://github.com/LuaLS/lua-language-server/releases/download/{lua_ls_version}/{download_name}"
# Create installation directory
install_dir = Path.home() / ".serena" / "language_servers" / "lua"
install_dir = Path(LuaLanguageServer.ls_resources_dir(solidlsp_settings)) / "lua"
install_dir.mkdir(parents=True, exist_ok=True)
# Download the file
print(f"Downloading lua-language-server from {download_url}...")
response = requests.get(download_url, stream=True)
response.raise_for_status()
# Save and extract
download_path = install_dir / download_name
with open(download_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Extracting lua-language-server to {install_dir}...")
if download_name.endswith(".tar.gz"):
with tarfile.open(download_path, "r:gz") as tar:
tar.extractall(install_dir)
elif download_name.endswith(".zip"):
with zipfile.ZipFile(download_path, "r") as zip_ref:
zip_ref.extractall(install_dir)
# Clean up download file
download_path.unlink()
log.info("Downloading lua-language-server from %s", download_url)
archive_type = "gztar" if download_name.endswith(".tar.gz") else "zip"
FileUtils.download_and_extract_archive_verified(
download_url,
str(install_dir),
archive_type,
expected_sha256=LUA_LS_SHA256_BY_ASSET.get(download_name) if lua_ls_version == LUA_LS_VERSION else None,
allowed_hosts=LUA_LS_ALLOWED_HOSTS,
)
# Make executable on Unix systems
if system != "Windows":
@@ -140,22 +147,22 @@ class LuaLanguageServer(SolidLanguageServer):
raise RuntimeError("Failed to find lua-language-server executable after extraction")
@staticmethod
def _setup_runtime_dependency() -> str:
def _setup_runtime_dependency(solidlsp_settings: SolidLSPSettings) -> str:
"""
Check if required Lua runtime dependencies are available.
Downloads lua-language-server if not present.
"""
lua_ls_path = LuaLanguageServer._get_lua_ls_path()
lua_ls_path = LuaLanguageServer._get_lua_ls_path(solidlsp_settings)
if not lua_ls_path:
print("lua-language-server not found. Downloading...")
lua_ls_path = LuaLanguageServer._download_lua_ls()
print(f"lua-language-server installed at: {lua_ls_path}")
log.info("lua-language-server not found. Downloading...")
lua_ls_path = LuaLanguageServer._download_lua_ls(solidlsp_settings)
log.info("lua-language-server installed at: %s", lua_ls_path)
return lua_ls_path
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
lua_ls_path = self._setup_runtime_dependency()
lua_ls_path = self._setup_runtime_dependency(solidlsp_settings)
super().__init__(
config, repository_root_path, ProcessLaunchInfo(cmd=lua_ls_path, cwd=repository_root_path), "lua", solidlsp_settings
+22 -21
View File
@@ -10,6 +10,8 @@ Requirements:
or it will be automatically downloaded from GitHub releases.
Advanced settings via ls_specific_settings["luau"]:
- luau_lsp_version: Override the pinned luau-lsp version downloaded by Serena
(default: the bundled Serena version)
- platform: "roblox" (default) or "standard"
- roblox_security_level: "None", "PluginSecurity" (default),
"LocalUserSecurity", or "RobloxScriptSecurity"
@@ -23,14 +25,13 @@ import pathlib
import platform
import shutil
import threading
import zipfile
from pathlib import Path
import requests
from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
@@ -38,6 +39,13 @@ log = logging.getLogger(__name__)
# Pin to a known stable release
LUAU_LSP_VERSION = "1.63.0"
LUAU_LSP_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
LUAU_LSP_SHA256_BY_ASSET = {
"luau-lsp-linux-x86_64.zip": "e4b633ad9a2c15437f60f9e721263f79aa0da606867d8458f0e159a325bf2db8",
"luau-lsp-linux-arm64.zip": "355be010f337a6772df6255c92e1fb28a59d194abe5c570453f4186472244355",
"luau-lsp-macos.zip": "01c1d6dd5fee27295b2968915dabb08c192192c46d9fe9c97bf31a130c96b8cb",
"luau-lsp-win64.zip": "eea596d47dc1c94a61ba1b78e6472bb4445bc3309780751515e6ab0a0abba57d",
}
# Luau built-in docs CDN
LUAU_DOCS_URL = "https://luau-lsp.pages.dev/api-docs/luau-en-us.json"
@@ -51,6 +59,7 @@ SUPPORTED_ROBLOX_SECURITY_LEVELS = {
"LocalUserSecurity",
"RobloxScriptSecurity",
}
LUAU_DOCS_ALLOWED_HOSTS = ("luau-lsp.pages.dev",)
class LuauLanguageServer(SolidLanguageServer):
@@ -96,24 +105,18 @@ class LuauLanguageServer(SolidLanguageServer):
if binary_path is not None:
return binary_path
luau_lsp_version = self._custom_settings.get("luau_lsp_version", LUAU_LSP_VERSION)
asset_name = self._get_luau_lsp_asset_name()
download_url = f"https://github.com/JohnnyMorganz/luau-lsp/releases/download/{LUAU_LSP_VERSION}/{asset_name}"
download_path = install_dir / asset_name
download_url = f"https://github.com/JohnnyMorganz/luau-lsp/releases/download/{luau_lsp_version}/{asset_name}"
log.info("Downloading luau-lsp %s from %s", LUAU_LSP_VERSION, download_url)
with requests.get(download_url, stream=True, timeout=60) as response:
response.raise_for_status()
with open(download_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
log.info("Extracting luau-lsp to %s", install_dir)
with zipfile.ZipFile(download_path, "r") as zip_ref:
zip_ref.extractall(install_dir)
if download_path.exists():
download_path.unlink()
log.info("Downloading luau-lsp %s from %s", luau_lsp_version, download_url)
FileUtils.download_and_extract_archive_verified(
download_url,
str(install_dir),
"zip",
expected_sha256=LUAU_LSP_SHA256_BY_ASSET.get(asset_name) if luau_lsp_version == LUAU_LSP_VERSION else None,
allowed_hosts=LUAU_LSP_ALLOWED_HOSTS,
)
binary_path = self._find_existing_binary(install_dir)
if binary_path is None:
@@ -164,9 +167,7 @@ class LuauLanguageServer(SolidLanguageServer):
try:
log.info("Downloading %s from %s", description, url)
response = requests.get(url, timeout=30)
response.raise_for_status()
path.write_bytes(response.content)
FileUtils.download_file_verified(url, str(path), allowed_hosts=LUAU_DOCS_ALLOWED_HOSTS)
return str(path)
except Exception as exc:
log.warning("Failed to download %s: %s", description, exc)
+63 -41
View File
@@ -26,57 +26,79 @@ from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
MARKSMAN_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
class Marksman(SolidLanguageServer):
"""
Provides Markdown specific instantiation of the LanguageServer class using marksman.
You can pass the following entries in ``ls_specific_settings["markdown"]``:
- marksman_version: Override the pinned Marksman release tag downloaded by
Serena (default: the bundled Serena version).
"""
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
marksman_releases = "https://github.com/artempyanykh/marksman/releases/download/2024-12-18"
runtime_dependencies = RuntimeDependencyCollection(
[
RuntimeDependency(
id="marksman",
url=f"{marksman_releases}/marksman-linux-x64",
platform_id="linux-x64",
archive_type="binary",
binary_name="marksman",
),
RuntimeDependency(
id="marksman",
url=f"{marksman_releases}/marksman-linux-arm64",
platform_id="linux-arm64",
archive_type="binary",
binary_name="marksman",
),
RuntimeDependency(
id="marksman",
url=f"{marksman_releases}/marksman-macos",
platform_id="osx-x64",
archive_type="binary",
binary_name="marksman",
),
RuntimeDependency(
id="marksman",
url=f"{marksman_releases}/marksman-macos",
platform_id="osx-arm64",
archive_type="binary",
binary_name="marksman",
),
RuntimeDependency(
id="marksman",
url=f"{marksman_releases}/marksman.exe",
platform_id="win-x64",
archive_type="binary",
binary_name="marksman.exe",
),
]
)
DEFAULT_MARKSMAN_VERSION = "2024-12-18"
@classmethod
def _runtime_dependencies(cls, version: str) -> RuntimeDependencyCollection:
marksman_releases = f"https://github.com/artempyanykh/marksman/releases/download/{version}"
default_version = version == cls.DEFAULT_MARKSMAN_VERSION
return RuntimeDependencyCollection(
[
RuntimeDependency(
id="marksman",
url=f"{marksman_releases}/marksman-linux-x64",
platform_id="linux-x64",
archive_type="binary",
binary_name="marksman",
sha256="b9cb666c643dfd9b699811fdfc445ed4c56be65c1d878c21d46847f0d7b0e475" if default_version else None,
allowed_hosts=MARKSMAN_ALLOWED_HOSTS,
),
RuntimeDependency(
id="marksman",
url=f"{marksman_releases}/marksman-linux-arm64",
platform_id="linux-arm64",
archive_type="binary",
binary_name="marksman",
sha256="b8d6972a56f3f9b7bbbf7c77ef8998e3b66fa82fb03c01398e224144486c9e73" if default_version else None,
allowed_hosts=MARKSMAN_ALLOWED_HOSTS,
),
RuntimeDependency(
id="marksman",
url=f"{marksman_releases}/marksman-macos",
platform_id="osx-x64",
archive_type="binary",
binary_name="marksman",
sha256="7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83" if default_version else None,
allowed_hosts=MARKSMAN_ALLOWED_HOSTS,
),
RuntimeDependency(
id="marksman",
url=f"{marksman_releases}/marksman-macos",
platform_id="osx-arm64",
archive_type="binary",
binary_name="marksman",
sha256="7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83" if default_version else None,
allowed_hosts=MARKSMAN_ALLOWED_HOSTS,
),
RuntimeDependency(
id="marksman",
url=f"{marksman_releases}/marksman.exe",
platform_id="win-x64",
archive_type="binary",
binary_name="marksman.exe",
sha256="39de9df039c8b0d627ac5918a9d8792ad20fc49e2461d1f5c906975c016799ec" if default_version else None,
allowed_hosts=MARKSMAN_ALLOWED_HOSTS,
),
]
)
def _get_or_install_core_dependency(self) -> str:
"""Setup runtime dependencies for marksman and return the command to start the server."""
deps = self.runtime_dependencies
marksman_version = self._custom_settings.get("marksman_version", self.DEFAULT_MARKSMAN_VERSION)
deps = self._runtime_dependencies(marksman_version)
dependency = deps.get_single_dep_for_current_platform()
marksman_ls_dir = self._ls_resources_dir
@@ -37,13 +37,11 @@ import pathlib
import platform
import shutil
import threading
import zipfile
from typing import Any, cast
import requests
from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation
from solidlsp.settings import SolidLSPSettings
@@ -52,10 +50,10 @@ log = logging.getLogger(__name__)
# Environment variable for MATLAB installation path
MATLAB_PATH_ENV_VAR = "MATLAB_PATH"
# VS Code Marketplace URL for MATLAB extension
MATLAB_EXTENSION_URL = (
"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/MathWorks/vsextensions/language-matlab/latest/vspackage"
)
MATLAB_EXTENSION_VERSION = "1.3.9"
MATLAB_EXTENSION_URL = f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/MathWorks/vsextensions/language-matlab/{MATLAB_EXTENSION_VERSION}/vspackage"
MATLAB_EXTENSION_SHA256 = "1da3add2c3a593fa0ebcdf1d15231faee8014de10f549c36915ab9d4f18390f2"
MATLAB_EXTENSION_ALLOWED_HOSTS = ("marketplace.visualstudio.com",)
class MatlabLanguageServer(SolidLanguageServer):
@@ -72,6 +70,8 @@ class MatlabLanguageServer(SolidLanguageServer):
You can pass the following entries in ls_specific_settings["matlab"]:
- matlab_path: Path to MATLAB installation (overrides MATLAB_PATH env var)
- matlab_extension_version: Override the pinned MathWorks VS Code extension
version downloaded by Serena (default: the bundled Serena version)
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
@@ -119,44 +119,14 @@ class MatlabLanguageServer(SolidLanguageServer):
"""
try:
log.info(f"Downloading MATLAB extension from {url}")
# Create target directory for the extension
os.makedirs(target_dir, exist_ok=True)
# Download with proper headers to mimic VS Code marketplace client
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/octet-stream, application/vsix, */*",
}
response = requests.get(url, headers=headers, stream=True, timeout=300)
response.raise_for_status()
# Save to temporary VSIX file
temp_file = os.path.join(target_dir, "matlab_extension_temp.vsix")
total_size = int(response.headers.get("content-length", 0))
log.info(f"Downloading {total_size / 1024 / 1024:.1f} MB...")
with open(temp_file, "wb") as f:
downloaded = 0
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0:
progress = (downloaded / total_size) * 100
log.info(f"Download progress: {progress:.1f}%")
log.info("Download complete, extracting...")
# Extract VSIX file (VSIX files are ZIP archives)
with zipfile.ZipFile(temp_file, "r") as zip_ref:
zip_ref.extractall(target_dir)
# Clean up temp file
os.remove(temp_file)
FileUtils.download_and_extract_archive_verified(
url,
target_dir,
"zip",
expected_sha256=MATLAB_EXTENSION_SHA256 if url == MATLAB_EXTENSION_URL else None,
allowed_hosts=MATLAB_EXTENSION_ALLOWED_HOSTS,
)
log.info("MATLAB extension extracted successfully")
return True
@@ -213,10 +183,15 @@ class MatlabLanguageServer(SolidLanguageServer):
"""
matlab_extension_dir = os.path.join(self._ls_resources_dir, "matlab-extension")
matlab_extension_version = self._custom_settings.get("matlab_extension_version", MATLAB_EXTENSION_VERSION)
matlab_extension_url = (
"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/MathWorks/"
f"vsextensions/language-matlab/{matlab_extension_version}/vspackage"
)
log.info(f"Downloading MATLAB extension from: {MATLAB_EXTENSION_URL}")
log.info(f"Downloading MATLAB extension from: {matlab_extension_url}")
if self._download_matlab_extension(MATLAB_EXTENSION_URL, matlab_extension_dir):
if self._download_matlab_extension(matlab_extension_url, matlab_extension_dir):
extension_path = os.path.join(matlab_extension_dir, "extension")
if os.path.exists(extension_path):
log.info("MATLAB extension downloaded and installed successfully")
+39 -3
View File
@@ -12,7 +12,7 @@ from collections.abc import Iterable
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
@@ -21,6 +21,10 @@ from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
OMNISHARP_ALLOWED_HOSTS = ("roslynomnisharp.blob.core.windows.net", "download.visualstudio.microsoft.com")
DEFAULT_OMNISHARP_VERSION = "1.39.10"
DEFAULT_RAZOR_OMNISHARP_VERSION = "7.0.0-preview.23363.1"
def breadth_first_file_scan(root: str) -> Iterable[str]:
"""
@@ -58,6 +62,10 @@ def find_least_depth_sln_file(root_dir: str) -> str | None:
class OmniSharp(SolidLanguageServer):
"""
Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#.
You can pass the following entries in ``ls_specific_settings["csharp_omnisharp"]``:
- omnisharp_version: Override the pinned OmniSharp version downloaded by Serena.
- razor_omnisharp_version: Override the pinned Razor plugin version downloaded by Serena.
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
@@ -150,6 +158,22 @@ class OmniSharp(SolidLanguageServer):
with open(os.path.join(os.path.dirname(__file__), "omnisharp", "runtime_dependencies.json"), encoding="utf-8") as f:
d = json.load(f)
del d["_description"]
omnisharp_settings = solidlsp_settings.get_ls_specific_settings(Language.CSHARP_OMNISHARP)
omnisharp_version = omnisharp_settings.get("omnisharp_version", DEFAULT_OMNISHARP_VERSION)
razor_omnisharp_version = omnisharp_settings.get("razor_omnisharp_version", DEFAULT_RAZOR_OMNISHARP_VERSION)
for dependency in d["runtimeDependencies"]:
if dependency["id"] == "OmniSharp":
dependency["url"] = dependency["url"].replace(DEFAULT_OMNISHARP_VERSION, omnisharp_version)
if "installPath" in dependency:
dependency["installPath"] = dependency["installPath"].replace(DEFAULT_OMNISHARP_VERSION, omnisharp_version)
if "installTestPath" in dependency:
dependency["installTestPath"] = dependency["installTestPath"].replace(DEFAULT_OMNISHARP_VERSION, omnisharp_version)
if omnisharp_version != DEFAULT_OMNISHARP_VERSION:
dependency["integrity"] = None
elif dependency["id"] == "RazorOmnisharp":
dependency["url"] = dependency["url"].replace(DEFAULT_RAZOR_OMNISHARP_VERSION, razor_omnisharp_version)
if razor_omnisharp_version != DEFAULT_RAZOR_OMNISHARP_VERSION:
dependency["integrity"] = None
assert platform_id in [
PlatformId.LINUX_x64,
@@ -186,7 +210,13 @@ class OmniSharp(SolidLanguageServer):
omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "OmniSharp")
if not os.path.exists(omnisharp_ls_dir):
os.makedirs(omnisharp_ls_dir)
FileUtils.download_and_extract_archive(runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip")
FileUtils.download_and_extract_archive_verified(
runtime_dependencies["OmniSharp"]["url"],
omnisharp_ls_dir,
"zip",
expected_sha256=runtime_dependencies["OmniSharp"].get("integrity"),
allowed_hosts=OMNISHARP_ALLOWED_HOSTS,
)
omnisharp_executable_path = os.path.join(omnisharp_ls_dir, runtime_dependencies["OmniSharp"]["binaryName"])
assert os.path.exists(omnisharp_executable_path)
os.chmod(omnisharp_executable_path, 0o755)
@@ -194,7 +224,13 @@ class OmniSharp(SolidLanguageServer):
razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RazorOmnisharp")
if not os.path.exists(razor_omnisharp_ls_dir):
os.makedirs(razor_omnisharp_ls_dir)
FileUtils.download_and_extract_archive(runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip")
FileUtils.download_and_extract_archive_verified(
runtime_dependencies["RazorOmnisharp"]["url"],
razor_omnisharp_ls_dir,
"zip",
expected_sha256=runtime_dependencies["RazorOmnisharp"].get("integrity"),
allowed_hosts=OMNISHARP_ALLOWED_HOSTS,
)
razor_omnisharp_dll_path = os.path.join(razor_omnisharp_ls_dir, runtime_dependencies["RazorOmnisharp"]["dll_path"])
assert os.path.exists(razor_omnisharp_dll_path)
@@ -310,7 +310,8 @@
"x86_64"
],
"platformId": "win-x64",
"dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll"
"dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll",
"integrity": "8D2DC5484018A2390A2366D05CADDD2BE21F619122DACD1BA1A87815B98D7361"
},
{
"id": "RazorOmnisharp",
@@ -324,7 +325,8 @@
"x86"
],
"platformId": "win-x86",
"dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll"
"dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll",
"integrity": "94C365B9D4B8D17612AF3574A3ABFB8CA75727866A8AF892EEEBE8F7559729A7"
},
{
"id": "RazorOmnisharp",
@@ -338,7 +340,8 @@
"arm64"
],
"platformId": "win-arm64",
"dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll"
"dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll",
"integrity": "2186E17AB20621C13A5B04F7053BDC744A9938AF6F9FA6259208F09F55999946"
},
{
"id": "RazorOmnisharp",
@@ -355,7 +358,8 @@
"./rzls"
],
"platformId": "linux-x64",
"dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll"
"dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll",
"integrity": "4B08C47D70AA96BE40AEDE22CDFCD909EB9073F1E1992E2B5B99D9D7A47F3276"
},
{
"id": "RazorOmnisharp",
@@ -371,7 +375,8 @@
"binaries": [
"./rzls"
],
"platformId": "linux-arm64"
"platformId": "linux-arm64",
"integrity": "A73FA7D0AC5E4DAC9FF15B9B675D90E30DFB45E40A66ECC70E22433EE028791C"
},
{
"id": "RazorOmnisharp",
@@ -387,7 +392,8 @@
"binaries": [
"./rzls"
],
"platformId": "linux-musl-x64"
"platformId": "linux-musl-x64",
"integrity": "7B0F8B1FEAC7726A5AC8D2C19F19B3530615F9226F75BB40D74EB0D8757CD11C"
},
{
"id": "RazorOmnisharp",
@@ -403,7 +409,8 @@
"binaries": [
"./rzls"
],
"platformId": "linux-musl-arm64"
"platformId": "linux-musl-arm64",
"integrity": "8BC53D60D53B7FC9E40144D862B0748BCB8768C7C18C8E6900D675DED14ABD08"
},
{
"id": "RazorOmnisharp",
@@ -419,7 +426,8 @@
"binaries": [
"./rzls"
],
"platformId": "osx-x64"
"platformId": "osx-x64",
"integrity": "7678022CBAC975A6D45E01A2985B88307E88F110578F3C04DB9D1C545BC69BDB"
},
{
"id": "RazorOmnisharp",
@@ -435,7 +443,8 @@
"binaries": [
"./rzls"
],
"platformId": "osx-arm64"
"platformId": "osx-arm64",
"integrity": "0A68EE21F5B8DDFDFC8D5E86E519C54D29B3A4D582F4D4284545C40A741AAFE6"
}
]
}
}
+37 -22
View File
@@ -4,21 +4,24 @@ Contains various configurations and settings specific to Pascal and Free Pascal.
pasls installation strategy:
1. Use existing pasls from PATH
2. Download prebuilt binary from GitHub releases (auto-updated)
2. Download a pinned prebuilt binary from GitHub releases
Supported platforms for binary download:
- linux-x64, linux-arm64
- osx-x64, osx-arm64
- win-x64
Auto-update features:
- Checks for updates every 24 hours via GitHub API
Integrity features:
- SHA256 checksum verification before installation
- Atomic update with rollback on failure
- Windows file locking detection
You can pass the following entries in ls_specific_settings["pascal"]:
Version management:
- pasls_version: Override the pinned pasls version downloaded by Serena
(default: the bundled Serena version).
Environment variables (recommended for CodeTools configuration):
- pp: Path to FPC compiler driver, must be "fpc.exe" (e.g., "D:/laz32/fpc/bin/i386-win32/fpc.exe").
Do NOT use backend compilers like ppc386.exe or ppcx64.exe - CodeTools queries fpc.exe for
@@ -59,23 +62,27 @@ import zipfile
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, quote_windows_path
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
PASLS_VERSION = "v0.2.0"
class PascalLanguageServer(SolidLanguageServer):
"""
Provides Pascal specific instantiation of the LanguageServer class using pasls.
Contains various configurations and settings specific to Free Pascal and Lazarus.
Supports overriding the bundled pasls version via ``pasls_version``.
"""
# URL configuration
PASLS_RELEASES_URL = "https://github.com/zen010101/pascal-language-server/releases/latest/download"
PASLS_API_URL = "https://api.github.com/repos/zen010101/pascal-language-server/releases/latest"
PASLS_VERSION = PASLS_VERSION
PASLS_RELEASES_URL = f"https://github.com/zen010101/pascal-language-server/releases/download/{PASLS_VERSION}"
PASLS_API_URL = f"https://api.github.com/repos/zen010101/pascal-language-server/releases/tags/{PASLS_VERSION}"
# Update check interval (seconds)
UPDATE_CHECK_INTERVAL = 86400 # 24 hours
@@ -523,21 +530,18 @@ class PascalLanguageServer(SolidLanguageServer):
return False
# 3. Verify SHA256 checksum (critical security step, before extraction)
if checksums:
expected_sha256 = checksums.get(archive_filename)
if expected_sha256:
log.info(f"Verifying SHA256 checksum for {archive_filename}...")
if not cls._verify_checksum(archive_path, expected_sha256):
log.error(f"SHA256 checksum verification FAILED for {archive_filename}")
log.error("Aborting installation due to checksum mismatch - possible security issue!")
try:
os.remove(archive_path)
except OSError:
pass
return False
log.info("SHA256 checksum verified successfully")
else:
log.warning(f"No checksum found for {archive_filename} in checksums file")
expected_sha256 = checksums.get(archive_filename) if checksums else dep.sha256
if expected_sha256:
log.info(f"Verifying SHA256 checksum for {archive_filename}...")
if not cls._verify_checksum(archive_path, expected_sha256):
log.error(f"SHA256 checksum verification FAILED for {archive_filename}")
log.error("Aborting installation due to checksum mismatch - possible security issue!")
try:
os.remove(archive_path)
except OSError:
pass
return False
log.info("SHA256 checksum verified successfully")
else:
log.warning("No checksums available - skipping verification (not recommended for production)")
@@ -607,12 +611,18 @@ class PascalLanguageServer(SolidLanguageServer):
def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str:
"""
Setup runtime dependencies for Pascal Language Server (pasls).
Automatically checks for updates every 24 hours with security verification.
Downloads the pinned Serena-supported pasls release with checksum verification.
Returns:
str: The command to start the pasls server
"""
pascal_settings = solidlsp_settings.get_ls_specific_settings(Language.PASCAL)
pasls_version = pascal_settings.get("pasls_version", PASLS_VERSION)
cls.PASLS_VERSION = pasls_version
cls.PASLS_RELEASES_URL = f"https://github.com/zen010101/pascal-language-server/releases/download/{pasls_version}"
cls.PASLS_API_URL = f"https://api.github.com/repos/zen010101/pascal-language-server/releases/tags/{pasls_version}"
# Check if pasls is already in PATH
pasls_in_path = shutil.which("pasls")
if pasls_in_path:
@@ -637,6 +647,7 @@ class PascalLanguageServer(SolidLanguageServer):
platform_id="linux-x64",
archive_type="gztar",
binary_name="pasls",
sha256="517259395b0a385a5e848cf48b967645a984be3dd456118bc08771283a822a5b",
),
RuntimeDependency(
id="PascalLanguageServer",
@@ -645,6 +656,7 @@ class PascalLanguageServer(SolidLanguageServer):
platform_id="linux-arm64",
archive_type="gztar",
binary_name="pasls",
sha256="cb4986941cfdcf9cb74ece6bbb53a443390a908e880108a37f4ccf82b2d6c502",
),
RuntimeDependency(
id="PascalLanguageServer",
@@ -653,6 +665,7 @@ class PascalLanguageServer(SolidLanguageServer):
platform_id="osx-x64",
archive_type="zip",
binary_name="pasls",
sha256="0abfcd98f63f77dba74094339a40d4407b69317c1c77b13a26b9b7dbdfd885f1",
),
RuntimeDependency(
id="PascalLanguageServer",
@@ -661,6 +674,7 @@ class PascalLanguageServer(SolidLanguageServer):
platform_id="osx-arm64",
archive_type="zip",
binary_name="pasls",
sha256="d4c2411e406af96ceae12b11e77fdb0c684ca15a68bfd8b4f9c6fe1fbdf515a7",
),
RuntimeDependency(
id="PascalLanguageServer",
@@ -669,6 +683,7 @@ class PascalLanguageServer(SolidLanguageServer):
platform_id="win-x64",
archive_type="zip",
binary_name="pasls.exe",
sha256="1493c31552e6f90a59800b2d44669e01fc4551d3647f3cea5dd105a9f6bc73e5",
),
]
)
+14 -2
View File
@@ -22,6 +22,8 @@ log = logging.getLogger(__name__)
PHPACTOR_VERSION = "2025.12.21.1"
PHPACTOR_PHAR_URL = f"https://github.com/phpactor/phpactor/releases/download/{PHPACTOR_VERSION}/phpactor.phar"
PHPACTOR_PHAR_SHA256 = "53bbe9625cd9b5e9b394bc2f595fbad13dbbe6dfc96950c56dea3b5d9a246cc3"
PHPACTOR_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
class PhpactorServer(SolidLanguageServer):
@@ -33,6 +35,8 @@ class PhpactorServer(SolidLanguageServer):
You can pass the following entries in ls_specific_settings["php_phpactor"]:
- ignore_vendor: whether to ignore directories named "vendor" (default: true)
- phpactor_version: Override the pinned Phpactor PHAR version downloaded by
Serena (default: the bundled Serena version)
"""
@override
@@ -44,6 +48,8 @@ class PhpactorServer(SolidLanguageServer):
"""
Setup runtime dependencies for Phpactor and return the path to the PHAR file.
"""
phpactor_version = self._custom_settings.get("phpactor_version", PHPACTOR_VERSION)
phpactor_phar_url = f"https://github.com/phpactor/phpactor/releases/download/{phpactor_version}/phpactor.phar"
# Verify PHP is installed
php_path = shutil.which("php")
assert php_path is not None, (
@@ -65,8 +71,14 @@ class PhpactorServer(SolidLanguageServer):
phpactor_phar_path = os.path.join(self._ls_resources_dir, "phpactor.phar")
if not os.path.exists(phpactor_phar_path):
os.makedirs(self._ls_resources_dir, exist_ok=True)
log.info(f"Downloading phpactor PHAR from {PHPACTOR_PHAR_URL}")
FileUtils.download_and_extract_archive(PHPACTOR_PHAR_URL, phpactor_phar_path, "binary")
log.info(f"Downloading phpactor PHAR from {phpactor_phar_url}")
FileUtils.download_and_extract_archive_verified(
phpactor_phar_url,
phpactor_phar_path,
"binary",
expected_sha256=PHPACTOR_PHAR_SHA256 if phpactor_version == PHPACTOR_VERSION else None,
allowed_hosts=PHPACTOR_ALLOWED_HOSTS,
)
assert os.path.exists(phpactor_phar_path), f"phpactor PHAR not found at {phpactor_phar_path}, download may have failed."
@@ -1,6 +1,10 @@
"""
Provides PowerShell specific instantiation of the LanguageServer class using PowerShell Editor Services.
Contains various configurations and settings specific to PowerShell scripting.
You can pass the following entries in ``ls_specific_settings["powershell"]``:
- pses_version: Override the pinned PowerShell Editor Services version
downloaded by Serena (default: the bundled Serena version).
"""
import logging
@@ -10,14 +14,13 @@ import platform
import shutil
import tempfile
import threading
import zipfile
from pathlib import Path
import requests
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -26,6 +29,8 @@ log = logging.getLogger(__name__)
# PowerShell Editor Services version to download
PSES_VERSION = "4.4.0"
PSES_SHA256 = "690b91092989a0f66e6f43986166aaef69d64b559a9fda51feed882e1103fbcc"
PSES_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
class PowerShellLanguageServer(SolidLanguageServer):
@@ -98,31 +103,24 @@ class PowerShellLanguageServer(SolidLanguageServer):
@classmethod
def _download_pses(cls, solidlsp_settings: SolidLSPSettings) -> str:
"""Download and install PowerShell Editor Services."""
ps_settings = solidlsp_settings.get_ls_specific_settings(Language.POWERSHELL)
pses_version = ps_settings.get("pses_version", PSES_VERSION)
download_url = (
f"https://github.com/PowerShell/PowerShellEditorServices/releases/download/v{PSES_VERSION}/PowerShellEditorServices.zip"
f"https://github.com/PowerShell/PowerShellEditorServices/releases/download/v{pses_version}/PowerShellEditorServices.zip"
)
# Create installation directory
install_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "powershell"
install_dir.mkdir(parents=True, exist_ok=True)
# Download the file
log.info(f"Downloading PowerShell Editor Services from {download_url}...")
response = requests.get(download_url, stream=True, timeout=120)
response.raise_for_status()
# Save the zip file
zip_path = install_dir / "PowerShellEditorServices.zip"
with open(zip_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
log.info(f"Extracting PowerShell Editor Services to {install_dir}...")
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(install_dir)
# Clean up zip file
zip_path.unlink()
FileUtils.download_and_extract_archive_verified(
download_url,
str(install_dir),
"zip",
expected_sha256=PSES_SHA256 if pses_version == PSES_VERSION else None,
allowed_hosts=PSES_ALLOWED_HOSTS,
)
start_script = install_dir / "PowerShellEditorServices" / "Start-EditorServices.ps1"
if not start_script.exists():
+26 -7
View File
@@ -1,6 +1,11 @@
"""
Ruby LSP Language Server implementation using Shopify's ruby-lsp.
Provides modern Ruby language server capabilities with improved performance.
You can pass the following entries in ``ls_specific_settings["ruby"]``:
- ruby_lsp_version: Override the pinned ruby-lsp gem version installed by
Serena when no project-local or global ruby-lsp is already available
(default: the bundled Serena version).
"""
import json
@@ -14,18 +19,21 @@ import threading
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
RUBY_LSP_VERSION = "0.26.8"
class RubyLsp(SolidLanguageServer):
"""
Provides Ruby specific instantiation of the LanguageServer class using ruby-lsp.
Contains various configurations and settings specific to Ruby with modern LSP features.
Supports overriding the bundled gem version via ``ruby_lsp_version``.
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
@@ -33,7 +41,7 @@ class RubyLsp(SolidLanguageServer):
Creates a RubyLsp instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
ruby_lsp_executable = self._setup_runtime_dependencies(config, repository_root_path)
ruby_lsp_executable = self._setup_runtime_dependencies(config, repository_root_path, solidlsp_settings)
super().__init__(
config, repository_root_path, ProcessLaunchInfo(cmd=ruby_lsp_executable, cwd=repository_root_path), "ruby", solidlsp_settings
)
@@ -92,11 +100,15 @@ class RubyLsp(SolidLanguageServer):
return shutil.which(executable_name)
@staticmethod
def _setup_runtime_dependencies(config: LanguageServerConfig, repository_root_path: str) -> list[str]:
def _setup_runtime_dependencies(
config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings
) -> list[str]:
"""
Setup runtime dependencies for ruby-lsp and return the command list to start the server.
Installation strategy: Bundler project > global ruby-lsp > gem install ruby-lsp
Installation strategy: Bundler project > global ruby-lsp > gem install ruby-lsp at the pinned version
"""
ls_specific_settings = solidlsp_settings.get_ls_specific_settings(Language.RUBY)
ruby_lsp_version = ls_specific_settings.get("ruby_lsp_version", RUBY_LSP_VERSION)
# Detect rbenv-managed Ruby environment
# When .ruby-version exists, it indicates the project uses rbenv for version management.
# rbenv automatically reads .ruby-version to determine which Ruby version to use.
@@ -215,7 +227,12 @@ class RubyLsp(SolidLanguageServer):
# Try to install ruby-lsp globally
log.info("ruby-lsp not found, attempting to install globally...")
try:
subprocess.run(["gem", "install", "ruby-lsp"], check=True, capture_output=True, cwd=repository_root_path)
subprocess.run(
["gem", "install", "ruby-lsp", "-v", ruby_lsp_version],
check=True,
capture_output=True,
cwd=repository_root_path,
)
log.info("Successfully installed ruby-lsp globally")
# Find the newly installed ruby-lsp executable
ruby_lsp_path = RubyLsp._find_executable_with_extensions("ruby-lsp")
@@ -226,9 +243,11 @@ class RubyLsp(SolidLanguageServer):
raise RuntimeError(
f"Failed to install ruby-lsp globally: {error_msg}\n"
"For Bundler projects, please add 'gem \"ruby-lsp\"' to your Gemfile and run 'bundle install'.\n"
"Alternatively, install globally: gem install ruby-lsp"
f"Alternatively, install globally: gem install ruby-lsp -v {ruby_lsp_version}"
) from e
raise RuntimeError(f"Failed to install ruby-lsp: {error_msg}\nPlease try installing manually: gem install ruby-lsp") from e
raise RuntimeError(
f"Failed to install ruby-lsp: {error_msg}\nPlease try installing manually: gem install ruby-lsp -v {ruby_lsp_version}"
) from e
@staticmethod
def _detect_rails_project(repository_root_path: str) -> bool:
@@ -12,7 +12,7 @@ import threading
from time import sleep
from typing import Any
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
@@ -69,13 +69,19 @@ class SolidityLanguageServer(SolidLanguageServer):
assert is_node_installed, "node is not installed or isn't in PATH. Please install Node.js 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."
solidity_language_server_version = self._custom_settings.get("solidity_language_server_version", "0.8.4")
npm_registry = self._custom_settings.get("npm_registry")
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="solidity-language-server",
description="Nomic Foundation Solidity Language Server",
command="npm install --prefix ./ @nomicfoundation/solidity-language-server@0.8.4",
command=build_npm_install_command(
"@nomicfoundation/solidity-language-server",
solidity_language_server_version,
npm_registry,
),
platform_id="any",
),
]
@@ -18,11 +18,17 @@ from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
VERIBLE_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
class SystemVerilogLanguageServer(SolidLanguageServer):
"""
SystemVerilog language server using verible-verilog-ls.
Supports .sv, .svh, .v, .vh files.
You can pass the following entries in ``ls_specific_settings["systemverilog"]``:
- verible_version: Override the pinned Verible release version downloaded
by Serena (default: the bundled Serena version).
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
@@ -67,6 +73,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer):
platform_id="linux-x64",
archive_type="gztar",
binary_name=f"verible-{verible_version}/bin/verible-verilog-ls",
sha256="f52e5920ef63f70620a6086e09dea8bd778147cd7a9ff827bb7de5d6316b1754",
allowed_hosts=VERIBLE_ALLOWED_HOSTS,
),
RuntimeDependency(
id="verible-ls",
@@ -75,6 +83,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer):
platform_id="linux-arm64",
archive_type="gztar",
binary_name=f"verible-{verible_version}/bin/verible-verilog-ls",
sha256="30dd9c6f6e0f4840d6ba0c9e81ea2774a50b5a1a523a855245f9a9b4beb6b58b",
allowed_hosts=VERIBLE_ALLOWED_HOSTS,
),
RuntimeDependency(
id="verible-ls",
@@ -83,6 +93,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer):
platform_id="osx-x64",
archive_type="gztar",
binary_name=f"verible-{verible_version}/bin/verible-verilog-ls",
sha256="9ef92e9ad345285dd593763e10ca61c8532fcf47bbb6cf4448f9a9423882d662",
allowed_hosts=VERIBLE_ALLOWED_HOSTS,
),
RuntimeDependency(
id="verible-ls",
@@ -91,6 +103,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer):
platform_id="osx-arm64",
archive_type="gztar",
binary_name=f"verible-{verible_version}/bin/verible-verilog-ls",
sha256="9ef92e9ad345285dd593763e10ca61c8532fcf47bbb6cf4448f9a9423882d662",
allowed_hosts=VERIBLE_ALLOWED_HOSTS,
),
RuntimeDependency(
id="verible-ls",
@@ -99,6 +113,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer):
platform_id="win-x64",
archive_type="zip",
binary_name=f"verible-{verible_version}/bin/verible-verilog-ls.exe",
sha256="729aa244036da4a4f87bc026d33555456fc7f7be79778d983ebe9c893f4a0ca3",
allowed_hosts=VERIBLE_ALLOWED_HOSTS,
),
]
)
+27 -72
View File
@@ -1,25 +1,22 @@
"""
Provides TOML specific instantiation of the LanguageServer class using Taplo.
Contains various configurations and settings specific to TOML files.
You can pass the following entries in ``ls_specific_settings["toml"]``:
- taplo_version: Override the pinned Taplo version downloaded by Serena
(default: the bundled Serena version).
"""
import gzip
import hashlib
import logging
import os
import platform
import shutil
import socket
import stat
import urllib.request
from typing import Any
# Download timeout in seconds (prevents indefinite hangs)
DOWNLOAD_TIMEOUT_SECONDS = 120
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PathUtils
from solidlsp.ls_utils import FileUtils, PathUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
@@ -28,6 +25,7 @@ log = logging.getLogger(__name__)
# Taplo release version and download URLs
TAPLO_VERSION = "0.10.0"
TAPLO_DOWNLOAD_BASE = f"https://github.com/tamasfe/taplo/releases/download/{TAPLO_VERSION}"
TAPLO_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
# SHA256 checksums for Taplo releases (verified from official GitHub releases)
# Source: https://github.com/tamasfe/taplo/releases/tag/0.10.0
@@ -43,17 +41,7 @@ TAPLO_SHA256_CHECKSUMS: dict[str, str] = {
}
def _verify_sha256(file_path: str, expected_hash: str) -> bool:
"""Verify SHA256 checksum of a downloaded file."""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256_hash.update(chunk)
actual_hash = sha256_hash.hexdigest()
return actual_hash.lower() == expected_hash.lower()
def _get_taplo_download_url() -> tuple[str, str]:
def _get_taplo_download_url(version: str = TAPLO_VERSION) -> tuple[str, str]:
"""
Get the appropriate Taplo download URL for the current platform.
@@ -88,7 +76,7 @@ def _get_taplo_download_url() -> tuple[str, str]:
filename = f"taplo-linux-{arch}.gz"
executable = "taplo"
return f"{TAPLO_DOWNLOAD_BASE}/{filename}", executable
return f"https://github.com/tamasfe/taplo/releases/download/{version}/{filename}", executable
class TaploServer(SolidLanguageServer):
@@ -144,7 +132,8 @@ class TaploServer(SolidLanguageServer):
taplo_dir = os.path.join(self._ls_resources_dir, "taplo")
os.makedirs(taplo_dir, exist_ok=True)
_, executable_name = _get_taplo_download_url()
taplo_version = self._custom_settings.get("taplo_version", TAPLO_VERSION)
_, executable_name = _get_taplo_download_url(taplo_version)
taplo_executable = os.path.join(taplo_dir, executable_name)
if os.path.exists(taplo_executable) and os.access(taplo_executable, os.X_OK):
@@ -152,8 +141,8 @@ class TaploServer(SolidLanguageServer):
return taplo_executable
# Download and install Taplo
log.info(f"Taplo not found. Downloading version {TAPLO_VERSION}...")
self._download_taplo(taplo_dir, taplo_executable)
log.info(f"Taplo not found. Downloading version {taplo_version}...")
self._download_taplo(taplo_dir, taplo_executable, taplo_version)
if not os.path.exists(taplo_executable):
raise FileNotFoundError(
@@ -167,64 +156,30 @@ class TaploServer(SolidLanguageServer):
return [core_path, "lsp", "stdio"]
@classmethod
def _download_taplo(cls, install_dir: str, executable_path: str) -> None:
"""Download and extract Taplo binary with SHA256 verification."""
# TODO: consider using existing download utilities in SolidLSP instead of the custom logic here
download_url, _ = _get_taplo_download_url()
def _download_taplo(cls, install_dir: str, executable_path: str, version: str = TAPLO_VERSION) -> None:
"""Download and extract Taplo binary using the shared verified download helper."""
download_url, _ = _get_taplo_download_url(version)
archive_filename = os.path.basename(download_url)
expected_hash = TAPLO_SHA256_CHECKSUMS.get(archive_filename) if version == TAPLO_VERSION else None
if expected_hash is None and version == TAPLO_VERSION:
raise RuntimeError(f"No SHA256 checksum configured for Taplo archive: {archive_filename}")
try:
log.info(f"Downloading Taplo from: {download_url}")
archive_path = os.path.join(install_dir, archive_filename)
# Download the archive with timeout to prevent indefinite hangs
old_timeout = socket.getdefaulttimeout()
try:
socket.setdefaulttimeout(DOWNLOAD_TIMEOUT_SECONDS)
urllib.request.urlretrieve(download_url, archive_path)
finally:
socket.setdefaulttimeout(old_timeout)
# Verify SHA256 checksum
expected_hash = TAPLO_SHA256_CHECKSUMS.get(archive_filename)
if expected_hash:
if not _verify_sha256(archive_path, expected_hash):
os.remove(archive_path)
raise RuntimeError(
f"SHA256 checksum verification failed for {archive_filename}. "
"The downloaded file may be corrupted or tampered with. "
"Try installing manually: cargo install taplo-cli --locked"
)
log.info(f"SHA256 checksum verified for {archive_filename}")
else:
log.warning(
f"No SHA256 checksum available for {archive_filename}. "
"Skipping verification - consider installing manually: cargo install taplo-cli --locked"
)
# Extract based on format
if archive_path.endswith(".gz") and not archive_path.endswith(".tar.gz"):
# Single file gzip
with gzip.open(archive_path, "rb") as f_in:
with open(executable_path, "wb") as f_out:
f_out.write(f_in.read())
elif archive_path.endswith(".zip"):
import zipfile
with zipfile.ZipFile(archive_path, "r") as zip_ref:
# Security: Validate paths to prevent zip slip vulnerability
for member in zip_ref.namelist():
member_path = os.path.normpath(os.path.join(install_dir, member))
if not member_path.startswith(os.path.normpath(install_dir)):
raise RuntimeError(f"Zip slip detected: {member} attempts to escape install directory")
zip_ref.extractall(install_dir)
archive_type = "zip" if archive_filename.endswith(".zip") else "gz"
target_path = install_dir if archive_type == "zip" else executable_path
FileUtils.download_and_extract_archive_verified(
download_url,
target_path,
archive_type,
expected_sha256=expected_hash,
allowed_hosts=TAPLO_ALLOWED_HOSTS,
)
# Make executable on Unix systems
if os.name != "nt":
os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Clean up archive
os.remove(archive_path)
log.info(f"Taplo installed successfully at: {executable_path}")
except Exception as e:
+24 -6
View File
@@ -6,7 +6,7 @@ from typing import cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import PathUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -16,10 +16,16 @@ from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
TERRAFORM_LS_ALLOWED_HOSTS = ("releases.hashicorp.com",)
class TerraformLS(SolidLanguageServer):
"""
Provides Terraform specific instantiation of the LanguageServer class using terraform-ls.
You can pass the following entries in ``ls_specific_settings["terraform"]``:
- terraform_ls_version: Override the pinned terraform-ls version downloaded
by Serena (default: the bundled Serena version).
"""
@override
@@ -92,48 +98,60 @@ class TerraformLS(SolidLanguageServer):
Downloads and installs terraform-ls if not already present.
"""
cls._ensure_tf_command_available()
terraform_settings = solidlsp_settings.get_ls_specific_settings(Language.TERRAFORM)
terraform_ls_version = terraform_settings.get("terraform_ls_version", "0.36.5")
platform_id = PlatformUtils.get_platform_id()
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="TerraformLS",
description="terraform-ls for macOS (ARM64)",
url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip",
url=f"https://releases.hashicorp.com/terraform-ls/{terraform_ls_version}/terraform-ls_{terraform_ls_version}_darwin_arm64.zip",
platform_id="osx-arm64",
archive_type="zip",
binary_name="terraform-ls",
sha256="fee8743aa71fe2d8b0b9b91283b844cfa57d58457306a62e53a8f38d143cec8c" if terraform_ls_version == "0.36.5" else None,
allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS,
),
RuntimeDependency(
id="TerraformLS",
description="terraform-ls for macOS (x64)",
url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip",
url=f"https://releases.hashicorp.com/terraform-ls/{terraform_ls_version}/terraform-ls_{terraform_ls_version}_darwin_amd64.zip",
platform_id="osx-x64",
archive_type="zip",
binary_name="terraform-ls",
sha256="17c5c480f8eec7e528292565f1c05d5097a41edf7ef8ee2a9f3a18d288a1415a" if terraform_ls_version == "0.36.5" else None,
allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS,
),
RuntimeDependency(
id="TerraformLS",
description="terraform-ls for Linux (ARM64)",
url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_arm64.zip",
url=f"https://releases.hashicorp.com/terraform-ls/{terraform_ls_version}/terraform-ls_{terraform_ls_version}_linux_arm64.zip",
platform_id="linux-arm64",
archive_type="zip",
binary_name="terraform-ls",
sha256="724f45029f32d02d88b1952c7d1526c59fc8cd5dae49e31b9fed676a83f6cae7" if terraform_ls_version == "0.36.5" else None,
allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS,
),
RuntimeDependency(
id="TerraformLS",
description="terraform-ls for Linux (x64)",
url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip",
url=f"https://releases.hashicorp.com/terraform-ls/{terraform_ls_version}/terraform-ls_{terraform_ls_version}_linux_amd64.zip",
platform_id="linux-x64",
archive_type="zip",
binary_name="terraform-ls",
sha256="37e645cc54fd03e863157e2a3e773e7a5ff1d6cb3d045e4c20860cac1f550a44" if terraform_ls_version == "0.36.5" else None,
allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS,
),
RuntimeDependency(
id="TerraformLS",
description="terraform-ls for Windows (x64)",
url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip",
url=f"https://releases.hashicorp.com/terraform-ls/{terraform_ls_version}/terraform-ls_{terraform_ls_version}_windows_amd64.zip",
platform_id="win-x64",
archive_type="zip",
binary_name="terraform-ls.exe",
sha256="a9223462cac9e1c0e6ba33043fbf9fb4483609b6970b5681a6306b04366698ec" if terraform_ls_version == "0.36.5" else None,
allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS,
),
]
)
+160
View File
@@ -0,0 +1,160 @@
"""
Python language server integration using Astral's ``ty``.
You can pass the following entries in ``ls_specific_settings["python_ty"]``:
- ls_path: Override the executable used to start ``ty``.
- ty_version: Override the pinned ``ty`` version used with ``uvx`` / ``uv x``
(default: the bundled Serena version).
"""
import logging
import os
import pathlib
import shutil
from typing import cast
from typing_extensions import override
from solidlsp.ls import LanguageServerDependencyProvider, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
TY_VERSION = "0.0.25"
class TyLanguageServer(SolidLanguageServer):
"""
Provides Python specific instantiation of the LanguageServer class using ``ty``.
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a TyLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
super().__init__(
config,
repository_root_path,
None,
str(config.code_language),
solidlsp_settings,
)
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
class DependencyProvider(LanguageServerDependencyProvider):
def create_launch_command(self) -> list[str]:
# respecting an explicit override
ls_path = self._custom_settings.get("ls_path")
if ls_path is not None:
return [ls_path, "server"]
ty_version = self._custom_settings.get("ty_version", TY_VERSION)
# preferring uvx for on-demand execution
uvx_path = os.environ.get("UVX") or shutil.which("uvx")
if uvx_path is not None:
return [uvx_path, "--from", f"ty=={ty_version}", "ty", "server"]
# falling back to uv's uvx-compatible subcommand when only `uv` is available
uv_path = shutil.which("uv")
if uv_path is not None:
return [uv_path, "x", "--from", f"ty=={ty_version}", "ty", "server"]
raise RuntimeError("Could not find 'uvx' or 'uv' in PATH.\nInstall uv or provide ls_specific_settings.python_ty.ls_path.")
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
@override
def _get_language_id_for_file(self, relative_file_path: str) -> str:
return "python"
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Ty language server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"clientInfo": {"name": "Serena", "version": "0.1.0"},
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"workspace": {
"workspaceEdit": {"documentChanges": True},
"didChangeConfiguration": {"dynamicRegistration": True},
"didChangeWatchedFiles": {"dynamicRegistration": True},
"symbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
},
"textDocument": {
"synchronization": {
"dynamicRegistration": True,
"willSave": True,
"willSaveWaitUntil": True,
"didSave": True,
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"definition": {"dynamicRegistration": True, "linkSupport": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
"hierarchicalDocumentSymbolSupport": True,
},
"implementation": {"dynamicRegistration": True, "linkSupport": True},
"publishDiagnostics": {"relatedInformation": True},
},
},
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
return cast(InitializeParams, initialize_params)
def _start_server(self) -> None:
"""
Starts the Ty language server.
"""
def execute_client_command_handler(params: dict) -> list:
return []
def do_nothing(params: dict) -> None:
return
def window_log_message(msg: dict) -> None:
log.info("LSP: window/logMessage: %s", msg.get("message", ""))
# setting up lightweight 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)
# starting and initializing the server
log.info("Starting ty language server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
log.info("Sending initialize request from LSP client to ty server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
log.info("Received initialize response from ty server: %s", init_response)
capabilities = init_response["capabilities"]
assert "textDocumentSync" in capabilities
assert "definitionProvider" in capabilities
assert "referencesProvider" in capabilities
assert "documentSymbolProvider" in capabilities
# completing the initialization handshake
self.server.notify.initialized({})
@@ -19,7 +19,7 @@ from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
log = logging.getLogger(__name__)
@@ -149,19 +149,20 @@ class TypeScriptLanguageServer(SolidLanguageServer):
language_specific_config = self._custom_settings
typescript_version = language_specific_config.get("typescript_version", "5.9.3")
typescript_language_server_version = language_specific_config.get("typescript_language_server_version", "5.1.3")
npm_registry = language_specific_config.get("npm_registry")
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="typescript",
description="typescript package",
command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"],
command=build_npm_install_command("typescript", typescript_version, npm_registry),
platform_id="any",
),
RuntimeDependency(
id="typescript-language-server",
description="typescript-language-server package",
command=["npm", "install", "--prefix", "./", f"typescript-language-server@{typescript_language_server_version}"],
command=build_npm_install_command("typescript-language-server", typescript_language_server_version, npm_registry),
platform_id="any",
),
]
@@ -14,13 +14,13 @@ from typing import cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
log = logging.getLogger(__name__)
@@ -72,13 +72,16 @@ class VtsLanguageServer(SolidLanguageServer):
PlatformId.WIN_arm64,
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for vtsls at the moment"
vts_config = solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT_VTS)
vtsls_version = vts_config.get("vtsls_version", "0.2.9")
npm_registry = vts_config.get("npm_registry")
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="vtsls",
description="vtsls language server package",
command="npm install --prefix ./ @vtsls/language-server@0.2.9",
command=build_npm_install_command("@vtsls/language-server", vtsls_version, npm_registry),
platform_id="any",
),
]
@@ -15,13 +15,13 @@ from typing import Any
from overrides import override
from solidlsp import ls_types
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
from solidlsp.language_servers.typescript_language_server import (
TypeScriptLanguageServer,
prefer_non_node_modules_definition,
)
from solidlsp.ls import LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_types import Location
from solidlsp.ls_utils import PathUtils
@@ -47,6 +47,11 @@ class VueTypeScriptServer(TypeScriptLanguageServer):
"""
return Language.TYPESCRIPT
def get_source_fn_matcher(self) -> FilenameMatcher:
# must override with Vue-specific matcher to ensure .vue files are included (as they can be discovered via references,
# for instance; otherwise, we may find references in .vue files but then filter the results out, because .vue files are ignored.)
return Language.VUE.get_source_fn_matcher()
class DependencyProvider(TypeScriptLanguageServer.DependencyProvider):
override_ts_ls_executable: str | None = None
@@ -407,31 +412,26 @@ class VueLanguageServer(SolidLanguageServer):
typescript_language_server_version = typescript_config.get("typescript_language_server_version", "5.1.3")
vue_config = solidlsp_settings.get_ls_specific_settings(Language.VUE)
vue_language_server_version = vue_config.get("vue_language_server_version", "3.1.5")
npm_registry = vue_config.get("npm_registry", typescript_config.get("npm_registry"))
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="vue-language-server",
description="Vue language server package (Volar)",
command=["npm", "install", "--prefix", "./", f"@vue/language-server@{vue_language_server_version}"],
command=build_npm_install_command("@vue/language-server", vue_language_server_version, npm_registry),
platform_id="any",
),
RuntimeDependency(
id="typescript",
description="TypeScript (required for tsdk)",
command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"],
command=build_npm_install_command("typescript", typescript_version, npm_registry),
platform_id="any",
),
RuntimeDependency(
id="typescript-language-server",
description="TypeScript language server (for Vue LS 3.x tsserver forwarding)",
command=[
"npm",
"install",
"--prefix",
"./",
f"typescript-language-server@{typescript_language_server_version}",
],
command=build_npm_install_command("typescript-language-server", typescript_language_server_version, npm_registry),
platform_id="any",
),
]
@@ -9,7 +9,7 @@ import pathlib
import shutil
from typing import Any
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
@@ -66,13 +66,15 @@ class YamlLanguageServer(SolidLanguageServer):
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."
yaml_language_server_version = self._custom_settings.get("yaml_language_server_version", "1.19.2")
npm_registry = self._custom_settings.get("npm_registry")
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="yaml-language-server",
description="yaml-language-server package (Red Hat)",
command="npm install --prefix ./ yaml-language-server@1.19.2",
command=build_npm_install_command("yaml-language-server", yaml_language_server_version, npm_registry),
platform_id="any",
),
]
+516 -174
View File
@@ -23,7 +23,7 @@ from sensai.util.string import ToStringMixin
from serena.util.file_system import match_path
from serena.util.text_utils import MatchedConsecutiveLines
from solidlsp import ls_types
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_process import LanguageServerProcess
from solidlsp.ls_types import UnifiedSymbolInformation
@@ -35,6 +35,7 @@ from solidlsp.lsp_protocol_handler.lsp_types import (
Definition,
DefinitionParams,
DocumentSymbol,
ImplementationParams,
LocationLink,
RenameParams,
SymbolInformation,
@@ -47,7 +48,13 @@ from solidlsp.lsp_protocol_handler.server import (
from solidlsp.settings import SolidLSPSettings
from solidlsp.util.cache import load_cache, save_cache
GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, ls_types.UnifiedSymbolInformation]
RawDocumentSymbol = Union[DocumentSymbol, SymbolInformation]
"""
Type alias for the raw symbol information returned by a language server in response to a
`textDocument/documentSymbol` request.
The `DocumentSymbol` is the preferred type, but the legacy type `SymbolInformation` is also still used.
"""
log = logging.getLogger(__name__)
_debug_enabled = log.isEnabledFor(logging.DEBUG)
@@ -210,7 +217,7 @@ class SymbolBodyFactory:
def __init__(self, file_buffer: LSPFileBuffer):
self._lines = file_buffer.split_lines()
def create_symbol_body(self, symbol: GenericDocumentSymbol) -> SymbolBody:
def create_symbol_body(self, symbol: UnifiedSymbolInformation) -> SymbolBody:
existing_body = symbol.get("body", None)
if existing_body and isinstance(existing_body, SymbolBody):
return existing_body
@@ -378,6 +385,13 @@ class SolidLanguageServer(ABC):
def get_language_enum_instance(cls) -> Language:
return Language.from_ls_class(cls)
@classmethod
def supports_implementation_request(cls) -> bool:
"""
Return whether this language server supports ``textDocument/implementation``.
"""
return False
@classmethod
def ls_resources_dir(cls, solidlsp_settings: SolidLSPSettings, mkdir: bool = True) -> str:
"""
@@ -575,6 +589,14 @@ class SolidLanguageServer(ABC):
"""
return self._ignore_spec
def get_source_fn_matcher(self) -> FilenameMatcher:
"""
:return: the source filename matcher for this language server, which must positively match all files that
are understood by this language server or are discovered as containing sources indirectly, e.g. via references
"""
# By default, use the matcher of the language
return self.language.get_source_fn_matcher()
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
@@ -592,7 +614,7 @@ class SolidLanguageServer(ABC):
# 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()
fn_matcher = self.get_source_fn_matcher()
if not fn_matcher.is_relevant_filename(abs_path):
return True
@@ -846,6 +868,149 @@ class SolidLanguageServer(ABC):
def _send_definition_request(self, definition_params: DefinitionParams) -> Definition | list[LocationLink] | None:
return self.server.send.definition(definition_params)
class SymbolLocationRequest(ABC):
def __init__(
self,
language_server: "SolidLanguageServer",
relative_file_path: str,
line: int,
column: int,
*,
request_name: str,
) -> None:
self.language_server = language_server
self.relative_file_path = relative_file_path
self.line = line
self.column = column
self.request_name = request_name
self.skip_ignored_paths = True
def execute(self) -> list[ls_types.Location]:
self._ensure_server_started()
t0 = perf_counter() if _debug_enabled else None
with self.language_server.open_file(self.relative_file_path):
self.language_server._wait_for_cross_file_references_if_needed()
try:
response = self.send_request()
except Exception as e:
mapped_exception = self.map_exception(e)
if mapped_exception is not None:
raise mapped_exception from e
raise
result = self.normalize_response(response)
if t0 is not None:
self.log_perf_result(t0, result)
return result
def _ensure_server_started(self) -> None:
if not self.language_server.server_started:
log.error("%s called before language server started", self.request_name)
raise SolidLSPException("Language Server not started")
@abstractmethod
def send_request(self) -> object | None:
pass
def map_exception(self, error: Exception) -> Exception | None:
if isinstance(error, LSPError) and getattr(error, "code", None) == -32603:
return RuntimeError(
f"LSP internal error (-32603) when requesting {self.request_name} for "
f"{self.relative_file_path}:{self.line}:{self.column}. "
"This often occurs when requesting a symbol in a way the language server cannot resolve."
)
return None
@abstractmethod
def normalize_response(self, response: object | None) -> list[ls_types.Location]:
pass
def convert_location_item(self, item: dict[str, object], *, allow_location_links: bool = False) -> ls_types.Location | None:
if LSPConstants.URI in item and LSPConstants.RANGE in item:
uri = cast(str, item[LSPConstants.URI])
range_d = cast(ls_types.Range, item[LSPConstants.RANGE])
elif (
allow_location_links
and LSPConstants.TARGET_URI in item
and LSPConstants.TARGET_RANGE in item
and LSPConstants.TARGET_SELECTION_RANGE in item
):
uri = cast(str, item[LSPConstants.TARGET_URI])
range_d = cast(ls_types.Range, item[LSPConstants.TARGET_SELECTION_RANGE])
else:
raise AssertionError(f"Unexpected response from Language Server: {item}")
abs_path = PathUtils.uri_to_path(uri)
rel_path_str = PathUtils.get_relative_path(abs_path, self.language_server.repository_root_path)
if rel_path_str is None:
log.warning(
"Found a %s in a path outside the repository, probably the LS is parsing things in installed packages or in the standardlib! "
"Path: %s. This is a bug but we currently simply skip these locations.",
self.request_name,
abs_path,
)
return None
if self.skip_ignored_paths and self.language_server.is_ignored_path(rel_path_str):
log.info("%s found symbol in ignored path: %s", self.request_name, rel_path_str)
return None
return ls_types.Location(uri=uri, range=range_d, absolutePath=str(abs_path), relativePath=rel_path_str)
def log_perf_result(self, t0: float, result: list[ls_types.Location]) -> None:
return
class DefinitionLocationRequest(SymbolLocationRequest):
def __init__(
self,
language_server: "SolidLanguageServer",
relative_file_path: str,
line: int,
column: int,
*,
request_name: str = "request_definition",
) -> None:
super().__init__(
language_server,
relative_file_path,
line,
column,
request_name=request_name,
)
def send_request(self) -> object | None:
return self.language_server._send_definition_request(
self.language_server._create_text_document_position_params(self.relative_file_path, self.line, self.column)
)
def normalize_response(self, response: object | None) -> list[ls_types.Location]:
if response is None:
log.warning(
"Language server returned None for %s request at %s:%s:%s",
self.request_name,
self.relative_file_path,
self.line,
self.column,
)
return []
ret: list[ls_types.Location] = []
if isinstance(response, list):
for item in response:
assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}"
if location := self.convert_location_item(cast(dict[str, object], item), allow_location_links=True):
ret.append(location)
return ret
if isinstance(response, dict):
if location := self.convert_location_item(cast(dict[str, object], response), allow_location_links=True):
ret.append(location)
return ret
assert False, f"Unexpected response from Language Server: {response}"
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
@@ -857,70 +1022,60 @@ class SolidLanguageServer(ABC):
:return: the list of locations where the symbol is defined
"""
if not self.server_started:
log.error("request_definition called before language server started")
raise SolidLSPException("Language Server not started")
request = self.DefinitionLocationRequest(self, relative_file_path, line, column)
return request.execute()
def _send_implementation_request(self, implementation_params: ImplementationParams) -> Definition | list[LocationLink] | None:
return self.server.send.implementation(implementation_params)
def _create_text_document_position_params(self, relative_file_path: str, line: int, column: int) -> DefinitionParams:
return 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,
},
},
)
def _wait_for_cross_file_references_if_needed(self) -> None:
if not self._has_waited_for_cross_file_references:
# Some LS require waiting for a while before they can return cross-file definitions.
# This is a workaround for such LS that don't have a reliable "finished initializing" signal.
# Some LS require waiting for a while before they can return accurate cross-file results.
# The waiting has to happen after at least one file was opened in the LS.
sleep(self._get_wait_time_for_cross_file_referencing())
self._has_waited_for_cross_file_references = True
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,
},
},
class ImplementationLocationRequest(DefinitionLocationRequest):
def __init__(self, language_server: "SolidLanguageServer", relative_file_path: str, line: int, column: int) -> None:
super().__init__(
language_server,
relative_file_path,
line,
column,
request_name="request_implementation",
)
response = self._send_definition_request(definition_params)
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: dict = {}
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(ls_types.Location(**new_item)) # type: ignore
elif LSPConstants.TARGET_URI in item and LSPConstants.TARGET_RANGE in item and LSPConstants.TARGET_SELECTION_RANGE in item:
new_item: dict = {} # type: ignore
new_item["uri"] = item[LSPConstants.TARGET_URI] # type: ignore
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] # type: ignore
ret.append(ls_types.Location(**new_item)) # type: ignore
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
def send_request(self) -> object | None:
return self.language_server._send_implementation_request(
self.language_server._create_text_document_position_params(self.relative_file_path, self.line, self.column),
)
new_item: dict = {} # type: ignore
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(ls_types.Location(**new_item)) # type: ignore
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
log.warning(f"Language server returned None for definition request at {relative_file_path}:{line}:{column}")
else:
assert False, f"Unexpected response from Language Server: {response}"
def request_implementation(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
"""
Raise a [textDocument/implementation](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_implementation)
request to the Language Server for the symbol at the given line and column in the given file.
return ret
:param relative_file_path: The relative path of the file that has the symbol for which implementations should be looked up
:param line: The line number of the symbol
:param column: The column number of the symbol
:return: the list of locations where the symbol is implemented
"""
request = self.ImplementationLocationRequest(self, relative_file_path, line, column)
return request.execute()
# 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:
@@ -932,6 +1087,46 @@ class SolidLanguageServer(ABC):
}
)
class ReferencesLocationRequest(SymbolLocationRequest):
def __init__(self, language_server: "SolidLanguageServer", relative_file_path: str, line: int, column: int) -> None:
super().__init__(
language_server,
relative_file_path,
line,
column,
request_name="request_references",
)
def send_request(self) -> object | None:
return self.language_server._send_references_request(self.relative_file_path, line=self.line, column=self.column)
def normalize_response(self, response: object | None) -> list[ls_types.Location]:
if response is None:
return []
assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
ret: list[ls_types.Location] = []
for item in response:
assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}"
if location := self.convert_location_item(cast(dict[str, object], item)):
ret.append(location)
return ret
def log_perf_result(self, t0: float, result: list[ls_types.Location]) -> None:
elapsed_ms = (perf_counter() - t0) * 1000
if not result:
log.debug("perf: request_references path=%s elapsed_ms=%.2f count=0", self.relative_file_path, elapsed_ms)
return
unique_files = len({r["relativePath"] for r in result})
log.debug(
"perf: request_references path=%s elapsed_ms=%.2f count=%d unique_files=%d",
self.relative_file_path,
elapsed_ms,
len(result),
unique_files,
)
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
@@ -944,111 +1139,8 @@ class SolidLanguageServer(ABC):
:return: A list of locations where the symbol is referenced (excluding ignored directories)
"""
if not self.server_started:
log.error("request_references called before Language Server started")
raise SolidLSPException("Language Server not started")
with self.open_file(relative_file_path):
if not self._has_waited_for_cross_file_references:
# Some LS require waiting for a while before they can return cross-file references.
# This is a workaround for such LS that don't have a reliable "finished initializing" signal.
# The waiting has to happen after at least one file was opened in the ls
sleep(self._get_wait_time_for_cross_file_referencing())
self._has_waited_for_cross_file_references = True
t0 = perf_counter() if _debug_enabled else 0.0
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, LSPError) 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:
if _debug_enabled:
elapsed_ms = (perf_counter() - t0) * 1000
log.debug("perf: request_references path=%s elapsed_ms=%.2f count=0", relative_file_path, elapsed_ms)
return []
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}"
assert LSPConstants.URI in item
assert LSPConstants.RANGE in item
abs_path = PathUtils.uri_to_path(item[LSPConstants.URI]) # type: ignore
if not Path(abs_path).is_relative_to(self.repository_root_path):
log.warning(
"Found a reference in a path outside the repository, probably the LS is parsing things in installed packages or in the standardlib! "
f"Path: {abs_path}. This is a bug but we currently simply skip these references."
)
continue
rel_path = Path(abs_path).relative_to(self.repository_root_path)
if self.is_ignored_path(str(rel_path)):
log.debug("Ignoring reference in %s since it should be ignored", rel_path)
continue
new_item: dict = {}
new_item.update(item)
new_item["absolutePath"] = str(abs_path)
new_item["relativePath"] = str(rel_path)
ret.append(ls_types.Location(**new_item)) # type: ignore
if _debug_enabled:
elapsed_ms = (perf_counter() - t0) * 1000
unique_files = len({r["relativePath"] for r in ret})
log.debug(
"perf: request_references path=%s elapsed_ms=%.2f count=%d unique_files=%d",
relative_file_path,
elapsed_ms,
len(ret),
unique_files,
)
return ret
def request_text_document_diagnostics(self, relative_file_path: str) -> list[ls_types.Diagnostic]:
"""
Raise a [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_diagnostic) request to the Language Server
to find diagnostics for the given file. Wait for the response and return the result.
:param relative_file_path: The relative path of the file to retrieve diagnostics for
:return: A list of diagnostics for the file
"""
if not self.server_started:
log.error("request_text_document_diagnostics called before Language Server started")
raise SolidLSPException("Language Server not started")
with self.open_file(relative_file_path):
response = self.server.send.text_document_diagnostic(
{
LSPConstants.TEXT_DOCUMENT: { # type: ignore
LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
}
}
)
if response is None:
return [] # type: ignore
assert isinstance(response, dict), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
ret: list[ls_types.Diagnostic] = []
for item in response["items"]: # type: ignore
new_item: ls_types.Diagnostic = {
"uri": pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri(),
"severity": item["severity"],
"message": item["message"],
"range": item["range"],
"code": item["code"], # type: ignore
}
ret.append(ls_types.Diagnostic(**new_item))
return ret
request = self.ReferencesLocationRequest(self, relative_file_path, line, column)
return request.execute()
def retrieve_full_file_content(self, file_path: str) -> str:
"""
@@ -1173,6 +1265,14 @@ class SolidLanguageServer(ABC):
"""
Sends a [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 - or returns a cached result if available.
The returned symbols are considered "raw document symbols" (in contrast to processed symbols returned by `request_document_symbols`).
NOTE: This method can be overridden in subclasses to post-process the raw results.
When doing so after the initial implementation, be sure to update the init parameter `cache_version_raw_document_symbols`
to a different version (add 1) to ensure that all caches are invalidated appropriately.
IMPORTANT: Since rebuilding the raw document symbol cache from the language server results
is potentially expensive, prefer overriding the `request_document_symbols` method
if the post-processing can also be done on the processed/high-level symbols.
:param relative_file_path: the relative path of the file that has the symbols.
:param file_data: the file data buffer, if already opened. If None, the file will be opened in this method.
@@ -1218,9 +1318,30 @@ class SolidLanguageServer(ABC):
with self._open_file_context(relative_file_path, file_buffer=file_data) as fd:
return get_raw_document_symbols(fd)
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
"""
Normalizes the name of the given symbol, e.g. by removing parameter lists from method symbols.
Override this method in subclasses to implement language-specific normalization logic.
NOTE: When changing the override of this method after the initial LS implementation,
be sure to also override `_document_symbols_cache_fingerprint` in order to ensure that
the caches are invalidated appropriately.
:param symbol: the symbol
:param relative_file_path: the relative path of the file the symbol is located in
:return: the normalized name of the symbol
"""
# the default implementation does not change the name
return symbol["name"]
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
"""
Retrieves the collection of symbols in the given file
Retrieves the collection of symbols in the given file.
NOTE: This method can be overridden in subclasses to post-process the results.
When doing so after the initial LS implementation, be sure to also override `_document_symbols_cache_fingerprint`
to ensure that the caches are invalidated appropriately.
DO NOT override this method to modify symbol names; override `_normalize_symbol_name` instead.
:param relative_file_path: The relative path of the file that has the symbols
:param file_buffer: an optional file buffer if the file is already opened.
@@ -1264,7 +1385,7 @@ class SolidLanguageServer(ABC):
body_factory = SymbolBodyFactory(file_data)
def convert_to_unified_symbol(original_symbol_dict: GenericDocumentSymbol) -> ls_types.UnifiedSymbolInformation:
def convert_to_unified_symbol(original_symbol_dict: RawDocumentSymbol) -> ls_types.UnifiedSymbolInformation:
"""
Converts the given symbol dictionary to the unified representation, ensuring
that all required fields are present (except 'children' which is handled separately).
@@ -1305,16 +1426,23 @@ class SolidLanguageServer(ABC):
return item
def convert_symbols_with_common_parent(
symbols: list[DocumentSymbol] | list[SymbolInformation] | list[UnifiedSymbolInformation],
symbols: list[DocumentSymbol] | list[SymbolInformation],
parent: ls_types.UnifiedSymbolInformation | None,
) -> list[ls_types.UnifiedSymbolInformation]:
"""
Converts the given symbols into UnifiedSymbolInformation with proper parent-child relationships,
adding overload indices for symbols with the same name under the same parent.
"""
# apply name normalization and count occurrences of each symbol name
total_name_counts: dict[str, int] = defaultdict(lambda: 0)
for symbol in symbols:
total_name_counts[symbol["name"]] += 1
name = self._normalize_symbol_name(symbol, relative_file_path=relative_file_path)
symbol["name"] = name
total_name_counts[name] += 1
# convert symbols to the unified representation and
# * add overload indices where necessary
# * ensure that the "parent" field is set correctly
name_counts: dict[str, int] = defaultdict(lambda: 0)
unified_symbols = []
for symbol in symbols:
@@ -1629,7 +1757,7 @@ class SolidLanguageServer(ABC):
def create_symbol_body(
self,
symbol: ls_types.UnifiedSymbolInformation | LSPTypes.SymbolInformation,
symbol: ls_types.UnifiedSymbolInformation,
factory: SymbolBodyFactory | None = None,
) -> SymbolBody:
if factory is None:
@@ -1945,6 +2073,153 @@ class SolidLanguageServer(ABC):
"""
return definitions[0]
def _get_document_symbols_with_locations(self, relative_file_path: str) -> list[ls_types.UnifiedSymbolInformation]:
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
document_symbols = self.request_document_symbols(relative_file_path)
symbols = list(document_symbols.iter_symbols())
# Make SymbolInformation and DocumentSymbol shapes consistent by ensuring every
# symbol exposes a normalized location/range in the current workspace.
for symbol in symbols:
location = symbol["location"]
location["absolutePath"] = absolute_file_path
location["relativePath"] = relative_file_path
location["uri"] = Path(absolute_file_path).as_uri()
return symbols
@staticmethod
def _position_matches_range(range_d: ls_types.Range, line: int, column: int | None = None) -> bool:
start = range_d["start"]
end = range_d["end"]
if not (start["line"] <= line <= end["line"]):
return False
if column is None:
return True
if line == start["line"] and column < start["character"]:
return False
if line == end["line"] and column > end["character"]:
return False
return True
@staticmethod
def _symbol_match_sort_key(symbol: ls_types.UnifiedSymbolInformation, match_priority: int) -> tuple[int, int, int, int, int]:
location = symbol["location"]
symbol_range = location["range"]
start = symbol_range["start"]
end = symbol_range["end"]
line_span = end["line"] - start["line"]
character_span = end["character"] - start["character"] if line_span == 0 else end["character"]
return match_priority, line_span, character_span, start["line"], start["character"]
def _request_symbol_at_location(
self,
relative_file_path: str,
line: int,
column: int,
include_body: bool = False,
body_factory: SymbolBodyFactory | None = None,
) -> ls_types.UnifiedSymbolInformation | None:
candidates: list[tuple[tuple[int, int, int, int, int], ls_types.UnifiedSymbolInformation]] = []
for symbol in self._get_document_symbols_with_locations(relative_file_path):
location = symbol["location"]
symbol_range = location["range"]
selection_range = symbol.get("selectionRange") or symbol_range
match_priority: int | None = None
if self._position_matches_range(selection_range, line, column):
match_priority = 0
elif self._position_matches_range(symbol_range, line, column):
match_priority = 1
else:
selection_start = selection_range["start"]
symbol_start = symbol_range["start"]
if (selection_start["line"], selection_start["character"]) == (line, column):
match_priority = 2
elif (symbol_start["line"], symbol_start["character"]) == (line, column):
match_priority = 3
elif selection_start["line"] == line and column <= selection_start["character"]:
match_priority = 4
elif symbol_start["line"] == line and column <= symbol_start["character"]:
match_priority = 5
if match_priority is None:
continue
candidates.append((self._symbol_match_sort_key(symbol, match_priority), symbol))
if not candidates:
return None
candidates.sort(key=lambda item: item[0])
best_symbol = candidates[0][1]
if include_body:
best_symbol["body"] = self.create_symbol_body(best_symbol, factory=body_factory)
return best_symbol
@staticmethod
def _iter_symbol_descendants(symbol: ls_types.UnifiedSymbolInformation) -> Iterator[ls_types.UnifiedSymbolInformation]:
"""Yield descendant symbols in depth-first order."""
for child in symbol.get("children", []):
yield child
yield from SolidLanguageServer._iter_symbol_descendants(child)
def _refine_implementing_symbol(
self,
target_symbol: ls_types.UnifiedSymbolInformation | None,
implementing_symbol: ls_types.UnifiedSymbolInformation,
include_body: bool = False,
) -> ls_types.UnifiedSymbolInformation:
"""Resolve member-level implementation symbols when the LS returns a containing type."""
if target_symbol is None:
return implementing_symbol
target_kind = target_symbol["kind"]
if target_kind not in (ls_types.SymbolKind.Method, ls_types.SymbolKind.Function):
return implementing_symbol
if implementing_symbol["kind"] == target_kind and implementing_symbol.get("name") == target_symbol.get("name"):
return implementing_symbol
candidate_descendants: list[ls_types.UnifiedSymbolInformation] = []
for descendant in self._iter_symbol_descendants(implementing_symbol):
if descendant.get("name") != target_symbol.get("name"):
continue
if descendant["kind"] != target_kind:
continue
candidate_descendants.append(descendant)
if not candidate_descendants:
return implementing_symbol
refined_symbol = min(
candidate_descendants,
key=lambda symbol: self._symbol_match_sort_key(symbol, match_priority=0),
)
if include_body:
refined_symbol["body"] = self.create_symbol_body(refined_symbol)
return refined_symbol
def request_symbol_at_location(
self,
relative_file_path: str,
line: int,
column: int,
include_body: bool = False,
) -> ls_types.UnifiedSymbolInformation | None:
"""
Finds the symbol at the given position, preferring exact identifier matches and otherwise
falling back to the innermost symbol whose body contains the position.
: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 at the given location, or None if no symbol could be resolved.
"""
if not self.server_started:
log.error("request_symbol_at_location called before language server started")
raise SolidLSPException("Language Server not started")
return self._request_symbol_at_location(relative_file_path, line, column, include_body=include_body)
def request_defining_symbol(
self,
relative_file_path: str,
@@ -1976,14 +2251,79 @@ class SolidLanguageServer(ABC):
# Select the preferred definition (subclasses can override _get_preferred_definition)
definition = self._get_preferred_definition(definitions)
def_path = definition["relativePath"]
assert def_path is not None
if def_path is None:
return None
def_line = definition["range"]["start"]["line"]
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)
return self._request_symbol_at_location(
def_path,
def_line,
def_col,
include_body=include_body,
)
return defining_symbol
def request_implementing_symbols(
self,
relative_file_path: str,
line: int,
column: int,
include_body: bool = False,
) -> list[ls_types.UnifiedSymbolInformation]:
"""
Finds the symbols that implement the symbol at the given location.
This method first finds implementation locations for the symbol at the given position,
then retrieves the full symbol information for each implementation and de-duplicates
results that map to the same containing 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_body: whether to include the body of the symbols in the result.
:return: The symbol information for each implementation.
"""
if not self.server_started:
log.error("request_implementing_symbols called before language server started")
raise SolidLSPException("Language Server not started")
target_symbol = self._request_symbol_at_location(relative_file_path, line, column, include_body=False)
implementation_locations = self.request_implementation(relative_file_path, line, column)
if not implementation_locations:
return []
result: list[ls_types.UnifiedSymbolInformation] = []
seen_keys: set[tuple[str, int, int, int]] = set()
for implementation in implementation_locations:
implementation_path = implementation["relativePath"]
assert implementation_path is not None
implementation_line = implementation["range"]["start"]["line"]
implementation_col = implementation["range"]["start"]["character"]
implementing_symbol = self._request_symbol_at_location(
implementation_path,
implementation_line,
implementation_col,
include_body=include_body,
body_factory=None,
)
if implementing_symbol is None:
continue
implementing_symbol = self._refine_implementing_symbol(target_symbol, implementing_symbol, include_body=include_body)
if "location" not in implementing_symbol:
continue
symbol_location = implementing_symbol["location"]
symbol_key = (
cast(str, symbol_location["relativePath"]),
symbol_location["range"]["start"]["line"],
symbol_location["range"]["start"]["character"],
implementing_symbol["kind"],
)
if symbol_key in seen_keys:
continue
seen_keys.add(symbol_key)
result.append(implementing_symbol)
return result
def _document_symbols_cache_fingerprint(self) -> Hashable | None:
"""
@@ -2001,7 +2341,9 @@ class SolidLanguageServer(ABC):
The value must be hashable and safe for inclusion in cache version tuples.
E.g. use an integer, a string or a tuple of integers/strings.
Returns None if no context-specific fingerprint is needed.
For example, if there is a single aspect being considered, use an integer to reflect the version
of this aspect (incrementing it whenever the implementation changes).
If multiple versioned aspects exist, use a tuple of versions, etc.
"""
return None
+8 -1
View File
@@ -87,6 +87,8 @@ class Language(str, Enum):
"""Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls"""
PYTHON_JEDI = "python_jedi"
"""Jedi language server for Python (instead of pyright, which is the default)"""
PYTHON_TY = "python_ty"
"""Ty language server for Python (instead of pyright, which is the default)."""
CSHARP_OMNISHARP = "csharp_omnisharp"
"""OmniSharp language server for C# (instead of the default csharp-ls by microsoft).
Currently has problems with finding references, and generally seems less stable and performant.
@@ -154,6 +156,7 @@ class Language(str, Enum):
self.ANSIBLE,
self.TYPESCRIPT_VTS,
self.PYTHON_JEDI,
self.PYTHON_TY,
self.CSHARP_OMNISHARP,
self.RUBY_SOLARGRAPH,
self.PHP_PHPACTOR,
@@ -187,7 +190,7 @@ class Language(str, Enum):
def get_source_fn_matcher(self) -> FilenameMatcher:
match self:
case self.PYTHON | self.PYTHON_JEDI:
case self.PYTHON | self.PYTHON_JEDI | self.PYTHON_TY:
return FilenameMatcher("*.py", "*.pyi")
case self.JAVA:
return FilenameMatcher("*.java")
@@ -321,6 +324,10 @@ class Language(str, Enum):
from solidlsp.language_servers.jedi_server import JediServer
return JediServer
case self.PYTHON_TY:
from solidlsp.language_servers.ty_server import TyLanguageServer
return TyLanguageServer
case self.JAVA:
from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS
+169 -21
View File
@@ -3,15 +3,19 @@ This file contains various utility functions like I/O operations, handling paths
"""
import gzip
import hashlib
import logging
import os
import platform
import shutil
import subprocess
import tarfile
import uuid
import zipfile
from enum import Enum
from pathlib import Path, PurePath
from typing import Literal, cast
from urllib.parse import urlparse
import charset_normalizer
import requests
@@ -204,58 +208,106 @@ class FileUtils:
"""
Downloads the file from the given URL to the given {target_path}
"""
os.makedirs(os.path.dirname(target_path), exist_ok=True)
FileUtils.download_file_verified(url, target_path)
@staticmethod
def download_file_verified(
url: str,
target_path: str,
expected_sha256: str | None = None,
allowed_hosts: tuple[str, ...] | list[str] | None = None,
) -> None:
"""
Downloads a file from ``url`` to ``target_path`` with optional integrity and host validation.
"""
# validating the requested host
FileUtils._validate_download_host(url, allowed_hosts)
# streaming the download into a temporary file
target_directory = os.path.dirname(target_path) or "."
os.makedirs(target_directory, exist_ok=True)
temp_file_path = str(PurePath(target_directory, f".{Path(target_path).name}.{uuid.uuid4().hex}.download"))
response: requests.Response | None = None
try:
response = requests.get(url, stream=True, timeout=60)
if response.status_code != 200:
log.error(f"Error downloading file '{url}': {response.status_code} {response.text}")
raise SolidLSPException("Error downloading file.")
with open(target_path, "wb") as f:
shutil.copyfileobj(response.raw, f)
FileUtils._validate_download_host(response.url, allowed_hosts)
with open(temp_file_path, "wb") as output_file:
for chunk in response.iter_content(chunk_size=1024 * 1024):
if chunk:
output_file.write(chunk)
FileUtils._verify_sha256_if_configured(temp_file_path, expected_sha256)
os.replace(temp_file_path, target_path)
except Exception as exc:
log.error(f"Error downloading file '{url}': {exc}")
raise SolidLSPException("Error downloading file.") from None
finally:
if response is not None:
response.close()
if os.path.exists(temp_file_path):
Path.unlink(Path(temp_file_path))
@staticmethod
def download_and_extract_archive(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}
"""
FileUtils.download_and_extract_archive_verified(url, target_path, archive_type)
@staticmethod
def download_and_extract_archive_verified(
url: str,
target_path: str,
archive_type: str,
expected_sha256: str | None = None,
allowed_hosts: tuple[str, ...] | list[str] | None = None,
) -> None:
"""
Downloads an archive from ``url`` and extracts it safely into ``target_path``.
"""
try:
tmp_files = []
# preparing the temporary download location
tmp_files: list[str] = []
tmp_file_name = str(PurePath(os.path.expanduser("~"), "solidlsp_tmp", uuid.uuid4().hex))
tmp_files.append(tmp_file_name)
os.makedirs(os.path.dirname(tmp_file_name), exist_ok=True)
FileUtils.download_file(url, tmp_file_name)
# downloading the archive with optional verification
FileUtils.download_file_verified(url, tmp_file_name, expected_sha256=expected_sha256, allowed_hosts=allowed_hosts)
tmp_files.append(tmp_file_name)
# extracting the archive according to its format
if archive_type in ["tar", "gztar", "bztar", "xztar"]:
os.makedirs(target_path, exist_ok=True)
shutil.unpack_archive(tmp_file_name, target_path, archive_type)
FileUtils._extract_tar_archive(tmp_file_name, target_path, archive_type)
elif archive_type == "zip":
os.makedirs(target_path, exist_ok=True)
with zipfile.ZipFile(tmp_file_name, "r") as zip_ref:
for zip_info in zip_ref.infolist():
extracted_path = zip_ref.extract(zip_info, target_path)
ZIP_SYSTEM_UNIX = 3 # zip file created on Unix system
if zip_info.create_system != ZIP_SYSTEM_UNIX:
continue
# extractall() does not preserve permissions
# see. https://github.com/python/cpython/issues/59999
attrs = (zip_info.external_attr >> 16) & 0o777
if attrs:
os.chmod(extracted_path, attrs)
FileUtils._extract_zip_archive(tmp_file_name, target_path)
elif archive_type == "zip.gz":
os.makedirs(target_path, exist_ok=True)
tmp_file_name_ungzipped = tmp_file_name + ".zip"
tmp_files.append(tmp_file_name_ungzipped)
with gzip.open(tmp_file_name, "rb") as f_in, open(tmp_file_name_ungzipped, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
shutil.unpack_archive(tmp_file_name_ungzipped, target_path, "zip")
FileUtils._extract_zip_archive(tmp_file_name_ungzipped, target_path)
elif archive_type == "gz":
with gzip.open(tmp_file_name, "rb") as f_in, open(target_path, "wb") as f_out:
target_directory = os.path.dirname(target_path) or "."
os.makedirs(target_directory, exist_ok=True)
temp_output_path = str(PurePath(target_directory, f".{Path(target_path).name}.{uuid.uuid4().hex}.extract"))
tmp_files.append(temp_output_path)
with gzip.open(tmp_file_name, "rb") as f_in, open(temp_output_path, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
os.replace(temp_output_path, target_path)
elif archive_type == "binary":
# For single binary files, just move to target without extraction
target_directory = os.path.dirname(target_path) or "."
os.makedirs(target_directory, exist_ok=True)
shutil.move(tmp_file_name, target_path)
tmp_files.remove(tmp_file_name)
else:
log.error(f"Unknown archive type '{archive_type}' for extraction")
raise SolidLSPException(f"Unknown archive type '{archive_type}'")
@@ -267,6 +319,102 @@ class FileUtils:
if os.path.exists(tmp_file_name):
Path.unlink(Path(tmp_file_name))
@staticmethod
def calculate_sha256(file_path: str) -> str:
"""
Calculates the SHA256 checksum of a file.
"""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as input_file:
for chunk in iter(lambda: input_file.read(8192), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
@staticmethod
def _verify_sha256_if_configured(file_path: str, expected_sha256: str | None) -> None:
"""
Verifies the SHA256 checksum of a file when an expected value is provided.
"""
if expected_sha256 is None:
return
actual_sha256 = FileUtils.calculate_sha256(file_path)
if actual_sha256.lower() != expected_sha256.lower():
raise SolidLSPException(f"Checksum verification failed for '{file_path}': expected {expected_sha256}, got {actual_sha256}")
@staticmethod
def _validate_download_host(url: str, allowed_hosts: tuple[str, ...] | list[str] | None) -> None:
"""
Validates that a download URL resolves to one of the configured hosts.
"""
if not allowed_hosts:
return
hostname = urlparse(url).hostname
normalized_allowed_hosts = {host.lower() for host in allowed_hosts}
if hostname is None or hostname.lower() not in normalized_allowed_hosts:
raise SolidLSPException(
f"Refusing to download from host '{hostname or '<unknown>'}'; allowed hosts: {sorted(normalized_allowed_hosts)}"
)
@staticmethod
def _validate_extraction_path(member_name: str, target_path: str) -> str:
"""
Validates that an archive member stays within the extraction root and returns its destination path.
"""
normalized_parts = Path(member_name).parts
if any(part == ".." for part in normalized_parts):
raise SolidLSPException(f"Unsafe archive member '{member_name}': path traversal is not allowed")
absolute_target_path = os.path.abspath(target_path)
absolute_member_path = os.path.abspath(os.path.join(target_path, member_name))
if not (absolute_member_path.startswith(absolute_target_path + os.sep) or absolute_member_path == absolute_target_path):
raise SolidLSPException(f"Unsafe archive member '{member_name}': path escapes extraction directory")
return absolute_member_path
@staticmethod
def _extract_zip_archive(archive_path: str, target_path: str) -> None:
"""
Extracts a ZIP archive safely while preserving Unix permissions when available.
"""
with zipfile.ZipFile(archive_path, "r") as zip_ref:
for zip_info in zip_ref.infolist():
extracted_path = FileUtils._validate_extraction_path(zip_info.filename, target_path)
if zip_info.is_dir():
os.makedirs(extracted_path, exist_ok=True)
continue
os.makedirs(os.path.dirname(extracted_path), exist_ok=True)
with zip_ref.open(zip_info, "r") as source_file, open(extracted_path, "wb") as output_file:
shutil.copyfileobj(source_file, output_file)
ZIP_SYSTEM_UNIX = 3
if zip_info.create_system == ZIP_SYSTEM_UNIX:
attrs = (zip_info.external_attr >> 16) & 0o777
if attrs:
os.chmod(extracted_path, attrs)
@staticmethod
def _extract_tar_archive(archive_path: str, target_path: str, archive_type: str) -> None:
"""
Extracts a tar archive safely into the target directory.
"""
archive_mode_by_type = {
"tar": "r:",
"gztar": "r:gz",
"bztar": "r:bz2",
"xztar": "r:xz",
}
tar_mode = cast(Literal["r:", "r:gz", "r:bz2", "r:xz"], archive_mode_by_type[archive_type])
with tarfile.open(archive_path, tar_mode) as tar_ref:
for tar_member in tar_ref.getmembers():
FileUtils._validate_extraction_path(tar_member.name, target_path)
tar_ref.extractall(target_path)
class PlatformId(str, Enum):
WIN_x86 = "win-x86"
+55
View File
@@ -0,0 +1,55 @@
from types import SimpleNamespace
from serena.dashboard import SerenaDashboardAPI
from solidlsp.ls_config import Language
class _DummyMemoryLogHandler:
def get_log_messages(self, from_idx: int = 0): # pragma: no cover - simple stub
return SimpleNamespace(messages=[], max_idx=-1)
def clear_log_messages(self) -> None: # pragma: no cover - simple stub
pass
class _DummyAgent:
def __init__(self, project: SimpleNamespace | None) -> None:
self._project = project
def execute_task(self, func, *, logged: bool | None = None, name: str | None = None):
del logged, name
return func()
def get_active_project(self):
return self._project
def _make_dashboard(project_languages: list[Language] | None) -> SerenaDashboardAPI:
project = None
if project_languages is not None:
project = SimpleNamespace(project_config=SimpleNamespace(languages=project_languages))
agent = _DummyAgent(project)
return SerenaDashboardAPI(
memory_log_handler=_DummyMemoryLogHandler(),
tool_names=[],
agent=agent,
shutdown_callback=None,
tool_usage_stats=None,
)
def test_available_languages_include_experimental_when_no_active_project():
dashboard = _make_dashboard(project_languages=None)
response = dashboard._get_available_languages()
expected = sorted(lang.value for lang in Language.iter_all(include_experimental=True))
assert response.languages == expected
def test_available_languages_exclude_project_languages():
dashboard = _make_dashboard(project_languages=[Language.PYTHON, Language.MARKDOWN])
response = dashboard._get_available_languages()
available = set(response.languages)
assert Language.PYTHON.value not in available
assert Language.MARKDOWN.value not in available
# ensure experimental languages remain available for selection
assert Language.ANSIBLE.value in available
@@ -1,6 +1,7 @@
"""Tests for C# language server NuGet package download from NuGet.org."""
import tempfile
from pathlib import Path
from unittest.mock import patch
import pytest
@@ -15,7 +16,7 @@ class TestNuGetOrgDownload:
"""Test downloading Roslyn language server packages from NuGet.org."""
def test_download_nuget_package_uses_direct_url(self):
"""Test that _download_nuget_package uses the URL from RuntimeDependency directly."""
"""Test that _download_nuget_package uses the URL and checksum from RuntimeDependency directly."""
with tempfile.TemporaryDirectory() as temp_dir:
# Create a RuntimeDependency with a NuGet.org URL
test_dependency = RuntimeDependency(
@@ -41,21 +42,38 @@ class TestNuGetOrgDownload:
repository_root_path="/fake/repo",
)
# Mock urllib.request.urlretrieve to capture the URL being used
with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve") as mock_retrieve:
with patch("solidlsp.language_servers.csharp_language_server.SafeZipExtractor"):
try:
dependency_provider._download_nuget_package(test_dependency)
except Exception:
# Expected to fail since we're mocking, but we want to check the URL
pass
captured_calls: list[tuple[str, str, str, str | None, tuple[str, ...] | list[str] | None]] = []
# Verify that urlretrieve was called with the NuGet.org URL
assert mock_retrieve.called, "urlretrieve should be called"
called_url = mock_retrieve.call_args[0][0]
assert called_url == test_dependency.url, f"Should use URL from RuntimeDependency: {test_dependency.url}"
assert "nuget.org" in called_url, "Should use NuGet.org URL"
assert "azure" not in called_url.lower(), "Should not use Azure feed"
def fake_download_and_extract(
url: str,
target_path: str,
archive_type: str,
expected_sha256: str | None = None,
allowed_hosts: tuple[str, ...] | list[str] | None = None,
) -> None:
captured_calls.append((url, target_path, archive_type, expected_sha256, allowed_hosts))
Path(target_path).mkdir(parents=True, exist_ok=True)
with patch(
"solidlsp.language_servers.csharp_language_server.FileUtils.download_and_extract_archive_verified",
side_effect=fake_download_and_extract,
):
package_dir = dependency_provider._download_nuget_package(test_dependency)
assert package_dir == Path(temp_dir) / "temp_downloads" / "roslyn-language-server.linux-x64.5.5.0-2.26078.4"
assert captured_calls == [
(
test_dependency.url,
str(package_dir),
"zip",
test_dependency.sha256,
test_dependency.allowed_hosts,
)
]
called_url = captured_calls[0][0]
assert called_url == test_dependency.url, f"Should use URL from RuntimeDependency: {test_dependency.url}"
assert "nuget.org" in called_url, "Should use NuGet.org URL"
assert "azure" not in called_url.lower(), "Should not use Azure feed"
def test_runtime_dependencies_use_nuget_org_urls(self):
"""Test that _RUNTIME_DEPENDENCIES are configured with NuGet.org URLs."""
@@ -106,13 +124,7 @@ class TestNuGetOrgDownload:
)
# Mock urllib.request.urlopen to track if Azure feed is accessed
with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlopen") as mock_urlopen:
with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve"):
with patch("solidlsp.language_servers.csharp_language_server.SafeZipExtractor"):
try:
dependency_provider._download_nuget_package(test_dependency)
except Exception:
pass
# Verify that urlopen was NOT called (no service index lookup)
assert not mock_urlopen.called, "Should not call urlopen for Azure service index lookup"
with patch(
"solidlsp.language_servers.csharp_language_server.FileUtils.download_and_extract_archive_verified",
):
dependency_provider._download_nuget_package(test_dependency)
+7 -2
View File
@@ -3,6 +3,7 @@ from pathlib import Path
import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
@@ -243,8 +244,12 @@ class TestDartLanguageServer:
def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a variable usage."""
file_path = os.path.join("lib", "main.dart")
# Line 14 contains 'final result = a + b;' - test position on 'result'
defining_symbol = language_server.request_defining_symbol(file_path, 13, 10)
# Find coordinates of 'final result = a + b;' - test position on 'result'
with language_server.open_file(file_path, open_in_ls=False) as f:
pos = find_text_coordinates(f.contents, r"final (result) = a \+ b;")
defining_symbol = language_server.request_defining_symbol(file_path, pos.line, pos.col)
# The defining symbol might be the variable itself or the containing method
# This is acceptable behavior - different language servers handle this differently
@@ -1,7 +1,5 @@
"""Tests for the Luau language server dependency provider."""
import io
import zipfile
from pathlib import Path
from unittest.mock import patch
@@ -21,23 +19,6 @@ def _make_provider(
)
class _FakeResponse:
def __init__(self, content: bytes) -> None:
self.content = content
def raise_for_status(self) -> None:
return
def iter_content(self, chunk_size: int = 8192):
yield self.content
def __enter__(self) -> "_FakeResponse":
return self
def __exit__(self, exc_type, exc, tb) -> None:
return None
@pytest.mark.luau
class TestLuauDependencyProvider:
def test_create_launch_command_uses_ls_path_override_and_adds_assets(self, tmp_path: Path) -> None:
@@ -110,13 +91,24 @@ class TestLuauDependencyProvider:
def test_download_luau_lsp_extracts_binary_into_ls_resources_dir(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path)
archive = io.BytesIO()
with zipfile.ZipFile(archive, "w") as zip_file:
zip_file.writestr("nested/luau-lsp", "#!/bin/sh\n")
def fake_extract(
url: str,
target_path: str,
archive_type: str,
expected_sha256: str | None = None,
allowed_hosts: tuple[str, ...] | list[str] | None = None,
) -> None:
del url, archive_type, expected_sha256, allowed_hosts
nested_dir = Path(target_path) / "nested"
nested_dir.mkdir(parents=True, exist_ok=True)
(nested_dir / "luau-lsp").write_text("#!/bin/sh\n", encoding="utf-8")
with patch("solidlsp.language_servers.luau_lsp.platform.system", return_value="Linux"):
with patch("solidlsp.language_servers.luau_lsp.platform.machine", return_value="aarch64"):
with patch("solidlsp.language_servers.luau_lsp.requests.get", return_value=_FakeResponse(archive.getvalue())):
with patch(
"solidlsp.language_servers.luau_lsp.FileUtils.download_and_extract_archive_verified",
side_effect=fake_extract,
):
binary_path = provider._download_luau_lsp()
resolved_binary = Path(binary_path)
@@ -127,10 +119,16 @@ class TestLuauDependencyProvider:
def test_download_roblox_support_files_writes_into_ls_resources_dir(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path)
with patch(
"solidlsp.language_servers.luau_lsp.requests.get",
side_effect=[_FakeResponse(b"types"), _FakeResponse(b"docs")],
):
def fake_download(
url: str, target_path: str, expected_sha256: str | None = None, allowed_hosts: tuple[str, ...] | list[str] | None = None
) -> None:
del expected_sha256, allowed_hosts
if "type-definitions" in url:
Path(target_path).write_bytes(b"types")
else:
Path(target_path).write_bytes(b"docs")
with patch("solidlsp.language_servers.luau_lsp.FileUtils.download_file_verified", side_effect=fake_download):
definitions_path, docs_path = provider._download_roblox_support_files("LocalUserSecurity")
assert definitions_path == str(tmp_path / "globalTypes.LocalUserSecurity.d.luau")
@@ -141,7 +139,10 @@ class TestLuauDependencyProvider:
def test_download_standard_docs_writes_into_ls_resources_dir(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path, {"platform": "standard"})
with patch("solidlsp.language_servers.luau_lsp.requests.get", return_value=_FakeResponse(b"docs")):
with patch(
"solidlsp.language_servers.luau_lsp.FileUtils.download_file_verified",
side_effect=lambda url, target_path, expected_sha256=None, allowed_hosts=None: Path(target_path).write_bytes(b"docs"),
):
docs_path = provider._download_standard_docs()
assert docs_path == str(tmp_path / "luau-en-us.json")
@@ -12,6 +12,7 @@ import os
import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
@@ -583,15 +584,17 @@ class TestRubyLanguageServerSymbols:
"""Test request_defining_symbol for a method call."""
# Test finding the definition of a method being called
file_path = os.path.join("examples", "user_management.rb")
# Position at a method call like create_user
defining_symbol = language_server.request_defining_symbol(file_path, 30, 15)
# Verify that we can find method definitions
if defining_symbol is not None:
assert "name" in defining_symbol
assert "kind" in defining_symbol
# Should be a method or constructor
assert defining_symbol.get("kind") in [SymbolKind.Method.value, SymbolKind.Constructor.value, SymbolKind.Function.value]
# Position at a method call like create_user
with language_server.open_file(file_path, open_in_ls=False) as fb:
pos = find_text_coordinates(fb.contents, r"user = @service\.(create_user)")
# Verify that we can find the method definition
defining_symbol = language_server.request_defining_symbol(file_path, pos.line, pos.col)
assert "name" in defining_symbol
assert "kind" in defining_symbol
assert defining_symbol.get("name") == "create_user"
assert defining_symbol.get("kind") == SymbolKind.Method.value
@pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None:
+42
View File
@@ -0,0 +1,42 @@
from __future__ import annotations
import hashlib
from pathlib import Path
from unittest.mock import patch
from solidlsp.ls_utils import FileUtils
class _FakeResponse:
def __init__(self, payload: bytes, final_url: str) -> None:
self.status_code = 200
self.headers = {"content-encoding": "gzip"}
self.url = final_url
self._payload = payload
def iter_content(self, chunk_size: int = 1):
for offset in range(0, len(self._payload), chunk_size):
yield self._payload[offset : offset + chunk_size]
def close(self) -> None:
return None
def test_download_file_verified_writes_decoded_response_body(tmp_path: Path) -> None:
"""Gzip-encoded transfer bodies should be written as decoded payload bytes."""
payload = b"PK\x03\x04zip-content"
target_path = tmp_path / "downloaded.vsix"
final_url = "https://marketplace.visualstudio.com/example.vsix"
with patch(
"solidlsp.ls_utils.requests.get",
return_value=_FakeResponse(payload, final_url),
):
FileUtils.download_file_verified(
"https://marketplace.visualstudio.com/example.vsix",
str(target_path),
expected_sha256=hashlib.sha256(payload).hexdigest(),
allowed_hosts=("marketplace.visualstudio.com",),
)
assert target_path.read_bytes() == payload
Generated
+768 -571
View File
File diff suppressed because it is too large. Load diff