mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 04:16:34 +00:00
Merge remote-tracking branch 'origin/main' into pywebview
Conflicts: src/serena/resources/serena_config.template.yml
This commit is contained in:
commit
d96cda3ee1
65 files changed
+3962
-1540
No files matched your search
@@ -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
@@ -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: {}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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]
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user