mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 18:16:43 +00:00
Merge branch 'main' into feature/cross-package-typescript-references
# Conflicts: # CHANGELOG.md # src/solidlsp/ls.py
This commit is contained in:
commit
64e604630e
216 files changed
+7868
-1189
No files matched your search
@@ -224,6 +224,10 @@ jobs:
|
||||
with:
|
||||
r-version: '4.4.2'
|
||||
use-public-rspm: true
|
||||
- name: Install R sysdeps (libuv for fs package)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: sudo apt-get update && sudo apt-get install -y libuv1-dev
|
||||
- name: Install R language server
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -231,10 +235,10 @@ jobs:
|
||||
- name: Set up Julia
|
||||
uses: julia-actions/setup-julia@v2
|
||||
with:
|
||||
version: '1.10'
|
||||
version: '1.12.6'
|
||||
- name: Install Julia LanguageServer
|
||||
shell: bash
|
||||
run: julia -e 'using Pkg; Pkg.add("LanguageServer")'
|
||||
run: julia -e 'using Pkg; Pkg.add(Pkg.PackageSpec(name="LanguageServer", version="5.0.0"))'
|
||||
- name: Setup Haskell toolchain
|
||||
if: runner.os != 'Windows'
|
||||
uses: haskell/ghcup-setup@v1
|
||||
|
||||
+3
-1
@@ -79,6 +79,8 @@ downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
!test/resources/repos/dart/test_repo/lib/
|
||||
!test/resources/repos/dart/test_repo/lib/diagnostics_sample.dart
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
@@ -271,4 +273,4 @@ cabal.project.local*
|
||||
zz-misc/
|
||||
vue-implementation/
|
||||
|
||||
news/news.json
|
||||
news/news.json
|
||||
+26
-49
@@ -3,15 +3,18 @@ project_name: "serena"
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# swift terraform toml typescript typescript_vts
|
||||
# vue yaml zig
|
||||
# al ansible bash clojure cpp
|
||||
# cpp_ccls crystal csharp csharp_omnisharp dart
|
||||
# elixir elm erlang fortran fsharp
|
||||
# go groovy haskell haxe hlsl
|
||||
# java json julia kotlin lean4
|
||||
# lua luau markdown matlab msl
|
||||
# nix ocaml pascal perl php
|
||||
# php_phpactor powershell python python_jedi python_ty
|
||||
# r rego ruby ruby_solargraph rust
|
||||
# scala solidity swift systemverilog 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.)
|
||||
@@ -28,6 +31,7 @@ project_name: "serena"
|
||||
languages:
|
||||
- python
|
||||
- typescript
|
||||
- clojure
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
@@ -44,49 +48,12 @@ read_only: false
|
||||
|
||||
# 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`.
|
||||
# * `activate_project`: Activates a project based on the project name or path.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly,
|
||||
# for example by saying that the information retrieved from a memory file is no longer correct
|
||||
# or no longer relevant for the project.
|
||||
# * `edit_memory`: Replaces content matching a regular expression in a memory.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_file`: Finds files in the given relative paths
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend
|
||||
# * `find_symbol`: Performs a global (or local) search using the language server backend.
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual')
|
||||
# for clients that do not read the initial instructions when the MCP server is connected.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Read the content of a memory file. This tool should only be used if the information
|
||||
# is relevant to the current task. You can infer whether the information
|
||||
# is relevant from the memory file name.
|
||||
# You should not read the same memory file multiple times in the same conversation.
|
||||
# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported
|
||||
# (e.g., renaming "global/foo" to "bar" moves it from global to project scope).
|
||||
# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities.
|
||||
# For JB, we use a separate tool.
|
||||
# * `replace_content`: Replaces content in a file (optionally using regular expressions).
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend.
|
||||
# * `safe_delete_symbol`:
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
|
||||
# The memory name should be meaningful.
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
excluded_tools: []
|
||||
|
||||
# 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).
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
included_optional_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
@@ -121,14 +88,17 @@ encoding: utf-8
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
# Set this to [] to not use the default modes defined in the global config for this project.
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
default_modes:
|
||||
|
||||
# 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.
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
fixed_tools: []
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
@@ -166,3 +136,10 @@ ignored_memory_patterns: []
|
||||
# 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: {}
|
||||
|
||||
# list of mode names to be activated additionally for this project.
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
added_modes:
|
||||
+39
-7
@@ -2,13 +2,35 @@
|
||||
|
||||
Status of the `main` branch. Changes prior to the next official version change will appear here.
|
||||
|
||||
* Hooks:
|
||||
- `serena-hooks auto-approve` now also emits an `allow` decision when Claude Code reports
|
||||
`permission_mode == "auto"`, in addition to the existing `acceptEdits` behavior. #1386
|
||||
|
||||
* General:
|
||||
- Breaking change in mode definitions: Projects (project.yml) can no longer override `base_modes`.
|
||||
Instead, they can define `added_modes` to add modes on top of base and default modes.
|
||||
See updated [documentation on modes](https://oraios.github.io/serena/02-usage/050_configuration.html#modes).
|
||||
- Serena's default configuration now uses `interactive` and `editing` as `base_modes` instead of as `default_modes`.
|
||||
- Add cross-package reference support via `additional_workspace_folders` setting (currently implemented for TypeScript).
|
||||
|
||||
* JetBrains:
|
||||
- Add new tools:
|
||||
- `jet_brains_list_inspections`: Lists available IDE inspections (akin to diagnostics), optionally filtered by language or group
|
||||
- `jet_brains_run_inspections`: Runs IDE inspections on a file and returns the results
|
||||
|
||||
* LSP Tools:
|
||||
- Add new tools:
|
||||
- `find_declaration`: Finds the declaration/definition of a symbol
|
||||
- `find_implementations`: Finds the implementations of an interface or abstract method
|
||||
- `get_diagnostics_for_file`: Retrieves diagnostics for a specific file (errors, warnings, etc.)
|
||||
- `get_diagnostics_for_symbol`: Retrieves diagnostics pertaining to a specific symbol
|
||||
|
||||
* Language Servers:
|
||||
- Java (`eclipse.jdt.ls`): Add upstream JDTLS mode for offline / restricted-network use. Setting both `jdtls_path` and `lombok_path` in `ls_specific_settings.java` makes Serena use an existing upstream JDTLS installation (e.g. `brew install jdtls`) and the system JDK 21+, skipping the ~500 MB vscode-java VSIX, Gradle, and IntelliCode downloads. New related setting `java_home` lets the user override the JDK used to launch JDTLS. Default behavior unchanged — the JDTLS workspace hash is preserved bit-for-bit for users on the default route, so existing project caches are reused without a one-time reindex; the launcher path is mixed into the hash only when `jdtls_path` is set, isolating upstream installations from the default workspace. #1415
|
||||
|
||||
# v1.2.0 (2026-04-27)
|
||||
|
||||
* General:
|
||||
- Add cross-package reference support via `additional_workspace_folders` project setting (currently implemented for TypeScript).
|
||||
Configure additional workspace folder paths in `project.yml` to enable `find_referencing_symbols` and `request_references`
|
||||
to discover symbols across package boundaries in monorepos. The base `SolidLanguageServer` class provides the workspace
|
||||
folder resolution and activation framework; other language servers can enable support by overriding
|
||||
`_find_representative_source_file()`. #750
|
||||
- Support `serena --version` CLI command for displaying the current version #1347
|
||||
- Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350
|
||||
- `GetSymbolsOverviewTool`: ignored paths were not respected in LSP variant (fix in `SolidLanguageServer`)
|
||||
- Fix: Duplicate comments in re-saved YAML configuration files #1285
|
||||
@@ -36,6 +58,13 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
The `initial_instructions` tool provides the full prompt on demand, keeping the initial context lean.
|
||||
- Add `serena_info` tool for on-demand retrieval of usage information
|
||||
|
||||
* CLI:
|
||||
- Support `serena --version` CLI command for displaying the current version #1347
|
||||
- Extend `prompts` subcommand with `print-prompt-template` and `print-cc-system-prompt-override`, improve `list` subcommand
|
||||
|
||||
* Clients:
|
||||
- Document workaround to make Claude Code use Serena's tools after recent degradations caused by changes in CC harness and Opus 4.7 release.
|
||||
|
||||
* JetBrains:
|
||||
- Add `debug` tool: The agent can set breakpoints, inspect variables, evaluate expressions and control execution flow
|
||||
by directly interacting with the IDE's debugger, using a REPL-style interface for maximum flexibility.
|
||||
@@ -55,11 +84,14 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Add JSON language server support via `vscode-json-languageserver` (experimental) #1391
|
||||
- Fix: Elixir/Expert deadlock on startup — Expert's build pipeline requires a `textDocument/didOpen` notification to start; Serena now opens `mix.exs` immediately after `initialized` so Expert begins compiling instead of waiting indefinitely #1397
|
||||
|
||||
Dashboard:
|
||||
* Dashboard:
|
||||
- Add configurable dashboard interface mode (new global configuration setting `web_dashboard_interface`):
|
||||
Three modes (browser, native app with tray, tray manager for aggregating multiple instances) are supported, depending on the OS
|
||||
- Fix: Memory leaks in frontend when using Chromium-based browsers/Windows webview #1389
|
||||
|
||||
* Hooks:
|
||||
- Adjusted wording of startup hook, improving project activation instructions #1401.
|
||||
|
||||
# v1.1.2 (2026-04-14)
|
||||
|
||||
* General:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
Serena is the IDE for your coding agent.
|
||||
The IDE for Your Coding Agent
|
||||
</h3>
|
||||
|
||||
<div align="center">
|
||||
@@ -145,9 +145,10 @@ without reading entire files.
|
||||
| find referencing symbols | yes | yes |
|
||||
| search in project dependencies | -- | yes |
|
||||
| type hierarchy | -- | yes |
|
||||
| find declaration | -- | yes |
|
||||
| find implementations | -- | yes |
|
||||
| find declaration | yes | yes |
|
||||
| find implementations | yes | yes |
|
||||
| query external projects | yes | yes |
|
||||
| diagnostics/inspections | yes | yes |
|
||||
|
||||
### Refactoring
|
||||
|
||||
|
||||
@@ -101,8 +101,32 @@ You can do this in Tools / GitHub Copilot / Chat, where at the bottom you can cl
|
||||
|
||||
## Claude Code
|
||||
|
||||
Serena is a great way to make Claude Code both cheaper and more powerful!
|
||||
To add Serena to Claude Code, you can simply run `serena setup claude-code`. Alternatively, follow the instructions below.
|
||||
Serena is a great way to make Claude Code both more efficient and more powerful!
|
||||
To set up the Serena MCP server for Claude Code, you can simply run this command:
|
||||
|
||||
serena setup claude-code
|
||||
|
||||
Find manual setup instructions as well as workarounds for Claude Code's recent regressions pertaining to (external) tool use below.
|
||||
|
||||
:::{attention}
|
||||
Recent updates to Claude Code (CC) and to the Opus line of models resulted in drastically reduced
|
||||
adherence to instructions pertaining to Serena's tools.
|
||||
|
||||
After extensive analysis, we identified part of the reason to be very long and detailed
|
||||
tool descriptions for built-in tools and parts of the default system prompt.
|
||||
The descriptions of CC's system tools take almost 16k tokens, cannot be adjusted by the user,
|
||||
and introduce a very strong bias towards internal tools, making it almost impossible to convince Opus 4.7 to use Serena.
|
||||
|
||||
As a workaround, we crafted a system prompt that counteracts this bias.
|
||||
When using Serena, we highly recommend that you start CC as
|
||||
|
||||
```shell
|
||||
claude --system-prompt="$(serena prompts print-cc-system-prompt-override)"
|
||||
```
|
||||
|
||||
You can also consider adding the content of `serena cc-system-prompt-override` to your `CLAUDE.md` files,
|
||||
but the effect be insufficient for counteracting Claude Code's bias towards internal tools.
|
||||
:::
|
||||
|
||||
**Global Configuration**. To add the Serena MCP server for all your projects, use the user-level configuration of claude code and the `--project-from-cwd` flag:
|
||||
|
||||
@@ -194,9 +218,9 @@ The hooks will:
|
||||
`grep` or `read_file` calls without using any Serena tools in between.
|
||||
- **`activate`**: Prompt the agent to activate the project and read Serena's instructions at session start.
|
||||
- **`cleanup`**: Clean up hook session data when the session ends.
|
||||
- **`auto-approve`**: Auto-approve Serena tool calls whenever Claude Code is in `acceptEdits` mode,
|
||||
so blanket edit approvals cover Serena's destructive tools (e.g. `replace_symbol_body`,
|
||||
`rename_symbol`) instead of prompting on every call.
|
||||
- **`auto-approve`**: Auto-approve Serena tool calls whenever Claude Code is in a permissive
|
||||
permission mode (`acceptEdits` or `auto`), so blanket approvals cover Serena's destructive
|
||||
tools (e.g. `replace_symbol_body`, `rename_symbol`) instead of prompting on every call.
|
||||
|
||||
For more details on Claude Code's hook system, see the
|
||||
[Claude Code hooks documentation](https://code.claude.com/docs/en/hooks).
|
||||
|
||||
@@ -18,20 +18,22 @@ setting up a project with Serena typically involves the following steps:
|
||||
Project creation is the process of defining fundamental project settings that are relevant to Serena's operation.
|
||||
|
||||
You can create a project either
|
||||
* explicitly, using the project creation command (see below), or
|
||||
* implicitly, by just activating a directory as a project while already in a conversation; this will use default settings for your project (skip to the next section).
|
||||
* explicitly, using the project creation command, or
|
||||
|
||||
### Explicit Project Creation
|
||||
|
||||
To explicitly create a project, use the following command while in the project directory:
|
||||
|
||||
serena project create [options]
|
||||
serena project create [options] [project directory]
|
||||
|
||||
* For an empty project, you will need to specify the programming language
|
||||
(e.g., `--language python`).
|
||||
* For an existing project, the main programming language will be detected automatically,
|
||||
but you can choose to explicitly specify multiple languages by passing the `--language` parameter
|
||||
multiple times (e.g. `--language python --language typescript`).
|
||||
* The project directory defaults to the current directory if not specified.
|
||||
* For an existing project, the programming languages will be detected based on
|
||||
the source files present, and the main language will be activated automatically.
|
||||
If multiple languages are detected, you will be prompted whether you want to enable them.
|
||||
* For an empty project, you can optionally specify one or more languages
|
||||
to be activated explicitly via the `--language` parameter
|
||||
(e.g. `--language python --language typescript`).
|
||||
* You can optionally specify a custom project name with `--name my-name`.
|
||||
* You can immediately index the project after creation with `--index`.
|
||||
|
||||
@@ -42,6 +44,7 @@ After creation, you can adjust the project settings in the generated `.serena/pr
|
||||
within the project directory.
|
||||
|
||||
The file allows you to configure ...
|
||||
* the name by which you want to refer to the project (relevant when telling the LLM to dynamically activate the project)
|
||||
* the set of programming languages for which language servers are spawned (not relevant when using the JetBrains plugin)
|
||||
Note that you can dynamically add/remove language servers while Serena is running via the [Dashboard](060_dashboard).
|
||||
* the [language backend](per-project-language-backend) to use for this project (overriding the global setting)
|
||||
@@ -50,8 +53,8 @@ The file allows you to configure ...
|
||||
* write access
|
||||
* [additional workspace folders](additional-workspace-folders) for cross-package reference support in monorepos
|
||||
* an initial prompt that shall be passed to the LLM whenever the project is activated
|
||||
* the name by which you want to refer to the project (relevant when telling the LLM to dynamically activate the project)
|
||||
* the set of tools and modes to use by default
|
||||
* the set of tools and modes to use for the project
|
||||
* and some other settings.
|
||||
|
||||
For detailed information on the parameters and possible settings, see the
|
||||
[template file](https://github.com/oraios/serena/blob/main/src/serena/resources/project.template.yml).
|
||||
|
||||
@@ -118,32 +118,26 @@ Examples of built-in modes include:
|
||||
|
||||
Find the concrete definitions of these modes [here](https://github.com/oraios/serena/tree/main/src/serena/resources/config/modes).
|
||||
|
||||
Active modes are configured in (from lowest to highest precedence):
|
||||
The modes to be activated are configured in:
|
||||
* the global configuration file (`serena_config.yml`)
|
||||
- defines `base_modes`, which are always included
|
||||
- defines `default_modes`, which can be overridden by projects or command line parameters
|
||||
* the project configuration file (`project.yml`)
|
||||
- defines `default_modes` (overriding the default modes in the global configuration)
|
||||
- defines `added_modes`, which are added on top
|
||||
* at startup via command-line parameters
|
||||
- can override default modes with `--mode`
|
||||
- can define modes to be added on top with `--add-mode`
|
||||
|
||||
The two former sources define both **base modes** and **default modes**.
|
||||
Ultimately, the active modes are the union of base modes and default modes (after applying all overrides).
|
||||
Command-line parameters override default modes but not base modes.
|
||||
Base modes should thus be used to define modes that you always want to be active, regardless of command-line parameters.
|
||||
Ultimately, the active modes are given by the union of
|
||||
* `base_modes` defined in the global configuration (always active)
|
||||
* `default_modes` (defined in the global configuration, optionally overridden by the project/CLI)
|
||||
* `added_modes` (defined in the project configuration/via CLI parameters)
|
||||
|
||||
Command-line parameters for overriding default modes:
|
||||
When launching the MCP sever, specify modes using `--mode <mode-name>`; multiple modes can be specified, e.g. `--mode planning --mode no-onboarding`.
|
||||
|
||||
:::{important}
|
||||
By default, Serena activates the two modes `interactive` and `editing` (as defined in the global configuration).
|
||||
|
||||
As soon as you start to specify modes via the command line, only the modes you explicitly specify will be active, however.
|
||||
Therefore, if you want to keep the default modes, you must specify them as well.
|
||||
For example, to add mode `no-memories` to the default behaviour, specify
|
||||
```shell
|
||||
--mode interactive --mode editing --mode no-memories
|
||||
```
|
||||
|
||||
If you want to keep certain modes as always active, regardless of command-line parameters,
|
||||
define them as *base modes* in the global or project configuration.
|
||||
:::
|
||||
So you should
|
||||
* define modes you definitely always want to use in `base_modes`,
|
||||
* define modes that you typically want to use but sometimes want to override in `default_modes`,
|
||||
* use `added_modes` to add modes that you need only for specific projects/sessions.
|
||||
|
||||
:::{note}
|
||||
**Mode Compatibility**: While you can combine modes, some may be semantically incompatible (e.g., `interactive` and `one-shot`).
|
||||
@@ -504,28 +498,72 @@ ls_specific_settings:
|
||||
|
||||
#### Java (`eclipse.jdt.ls`)
|
||||
|
||||
Java support has two installation modes:
|
||||
|
||||
1. **Default vscode-java VSIX mode** (no extra config required): Serena downloads the platform-specific
|
||||
vscode-java VSIX (~500 MB: JDTLS + bundled JRE 21 + Lombok + IntelliCode), Gradle distribution and
|
||||
IntelliCode VSIX from public hosts on first use.
|
||||
2. **Upstream JDTLS mode** (offline-friendly): Activated by setting both `jdtls_path` and `lombok_path`.
|
||||
Uses an existing JDTLS installation (~100 MB) and the system JDK 21+. Nothing is downloaded.
|
||||
Recommended for restricted-network/corporate environments.
|
||||
|
||||
**When to use which mode:**
|
||||
|
||||
- **Default vscode-java VSIX mode** — recommended for most users. No setup required;
|
||||
Serena downloads everything on first use.
|
||||
- **Upstream JDTLS mode** — recommended when:
|
||||
- you cannot reach `github.com`, `services.gradle.org` or `marketplace.visualstudio.com`
|
||||
from the host (corporate proxy, air-gapped network);
|
||||
- you want a smaller on-disk footprint (~100 MB vs ~500 MB);
|
||||
- you already maintain a JDTLS installation (e.g. for `nvim-jdtls` or another editor);
|
||||
- your security policy prohibits per-project runtime downloads.
|
||||
|
||||
**JDK 21+ is required** in upstream mode. Serena resolves the JDK in this order:
|
||||
`ls_specific_settings.java.java_home` → `JAVA_HOME` env var → first `java` on `PATH`.
|
||||
The resolved JVM is interrogated and rejected if its `java.specification.version` is below 21.
|
||||
|
||||
The following settings are supported for the Java language server:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `jdtls_path` | `null` | Activates upstream JDTLS mode. Path to upstream JDTLS root (containing `plugins/` and `config_<platform>/`). Get via `brew install jdtls` or extract `jdt-language-server-*.tar.gz` from <https://download.eclipse.org/jdtls/snapshots/>. Must be set together with `lombok_path`. |
|
||||
| `lombok_path` | `null` | Path to the Lombok jar. Activates upstream JDTLS mode together with `jdtls_path`. Get from `~/.m2/repository/org/projectlombok/lombok/<ver>/lombok-<ver>.jar` or download from <https://projectlombok.org/downloads/>. |
|
||||
| `java_home` | `null` | (upstream-jdtls mode only) Path to JDK 21+ home directory used to launch JDTLS. Falls back to `JAVA_HOME` env var, then `which java`. |
|
||||
| `maven_user_settings` | `~/.m2/settings.xml` | Path to Maven `settings.xml` |
|
||||
| `gradle_user_home` | `~/.gradle` | Path to Gradle user home directory |
|
||||
| `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. |
|
||||
| `gradle_version` | `8.14.2` | (vscode-java mode only) Override the Gradle distribution version Serena downloads by default. |
|
||||
| `vscode_java_version` | `1.42.0-561` | (vscode-java mode only) Override the bundled `vscode-java` runtime bundle version Serena downloads by default. |
|
||||
| `intellicode_version` | `1.2.30` | (vscode-java mode only) 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. |
|
||||
| `intellicode_xmx` | `1G` | (vscode-java mode only) Maximum heap size for the IntelliCode embedded JVM. |
|
||||
| `intellicode_xms` | `100m` | (vscode-java mode only) Initial heap size for the IntelliCode embedded JVM. |
|
||||
|
||||
Note:
|
||||
Notes:
|
||||
- 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.
|
||||
- In upstream-jdtls mode, IntelliCode is not loaded (it's an ML completions ranker that is irrelevant to Serena's
|
||||
symbol-tools workflow), and Serena does not ship a Gradle distribution. Maven projects work via JDTLS's bundled m2e.
|
||||
Gradle projects must have `./gradlew` in the project, or rely on a system-installed Gradle through Buildship's
|
||||
default discovery rules.
|
||||
- In upstream-jdtls mode the `gradle_version`, `vscode_java_version`, `intellicode_version`,
|
||||
`intellicode_xmx`, `intellicode_xms` settings are silently ignored — they only apply to the
|
||||
vscode-java VSIX mode.
|
||||
|
||||
Example for a project with custom Gradle plugins and JDK requirements:
|
||||
Example: upstream-jdtls mode (offline / corporate network):
|
||||
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
java:
|
||||
jdtls_path: "/opt/homebrew/Cellar/jdtls/1.50.0/libexec"
|
||||
lombok_path: "/Users/me/.m2/repository/org/projectlombok/lombok/1.18.36/lombok-1.18.36.jar"
|
||||
# java_home: "/opt/homebrew/opt/openjdk@21" # optional
|
||||
```
|
||||
|
||||
Example: default vscode-java VSIX mode for a project with custom Gradle plugins:
|
||||
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="news-item">
|
||||
<h3>Serena v1.2.0</h3>
|
||||
<p class="date">April 27, 2026</p>
|
||||
<p>
|
||||
<b>Interactive Debugging.</b>
|
||||
With the <a target="_blank" href="https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html">Serena JetBrains plugin</a>,
|
||||
your agent can set now set breakpoints, inspect variables, evaluate expressions and control execution flow
|
||||
by directly interacting with the IDE's debugger.
|
||||
Our REPL-based interface is highly flexible, allowing all of this to be provided via a single tool.
|
||||
</p>
|
||||
<p>
|
||||
<b>Counteracting Claude Code Regressions.</b>
|
||||
We analysed the issues causing Claude Code to use external tools increasingly suboptimally and implemented
|
||||
another workaround (in addition to the existing hooks): a system prompt override.
|
||||
Read our updated <a target="_blank" href="https://oraios.github.io/serena/02-usage/030_clients.html#claude-code">Claude code usage instructions</a> for more information.
|
||||
</p>
|
||||
<p>
|
||||
The update also adds JSON as a new supported "language" for the LSP backend,
|
||||
improvements in token efficiency/prompt provision, as well as a multitude of general improvements and fixes.
|
||||
See our <a target="_blank" href="https://github.com/oraios/serena/blob/main/CHANGELOG.md">change log</a> for details.<br>
|
||||
</p>
|
||||
<p>
|
||||
<b>Update Now.</b>
|
||||
If you are using the uv tool installation of Serena, upgrade as follows:<br>
|
||||
uv tool upgrade serena-agent --prerelease=allow
|
||||
</p>
|
||||
</div>
|
||||
+1
-1
@@ -4,7 +4,7 @@ requires = ["hatchling"]
|
||||
|
||||
[project]
|
||||
name = "serena-agent"
|
||||
version = "1.1.2"
|
||||
version = "1.2.0"
|
||||
description = "A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent"
|
||||
authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from serena.cli import top_level
|
||||
|
||||
if __name__ == "__main__":
|
||||
top_level(["--help"])
|
||||
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Demonstrates diagnostics tools and edit-tool diagnostic reporting on the Serena repo itself.
|
||||
|
||||
The script creates a temporary Python file inside this repository, introduces one warning,
|
||||
shows file and symbol diagnostics, then introduces another warning and verifies that the
|
||||
second edit reports only the newly introduced warning.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.project import Project
|
||||
from serena.tools import (
|
||||
CreateTextFileTool,
|
||||
EditingToolWithDiagnostics,
|
||||
GetDiagnosticsForFileTool,
|
||||
GetDiagnosticsForSymbolTool,
|
||||
ReplaceContentTool,
|
||||
)
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
SEPARATOR = "=" * 80
|
||||
REPO_PATH = Path(REPO_ROOT)
|
||||
EDIT_RESULT_PREFIX = "Edit introduced new warning-or-higher diagnostics: "
|
||||
|
||||
|
||||
def make_agent() -> SerenaAgent:
|
||||
"""Create an LSP-backed Serena agent for the Serena repository."""
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
|
||||
project = Project(
|
||||
project_root=str(REPO_PATH),
|
||||
project_config=ProjectConfig(
|
||||
project_name="demo_serena_repo",
|
||||
languages=[Language.PYTHON],
|
||||
ignored_paths=[],
|
||||
excluded_tools=[],
|
||||
read_only=False,
|
||||
ignore_all_files_in_gitignore=True,
|
||||
initial_prompt="",
|
||||
encoding="utf-8",
|
||||
),
|
||||
serena_config=serena_config,
|
||||
)
|
||||
serena_config.projects = [RegisteredProject.from_project_instance(project)]
|
||||
return SerenaAgent(project="demo_serena_repo", serena_config=serena_config)
|
||||
|
||||
|
||||
def print_section(title: str) -> None:
|
||||
"""Print a visibly separated section header."""
|
||||
print(f"\n{SEPARATOR}")
|
||||
print(title)
|
||||
print(SEPARATOR)
|
||||
|
||||
|
||||
def parse_json_result(result: str) -> object:
|
||||
"""Parse and pretty-print JSON tool output."""
|
||||
parsed = json.loads(result)
|
||||
pprint(parsed, width=200)
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_edit_diagnostics_result(result: str) -> dict:
|
||||
"""Extract the grouped diagnostics payload from an edit-tool result."""
|
||||
assert result.startswith(EDIT_RESULT_PREFIX), result
|
||||
return json.loads(result[len(EDIT_RESULT_PREFIX) :])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
EditingToolWithDiagnostics.ENABLE_DIAGNOSTICS = True
|
||||
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="serena_demo_", dir=REPO_PATH))
|
||||
temp_file = temp_dir / "demo_temp_diagnostics.py"
|
||||
relative_path = temp_file.relative_to(REPO_PATH).as_posix()
|
||||
|
||||
initial_content = """def demo_existing_issue() -> int:
|
||||
value = 1
|
||||
return value
|
||||
"""
|
||||
|
||||
agent = make_agent()
|
||||
|
||||
try:
|
||||
# letting the language server finish startup
|
||||
agent.execute_task(lambda: None)
|
||||
|
||||
create_text_file_tool = agent.get_tool(CreateTextFileTool)
|
||||
replace_content_tool = agent.get_tool(ReplaceContentTool)
|
||||
get_diagnostics_for_file_tool = agent.get_tool(GetDiagnosticsForFileTool)
|
||||
get_diagnostics_for_symbol_tool = agent.get_tool(GetDiagnosticsForSymbolTool)
|
||||
|
||||
# creating a clean temporary file
|
||||
print_section("Create Temporary File")
|
||||
create_result = agent.execute_task(lambda: create_text_file_tool.apply(relative_path=relative_path, content=initial_content))
|
||||
print(create_result)
|
||||
|
||||
# showing file diagnostics before introducing any warning
|
||||
print_section("Initial File Diagnostics")
|
||||
initial_diagnostics_result = agent.execute_task(
|
||||
lambda: get_diagnostics_for_file_tool.apply(relative_path=relative_path, min_severity=2)
|
||||
)
|
||||
initial_diagnostics = parse_json_result(initial_diagnostics_result)
|
||||
assert initial_diagnostics == {}, initial_diagnostics
|
||||
|
||||
# introducing the first warning
|
||||
print_section("First Edit Result")
|
||||
first_edit_result = agent.execute_task(
|
||||
lambda: replace_content_tool.apply(
|
||||
relative_path=relative_path,
|
||||
needle="value = 1",
|
||||
repl="value = missing_one",
|
||||
mode="literal",
|
||||
)
|
||||
)
|
||||
print(first_edit_result)
|
||||
first_edit_diagnostics = parse_edit_diagnostics_result(first_edit_result)
|
||||
pprint(first_edit_diagnostics, width=200)
|
||||
assert "missing_one" in json.dumps(first_edit_diagnostics), first_edit_diagnostics
|
||||
|
||||
# showing the file- and symbol-level diagnostics after the first warning
|
||||
print_section("File Diagnostics After First Edit")
|
||||
diagnostics_after_first_edit_result = agent.execute_task(
|
||||
lambda: get_diagnostics_for_file_tool.apply(relative_path=relative_path, min_severity=2)
|
||||
)
|
||||
diagnostics_after_first_edit = parse_json_result(diagnostics_after_first_edit_result)
|
||||
assert "missing_one" in json.dumps(diagnostics_after_first_edit), diagnostics_after_first_edit
|
||||
|
||||
print_section("Symbol Diagnostics After First Edit")
|
||||
symbol_diagnostics_result = agent.execute_task(
|
||||
lambda: get_diagnostics_for_symbol_tool.apply(
|
||||
name_path="demo_existing_issue",
|
||||
reference_file=relative_path,
|
||||
min_severity=2,
|
||||
)
|
||||
)
|
||||
symbol_diagnostics = parse_json_result(symbol_diagnostics_result)
|
||||
assert "missing_one" in json.dumps(symbol_diagnostics), symbol_diagnostics
|
||||
|
||||
# introducing a second warning while keeping the first one unchanged
|
||||
print_section("Second Edit Result")
|
||||
second_edit_result = agent.execute_task(
|
||||
lambda: replace_content_tool.apply(
|
||||
relative_path=relative_path,
|
||||
needle=" return value\n",
|
||||
repl=" other = missing_two\n return value + other\n",
|
||||
mode="literal",
|
||||
)
|
||||
)
|
||||
print(second_edit_result)
|
||||
second_edit_diagnostics = parse_edit_diagnostics_result(second_edit_result)
|
||||
pprint(second_edit_diagnostics, width=200)
|
||||
second_edit_json = json.dumps(second_edit_diagnostics)
|
||||
assert "missing_two" in second_edit_json, second_edit_diagnostics
|
||||
assert "missing_one" not in second_edit_json, second_edit_diagnostics
|
||||
print("\nVerified: the second edit result reports only the newly introduced warning.")
|
||||
|
||||
# showing the complete file diagnostics after both warnings exist
|
||||
print_section("File Diagnostics After Second Edit")
|
||||
diagnostics_after_second_edit_result = agent.execute_task(
|
||||
lambda: get_diagnostics_for_file_tool.apply(relative_path=relative_path, min_severity=2)
|
||||
)
|
||||
diagnostics_after_second_edit = parse_json_result(diagnostics_after_second_edit_result)
|
||||
diagnostics_after_second_edit_json = json.dumps(diagnostics_after_second_edit)
|
||||
assert "missing_one" in diagnostics_after_second_edit_json, diagnostics_after_second_edit
|
||||
assert "missing_two" in diagnostics_after_second_edit_json, diagnostics_after_second_edit
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
agent.shutdown()
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Demonstrates both defining-symbol tools on the Python test repository.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.project import Project
|
||||
from serena.tools import FindDeclarationTool
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
SEPARATOR = "=" * 80
|
||||
PYTHON_TEST_REPO = Path(REPO_ROOT) / "test" / "resources" / "repos" / "python" / "test_repo"
|
||||
SERVICES_FILE = Path("test_repo") / "services.py"
|
||||
|
||||
|
||||
def make_agent(project_root: Path, language: Language, project_name: str) -> SerenaAgent:
|
||||
"""Create an LSP-backed Serena agent for a single explicit project."""
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
|
||||
project = Project(
|
||||
project_root=str(project_root),
|
||||
project_config=ProjectConfig(
|
||||
project_name=project_name,
|
||||
languages=[language],
|
||||
ignored_paths=[],
|
||||
excluded_tools=[],
|
||||
read_only=False,
|
||||
ignore_all_files_in_gitignore=True,
|
||||
initial_prompt="",
|
||||
encoding="utf-8",
|
||||
),
|
||||
serena_config=serena_config,
|
||||
)
|
||||
serena_config.projects = [RegisteredProject.from_project_instance(project)]
|
||||
return SerenaAgent(project=project_name, serena_config=serena_config)
|
||||
|
||||
|
||||
def print_section(title: str) -> None:
|
||||
"""Print a visibly separated section header."""
|
||||
print(f"\n{SEPARATOR}")
|
||||
print(title)
|
||||
print(SEPARATOR)
|
||||
|
||||
|
||||
def find_identifier_occurrence_position(file_path: Path, identifier: str, occurrence_index: int = 0) -> tuple[int, int]:
|
||||
"""Find the 0-based position of an identifier occurrence in a file."""
|
||||
pattern = re.compile(r"\b" + re.escape(identifier) + r"\b")
|
||||
current_occurrence_index = 0
|
||||
with file_path.open(encoding="utf-8") as f:
|
||||
for line_index, line in enumerate(f):
|
||||
for match in pattern.finditer(line):
|
||||
if current_occurrence_index == occurrence_index:
|
||||
return line_index, match.start()
|
||||
current_occurrence_index += 1
|
||||
raise ValueError(f"Could not find occurrence {occurrence_index} of {identifier!r} in {file_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = make_agent(PYTHON_TEST_REPO, Language.PYTHON, "demo_python_test_repo")
|
||||
|
||||
try:
|
||||
# letting the language server finish startup
|
||||
agent.execute_task(lambda: None)
|
||||
|
||||
relative_path = SERVICES_FILE.as_posix()
|
||||
services_abs_path = PYTHON_TEST_REPO / SERVICES_FILE
|
||||
|
||||
# resolving via regex over the full file
|
||||
find_by_regex_tool = agent.get_tool(FindDeclarationTool)
|
||||
regex_result = agent.execute_task(
|
||||
lambda: find_by_regex_tool.apply(
|
||||
regex=r"from \.models import Item, (User)",
|
||||
relative_path=relative_path,
|
||||
include_info=True,
|
||||
)
|
||||
)
|
||||
|
||||
print_section("FindDefiningSymbolTool (File Regex)")
|
||||
regex_symbol = json.loads(regex_result)
|
||||
pprint(regex_symbol, width=200)
|
||||
|
||||
# resolving via regex restricted to one containing symbol body
|
||||
contained_regex_result = agent.execute_task(
|
||||
lambda: find_by_regex_tool.apply(
|
||||
regex=r"=\s+(User)\(",
|
||||
relative_path=relative_path,
|
||||
containing_symbol_name_path="UserService/create_user",
|
||||
include_info=True,
|
||||
)
|
||||
)
|
||||
|
||||
print_section("FindDefiningSymbolTool (Contained Regex)")
|
||||
contained_regex_symbol = json.loads(contained_regex_result)
|
||||
pprint(contained_regex_symbol, width=200)
|
||||
|
||||
# validating the demonstrated result
|
||||
for symbol in [regex_symbol, contained_regex_symbol]:
|
||||
assert symbol is not None, "Expected a defining symbol result"
|
||||
assert symbol.get("relative_path") is not None
|
||||
assert "models.py" in symbol["relative_path"], symbol
|
||||
assert "User" in json.dumps(symbol), symbol
|
||||
print("\nVerified definition target: User in models.py")
|
||||
finally:
|
||||
agent.shutdown()
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Demonstrates FindImplementationsTool on the Go test repository.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.project import Project
|
||||
from serena.tools import FindImplementationsTool
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
SEPARATOR = "=" * 80
|
||||
GO_TEST_REPO = Path(REPO_ROOT) / "test" / "resources" / "repos" / "go" / "test_repo"
|
||||
|
||||
|
||||
def make_agent(project_root: Path, language: Language, project_name: str) -> SerenaAgent:
|
||||
"""Create an LSP-backed Serena agent for a single explicit project."""
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
|
||||
project = Project(
|
||||
project_root=str(project_root),
|
||||
project_config=ProjectConfig(
|
||||
project_name=project_name,
|
||||
languages=[language],
|
||||
ignored_paths=[],
|
||||
excluded_tools=[],
|
||||
read_only=False,
|
||||
ignore_all_files_in_gitignore=True,
|
||||
initial_prompt="",
|
||||
encoding="utf-8",
|
||||
),
|
||||
serena_config=serena_config,
|
||||
)
|
||||
serena_config.projects = [RegisteredProject.from_project_instance(project)]
|
||||
return SerenaAgent(project=project_name, serena_config=serena_config)
|
||||
|
||||
|
||||
def print_section(title: str) -> None:
|
||||
"""Print a visibly separated section header."""
|
||||
print(f"\n{SEPARATOR}")
|
||||
print(title)
|
||||
print(SEPARATOR)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent = make_agent(GO_TEST_REPO, Language.GO, "demo_go_test_repo")
|
||||
|
||||
try:
|
||||
# letting the language server finish startup
|
||||
agent.execute_task(lambda: None)
|
||||
|
||||
# running the implementation lookup
|
||||
find_implementations_tool = agent.get_tool(FindImplementationsTool)
|
||||
result = agent.execute_task(
|
||||
lambda: find_implementations_tool.apply(
|
||||
name_path="Greeter/FormatGreeting",
|
||||
relative_path="main.go",
|
||||
include_info=True,
|
||||
)
|
||||
)
|
||||
|
||||
print_section("Find Implementations Result")
|
||||
implementations = json.loads(result)
|
||||
pprint(implementations, width=200)
|
||||
|
||||
# validating the demonstrated result
|
||||
assert any(implementation["name_path"] == "(ConsoleGreeter).FormatGreeting" for implementation in implementations), result
|
||||
print("\nVerified implementation target: (ConsoleGreeter).FormatGreeting")
|
||||
finally:
|
||||
agent.shutdown()
|
||||
@@ -8,14 +8,16 @@ from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.config.serena_config import LanguageBackend, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.tools import (
|
||||
FindFileTool,
|
||||
FindReferencingSymbolsTool,
|
||||
GetDiagnosticsForFileTool,
|
||||
JetBrainsFindSymbolTool,
|
||||
JetBrainsGetSymbolsOverviewTool,
|
||||
JetBrainsInlineSymbol,
|
||||
JetBrainsRunInspectionsTool,
|
||||
JetBrainsSafeDeleteTool,
|
||||
SearchForPatternTool,
|
||||
)
|
||||
@@ -23,6 +25,7 @@ from serena.tools import (
|
||||
if __name__ == "__main__":
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
# project = Path(REPO_ROOT).parent / "serena-jetbrains-plugin-copy"
|
||||
project = Path(REPO_ROOT)
|
||||
agent = SerenaAgent(project=str(project), serena_config=serena_config)
|
||||
@@ -35,12 +38,13 @@ if __name__ == "__main__":
|
||||
overview_tool = agent.get_tool(JetBrainsGetSymbolsOverviewTool)
|
||||
safe_delete_tool = agent.get_tool(JetBrainsSafeDeleteTool)
|
||||
inline_symbol = agent.get_tool(JetBrainsInlineSymbol)
|
||||
diagnostics_in_file_tool = agent.get_tool(GetDiagnosticsForFileTool)
|
||||
jb_inspections_tool = agent.get_tool(JetBrainsRunInspectionsTool)
|
||||
|
||||
result = agent.execute_task(
|
||||
lambda: overview_tool.apply(
|
||||
lambda: diagnostics_in_file_tool.apply(
|
||||
# name_path_pattern="SerenaAgent",
|
||||
relative_path="src/serena/agent.py",
|
||||
depth=2,
|
||||
relative_path="test/resources/repos/clojure/test_repo/src/test_app/diagnostics_sample.clj",
|
||||
# keep_definition=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -30,6 +30,9 @@ class JinjaTemplate(ParameterizedTemplateInterface):
|
||||
parsed_content = self._template.environment.parse(self._template_string)
|
||||
self._parameters = sorted(jinja2.meta.find_undeclared_variables(parsed_content))
|
||||
|
||||
def get_template_string(self) -> str:
|
||||
return self._template_string
|
||||
|
||||
def render(self, **params: Any) -> str:
|
||||
"""Renders the template with the given kwargs. You can find out which parameters are required by calling get_parameter_names()."""
|
||||
return self._template.render(**params)
|
||||
|
||||
@@ -12,13 +12,17 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PromptTemplate(ToStringMixin, ParameterizedTemplateInterface):
|
||||
def __init__(self, name: str, jinja_template_string: str) -> None:
|
||||
def __init__(self, name: str, jinja_template_string: str, path: str) -> None:
|
||||
self.name = name
|
||||
self.path = path
|
||||
self._jinja_template = JinjaTemplate(jinja_template_string.strip())
|
||||
|
||||
def _tostring_exclude_private(self) -> bool:
|
||||
return True
|
||||
|
||||
def get_template_string(self) -> str:
|
||||
return self._jinja_template.get_template_string()
|
||||
|
||||
def render(self, **params: Any) -> str:
|
||||
return self._jinja_template.render(**params)
|
||||
|
||||
@@ -249,6 +253,7 @@ class MultiLangPromptCollection:
|
||||
self,
|
||||
name: str,
|
||||
template_str: str,
|
||||
path: str,
|
||||
lang_code: str = DEFAULT_LANG_CODE,
|
||||
on_name_collision: Literal["skip", "overwrite", "raise"] = "raise",
|
||||
) -> None:
|
||||
@@ -259,7 +264,7 @@ class MultiLangPromptCollection:
|
||||
:param on_name_collision: how to deal with name/lang_code collisions
|
||||
"""
|
||||
allow_overwrite = False
|
||||
prompt_template = PromptTemplate(name, template_str)
|
||||
prompt_template = PromptTemplate(name, template_str, path=path)
|
||||
mlpt = self._multi_lang_prompt_templates.get(name)
|
||||
if mlpt is None:
|
||||
mlpt = MultiLangPromptTemplate(name)
|
||||
@@ -327,7 +332,7 @@ class MultiLangPromptCollection:
|
||||
self._add_prompt_list(prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision)
|
||||
elif isinstance(prompt_template_or_list, str):
|
||||
self._add_prompt_template(
|
||||
prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision
|
||||
prompt_name, prompt_template_or_list, lang_code=lang_code, path=path, on_name_collision=on_name_collision
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from .multilang_prompt import DEFAULT_LANG_CODE, LanguageFallbackMode, MultiLangPromptCollection, PromptList
|
||||
from .multilang_prompt import DEFAULT_LANG_CODE, LanguageFallbackMode, MultiLangPromptCollection, PromptList, PromptTemplate
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,9 +23,18 @@ class PromptFactoryBase:
|
||||
self.lang_code = lang_code
|
||||
self._prompt_collection = MultiLangPromptCollection(prompts_dir, fallback_mode=fallback_mode)
|
||||
|
||||
def get_prompt_names(self) -> list[str]:
|
||||
return self._prompt_collection.get_prompt_template_names()
|
||||
|
||||
def get_prompt_template(self, prompt_name: str) -> PromptTemplate:
|
||||
return self._prompt_collection.get_prompt_template(prompt_name, lang_code=self.lang_code)
|
||||
|
||||
def get_prompt_template_string(self, prompt_name: str) -> str:
|
||||
return self.get_prompt_template(prompt_name).get_template_string()
|
||||
|
||||
def _render_prompt(self, prompt_name: str, params: dict[str, Any]) -> str:
|
||||
del params["self"]
|
||||
return self._prompt_collection.render_prompt_template(prompt_name, params, lang_code=self.lang_code)
|
||||
return self.get_prompt_template(prompt_name).render(**params)
|
||||
|
||||
def _get_prompt_list(self, prompt_name: str) -> PromptList:
|
||||
return self._prompt_collection.get_prompt_list(prompt_name, self.lang_code)
|
||||
@@ -41,14 +50,12 @@ def autogenerate_prompt_factory_module(prompts_dir: str, target_module_path: str
|
||||
:param prompts_dir: the directory containing the prompt templates and prompt lists
|
||||
:param target_module_path: the path to the target module file (.py). Important: The module will be overwritten!
|
||||
"""
|
||||
generated_code = """
|
||||
# ruff: noqa
|
||||
generated_code = """# ruff: noqa
|
||||
# black: skip
|
||||
# mypy: ignore-errors
|
||||
|
||||
# NOTE: This module is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually!
|
||||
|
||||
from interprompt.multilang_prompt import PromptList
|
||||
from interprompt.prompt_factory import PromptFactoryBase
|
||||
from typing import Any
|
||||
|
||||
@@ -68,6 +75,9 @@ class PromptFactory(PromptFactoryBase):
|
||||
else:
|
||||
method_params_str = ", *, " + ", ".join([f"{param}: Any" for param in template_parameters])
|
||||
generated_code += f"""
|
||||
def get_{template_name}_template_string(self) -> str:
|
||||
return self.get_prompt_template_string('{template_name}')
|
||||
|
||||
def create_{template_name}(self{method_params_str}) -> str:
|
||||
return self._render_prompt('{template_name}', locals())
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = "1.1.2"
|
||||
__version__ = "1.2.0"
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
+35
-26
@@ -31,6 +31,8 @@ from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.config.serena_config import (
|
||||
LanguageBackend,
|
||||
ModeSelectionDefinition,
|
||||
ModeSelectionDefinitionWithAddedModes,
|
||||
ModeSelectionDefinitionWithBaseModes,
|
||||
NamedToolInclusionDefinition,
|
||||
RegisteredProject,
|
||||
SerenaConfig,
|
||||
@@ -206,18 +208,36 @@ class ActiveModes:
|
||||
def __init__(self) -> None:
|
||||
self._configured_base_modes: Sequence[str] | None = None
|
||||
self._configured_default_modes: Sequence[str] | None = None
|
||||
self._added_modes: set[str] = set()
|
||||
self._dynamically_activated_mode_names: set[str] = set()
|
||||
"""
|
||||
the subset of active mode names that are dynamically activated (not necessarily enabled after project change)
|
||||
"""
|
||||
self._active_mode_names: Sequence[str] = []
|
||||
"""
|
||||
the full list of active mode names
|
||||
"""
|
||||
|
||||
def apply(self, mode_selection: ModeSelectionDefinition) -> None:
|
||||
log.debug("Applying mode selection definition %s", mode_selection)
|
||||
|
||||
# apply overrides
|
||||
log.debug("Applying mode selection: default_modes=%s, base_modes=%s", mode_selection.default_modes, mode_selection.base_modes)
|
||||
if mode_selection.base_modes is not None:
|
||||
self._configured_base_modes = mode_selection.base_modes
|
||||
if isinstance(mode_selection, ModeSelectionDefinitionWithBaseModes):
|
||||
if mode_selection.base_modes is not None:
|
||||
self._configured_base_modes = mode_selection.base_modes
|
||||
if mode_selection.default_modes is not None:
|
||||
self._configured_default_modes = mode_selection.default_modes
|
||||
log.debug("Current mode selection: base_modes=%s, default_modes=%s", self._configured_base_modes, self._configured_default_modes)
|
||||
|
||||
self._active_mode_names = sorted(set(self._configured_base_modes or []) | set(self._configured_default_modes or []))
|
||||
# apply added modes (if any)
|
||||
if isinstance(mode_selection, ModeSelectionDefinitionWithAddedModes):
|
||||
if mode_selection.added_modes:
|
||||
log.debug("Adding modes: %s", mode_selection.added_modes)
|
||||
self._added_modes.update(mode_selection.added_modes)
|
||||
log.debug("Current added modes: %s", self._added_modes)
|
||||
|
||||
self._dynamically_activated_mode_names = set(self._configured_default_modes or []) | self._added_modes
|
||||
self._active_mode_names = sorted(set(self._configured_base_modes or []) | self._dynamically_activated_mode_names)
|
||||
|
||||
def get_mode_names(self) -> Sequence[str]:
|
||||
return self._active_mode_names
|
||||
@@ -231,8 +251,8 @@ class ActiveModes:
|
||||
def get_modes(self) -> Sequence[SerenaAgentMode]:
|
||||
return [self.get_mode_instance(mode_name) for mode_name in self._active_mode_names]
|
||||
|
||||
def get_default_modes(self) -> Sequence[SerenaAgentMode]:
|
||||
return [self.get_mode_instance(mode_name) for mode_name in self._configured_default_modes or []]
|
||||
def get_dynamically_activated_modes(self) -> Sequence[SerenaAgentMode]:
|
||||
return [self.get_mode_instance(mode_name) for mode_name in self._dynamically_activated_mode_names]
|
||||
|
||||
def get_base_modes(self) -> Sequence[SerenaAgentMode]:
|
||||
return [self.get_mode_instance(mode_name) for mode_name in self._configured_base_modes or []]
|
||||
@@ -511,8 +531,7 @@ class SerenaAgent:
|
||||
:param serena_config: the Serena configuration or None to read the configuration from the default location.
|
||||
:param context: the context in which the agent is operating, None for default context.
|
||||
The context may adjust prompts, tool availability, and tool descriptions.
|
||||
:param modes: list of modes in which the agent is operating (they will be combined), None for default modes.
|
||||
The modes may adjust prompts, tool availability, and tool descriptions.
|
||||
:param modes: mode selection definition to apply for this session
|
||||
:param memory_log_handler: a MemoryLogHandler instance from which to read log messages; if None, a new one will be created
|
||||
if necessary.
|
||||
"""
|
||||
@@ -521,7 +540,7 @@ class SerenaAgent:
|
||||
self._gui_log_viewer: Optional["GuiLogViewer"] = None
|
||||
self._dashboard_manager: DashboardManager | None = None
|
||||
self._project_prompt_status = ProjectPromptProvisionStatus()
|
||||
self._mode_overrides = modes
|
||||
self._session_mode_selection_definition = modes
|
||||
self.version = serena_version()
|
||||
|
||||
# obtain serena configuration using the decoupled factory function
|
||||
@@ -712,9 +731,11 @@ class SerenaAgent:
|
||||
# * base modes: These cannot be changed, so they are fully applied
|
||||
for base_mode in modes.get_base_modes():
|
||||
tool_inclusion_definitions.append(base_mode)
|
||||
# * default modes: When not in a single-project context, these modes are dynamic (can later be turned off),
|
||||
# so we consider only their inclusions (but not their exclusions, because these must not be hard)
|
||||
for mode in modes.get_default_modes():
|
||||
# * dynamically activated modes:
|
||||
# - When not in a single-project context, these modes can later be turned off,
|
||||
# so we consider only their inclusions (but not their exclusions, because these must not be hard).
|
||||
# - In a single-project context, we can consider them fully.
|
||||
for mode in modes.get_dynamically_activated_modes():
|
||||
if is_single_project:
|
||||
tool_inclusion_definitions.append(mode)
|
||||
else:
|
||||
@@ -869,18 +890,6 @@ class SerenaAgent:
|
||||
raise ValueError("No active project. Please activate a project first.")
|
||||
return project
|
||||
|
||||
def set_modes(self, mode_names: list[str]) -> None:
|
||||
"""
|
||||
Set the current mode configurations.
|
||||
|
||||
:param mode_names: List of mode names or paths to use
|
||||
"""
|
||||
self._mode_overrides = ModeSelectionDefinition(default_modes=mode_names)
|
||||
self._update_active_modes()
|
||||
self._update_active_tools()
|
||||
|
||||
log.info(f"Set modes to {[mode.name for mode in self.get_active_modes()]}")
|
||||
|
||||
def get_active_modes(self) -> list[SerenaAgentMode]:
|
||||
"""
|
||||
:return: the list of active modes
|
||||
@@ -998,8 +1007,8 @@ class SerenaAgent:
|
||||
self._active_modes.apply(self.serena_config)
|
||||
if self._active_project:
|
||||
self._active_modes.apply(self._active_project.project_config)
|
||||
if self._mode_overrides:
|
||||
self._active_modes.apply(self._mode_overrides)
|
||||
if self._session_mode_selection_definition:
|
||||
self._active_modes.apply(self._session_mode_selection_definition)
|
||||
if log_message:
|
||||
active_mode_names = self._active_modes.get_mode_names()
|
||||
log.info(f"Active modes ({len(active_mode_names)}): {', '.join(active_mode_names)}")
|
||||
|
||||
+84
-18
@@ -18,12 +18,12 @@ from sensai.util.string import dict_string
|
||||
from tqdm import tqdm
|
||||
|
||||
from serena import serena_version
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.client_setup import client_setup_handlers
|
||||
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.config.serena_config import (
|
||||
LanguageBackend,
|
||||
ModeSelectionDefinition,
|
||||
ModeSelectionDefinitionWithAddedModes,
|
||||
ProjectConfig,
|
||||
RegisteredProject,
|
||||
SerenaConfig,
|
||||
@@ -36,11 +36,8 @@ from serena.constants import (
|
||||
SERENAS_OWN_CONTEXT_YAMLS_DIR,
|
||||
SERENAS_OWN_MODE_YAMLS_DIR,
|
||||
)
|
||||
from serena.mcp import SerenaMCPFactory
|
||||
from serena.project import Project
|
||||
from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsOverviewTool, SearchForPatternTool, ToolRegistry
|
||||
from serena.prompt_factory import SerenaPromptFactory
|
||||
from serena.util.cli_util import AutoRegisteringGroup
|
||||
from serena.util.dataclass import get_dataclass_default
|
||||
from serena.util.logging import MemoryLogHandler
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
@@ -49,15 +46,17 @@ from solidlsp.util.subprocess_util import subprocess_kwargs
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_MAX_CONTENT_WIDTH = 200
|
||||
_MODES_EXPLANATION = f"""\b\nBuilt-in mode names or paths to custom mode YAMLs with which to
|
||||
override the default modes defined in the global Serena configuration or
|
||||
_MODES_EXPLANATION = """\b\nBuilt-in mode names or paths to custom mode YAMLs with which to
|
||||
override the default_modes defined in the global Serena configuration or
|
||||
the active project.
|
||||
For details on mode configuration, see
|
||||
https://oraios.github.io/serena/02-usage/050_configuration.html#modes.
|
||||
If no configuration changes were made, the base defaults are:
|
||||
{get_dataclass_default(SerenaConfig, "default_modes")}.
|
||||
Overriding them means that they no longer apply, so you will need to
|
||||
re-specify them in addition to further modes if you want to keep them."""
|
||||
"""
|
||||
_ADD_MODES_EXPLANATION = """\b\nMode names or paths to custom mode YAMLs which shall
|
||||
be added on top of the other modes specified by the global/project configuration.
|
||||
For details on mode configuration, see
|
||||
https://oraios.github.io/serena/02-usage/050_configuration.html#modes.
|
||||
"""
|
||||
|
||||
|
||||
def find_project_root(root: str | Path | None = None) -> str | None:
|
||||
@@ -231,13 +230,22 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
)
|
||||
@click.option(
|
||||
"--mode",
|
||||
"modes",
|
||||
"default_modes",
|
||||
type=str,
|
||||
multiple=True,
|
||||
default=(),
|
||||
show_default=False,
|
||||
help=_MODES_EXPLANATION,
|
||||
)
|
||||
@click.option(
|
||||
"--add-mode",
|
||||
"added_modes",
|
||||
type=str,
|
||||
multiple=True,
|
||||
default=(),
|
||||
show_default=False,
|
||||
help=_ADD_MODES_EXPLANATION,
|
||||
)
|
||||
@click.option(
|
||||
"--language-backend",
|
||||
type=click.Choice([lb.value for lb in LanguageBackend]),
|
||||
@@ -303,7 +311,8 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
project_file_arg: str | None,
|
||||
project_from_cwd: bool | None,
|
||||
context: str,
|
||||
modes: Sequence[str],
|
||||
default_modes: Sequence[str],
|
||||
added_modes: Sequence[str],
|
||||
language_backend: str | None,
|
||||
transport: Literal["stdio", "sse", "streamable-http"],
|
||||
host: str,
|
||||
@@ -315,6 +324,8 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
trace_lsp_communication: bool | None,
|
||||
tool_timeout: float | None,
|
||||
) -> None:
|
||||
from serena.mcp import SerenaMCPFactory
|
||||
|
||||
# initialize logging, using INFO level initially (will later be adjusted by SerenaAgent according to the config)
|
||||
# * memory log handler (for use by GUI/Dashboard)
|
||||
# * stream handler for stderr (for direct console output, which will also be captured by clients like Claude Desktop)
|
||||
@@ -346,11 +357,16 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
log.warning("No project root found from %s; not activating any project", os.getcwd())
|
||||
|
||||
project_file = project_file_arg or project
|
||||
|
||||
mode_selection_def: ModeSelectionDefinition | None = None
|
||||
if default_modes or added_modes:
|
||||
mode_selection_def = ModeSelectionDefinitionWithAddedModes(default_modes=default_modes or None, added_modes=added_modes or None)
|
||||
|
||||
factory = SerenaMCPFactory(context=context, project=project_file, memory_log_handler=memory_log_handler)
|
||||
server = factory.create_mcp_server(
|
||||
host=host,
|
||||
port=port,
|
||||
modes=modes,
|
||||
mode_selection_def=mode_selection_def,
|
||||
language_backend=LanguageBackend.from_str(language_backend) if language_backend else None,
|
||||
enable_web_dashboard=enable_web_dashboard,
|
||||
open_web_dashboard=open_web_dashboard,
|
||||
@@ -394,6 +410,8 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
def print_system_prompt(
|
||||
project: str, log_level: str, only_instructions: bool, context: str, modes: Sequence[str] | None = None
|
||||
) -> None:
|
||||
from serena.agent import SerenaAgent
|
||||
|
||||
prefix = "You will receive access to Serena's symbolic tools. Below are instructions for using them, take them into account."
|
||||
postfix = "You begin by acknowledging that you understood the above instructions and are ready to receive tasks."
|
||||
|
||||
@@ -403,9 +421,14 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
modes_selection_def: ModeSelectionDefinition | None = None
|
||||
if modes:
|
||||
modes_selection_def = ModeSelectionDefinition(default_modes=modes)
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
print(serena_config.default_modes)
|
||||
print(serena_config.base_modes)
|
||||
|
||||
agent = SerenaAgent(
|
||||
project=os.path.abspath(project),
|
||||
serena_config=SerenaConfig(web_dashboard=False, log_level=lvl),
|
||||
serena_config=serena_config,
|
||||
context=context_instance,
|
||||
modes=modes_selection_def,
|
||||
)
|
||||
@@ -828,6 +851,8 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
:param path: The path to check.
|
||||
:param project: The path to the project directory, defaults to the current working directory.
|
||||
"""
|
||||
from serena.project import Project
|
||||
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
proj = Project.load(os.path.abspath(project), serena_config=serena_config)
|
||||
if os.path.isabs(path):
|
||||
@@ -851,6 +876,8 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
:param project: path to the project directory, defaults to the current working directory.
|
||||
:param verbose: if set, prints detailed information about the indexed symbols.
|
||||
"""
|
||||
from serena.project import Project
|
||||
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
proj = Project.load(os.path.abspath(project), serena_config=serena_config)
|
||||
if os.path.isabs(file):
|
||||
@@ -887,6 +914,10 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
:param project: path to the project directory, defaults to the current working directory.
|
||||
"""
|
||||
# NOTE: completely written by Claude Code, only functionality was reviewed, not implementation
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.project import Project
|
||||
from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsOverviewTool, SearchForPatternTool
|
||||
|
||||
logging.configure(level=logging.INFO)
|
||||
project_path = os.path.abspath(project)
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
@@ -1040,6 +1071,8 @@ class ToolCommands(AutoRegisteringGroup):
|
||||
@click.option("--all", "-a", "include_optional", is_flag=True, help="List all tools, including those not enabled by default.")
|
||||
@click.option("--only-optional", is_flag=True, help="List only optional tools (those not enabled by default).")
|
||||
def list(quiet: bool = False, include_optional: bool = False, only_optional: bool = False) -> None:
|
||||
from serena.tools import ToolRegistry
|
||||
|
||||
tool_registry = ToolRegistry()
|
||||
if quiet:
|
||||
if only_optional:
|
||||
@@ -1062,6 +1095,9 @@ class ToolCommands(AutoRegisteringGroup):
|
||||
@click.argument("tool_name", type=str)
|
||||
@click.option("--context", type=str, default=None, help="Context name or path to context file.")
|
||||
def description(tool_name: str, context: str | None = None) -> None:
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.mcp import SerenaMCPFactory
|
||||
|
||||
# Load the context
|
||||
serena_context = None
|
||||
if context:
|
||||
@@ -1089,16 +1125,26 @@ class PromptCommands(AutoRegisteringGroup):
|
||||
|
||||
@staticmethod
|
||||
@click.command(
|
||||
"list", help="Lists yamls that are used for defining prompts.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH}
|
||||
"list", help="Lists prompt names and YAML files that can be overridden.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH}
|
||||
)
|
||||
def list() -> None:
|
||||
# list prompt names
|
||||
click.echo("Prompts:")
|
||||
factory = SerenaPromptFactory()
|
||||
for key in factory.get_prompt_names():
|
||||
template = factory.get_prompt_template(key)
|
||||
is_overridden = not template.path.startswith(PROMPT_TEMPLATES_DIR_INTERNAL)
|
||||
click.echo(f" * '{key}' ({template.path if is_overridden else 'default'})")
|
||||
|
||||
# list prompts files
|
||||
click.echo("\nPrompt files (which you can override with the create-override command):")
|
||||
serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")]
|
||||
for prompt_yaml_name in serena_prompt_yaml_names:
|
||||
user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name)
|
||||
if os.path.exists(user_prompt_yaml_path):
|
||||
click.echo(f"{user_prompt_yaml_path} merged with default prompts in {prompt_yaml_name}")
|
||||
click.echo(f" * {user_prompt_yaml_path} merged with default prompts in {prompt_yaml_name}")
|
||||
else:
|
||||
click.echo(prompt_yaml_name)
|
||||
click.echo(f" * {prompt_yaml_name}")
|
||||
|
||||
@staticmethod
|
||||
@click.command(
|
||||
@@ -1171,6 +1217,26 @@ class PromptCommands(AutoRegisteringGroup):
|
||||
os.remove(user_prompt_yaml_path)
|
||||
click.echo(f"Deleted override file '{prompt_yaml_name}'.")
|
||||
|
||||
@staticmethod
|
||||
@click.command(
|
||||
"print-prompt-template",
|
||||
help="prints the (unrendered) template for the corresponding prompt name. "
|
||||
"This respects custom prompt yaml overrides and thus will print the value that will be used in Serena",
|
||||
context_settings={"max_content_width": _MAX_CONTENT_WIDTH},
|
||||
)
|
||||
@click.argument("prompt_name", type=str)
|
||||
def print_prompt_template(prompt_name: str) -> None:
|
||||
click.echo(SerenaPromptFactory().get_prompt_template_string(prompt_name))
|
||||
|
||||
@staticmethod
|
||||
@click.command(
|
||||
"print-cc-system-prompt-override",
|
||||
help="To be used specifically in Claude Code as value for `--system-prompt`",
|
||||
context_settings={"max_content_width": _MAX_CONTENT_WIDTH},
|
||||
)
|
||||
def print_cc_system_prompt_override() -> None:
|
||||
click.echo(SerenaPromptFactory().create_cc_system_prompt_override())
|
||||
|
||||
|
||||
_mode = ModeCommands()
|
||||
_context = ContextCommands()
|
||||
|
||||
@@ -57,15 +57,21 @@ class CodeEditor(Generic[TSymbol], ABC):
|
||||
"""
|
||||
raise NotImplementedError("This method must be overridden for each subclass")
|
||||
|
||||
def read_file(self, relative_path: str) -> str:
|
||||
def read_file(self, relative_path: str, lines: tuple[int, int] | None = None) -> str:
|
||||
"""
|
||||
Reads the content of a file.
|
||||
|
||||
:param relative_path: the relative path of the file to read
|
||||
:param lines: tuple of (first_line, last_line) to read only a specific line range (0-based, inclusive)
|
||||
:return: the content of the file
|
||||
"""
|
||||
with self._open_file_context(relative_path) as file:
|
||||
return file.get_contents()
|
||||
contents = file.get_contents()
|
||||
if lines is None:
|
||||
return contents
|
||||
else:
|
||||
first_line, last_line = lines
|
||||
return TextUtils.get_text_in_lines_range(contents, first_line, last_line)
|
||||
|
||||
@contextmanager
|
||||
def edited_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]:
|
||||
|
||||
@@ -168,10 +168,22 @@ class NamedToolInclusionDefinition(ToolInclusionDefinition):
|
||||
|
||||
@dataclass
|
||||
class ModeSelectionDefinition:
|
||||
base_modes: Sequence[str] | None = None
|
||||
default_modes: Sequence[str] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModeSelectionDefinitionWithBaseModes(ModeSelectionDefinition):
|
||||
base_modes: Sequence[str] | None = ("interactive", "editing")
|
||||
"""
|
||||
the base modes to use, which are always guaranteed to be included
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModeSelectionDefinitionWithAddedModes(ModeSelectionDefinition):
|
||||
added_modes: Sequence[str] | None = None
|
||||
|
||||
|
||||
class LanguageBackend(Enum):
|
||||
LSP = "LSP"
|
||||
"""
|
||||
@@ -228,7 +240,7 @@ class LineEnding(Enum):
|
||||
|
||||
|
||||
@dataclass
|
||||
class SharedConfig(ModeSelectionDefinition, ToolInclusionDefinition, ToStringMixin):
|
||||
class SharedConfig(ToolInclusionDefinition, ToStringMixin):
|
||||
"""Shared between SerenaConfig and ProjectConfig, the latter used to override values in the form
|
||||
(same as in ModeSelectionDefinition).
|
||||
The defaults here shall be none and should be set to the global default values in SerenaConfig.
|
||||
@@ -255,7 +267,7 @@ Uses $projectDir and $projectFolderName as placeholders.
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ProjectConfig(SharedConfig):
|
||||
class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
project_name: str
|
||||
languages: list[Language]
|
||||
ignored_paths: list[str] = field(default_factory=list)
|
||||
@@ -473,6 +485,9 @@ class ProjectConfig(SharedConfig):
|
||||
included_optional_tools = data["included_optional_tools"] or []
|
||||
additional_workspace_folders = data.get("additional_workspace_folders") or []
|
||||
|
||||
if "base_modes" in data and data["base_modes"] is not None:
|
||||
log.warning("The base_modes setting in project.yml is deprecated and will be ignored.")
|
||||
|
||||
return cls(
|
||||
project_name=data["project_name"],
|
||||
languages=languages,
|
||||
@@ -489,7 +504,7 @@ class ProjectConfig(SharedConfig):
|
||||
encoding=data["encoding"],
|
||||
line_ending=line_ending,
|
||||
language_backend=language_backend,
|
||||
base_modes=data["base_modes"],
|
||||
added_modes=data["added_modes"],
|
||||
default_modes=data["default_modes"],
|
||||
symbol_info_budget=symbol_info_budget,
|
||||
ls_specific_settings=data.get("ls_specific_settings", {}),
|
||||
@@ -680,7 +695,7 @@ class RegisteredProject(ToStringMixin):
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class SerenaConfig(SharedConfig):
|
||||
class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
"""
|
||||
Holds the Serena agent configuration, which is typically loaded from a YAML configuration file
|
||||
(when instantiated via :method:`from_config_file`), which is updated when projects are added or removed.
|
||||
@@ -734,7 +749,6 @@ class SerenaConfig(SharedConfig):
|
||||
"""
|
||||
the language backend to use for code understanding features
|
||||
"""
|
||||
default_modes: Sequence[str] | None = ("interactive", "editing")
|
||||
line_ending: LineEnding = LineEnding.NATIVE
|
||||
symbol_info_budget: float = 10.0
|
||||
"""
|
||||
@@ -872,7 +886,18 @@ class SerenaConfig(SharedConfig):
|
||||
if path is None:
|
||||
continue
|
||||
num_migrations += 1
|
||||
project_config = ProjectConfig.load(path, serena_config=instance) # instance is sufficiently populated
|
||||
try:
|
||||
project_config = ProjectConfig.load(path, serena_config=instance) # instance is sufficiently populated
|
||||
except Exception as e:
|
||||
log.error(
|
||||
"Failed to load project configuration for %s: %s. "
|
||||
"This project will be skipped. Fix or delete its "
|
||||
".serena/project.yml (or remove it from "
|
||||
"serena_config.yml) to re-enable it.",
|
||||
path,
|
||||
e,
|
||||
)
|
||||
continue
|
||||
project = RegisteredProject(
|
||||
project_root=str(path),
|
||||
project_config=project_config,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
# NOTE: This module is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually!
|
||||
|
||||
from interprompt.multilang_prompt import PromptList
|
||||
from interprompt.prompt_factory import PromptFactoryBase
|
||||
from typing import Any
|
||||
|
||||
@@ -14,15 +13,27 @@ class PromptFactory(PromptFactoryBase):
|
||||
A class for retrieving and rendering prompt templates and prompt lists.
|
||||
"""
|
||||
|
||||
def get_info_jet_brains_debug_repl_template_string(self) -> str:
|
||||
return self.get_prompt_template_string("info_jet_brains_debug_repl")
|
||||
|
||||
def create_info_jet_brains_debug_repl(self) -> str:
|
||||
return self._render_prompt("info_jet_brains_debug_repl", locals())
|
||||
|
||||
def get_onboarding_prompt_template_string(self) -> str:
|
||||
return self.get_prompt_template_string("onboarding_prompt")
|
||||
|
||||
def create_onboarding_prompt(self, *, system: Any) -> str:
|
||||
return self._render_prompt("onboarding_prompt", locals())
|
||||
|
||||
def get_connection_prompt_template_string(self) -> str:
|
||||
return self.get_prompt_template_string("connection_prompt")
|
||||
|
||||
def create_connection_prompt(self) -> str:
|
||||
return self._render_prompt("connection_prompt", locals())
|
||||
|
||||
def get_system_prompt_template_string(self) -> str:
|
||||
return self.get_prompt_template_string("system_prompt")
|
||||
|
||||
def create_system_prompt(
|
||||
self,
|
||||
*,
|
||||
@@ -33,3 +44,9 @@ class PromptFactory(PromptFactoryBase):
|
||||
mode_system_prompts: Any,
|
||||
) -> str:
|
||||
return self._render_prompt("system_prompt", locals())
|
||||
|
||||
def get_cc_system_prompt_override_template_string(self) -> str:
|
||||
return self.get_prompt_template_string("cc_system_prompt_override")
|
||||
|
||||
def create_cc_system_prompt_override(self) -> str:
|
||||
return self._render_prompt("cc_system_prompt_override", locals())
|
||||
+29
-15
@@ -339,8 +339,9 @@ class PreToolUseRemindAboutSerenaHook(PreToolUseHook):
|
||||
class SessionStartActivateProjectHook(Hook):
|
||||
def execute(self) -> None:
|
||||
message = (
|
||||
"**IMPORTANT**: Activate the current working directory as project using Serena's tools if not already done. "
|
||||
"If you haven't read Serena Instructions Manual yet, do so. "
|
||||
"**IMPORTANT**: If the current directory is a coding project you are working on:"
|
||||
" 1. activate it using Serena's activate_project tool unless already done. "
|
||||
" 2. if you haven't read Serena Instructions Manual yet, do so. "
|
||||
"Follow this instruction before doing anything else."
|
||||
)
|
||||
result = {
|
||||
@@ -358,29 +359,41 @@ class SessionEndCleanupHook(Hook):
|
||||
|
||||
|
||||
class PreToolUseAutoApproveSerenaHook(PreToolUseHook):
|
||||
"""Pre-tool-use hook that auto-approves Serena tool calls while the client is in accept-edits mode.
|
||||
"""Pre-tool-use hook that auto-approves Serena tool calls while the client is in a permissive permission mode.
|
||||
|
||||
Claude Code's ``acceptEdits`` permission mode only applies to its built-in editing tools;
|
||||
Serena's destructive tools (e.g. ``replace_symbol_body`` or ``rename_symbol``) would still
|
||||
prompt the user on every call. This hook emits an ``allow`` decision for any Serena MCP
|
||||
tool call whenever the client reports ``acceptEdits`` as the active permission mode, so
|
||||
blanket edit approvals also cover Serena's tools. In all other situations it stays silent,
|
||||
preserving the default approval flow.
|
||||
Claude Code's permissive permission modes (``acceptEdits`` for blanket edit approval and
|
||||
``auto`` for hands-off autonomous execution) only apply to its built-in editing tools or
|
||||
its auto-mode classifier; Serena's destructive tools (e.g. ``replace_symbol_body`` or
|
||||
``rename_symbol``) would still prompt the user on every call. This hook emits an ``allow``
|
||||
decision for any Serena MCP tool call whenever the client reports one of these modes as
|
||||
the active permission mode, so blanket approvals also cover Serena's tools. In all other
|
||||
situations it stays silent, preserving the default approval flow.
|
||||
|
||||
``bypassPermissions`` and ``dontAsk`` are deliberately excluded. ``bypassPermissions``
|
||||
already approves everything before the hook would matter, so silence here is harmless.
|
||||
``dontAsk`` is the user's deliberate deny-by-default posture (auto-deny unless an explicit
|
||||
allow rule matches); the hook honors that choice and stays silent rather than blanket
|
||||
overriding it.
|
||||
"""
|
||||
|
||||
_ACCEPT_EDITS_MODE = "acceptEdits"
|
||||
#: permission modes for which this hook emits an ``allow`` decision. Frozen so the
|
||||
#: set is immutable at the class level; matching is exact (case-sensitive) against
|
||||
#: the canonical mode strings emitted by Claude Code's hook payload.
|
||||
_AUTO_APPROVE_MODES: frozenset[str] = frozenset({"acceptEdits", "auto"})
|
||||
|
||||
def is_accept_edits_mode(self) -> bool:
|
||||
return self._permission_mode == self._ACCEPT_EDITS_MODE
|
||||
def is_auto_approve_mode(self) -> bool:
|
||||
return self._permission_mode in self._AUTO_APPROVE_MODES
|
||||
|
||||
def execute(self) -> None:
|
||||
# only emit a decision when both the tool and the mode match; stay silent otherwise
|
||||
if not self.is_serena_tool() or not self.is_accept_edits_mode():
|
||||
if not self.is_serena_tool() or not self.is_auto_approve_mode():
|
||||
return
|
||||
|
||||
# name the actual mode in the reason so logs/debug output are unambiguous
|
||||
# (the same hook handles multiple modes now)
|
||||
output_data = self.OutputData(
|
||||
permission_decision="allow",
|
||||
permission_decision_reason="Auto-approved: Serena tool call while client is in acceptEdits mode.",
|
||||
permission_decision_reason=f"Auto-approved: Serena tool call while client is in {self._permission_mode} mode.",
|
||||
)
|
||||
click.echo(output_data.to_json_string())
|
||||
|
||||
@@ -425,7 +438,8 @@ class HookCommands(AutoRegisteringGroup):
|
||||
@staticmethod
|
||||
@click.command(
|
||||
"auto-approve",
|
||||
help="Set this as hook at PreToolUse to auto-approve Serena tool calls while the client is in acceptEdits mode (Claude Code).",
|
||||
help="Set this as hook at PreToolUse to auto-approve Serena tool calls while the client is in a "
|
||||
"permissive permission mode (acceptEdits or auto, Claude Code).",
|
||||
)
|
||||
@_client_option
|
||||
def auto_approve(client: str) -> None:
|
||||
|
||||
@@ -661,6 +661,54 @@ class JetBrainsPluginClient(ToStringMixin):
|
||||
}
|
||||
return self._make_request("POST", "/debugReplClose", request_data)
|
||||
|
||||
def run_inspections(
|
||||
self,
|
||||
relative_path: str,
|
||||
min_severity: str | None = None,
|
||||
inspection_names: list[str] | None = None,
|
||||
start_line: int | None = None,
|
||||
end_line: int | None = None,
|
||||
) -> jb.RunInspectionsResponse:
|
||||
"""
|
||||
Runs IDE inspections on the given file and returns the results.
|
||||
|
||||
:param relative_path: the relative path to the file to inspect
|
||||
:param min_severity: minimum severity level to include (e.g. "WARNING", "ERROR")
|
||||
:param inspection_names: optional list of specific inspection names to run
|
||||
:param start_line: optional start line to restrict the inspection range
|
||||
:param end_line: optional end line to restrict the inspection range
|
||||
"""
|
||||
request_data: dict[str, Any] = {
|
||||
"relativePath": relative_path,
|
||||
}
|
||||
if min_severity is not None:
|
||||
request_data["minSeverity"] = min_severity
|
||||
if inspection_names is not None:
|
||||
request_data["inspectionNames"] = inspection_names
|
||||
if start_line is not None:
|
||||
request_data["startLine"] = start_line
|
||||
if end_line is not None:
|
||||
request_data["endLine"] = end_line
|
||||
return cast(jb.RunInspectionsResponse, self._make_request("POST", "/runInspectionsOnFile", request_data))
|
||||
|
||||
def list_inspections(
|
||||
self,
|
||||
language: str | None = None,
|
||||
group_path_contains: str | None = None,
|
||||
) -> jb.ListInspectionsResponse:
|
||||
"""
|
||||
Lists available IDE inspections, optionally filtered by language and/or group path.
|
||||
|
||||
:param language: optional language to filter inspections by (e.g. "Java", "Python")
|
||||
:param group_path_contains: optional substring to filter inspection group paths
|
||||
"""
|
||||
request_data: dict[str, Any] = {}
|
||||
if language is not None:
|
||||
request_data["language"] = language
|
||||
if group_path_contains is not None:
|
||||
request_data["groupPathContains"] = group_path_contains
|
||||
return cast(jb.ListInspectionsResponse, self._make_request("POST", "/listInspections", request_data))
|
||||
|
||||
def close(self) -> None:
|
||||
self._session.close()
|
||||
|
||||
|
||||
@@ -89,3 +89,30 @@ class TypeHierarchyNodeDTO(TypedDict):
|
||||
class TypeHierarchyResponse(TypedDict):
|
||||
hierarchy: NotRequired[list[TypeHierarchyNodeDTO]]
|
||||
num_levels_not_included: NotRequired[int]
|
||||
|
||||
|
||||
class InspectionResultDTO(TypedDict):
|
||||
inspection_name: str
|
||||
severity: str
|
||||
message: str
|
||||
relative_path: str
|
||||
start_line: int
|
||||
end_line: int
|
||||
start_col: NotRequired[int]
|
||||
end_col: NotRequired[int]
|
||||
quick_fix_available: NotRequired[bool]
|
||||
|
||||
|
||||
class RunInspectionsResponse(TypedDict):
|
||||
inspections: list[InspectionResultDTO]
|
||||
|
||||
|
||||
class InspectionInfoDTO(TypedDict):
|
||||
name: str
|
||||
group_path: str
|
||||
language: NotRequired[str]
|
||||
description: NotRequired[str]
|
||||
|
||||
|
||||
class ListInspectionsResponse(TypedDict):
|
||||
inspections: list[InspectionInfoDTO]
|
||||
+3
-6
@@ -3,7 +3,7 @@ The Serena Model Context Protocol (MCP) Server
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from contextlib import asynccontextmanager
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
@@ -272,7 +272,7 @@ class SerenaMCPFactory:
|
||||
self,
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 8000,
|
||||
modes: Sequence[str] = (),
|
||||
mode_selection_def: ModeSelectionDefinition | None = None,
|
||||
language_backend: LanguageBackend | None = None,
|
||||
enable_web_dashboard: bool | None = None,
|
||||
enable_gui_log_window: bool | None = None,
|
||||
@@ -286,7 +286,7 @@ class SerenaMCPFactory:
|
||||
|
||||
:param host: The host to bind to
|
||||
:param port: The port to bind to
|
||||
:param modes: List of mode names or paths to mode files
|
||||
:param mode_selection_def: the mode selection definition to apply
|
||||
:param language_backend: the language backend to use, overriding the configuration setting.
|
||||
:param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration.
|
||||
:param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then.
|
||||
@@ -318,9 +318,6 @@ class SerenaMCPFactory:
|
||||
if language_backend is not None:
|
||||
config.language_backend = language_backend
|
||||
|
||||
mode_selection_def: ModeSelectionDefinition | None = None
|
||||
if modes:
|
||||
mode_selection_def = ModeSelectionDefinition(default_modes=modes)
|
||||
self.agent = self._create_serena_agent(config, mode_selection_def)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -3,14 +3,35 @@ prompt: |
|
||||
You are running in a CLI coding agent context where file operations, basic (line-based) edits and reads
|
||||
as well as shell commands are handled by your own, internal tools.
|
||||
|
||||
You have access to Serena's code intelligence tools that exploit the symbolic
|
||||
structure of the code and are much more efficient than your own tools for most coding scenarios.
|
||||
If you are working on any coding task and if Serena's tools are deferred, you
|
||||
should load them all immediately, before performing any read, grep or bash commands.
|
||||
If Serena's tools can be used to achieve your task, you should prioritize them.
|
||||
**IMPORTANT**: If you haven't seen Serena's tools yet, you should use the tool search tool to load all of them
|
||||
right now.
|
||||
**CRITICAL**: If you haven't seen Serena's tools yet, you should use the tool search tool to load all of them
|
||||
**right now**.
|
||||
For exploring and reading code in a token-efficient manner, use Serena's code-centric overview and symbolic search tools.
|
||||
For edits, prefer Serena's editing tools since they are more token efficient.
|
||||
Especially when editing code you have read through Serena, ALWAYS use Serena's tools for editing, because your built-in tools will deny such edits (they will assume you haven't read the content).
|
||||
**IMPORTANT**: Never use a tool before having read its schema via the tool-search tool!
|
||||
Especially when editing code you have read through Serena, **ALWAYS** use Serena's tools for editing, because your built-in tools will deny such edits (they will assume you haven't read the content).
|
||||
**CRITICAL**: Never use a tool before having read its schema via the tool-search tool!
|
||||
|
||||
**CRITICAL**: You have an preference for relying on your own tools like grep, read and edit. It is of paramount
|
||||
importance that you overcome this preference and instead develop a preference for using Serena's tools since they
|
||||
are superior for most coding tasks. Follow the following rules:
|
||||
|
||||
For any code files:
|
||||
- Read -> FORBIDDEN for discovery. Use get_symbols_overview, then find_symbol with include_body.
|
||||
You may use read only if you already have an overview of the file and if reading whole symbols
|
||||
is inappropriate (e.g. a few lines of code).
|
||||
- Glob (by name) -> Allowed for discovery only.
|
||||
- Grep (content) -> Allowed for discovery only; follow up reads or reference searches must be Serena.
|
||||
- Edit -> FORBIDDEN. Use replace_symbol_body / insert_*_symbol / replace_content.
|
||||
|
||||
Disallowed reasoning. Do NOT use any of the following to justify Read/Edit on a code file:
|
||||
- "I already know the path"
|
||||
- "one Read call is faster than three Serena calls"
|
||||
- "the built-in tool description says to use Read for known paths"
|
||||
If you catch yourself reaching for one of these, that is the signal to switch to Serena.
|
||||
|
||||
excluded_tools:
|
||||
- create_text_file
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
description: JetBrains tools replace language server-based tools
|
||||
prompt: |
|
||||
You have access to the very powerful JetBrains tools for symbolic operations.
|
||||
These party replace regular tools you were informed about:
|
||||
These partly replace regular tools you were informed about:
|
||||
* `jet_brains_find_symbol` replaces `find_symbol`
|
||||
* `jet_brains_find_referencing_symbols` replaces `find_referencing_symbols`
|
||||
* `jet_brains_get_symbols_overview` replaces `get_symbols_overview`
|
||||
* `jet_brains_rename` replaces `rename_symbol`
|
||||
excluded_tools:
|
||||
- find_symbol
|
||||
- find_referencing_symbols
|
||||
@@ -25,3 +26,5 @@ included_optional_tools:
|
||||
- jet_brains_safe_delete
|
||||
- jet_brains_debug
|
||||
- serena_info
|
||||
- jet_brains_run_inspections
|
||||
- jet_brains_list_inspections
|
||||
@@ -58,3 +58,159 @@ prompts:
|
||||
{% endfor %}
|
||||
|
||||
You have hereby read the 'Serena Instructions Manual' and do not need to read it again.
|
||||
|
||||
cc_system_prompt_override: |
|
||||
You are Claude Code, Anthropic's official CLI for Claude. You are an interactive
|
||||
software-engineering agent. The user works with you through a terminal; your text
|
||||
output is what they see, and your tool calls are what change the world.
|
||||
|
||||
# Tool selection (read this before every tool call on a code file)
|
||||
|
||||
This project uses Serena, an MCP server that exposes semantic, symbol-aware tools
|
||||
for reading and editing code. Serena's tools are the PRIMARY tools for code work
|
||||
in this project. The built-in Read, Glob, Grep, and Edit tools are SECONDARY and
|
||||
must not be used on code files when a Serena equivalent exists.
|
||||
|
||||
The built-in tool descriptions in your context will tell you things like "use Read
|
||||
for a known path" and "prefer dedicated tools (Read, Edit, Write, Glob, Grep)".
|
||||
Those descriptions are written for projects without Serena and are SUPERSEDED here.
|
||||
When they conflict with this section, this section wins. Do not rationalize the
|
||||
built-in tools with "the file is small," "I already know what I need," "this is
|
||||
one call versus three," or "the path is known" — those rationalizations have
|
||||
produced incorrect behavior before and are explicitly disallowed.
|
||||
|
||||
## Mapping (use the right column, not the left)
|
||||
|
||||
Task Tool to use
|
||||
-------------------------------------- ----------------------------------------
|
||||
See a code file's structure get_symbols_overview
|
||||
Read a specific symbol's body find_symbol (include_body=true)
|
||||
Find a symbol by name across the repo find_symbol
|
||||
Find references / callers find_referencing_symbols
|
||||
Find declarations / implementations find_declaration / _find_implementations
|
||||
Edit a symbol's body replace_symbol_body
|
||||
Insert near a symbol insert_before_symbol / _insert_after_symbol
|
||||
Pattern replace inside a file replace_content
|
||||
Rename / move / delete a symbol rename / _move / _safe_delete
|
||||
Inline a symbol inline_symbol
|
||||
Type hierarchy type_hierarchy
|
||||
|
||||
Built-in Read/Edit/Glob/Grep are permitted on code files ONLY when:
|
||||
- Serena has been tried on the target and failed, OR
|
||||
- The file is not parseable as code (e.g., generated, malformed), OR
|
||||
- You need a regex search across many files that Serena's symbolic tools cannot
|
||||
express — in which case Grep is acceptable as a discovery step, but follow-up
|
||||
reads/edits on matched code files must still go through Serena.
|
||||
- You need to read a few lines and symbolic reads would be an overkill.
|
||||
- You absolutely have to read the full file for some reason.
|
||||
|
||||
Read/Edit/Glob are fine for non-code files: markdown, JSON, YAML, TOML, .env,
|
||||
config files, lockfiles, plain text, images.
|
||||
|
||||
## Required workflow before editing code
|
||||
|
||||
1. get_symbols_overview on the target file (skip if already done this session).
|
||||
2. find_symbol with include_body=true for the specific symbols you'll touch.
|
||||
Read only the symbols you need — not the whole file.
|
||||
3. Edit with replace_symbol_body, insert_before_symbol, insert_after_symbol, or
|
||||
replace_content. Never use the built-in Edit on a code file when one of these
|
||||
fits.
|
||||
|
||||
## Self-check
|
||||
|
||||
Before every Read, Glob, Grep, or Edit call: "Does this target a code file, and
|
||||
does the mapping above name a Serena tool for this task?" If yes, switch. Do this
|
||||
check every time — not just once per session.
|
||||
|
||||
# Doing tasks
|
||||
|
||||
The user will ask you to fix bugs, add features, refactor, explain code, and
|
||||
similar. Approach each task with these defaults:
|
||||
|
||||
- Understand before changing. Use the symbolic tools to build a precise picture of
|
||||
what's there, then make the smallest change that satisfies the request.
|
||||
- Don't add scope. No surrounding cleanup on a bug fix, no abstractions for
|
||||
hypothetical future needs, no error handling for cases that can't happen, no
|
||||
feature flags or backwards-compat shims unless asked. Three similar lines beats
|
||||
a premature abstraction.
|
||||
- Don't write comments unless the WHY is non-obvious — a hidden constraint, a
|
||||
workaround, a subtle invariant. Don't narrate WHAT the code does; well-named
|
||||
identifiers handle that. Don't reference the current task or PR in comments.
|
||||
- Prefer editing existing files to creating new ones. Never create *.md or README
|
||||
files unless the user explicitly asks.
|
||||
- For exploratory questions ("what could we do about X?"), reply in 2–3 sentences
|
||||
with a recommendation and the main tradeoff. Don't implement until the user
|
||||
agrees.
|
||||
- For UI/frontend changes you can't test in a browser, say so explicitly rather
|
||||
than claiming success.
|
||||
- Watch for security issues (injection, XSS, SQL injection, path traversal, secret
|
||||
leaks). Fix them when you spot them.
|
||||
|
||||
# Executing actions with care
|
||||
|
||||
Local, reversible actions (editing files, running tests, reading state) are free
|
||||
to take. Pause and confirm before:
|
||||
|
||||
- Destructive ops: deleting files/branches, dropping tables, killing processes,
|
||||
rm -rf, overwriting uncommitted changes, git reset --hard, force-push.
|
||||
- Hard-to-reverse ops: amending published commits, removing dependencies,
|
||||
modifying CI/CD.
|
||||
- Externally visible actions: pushing, opening/closing/commenting on PRs or
|
||||
issues, sending messages, posting to third-party services.
|
||||
- Uploading content to third-party tools (renderers, pastebins) — assume it's
|
||||
public and may be cached.
|
||||
|
||||
If you hit an obstacle, find the root cause. Don't bypass it with --no-verify,
|
||||
--force, or by deleting the thing in your way. If you find unfamiliar files,
|
||||
branches, or config, investigate before deleting — it may be the user's
|
||||
in-progress work.
|
||||
|
||||
A user approving an action once does not approve it forever. Match the scope of
|
||||
your action to what was actually requested.
|
||||
|
||||
# Git and commits
|
||||
|
||||
- Only commit when the user asks. Never proactively.
|
||||
- Never update git config. Never skip hooks (--no-verify, --no-gpg-sign) unless
|
||||
the user explicitly asks.
|
||||
- Prefer new commits over --amend. If a pre-commit hook fails, the commit didn't
|
||||
happen — fix the issue, re-stage, and create a NEW commit (not --amend, which
|
||||
would modify the previous commit).
|
||||
- Stage files by name, not `git add -A` or `git add .` — those can sweep in
|
||||
secrets or large binaries.
|
||||
- Don't commit files that look like secrets (.env, credentials.json, *.pem). If
|
||||
the user explicitly asks, warn first.
|
||||
- For commit messages, use a HEREDOC to preserve formatting. End the trailer with:
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
- Don't push unless asked. Never force-push to main/master; warn if asked.
|
||||
- For PRs, use `gh` via Bash. Look at the full diff against the base branch (not
|
||||
just the latest commit) before drafting title/body.
|
||||
|
||||
# Tone and output
|
||||
|
||||
- Your tool calls aren't visible to the user — only your text is. Before your
|
||||
first tool call, say in one sentence what you're about to do. While working,
|
||||
give short updates at key moments: a finding, a direction change, a blocker.
|
||||
Brief is good; silent is not.
|
||||
- Don't narrate internal deliberation. State results and decisions; skip the
|
||||
thinking-aloud.
|
||||
- End-of-turn summary: one or two sentences max. What changed, what's next.
|
||||
Nothing else.
|
||||
- Match response shape to the task: a simple question gets a direct answer, not
|
||||
headers and sections.
|
||||
- No emojis unless the user asks.
|
||||
- Use Github-flavored markdown. Reference code locations as `path:line` so the
|
||||
user can jump.
|
||||
|
||||
# Parallel tool calls
|
||||
|
||||
When tool calls don't depend on each other, issue them in a single response.
|
||||
When they do depend on each other, issue them sequentially with the dependent
|
||||
values resolved. Don't use placeholders or guess.
|
||||
|
||||
# Asking for help vs. acting
|
||||
|
||||
When a request is ambiguous in a way that materially changes the work, ask one
|
||||
focused question. When it's only ambiguous in ways that don't change the work,
|
||||
pick the reasonable interpretation and proceed — and say which interpretation
|
||||
you picked.
|
||||
@@ -3,16 +3,18 @@ project_name: "project_name"
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# haxe java julia kotlin lua
|
||||
# markdown
|
||||
# matlab nix pascal perl php
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# swift terraform toml typescript typescript_vts
|
||||
# vue yaml zig
|
||||
# al ansible bash clojure cpp
|
||||
# cpp_ccls crystal csharp csharp_omnisharp dart
|
||||
# elixir elm erlang fortran fsharp
|
||||
# go groovy haskell haxe hlsl
|
||||
# java json julia kotlin lean4
|
||||
# lua luau markdown matlab msl
|
||||
# nix ocaml pascal perl php
|
||||
# php_phpactor powershell python python_jedi python_ty
|
||||
# r rego ruby ruby_solargraph rust
|
||||
# scala solidity swift systemverilog 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.)
|
||||
@@ -76,71 +78,34 @@ read_only: false
|
||||
|
||||
# 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`.
|
||||
#
|
||||
# * `activate_project`: Activates a project based on the project name or path.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly,
|
||||
# for example by saying that the information retrieved from a memory file is no longer correct
|
||||
# or no longer relevant for the project.
|
||||
# * `edit_memory`: Replaces content matching a regular expression in a memory.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_file`: Finds files in the given relative paths
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend
|
||||
# * `find_symbol`: Performs a global (or local) search using the language server backend.
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual')
|
||||
# for clients that do not read the initial instructions when the MCP server is connected.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Read the content of a memory file. This tool should only be used if the information
|
||||
# is relevant to the current task. You can infer whether the information
|
||||
# is relevant from the memory file name.
|
||||
# You should not read the same memory file multiple times in the same conversation.
|
||||
# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported
|
||||
# (e.g., renaming "global/foo" to "bar" moves it from global to project scope).
|
||||
# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities.
|
||||
# For JB, we use a separate tool.
|
||||
# * `replace_content`: Replaces content in a file (optionally using regular expressions).
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend.
|
||||
# * `safe_delete_symbol`:
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
|
||||
# The memory name should be meaningful.
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
excluded_tools: []
|
||||
|
||||
# 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).
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
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.
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
fixed_tools: []
|
||||
|
||||
# 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.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# list of mode names that are to be activated by default, overriding the setting in the global configuration.
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
|
||||
# for this project.
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
default_modes:
|
||||
|
||||
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
added_modes:
|
||||
|
||||
# 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: ""
|
||||
|
||||
@@ -37,13 +37,15 @@ gui_log_window: False
|
||||
# Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html
|
||||
web_dashboard: True
|
||||
|
||||
# whether to open the Dashboard window/browser tab when Serena starts (provided that web_dashboard is enabled).
|
||||
# If set to false, you can still open the dashboard manually by clicking on the Serena icon in your system
|
||||
# tray on Windows and macOS. On Linux, there is no system tray support, so you can only open the dashboard by
|
||||
# a) telling the LLM to "open the dashboard" (provided that the open_dashboard tool is enabled) or by
|
||||
# b) manually navigating to http://localhost:24282/dashboard/ in your web browser (actual port
|
||||
# may be higher if you have multiple instances running; try ports 24283, 24284, etc.)
|
||||
# See also: https://oraios.github.io/serena/02-usage/060_dashboard.html
|
||||
# whether to open the Dashboard window/browser tab when Serena starts (provided that `web_dashboard` is enabled).
|
||||
# If set to false, you can still open the dashboard manually:
|
||||
# * When using an interface that supports a tray icon (see setting `web_dashboard_interface`),
|
||||
# you can conveniently open the dashboard from the system tray.
|
||||
# * When using the `browser` interface (no tray icon), so you can only open the dashboard by
|
||||
# a) telling the LLM to "open the dashboard" (provided that the open_dashboard tool is enabled) or by
|
||||
# b) manually navigating to http://localhost:24282/dashboard/ in your web browser (actual port
|
||||
# may be higher if you have multiple instances running; try ports 24283, 24284, etc.)
|
||||
# Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html
|
||||
web_dashboard_open_on_launch: True
|
||||
|
||||
# defines the interface (application mode) used for the web dashboard (if enabled).
|
||||
@@ -59,6 +61,7 @@ web_dashboard_open_on_launch: True
|
||||
# opening the dashboard in browser tabs when selected from the tray menu.
|
||||
# This is EXPERIMENTAL. It is tested on Windows only. We will establish macOS support, but it is yet untested.
|
||||
# On Linux, this cannot be universally supported, but it may work in some desktop environments.
|
||||
# See https://oraios.github.io/serena/02-usage/060_dashboard.html
|
||||
web_dashboard_interface:
|
||||
|
||||
# the address the web dashboard will listen on (bind address).
|
||||
@@ -112,19 +115,23 @@ included_optional_tools: []
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
fixed_tools: []
|
||||
|
||||
# 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 this is undefined, no base modes are included.
|
||||
# The project configuration (project.yml) may override this setting.
|
||||
# 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 + added_modes,
|
||||
# where added_modes can be defined by projects/CLI parameters.
|
||||
# If this is undefined/empty, no base modes are included.
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# These modes can be overridden by the project configuration (project.yml) or through the CLI (--mode).
|
||||
default_modes:
|
||||
- interactive
|
||||
- editing
|
||||
|
||||
# list of mode names that are to be activated by default (but can be overridden by projects/CLI params).
|
||||
# The full set of modes to be activated is base_modes + default_modes + added_modes,
|
||||
# where added_modes are defined by projects/CLI parameters.
|
||||
# If this is undefined/empty, no default modes are defined.
|
||||
# These modes can be overridden by the project configuration (project.yml) or through the CLI (--mode).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
default_modes:
|
||||
|
||||
# Used as default for tools where the apply method has a default maximal answer length.
|
||||
# Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default
|
||||
# through the global configuration.
|
||||
|
||||
+266
-1
@@ -3,6 +3,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||
from dataclasses import asdict, dataclass
|
||||
from time import perf_counter
|
||||
@@ -11,7 +12,7 @@ from typing import Any, Generic, Literal, NotRequired, Self, TypedDict, TypeVar
|
||||
from sensai.util.string import ToStringMixin
|
||||
|
||||
import serena.jetbrains.jetbrains_types as jb
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp import SolidLanguageServer, ls_types
|
||||
from solidlsp.ls import LSPFileBuffer
|
||||
from solidlsp.ls import ReferenceInSymbol as LSPReferenceInSymbol
|
||||
from solidlsp.ls_types import Position, SymbolKind, UnifiedSymbolInformation
|
||||
@@ -309,6 +310,12 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
end_line = end_pos["line"] if end_pos else None
|
||||
return start_line, end_line
|
||||
|
||||
def get_body_line_numbers_or_raise(self) -> tuple[int, int]:
|
||||
start_line, end_line = self.get_body_line_numbers()
|
||||
if start_line is None or end_line is None:
|
||||
raise ValueError(f"Body line numbers could not be determined for {self.get_name_path()}")
|
||||
return start_line, end_line
|
||||
|
||||
@property
|
||||
def line(self) -> int | None:
|
||||
"""
|
||||
@@ -869,6 +876,264 @@ class LanguageServerSymbolRetriever:
|
||||
|
||||
return [ReferenceInLanguageServerSymbol.from_lsp_reference(r) for r in references]
|
||||
|
||||
def find_implementing_symbols(
|
||||
self,
|
||||
name_path: str,
|
||||
relative_file_path: str,
|
||||
include_body: bool = False,
|
||||
include_kinds: Sequence[SymbolKind] | None = None,
|
||||
exclude_kinds: Sequence[SymbolKind] | None = None,
|
||||
) -> list[LanguageServerSymbol]:
|
||||
"""
|
||||
Find all symbols that implement the specified symbol, which is assumed to be unique.
|
||||
|
||||
:param name_path: the name path of the symbol to find implementations for. While this can be a matching pattern,
|
||||
it should usually be the full path to ensure uniqueness.
|
||||
:param relative_file_path: the relative path of the file in which the implemented symbol is defined.
|
||||
:param include_body: whether to include the body of all symbols in the result.
|
||||
:param include_kinds: which kinds of symbols to include in the result.
|
||||
:param exclude_kinds: which kinds of symbols to exclude from the result.
|
||||
"""
|
||||
symbol = self.find_unique(name_path, substring_matching=False, within_relative_path=relative_file_path)
|
||||
return self.find_implementing_symbols_by_location(
|
||||
symbol.location,
|
||||
include_body=include_body,
|
||||
include_kinds=include_kinds,
|
||||
exclude_kinds=exclude_kinds,
|
||||
)
|
||||
|
||||
def find_implementing_symbols_by_location(
|
||||
self,
|
||||
symbol_location: LanguageServerSymbolLocation,
|
||||
include_body: bool = False,
|
||||
include_kinds: Sequence[SymbolKind] | None = None,
|
||||
exclude_kinds: Sequence[SymbolKind] | None = None,
|
||||
) -> list[LanguageServerSymbol]:
|
||||
"""
|
||||
Find all symbols that implement the symbol at the given location.
|
||||
|
||||
:param symbol_location: the location of the symbol for which to find implementations.
|
||||
Does not need to include an end_line, as it is unused in the search.
|
||||
:param include_body: whether to include the body of all symbols in the result.
|
||||
:param include_kinds: an optional sequence of ints representing the LSP symbol kind.
|
||||
If provided, only symbols of the given kinds will be included in the result.
|
||||
:param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result.
|
||||
Takes precedence over include_kinds.
|
||||
:return: a list of symbols that implement the given symbol
|
||||
"""
|
||||
if not symbol_location.has_position_in_file():
|
||||
raise ValueError("Symbol location does not contain a valid position in a file")
|
||||
assert symbol_location.relative_path is not None
|
||||
assert symbol_location.line is not None
|
||||
assert symbol_location.column is not None
|
||||
lang_server = self.get_language_server(symbol_location.relative_path)
|
||||
implementing_symbols = lang_server.request_implementing_symbols(
|
||||
relative_file_path=symbol_location.relative_path,
|
||||
line=symbol_location.line,
|
||||
column=symbol_location.column,
|
||||
include_body=include_body,
|
||||
)
|
||||
|
||||
if include_kinds is not None:
|
||||
implementing_symbols = [s for s in implementing_symbols if s["kind"] in include_kinds]
|
||||
|
||||
if exclude_kinds is not None:
|
||||
implementing_symbols = [s for s in implementing_symbols if s["kind"] not in exclude_kinds]
|
||||
|
||||
return [LanguageServerSymbol(s) for s in implementing_symbols]
|
||||
|
||||
def find_declaration(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
column: int,
|
||||
include_body: bool = False,
|
||||
) -> LanguageServerSymbol | None:
|
||||
"""
|
||||
Find the declaration/definition of the symbol at the given file position.
|
||||
|
||||
:param relative_file_path: the relative path to the file in which the symbol usage occurs.
|
||||
:param line: the 0-based line number of the symbol usage.
|
||||
:param column: the 0-based column number of the symbol usage.
|
||||
:param include_body: whether to include the body of the defining symbol in the result.
|
||||
:return: the defining symbol, or None if no definition could be resolved.
|
||||
"""
|
||||
lang_server = self.get_language_server(relative_file_path)
|
||||
defining_symbol = lang_server.request_defining_symbol(
|
||||
relative_file_path=relative_file_path,
|
||||
line=line,
|
||||
column=column,
|
||||
include_body=include_body,
|
||||
)
|
||||
if defining_symbol is None:
|
||||
return None
|
||||
return LanguageServerSymbol(defining_symbol)
|
||||
|
||||
def get_file_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
"""
|
||||
Get diagnostics for a file, optionally restricted to a line range and minimum severity.
|
||||
|
||||
:param relative_file_path: the relative path to the file.
|
||||
:param start_line: the first 0-based line to include.
|
||||
:param end_line: the last 0-based line to include. `-1` means until end of file.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
:return: the diagnostics matching the requested constraints.
|
||||
"""
|
||||
lang_server = self.get_language_server(relative_file_path)
|
||||
return lang_server.request_text_document_diagnostics(
|
||||
relative_file_path=relative_file_path,
|
||||
start_line=start_line,
|
||||
end_line=end_line,
|
||||
min_severity=min_severity,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _symbol_identity(symbol: LanguageServerSymbol) -> tuple[str | None, int | None, int | None, str]:
|
||||
return (symbol.relative_path, symbol.line, symbol.column, symbol.get_name_path())
|
||||
|
||||
@staticmethod
|
||||
def _normalize_symbol_for_diagnostics(symbol: LanguageServerSymbol) -> LanguageServerSymbol:
|
||||
current_symbol = symbol
|
||||
while current_symbol.is_low_level():
|
||||
parent_symbol = current_symbol.get_parent()
|
||||
if parent_symbol is None:
|
||||
break
|
||||
current_symbol = parent_symbol
|
||||
return current_symbol
|
||||
|
||||
def find_diagnostic_owner_symbol(self, relative_file_path: str, line: int, column: int) -> LanguageServerSymbol | None:
|
||||
"""
|
||||
Find the symbol that should own a diagnostic at the given position.
|
||||
|
||||
This prefers the structural container of the diagnostic over low-level symbols such as
|
||||
local variables, because diagnostics are typically more meaningful when grouped by the
|
||||
surrounding function, method, class, or analogous construct.
|
||||
|
||||
:param relative_file_path: the relative path to the file containing the diagnostic.
|
||||
:param line: the 0-based line of the diagnostic.
|
||||
:param column: the 0-based column of the diagnostic.
|
||||
:return: the owning symbol, or None if no symbol could be resolved.
|
||||
"""
|
||||
lang_server = self.get_language_server(relative_file_path)
|
||||
symbol_dict = lang_server.request_symbol_at_location(
|
||||
relative_file_path=relative_file_path,
|
||||
line=line,
|
||||
column=column,
|
||||
)
|
||||
if symbol_dict is None:
|
||||
return None
|
||||
return self._normalize_symbol_for_diagnostics(LanguageServerSymbol(symbol_dict))
|
||||
|
||||
def _get_diagnostics_for_symbol(self, symbol: LanguageServerSymbol, min_severity: int) -> list[ls_types.Diagnostic]:
|
||||
relative_path = symbol.relative_path
|
||||
if relative_path is None:
|
||||
return []
|
||||
|
||||
start_line, end_line = symbol.get_body_line_numbers()
|
||||
if start_line is None:
|
||||
if symbol.line is None:
|
||||
return []
|
||||
start_line = symbol.line
|
||||
if end_line is None:
|
||||
end_line = start_line
|
||||
|
||||
return self.get_file_diagnostics(
|
||||
relative_file_path=relative_path,
|
||||
start_line=start_line,
|
||||
end_line=end_line,
|
||||
min_severity=min_severity,
|
||||
)
|
||||
|
||||
def get_symbol_diagnostics(
|
||||
self,
|
||||
name_path: str,
|
||||
reference_file: str | None = None,
|
||||
check_symbol_references: bool = False,
|
||||
min_severity: int = 4,
|
||||
) -> dict[LanguageServerSymbol, list[ls_types.Diagnostic]]:
|
||||
"""
|
||||
Get diagnostics for the specified symbol and, optionally, for all symbols that reference it.
|
||||
|
||||
:param name_path: the name path of the symbol to find. It should usually be unique.
|
||||
:param reference_file: optional file path used to disambiguate the symbol search.
|
||||
:param check_symbol_references: whether to additionally collect diagnostics for referencing symbols.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
:return: a mapping from symbols to the diagnostics that overlap their body ranges.
|
||||
"""
|
||||
symbol = self.find_unique(name_path, substring_matching=False, within_relative_path=reference_file or None)
|
||||
return self.get_symbol_diagnostics_by_location(
|
||||
symbol.location,
|
||||
check_symbol_references=check_symbol_references,
|
||||
min_severity=min_severity,
|
||||
)
|
||||
|
||||
def get_symbol_diagnostics_by_location(
|
||||
self,
|
||||
symbol_location: LanguageServerSymbolLocation,
|
||||
check_symbol_references: bool = False,
|
||||
min_severity: int = 4,
|
||||
) -> dict[LanguageServerSymbol, list[ls_types.Diagnostic]]:
|
||||
"""
|
||||
Get diagnostics for the symbol at the given location and, optionally, for all referencing symbols.
|
||||
|
||||
:param symbol_location: location of the symbol to inspect.
|
||||
:param check_symbol_references: whether to additionally collect diagnostics for referencing symbols.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
:return: an ordered mapping from symbols to the diagnostics that overlap their body ranges.
|
||||
"""
|
||||
if not symbol_location.has_position_in_file():
|
||||
raise ValueError("Symbol location does not contain a valid position in a file")
|
||||
|
||||
symbol = self.find_by_location(symbol_location)
|
||||
if symbol is None:
|
||||
assert symbol_location.relative_path is not None
|
||||
assert symbol_location.line is not None
|
||||
assert symbol_location.column is not None
|
||||
lang_server = self.get_language_server(symbol_location.relative_path)
|
||||
symbol_dict = lang_server.request_symbol_at_location(
|
||||
relative_file_path=symbol_location.relative_path,
|
||||
line=symbol_location.line,
|
||||
column=symbol_location.column,
|
||||
)
|
||||
if symbol_dict is None:
|
||||
return {}
|
||||
symbol = LanguageServerSymbol(symbol_dict)
|
||||
|
||||
symbols_to_check: "OrderedDict[tuple[str | None, int | None, int | None, str], LanguageServerSymbol]" = OrderedDict()
|
||||
symbols_to_check[self._symbol_identity(symbol)] = symbol
|
||||
|
||||
if check_symbol_references:
|
||||
reference_symbols = self.find_referencing_symbols_by_location(
|
||||
symbol.location,
|
||||
include_body=False,
|
||||
exclude_kinds=[SymbolKind.File, SymbolKind.Module, SymbolKind.Package, SymbolKind.Namespace],
|
||||
)
|
||||
for reference in reference_symbols:
|
||||
reference_relative_path = reference.get_relative_path()
|
||||
normalized_reference_symbol = None
|
||||
if reference_relative_path is not None:
|
||||
normalized_reference_symbol = self.find_diagnostic_owner_symbol(
|
||||
relative_file_path=reference_relative_path,
|
||||
line=reference.line,
|
||||
column=reference.character,
|
||||
)
|
||||
if normalized_reference_symbol is None:
|
||||
normalized_reference_symbol = self._normalize_symbol_for_diagnostics(reference.symbol)
|
||||
symbols_to_check.setdefault(self._symbol_identity(normalized_reference_symbol), normalized_reference_symbol)
|
||||
|
||||
result: dict[LanguageServerSymbol, list[ls_types.Diagnostic]] = {}
|
||||
for current_symbol in symbols_to_check.values():
|
||||
diagnostics = self._get_diagnostics_for_symbol(current_symbol, min_severity=min_severity)
|
||||
if diagnostics:
|
||||
result[current_symbol] = diagnostics
|
||||
return result
|
||||
|
||||
def get_symbol_overview(self, relative_path: str) -> dict[str, list[LanguageServerSymbol]]:
|
||||
"""
|
||||
:param relative_path: the path of the file for which to get the symbol overview
|
||||
|
||||
@@ -12,7 +12,7 @@ from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from serena.tools import SUCCESS_RESULT, EditedFileContext, Tool, ToolMarkerCanEdit, ToolMarkerOptional
|
||||
from serena.tools import SUCCESS_RESULT, EditedFileContext, EditingToolWithDiagnostics, Tool, ToolMarkerOptional
|
||||
from serena.util.file_system import scan_directory
|
||||
from serena.util.text_utils import ContentReplacer, search_files
|
||||
|
||||
@@ -48,7 +48,7 @@ class ReadFileTool(Tool):
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class CreateTextFileTool(Tool, ToolMarkerCanEdit):
|
||||
class CreateTextFileTool(EditingToolWithDiagnostics):
|
||||
"""
|
||||
Creates/overwrites a file in the project directory.
|
||||
"""
|
||||
@@ -61,23 +61,27 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit):
|
||||
:param content: the (appropriately encoded) content to write to the file
|
||||
:return: a message indicating success or failure
|
||||
"""
|
||||
project_root = self.get_project_root()
|
||||
abs_path = (Path(project_root) / relative_path).resolve()
|
||||
will_overwrite_existing = abs_path.exists()
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
# validating the destination path
|
||||
project_root = self.get_project_root()
|
||||
abs_path = (Path(project_root) / relative_path).resolve()
|
||||
will_overwrite_existing = abs_path.exists()
|
||||
|
||||
if will_overwrite_existing:
|
||||
self.project.validate_relative_path(relative_path, require_not_ignored=True)
|
||||
else:
|
||||
assert abs_path.is_relative_to(self.get_project_root()), (
|
||||
f"Cannot create file outside of the project directory, got {relative_path=}"
|
||||
)
|
||||
if will_overwrite_existing:
|
||||
self.project.validate_relative_path(relative_path, require_not_ignored=True)
|
||||
else:
|
||||
assert abs_path.is_relative_to(self.get_project_root()), (
|
||||
f"Cannot create file outside of the project directory, got {relative_path=}"
|
||||
)
|
||||
|
||||
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
abs_path.write_text(content, encoding=self.project.project_config.encoding, newline=self.project.line_ending.newline_str)
|
||||
answer = f"File created: {relative_path}."
|
||||
if will_overwrite_existing:
|
||||
answer += " Overwrote existing file."
|
||||
return answer
|
||||
# writing the file
|
||||
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
abs_path.write_text(content, encoding=self.project.project_config.encoding, newline=self.project.line_ending.newline_str)
|
||||
answer = f"File created: {relative_path}."
|
||||
if will_overwrite_existing:
|
||||
answer += " Overwrote existing file."
|
||||
|
||||
return diagnostics_context.format_result(answer)
|
||||
|
||||
|
||||
class ListDirTool(Tool):
|
||||
@@ -156,7 +160,7 @@ class FindFileTool(Tool):
|
||||
return result
|
||||
|
||||
|
||||
class ReplaceContentTool(Tool, ToolMarkerCanEdit):
|
||||
class ReplaceContentTool(EditingToolWithDiagnostics):
|
||||
"""
|
||||
Replaces content in a file (optionally using regular expressions).
|
||||
"""
|
||||
@@ -212,16 +216,17 @@ class ReplaceContentTool(Tool, ToolMarkerCanEdit):
|
||||
Performs the replacement, with additional options not exposed in the tool.
|
||||
This function can be used internally by other tools.
|
||||
"""
|
||||
self.project.validate_relative_path(relative_path, require_not_ignored=require_not_ignored)
|
||||
with EditedFileContext(relative_path, self.create_code_editor()) as context:
|
||||
original_content = context.get_original_content()
|
||||
replacer = ContentReplacer(mode=mode, allow_multiple_occurrences=allow_multiple_occurrences)
|
||||
updated_content = replacer.replace(original_content, needle, repl)
|
||||
context.set_updated_content(updated_content)
|
||||
return SUCCESS_RESULT
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
self.project.validate_relative_path(relative_path, require_not_ignored=require_not_ignored)
|
||||
with EditedFileContext(relative_path, self.create_code_editor()) as context:
|
||||
original_content = context.get_original_content()
|
||||
replacer = ContentReplacer(mode=mode, allow_multiple_occurrences=allow_multiple_occurrences)
|
||||
updated_content = replacer.replace(original_content, needle, repl)
|
||||
context.set_updated_content(updated_content)
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class DeleteLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
class DeleteLinesTool(EditingToolWithDiagnostics, ToolMarkerOptional):
|
||||
"""
|
||||
Deletes a range of lines within a file.
|
||||
"""
|
||||
@@ -241,12 +246,13 @@ class DeleteLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
:param start_line: the 0-based index of the first line to be deleted
|
||||
:param end_line: the 0-based index of the last line to be deleted
|
||||
"""
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.delete_lines(relative_path, start_line, end_line)
|
||||
return SUCCESS_RESULT
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.delete_lines(relative_path, start_line, end_line)
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class ReplaceLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
class ReplaceLinesTool(EditingToolWithDiagnostics, ToolMarkerOptional):
|
||||
"""
|
||||
Replaces a range of lines within a file with new content.
|
||||
"""
|
||||
@@ -268,16 +274,19 @@ class ReplaceLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
:param end_line: the 0-based index of the last line to be deleted
|
||||
:param content: the content to insert
|
||||
"""
|
||||
# normalizing the replacement content
|
||||
if not content.endswith("\n"):
|
||||
content += "\n"
|
||||
result = self.agent.get_tool(DeleteLinesTool).apply(relative_path, start_line, end_line)
|
||||
if result != SUCCESS_RESULT:
|
||||
return result
|
||||
self.agent.get_tool(InsertAtLineTool).apply(relative_path, start_line, content)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.delete_lines(relative_path, start_line, end_line)
|
||||
code_editor.insert_at_line(relative_path, start_line, content)
|
||||
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class InsertAtLineTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
class InsertAtLineTool(EditingToolWithDiagnostics, ToolMarkerOptional):
|
||||
"""
|
||||
Inserts content at a given line in a file.
|
||||
"""
|
||||
@@ -298,11 +307,15 @@ class InsertAtLineTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
|
||||
:param line: the 0-based index of the line to insert content at
|
||||
:param content: the content to be inserted
|
||||
"""
|
||||
# normalizing the inserted content
|
||||
if not content.endswith("\n"):
|
||||
content += "\n"
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_at_line(relative_path, line, content)
|
||||
return SUCCESS_RESULT
|
||||
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_at_line(relative_path, line, content)
|
||||
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class SearchForPatternTool(Tool):
|
||||
|
||||
@@ -595,3 +595,76 @@ class JetBrainsDebugTool(Tool, ToolMarkerOptional, ToolMarkerBeta):
|
||||
else:
|
||||
response = client.debug_close(repl_key=repl_key)
|
||||
return response.get("result", str(response))
|
||||
|
||||
|
||||
class JetBrainsRunInspectionsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
"""
|
||||
Runs JetBrains IDE inspections on a file and returns the results.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
min_severity: str | None = None,
|
||||
inspection_names: list[str] | None = None,
|
||||
start_line: int | None = None,
|
||||
end_line: int | None = None,
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Runs IDE inspections (code analysis) on the given file and returns the problems found.
|
||||
This leverages the full power of JetBrains' static analysis engine, including language-specific
|
||||
inspections, type checking, potential bugs, code style issues, and more.
|
||||
|
||||
:param relative_path: the relative path to the file to inspect.
|
||||
:param min_severity: minimum severity level to include in results (e.g. "ERROR", "WARNING", "WEAK_WARNING", "INFO").
|
||||
If not specified, all severities are returned.
|
||||
:param inspection_names: optional list of specific inspection names to run (e.g. ["UnusedImport", "TypeMismatch"]).
|
||||
If not specified, all applicable inspections are run.
|
||||
:param start_line: optional 1-based start line to restrict the inspection range.
|
||||
:param end_line: optional 1-based end line to restrict the inspection range.
|
||||
: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 with inspection results including severity, message, and location.
|
||||
"""
|
||||
with JetBrainsPluginClient.from_project(self.project) as client:
|
||||
response_dict = client.run_inspections(
|
||||
relative_path=relative_path,
|
||||
min_severity=min_severity,
|
||||
inspection_names=inspection_names,
|
||||
start_line=start_line,
|
||||
end_line=end_line,
|
||||
)
|
||||
result = self._to_json(response_dict)
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class JetBrainsListInspectionsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
"""
|
||||
Lists available JetBrains IDE inspections, optionally filtered by language or group.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
language: str | None = None,
|
||||
group_path_contains: str | None = None,
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Lists the available IDE inspections. Use this to discover which inspections can be passed
|
||||
to the run_inspections tool's `inspection_names` parameter.
|
||||
|
||||
:param language: optional language to filter by (e.g. "Java", "Python", "Kotlin").
|
||||
:param group_path_contains: optional substring to match against the inspection group path
|
||||
(e.g. "probable bugs", "code style").
|
||||
: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 with the list of available inspections including name, group path, and language.
|
||||
"""
|
||||
with JetBrainsPluginClient.from_project(self.project) as client:
|
||||
response_dict = client.list_inspections(
|
||||
language=language,
|
||||
group_path_contains=group_path_contains,
|
||||
)
|
||||
result = self._to_json(response_dict)
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
@@ -6,20 +6,24 @@ import copy
|
||||
import os
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from serena.symbol import LanguageServerSymbol, LanguageServerSymbolDictGrouper
|
||||
from serena.tools import (
|
||||
SUCCESS_RESULT,
|
||||
EditingToolWithDiagnostics,
|
||||
Tool,
|
||||
ToolMarkerSymbolicEdit,
|
||||
ToolMarkerSymbolicRead,
|
||||
)
|
||||
from serena.tools.tools_base import ToolMarkerOptional
|
||||
from serena.util.ls_diagnostics import GroupedDiagnostics
|
||||
from serena.util.text_utils import find_text_coordinates
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
|
||||
|
||||
class RestartLanguageServerTool(Tool, ToolMarkerOptional):
|
||||
"""Restarts the language server, may be necessary when edits not through Serena happen."""
|
||||
"""Restarts the language server(s)."""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""Use this tool only on explicit user request or after confirmation.
|
||||
@@ -324,7 +328,242 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
|
||||
return self._limit_length(result_json, max_answer_chars, shortened_result_factories=shortened_results)
|
||||
|
||||
|
||||
class ReplaceSymbolBodyTool(Tool, ToolMarkerSymbolicEdit):
|
||||
class FindImplementationsTool(Tool, ToolMarkerSymbolicRead):
|
||||
"""
|
||||
Finds symbols that implement the given symbol using the language server backend.
|
||||
"""
|
||||
|
||||
# noinspection PyDefaultArgument
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
relative_path: str,
|
||||
include_info: bool = False,
|
||||
include_kinds: list[int] = [], # noqa: B006
|
||||
exclude_kinds: list[int] = [], # noqa: B006
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Finds implementations of the symbol at the given `name_path`.
|
||||
|
||||
:param name_path: for finding the symbol to find implementations for, same logic as in the `find_symbol` tool.
|
||||
:param relative_path: the relative path to the file containing the symbol for which to find implementations.
|
||||
Note that here you can't pass a directory but must pass a file.
|
||||
:param include_info: whether to include additional info (hover-like, typically including docstring and signature),
|
||||
about the implementing symbols.
|
||||
:param include_kinds: same as in the `find_symbol` tool.
|
||||
:param exclude_kinds: same as in the `find_symbol` tool.
|
||||
:param max_answer_chars: same as in the `find_symbol` tool.
|
||||
:return: a list of JSON objects with the symbols implementing the requested symbol
|
||||
"""
|
||||
include_body = False
|
||||
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()
|
||||
|
||||
implementing_symbols = symbol_retriever.find_implementing_symbols(
|
||||
name_path,
|
||||
relative_file_path=relative_path,
|
||||
include_body=include_body,
|
||||
include_kinds=parsed_include_kinds,
|
||||
exclude_kinds=parsed_exclude_kinds,
|
||||
)
|
||||
|
||||
symbol_dicts = [
|
||||
dict(s.to_dict(kind=True, relative_path=True, depth=0, body=include_body, body_location=True)) for s in implementing_symbols
|
||||
]
|
||||
if include_info:
|
||||
info_by_symbol = symbol_retriever.request_info_for_symbol_batch(implementing_symbols)
|
||||
for s, s_dict in zip(implementing_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)
|
||||
|
||||
|
||||
class FindDeclarationTool(Tool, ToolMarkerSymbolicRead):
|
||||
"""
|
||||
Finds the declaration/definition of a symbol
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
regex: str,
|
||||
containing_symbol_name_path: str | None = None,
|
||||
include_body: bool = False,
|
||||
include_info: bool = False,
|
||||
) -> str:
|
||||
r"""
|
||||
Finds the declaration of a symbol.
|
||||
|
||||
:param relative_path: the relative path to the source file containing the symbol for which to find the declaration.
|
||||
:param regex: a regular expression with one group, where the group matches the symbol for which to perform the lookup.
|
||||
For example, to find the declaration of the `process` method in a call like `obj.process()`,
|
||||
pass an expression like "obj\.(process)\(process_input_arg=37\)".
|
||||
Prefer regexes with sufficiently large context around the group to render the match unambiguous.
|
||||
Uses Python syntax with MULTILINE and DOTALL flags enabled.
|
||||
:param containing_symbol_name_path: optional name path of a containing symbol whose body shall be searched instead of the full file.
|
||||
:param include_body: whether to include the symbol's body in the result. Default False.
|
||||
:param include_info: whether to include additional info (hover-like). Default False.
|
||||
"""
|
||||
symbol_retriever = self.create_language_server_symbol_retriever()
|
||||
relative_path = self._sanitize_input_param(relative_path)
|
||||
regex = self._sanitize_input_param(regex)
|
||||
|
||||
# find relevant location for lookup
|
||||
editor = self.create_code_editor()
|
||||
if not containing_symbol_name_path:
|
||||
content = editor.read_file(relative_path)
|
||||
coords = find_text_coordinates(content, regex, require_unique=True)
|
||||
assert coords is not None
|
||||
else:
|
||||
symbol = symbol_retriever.find_unique(name_path_pattern=containing_symbol_name_path, within_relative_path=relative_path)
|
||||
body_line_numers = symbol.get_body_line_numbers_or_raise()
|
||||
content = editor.read_file(relative_path, lines=body_line_numers)
|
||||
coords = find_text_coordinates(content, regex, require_unique=True)
|
||||
assert coords is not None
|
||||
coords.line += body_line_numers[0]
|
||||
|
||||
# retrieve declaration
|
||||
defining_symbol = symbol_retriever.find_declaration(
|
||||
relative_file_path=relative_path,
|
||||
line=coords.line,
|
||||
column=coords.col,
|
||||
include_body=include_body,
|
||||
)
|
||||
if defining_symbol is None:
|
||||
raise ValueError(
|
||||
f"No symbol declaration found at the location of the regex match. Location: {relative_path}:{coords.line}:{coords.col}."
|
||||
)
|
||||
|
||||
# create output
|
||||
symbol_dict = self._defining_symbol_to_result_dict(
|
||||
symbol_retriever,
|
||||
defining_symbol,
|
||||
include_body,
|
||||
include_info,
|
||||
)
|
||||
result = self._to_json(symbol_dict)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _defining_symbol_to_result_dict(
|
||||
symbol_retriever: Any,
|
||||
defining_symbol: LanguageServerSymbol,
|
||||
include_body: bool,
|
||||
include_info: bool,
|
||||
) -> dict[str, Any]:
|
||||
symbol_dict = dict(defining_symbol.to_dict(kind=True, relative_path=True, depth=0, body=include_body, body_location=True))
|
||||
if not include_body and include_info:
|
||||
if symbol_info := symbol_retriever.request_info_for_symbol(defining_symbol):
|
||||
symbol_dict["info"] = symbol_info
|
||||
symbol_dict.pop("name", None)
|
||||
return symbol_dict
|
||||
|
||||
|
||||
class GetDiagnosticsForFileTool(Tool, ToolMarkerSymbolicRead):
|
||||
"""
|
||||
Gets diagnostics for a file, optionally restricted to a line range, grouped by file, severity, and containing symbol.
|
||||
"""
|
||||
|
||||
FILE_LEVEL_DIAGNOSTIC_BUCKET = "<file>"
|
||||
|
||||
def apply(
|
||||
self,
|
||||
relative_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Gets diagnostics for a file. Diagnostics are grouped as `relative_path -> severity -> name_path -> diagnostics_results`.
|
||||
If a diagnostic cannot be mapped to a symbol, it is grouped under the special name path `<file>`.
|
||||
|
||||
:param relative_path: the relative path to the file to inspect.
|
||||
:param start_line: the first 0-based line to include. Defaults to 0.
|
||||
:param end_line: the last 0-based line to include. Defaults to -1, which means until the end of the file.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
Diagnostics with lower-or-equal numeric severity are returned.
|
||||
:param max_answer_chars: same as in the `find_symbol` tool.
|
||||
:return: grouped diagnostics for the requested file.
|
||||
"""
|
||||
symbol_retriever = self.create_language_server_symbol_retriever()
|
||||
diagnostics = symbol_retriever.get_file_diagnostics(
|
||||
relative_file_path=relative_path,
|
||||
start_line=start_line,
|
||||
end_line=end_line,
|
||||
min_severity=min_severity,
|
||||
)
|
||||
|
||||
grouped_diagnostics = GroupedDiagnostics()
|
||||
for diagnostic in diagnostics:
|
||||
diag_range = diagnostic["range"]["start"]
|
||||
name_path = self.FILE_LEVEL_DIAGNOSTIC_BUCKET
|
||||
owner_symbol = symbol_retriever.find_diagnostic_owner_symbol(
|
||||
relative_file_path=relative_path,
|
||||
line=diag_range["line"],
|
||||
column=diag_range["character"],
|
||||
)
|
||||
if owner_symbol is not None:
|
||||
name_path = owner_symbol.get_name_path()
|
||||
grouped_diagnostics.add(relative_path, name_path, diagnostic)
|
||||
|
||||
result = self._to_json(grouped_diagnostics.get_dict())
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class GetDiagnosticsForSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
|
||||
"""
|
||||
Gets diagnostics for a symbol and, optionally, for symbols that reference it.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
name_path: str,
|
||||
reference_file: str = "",
|
||||
check_symbol_references: bool = False,
|
||||
min_severity: int = 4,
|
||||
max_answer_chars: int = -1,
|
||||
) -> str:
|
||||
"""
|
||||
Gets diagnostics for the specified symbol. When `check_symbol_references` is true, diagnostics for all
|
||||
referencing symbols are also included. The result is grouped as
|
||||
`relative_path -> severity -> name_path -> diagnostics_results`.
|
||||
|
||||
:param name_path: the name path of the symbol to inspect.
|
||||
:param reference_file: optional file path used to disambiguate the symbol search.
|
||||
:param check_symbol_references: whether to additionally collect diagnostics for symbols that reference the symbol.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
Diagnostics with lower-or-equal numeric severity are returned.
|
||||
:param max_answer_chars: same as in the `find_symbol` tool.
|
||||
:return: grouped diagnostics for the requested symbol and, optionally, its referencing symbols.
|
||||
"""
|
||||
symbol_retriever = self.create_language_server_symbol_retriever()
|
||||
diagnostics_by_symbol = symbol_retriever.get_symbol_diagnostics(
|
||||
name_path=name_path,
|
||||
reference_file=reference_file or None,
|
||||
check_symbol_references=check_symbol_references,
|
||||
min_severity=min_severity,
|
||||
)
|
||||
|
||||
grouped_diagnostics = GroupedDiagnostics()
|
||||
for symbol, diagnostics in diagnostics_by_symbol.items():
|
||||
relative_path = symbol.relative_path
|
||||
if relative_path is None:
|
||||
continue
|
||||
symbol_name_path = symbol.get_name_path()
|
||||
for diagnostic in diagnostics:
|
||||
grouped_diagnostics.add(relative_path, symbol_name_path, diagnostic)
|
||||
|
||||
result = self._to_json(grouped_diagnostics.get_dict())
|
||||
return self._limit_length(result, max_answer_chars)
|
||||
|
||||
|
||||
class ReplaceSymbolBodyTool(EditingToolWithDiagnostics):
|
||||
"""
|
||||
Replaces the full definition of a symbol using the language server backend.
|
||||
"""
|
||||
@@ -348,16 +587,17 @@ class ReplaceSymbolBodyTool(Tool, ToolMarkerSymbolicEdit):
|
||||
in the programming language, including e.g. the signature line for functions.
|
||||
IMPORTANT: The body does NOT include any preceding docstrings/comments or imports, in particular.
|
||||
"""
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.replace_body(
|
||||
name_path,
|
||||
relative_file_path=relative_path,
|
||||
body=body,
|
||||
)
|
||||
return SUCCESS_RESULT
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.replace_body(
|
||||
name_path,
|
||||
relative_file_path=relative_path,
|
||||
body=body,
|
||||
)
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class InsertAfterSymbolTool(Tool, ToolMarkerSymbolicEdit):
|
||||
class InsertAfterSymbolTool(EditingToolWithDiagnostics):
|
||||
"""
|
||||
Inserts content after the end of the definition of a given symbol.
|
||||
"""
|
||||
@@ -377,12 +617,13 @@ class InsertAfterSymbolTool(Tool, ToolMarkerSymbolicEdit):
|
||||
:param body: the body/content to be inserted. The inserted code shall begin with the next line after
|
||||
the symbol.
|
||||
"""
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_after_symbol(name_path, relative_file_path=relative_path, body=body)
|
||||
return SUCCESS_RESULT
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_after_symbol(name_path, relative_file_path=relative_path, body=body)
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class InsertBeforeSymbolTool(Tool, ToolMarkerSymbolicEdit):
|
||||
class InsertBeforeSymbolTool(EditingToolWithDiagnostics):
|
||||
"""
|
||||
Inserts content before the beginning of the definition of a given symbol.
|
||||
"""
|
||||
@@ -402,9 +643,10 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerSymbolicEdit):
|
||||
:param relative_path: the relative path to the file containing the symbol
|
||||
:param body: the body/content to be inserted before the line in which the referenced symbol is defined
|
||||
"""
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_before_symbol(name_path, relative_file_path=relative_path, body=body)
|
||||
return SUCCESS_RESULT
|
||||
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
|
||||
code_editor = self.create_code_editor()
|
||||
code_editor.insert_before_symbol(name_path, relative_file_path=relative_path, body=body)
|
||||
return diagnostics_context.format_result(SUCCESS_RESULT)
|
||||
|
||||
|
||||
class RenameSymbolTool(Tool, ToolMarkerSymbolicEdit):
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections.abc import Callable, Iterable
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Protocol, Self, TypeVar, cast
|
||||
from typing import TYPE_CHECKING, Any, Optional, Protocol, Self, TypeVar, cast
|
||||
|
||||
from mcp import Implementation
|
||||
from mcp.server.fastmcp import Context
|
||||
@@ -18,6 +18,7 @@ from serena.project import MemoriesManager, Project
|
||||
from serena.prompt_factory import PromptFactory
|
||||
from serena.util.class_decorators import singleton
|
||||
from serena.util.inspection import iter_subclasses
|
||||
from serena.util.ls_diagnostics import DiagnosticsDiff, EditedFilePath, PublishedDiagnosticsSnapshot
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -319,7 +320,7 @@ class Tool(Component):
|
||||
if client_str != self.get_last_tool_call_client_str():
|
||||
log.debug(f"Updating client info: {client_info}")
|
||||
self.set_last_tool_call_client_str(client_str)
|
||||
except BaseException as e:
|
||||
except Exception as e:
|
||||
log.info(f"Failed to get client info: {e}.")
|
||||
|
||||
def task() -> str:
|
||||
@@ -403,6 +404,62 @@ class Tool(Component):
|
||||
return json.dumps(x, ensure_ascii=False)
|
||||
|
||||
|
||||
class EditingToolWithDiagnostics(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Base class for editing tools that want to capture and report changes in LSP diagnostics before and after the edit.
|
||||
"""
|
||||
|
||||
ENABLE_DIAGNOSTICS: bool = False
|
||||
"""
|
||||
Global flag to enable/disable diagnostics for LSP-based editing tools derived from this class.
|
||||
The feature is currently disabled, because per-edit diagnostics are a questionable feature, since individual
|
||||
edits often intentionally introduce diagnostics (e.g. function signature mismatches or even syntax errors) that
|
||||
are then resolved in subsequent edits.
|
||||
"""
|
||||
|
||||
DIAGNOSTICS_KEY = "diagnostics[warning-or-higher]"
|
||||
|
||||
class DiagnosticsContext:
|
||||
def __init__(self, tool: "EditingToolWithDiagnostics", *edited_relative_paths: str) -> None:
|
||||
self._tool = tool
|
||||
self._is_diagnostics_enabled = tool.ENABLE_DIAGNOSTICS and tool.agent.is_using_language_server()
|
||||
self._edited_files = [EditedFilePath(path, path) for path in edited_relative_paths]
|
||||
self._before_edit_diagnostics_snapshot: PublishedDiagnosticsSnapshot | None = None
|
||||
self._symbol_retriever: Optional["LanguageServerSymbolRetriever"] | None = None
|
||||
if self._is_diagnostics_enabled:
|
||||
self._symbol_retriever = tool.create_language_server_symbol_retriever()
|
||||
self._before_edit_diagnostics_snapshot = PublishedDiagnosticsSnapshot(self._edited_files, self._symbol_retriever)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore
|
||||
pass
|
||||
|
||||
def format_result(
|
||||
self,
|
||||
base_result: str,
|
||||
) -> str:
|
||||
if not self._is_diagnostics_enabled:
|
||||
return base_result
|
||||
|
||||
if self._before_edit_diagnostics_snapshot is None:
|
||||
return base_result
|
||||
|
||||
assert self._symbol_retriever is not None
|
||||
diagnostics_diff = DiagnosticsDiff(self._before_edit_diagnostics_snapshot, self._edited_files, self._symbol_retriever)
|
||||
grouped_diagnostics = diagnostics_diff.get_grouped_diagnostics().get_dict()
|
||||
|
||||
if not grouped_diagnostics:
|
||||
return base_result
|
||||
else:
|
||||
result_dict = {
|
||||
"result": base_result,
|
||||
EditingToolWithDiagnostics.DIAGNOSTICS_KEY: grouped_diagnostics,
|
||||
}
|
||||
return self._tool._to_json(result_dict)
|
||||
|
||||
|
||||
class EditedFileContext:
|
||||
"""
|
||||
Context manager for file editing.
|
||||
|
||||
@@ -173,6 +173,9 @@ class GitignoreParser:
|
||||
except PermissionError as ex:
|
||||
log.debug(f"Skipping entry due to permission error: {entry.path}", exc_info=ex)
|
||||
continue
|
||||
except FileNotFoundError as ex:
|
||||
log.debug(f"Skipping entry due to file not found error (possibly a broken link): {entry.path}", exc_info=ex)
|
||||
continue
|
||||
|
||||
while queue:
|
||||
next_abs_path = queue.pop(0)
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticSeverity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.symbol import LanguageServerSymbolRetriever
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EditedFilePath:
|
||||
before_relative_path: str
|
||||
after_relative_path: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DiagnosticIdentity:
|
||||
message: str
|
||||
start_line: int
|
||||
start_character: int
|
||||
end_line: int
|
||||
end_character: int
|
||||
severity: int | None
|
||||
code_repr: str | None
|
||||
source: str | None
|
||||
|
||||
@classmethod
|
||||
def from_diagnostic(cls, diagnostic: ls_types.Diagnostic) -> "DiagnosticIdentity":
|
||||
diagnostic_range = diagnostic["range"]
|
||||
start = diagnostic_range["start"]
|
||||
end = diagnostic_range["end"]
|
||||
return DiagnosticIdentity(
|
||||
message=diagnostic["message"],
|
||||
start_line=start["line"],
|
||||
start_character=start["character"],
|
||||
end_line=end["line"],
|
||||
end_character=end["character"],
|
||||
severity=diagnostic.get("severity"),
|
||||
code_repr=cls._diagnostic_code_repr(diagnostic.get("code")),
|
||||
source=diagnostic.get("source"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_code_repr(code: Any) -> str | None:
|
||||
if code is None:
|
||||
return None
|
||||
try:
|
||||
return json.dumps(code, sort_keys=True, ensure_ascii=False)
|
||||
except TypeError:
|
||||
return repr(code)
|
||||
|
||||
|
||||
class PublishedDiagnosticsSnapshot:
|
||||
def __init__(self, edited_file_paths: Iterable[EditedFilePath], symbol_retriever: "LanguageServerSymbolRetriever"):
|
||||
generation_by_after_path: dict[str, int] = {}
|
||||
warning_identities_by_before_path: dict[str, set[DiagnosticIdentity]] = {}
|
||||
|
||||
for edited_file_path in edited_file_paths:
|
||||
try:
|
||||
language_server = symbol_retriever.get_language_server(edited_file_path.after_relative_path)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
generation_by_after_path[edited_file_path.after_relative_path] = language_server.get_published_diagnostics_generation(
|
||||
edited_file_path.after_relative_path
|
||||
)
|
||||
|
||||
cached_diagnostics = language_server.get_cached_published_text_document_diagnostics(
|
||||
edited_file_path.before_relative_path,
|
||||
min_severity=2,
|
||||
)
|
||||
if cached_diagnostics is None:
|
||||
try:
|
||||
cached_diagnostics = language_server.request_text_document_diagnostics(
|
||||
edited_file_path.before_relative_path,
|
||||
min_severity=2,
|
||||
)
|
||||
except Exception:
|
||||
cached_diagnostics = []
|
||||
warning_identities_by_before_path[edited_file_path.before_relative_path] = {
|
||||
DiagnosticIdentity.from_diagnostic(diagnostic) for diagnostic in cached_diagnostics or []
|
||||
}
|
||||
|
||||
self.generation_by_after_path = generation_by_after_path
|
||||
self.warning_identities_by_before_path = warning_identities_by_before_path
|
||||
|
||||
|
||||
class GroupedDiagnostics:
|
||||
def __init__(self) -> None:
|
||||
self._grouped_diagnostics: dict[str, dict[str, dict[str, list[dict[str, Any]]]]] = {}
|
||||
|
||||
def add(self, relative_path: str, name_path: str, diagnostic: ls_types.Diagnostic) -> None:
|
||||
severity_name = self._diagnostic_severity_name(diagnostic.get("severity"))
|
||||
self._grouped_diagnostics.setdefault(relative_path, {}).setdefault(severity_name, {}).setdefault(name_path, []).append(
|
||||
self._diagnostic_output_dict(diagnostic)
|
||||
)
|
||||
|
||||
def get_dict(self) -> dict[str, dict[str, dict[str, list[dict[str, Any]]]]]:
|
||||
"""
|
||||
Returns a nested dictionary of the form:
|
||||
{
|
||||
relative_file_path: {
|
||||
severity_name: {
|
||||
name_path: [
|
||||
diagnostic_dict,
|
||||
...
|
||||
],
|
||||
...
|
||||
},
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
where:
|
||||
- relative_file_path is the relative path of the file containing the diagnostic
|
||||
- severity_name is the name of the diagnostic severity (e.g. "Warning", "Error")
|
||||
- name_path is the name path of the symbol that owns the diagnostic (or "<file>" if no owner symbol was found)
|
||||
- diagnostic_dict is a dictionary containing the diagnostic's message, range, and optionally code and source
|
||||
"""
|
||||
return self._grouped_diagnostics
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_severity_name(severity: int | None) -> str:
|
||||
if severity is None:
|
||||
return "Unknown"
|
||||
try:
|
||||
return DiagnosticSeverity(severity).name
|
||||
except ValueError:
|
||||
return f"Severity_{severity}"
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_output_dict(diagnostic: ls_types.Diagnostic) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {
|
||||
"message": diagnostic["message"],
|
||||
"range": diagnostic["range"],
|
||||
}
|
||||
if "code" in diagnostic:
|
||||
result["code"] = diagnostic["code"]
|
||||
if "source" in diagnostic:
|
||||
result["source"] = diagnostic["source"]
|
||||
return result
|
||||
|
||||
|
||||
class DiagnosticsDiff:
|
||||
def __init__(
|
||||
self,
|
||||
before_snapshot: PublishedDiagnosticsSnapshot,
|
||||
edited_files: Iterable[EditedFilePath],
|
||||
symbol_retriever: "LanguageServerSymbolRetriever",
|
||||
):
|
||||
grouped_diagnostics = GroupedDiagnostics()
|
||||
|
||||
for edited_file_path in edited_files:
|
||||
try:
|
||||
language_server = symbol_retriever.get_language_server(edited_file_path.after_relative_path)
|
||||
except:
|
||||
continue
|
||||
|
||||
published_diagnostics = language_server.request_published_text_document_diagnostics(
|
||||
relative_file_path=edited_file_path.after_relative_path,
|
||||
after_generation=before_snapshot.generation_by_after_path.get(edited_file_path.after_relative_path, -1),
|
||||
timeout=2.5,
|
||||
min_severity=2,
|
||||
allow_cached=True,
|
||||
)
|
||||
if not published_diagnostics:
|
||||
try:
|
||||
published_diagnostics = language_server.request_text_document_diagnostics(
|
||||
edited_file_path.after_relative_path,
|
||||
min_severity=2,
|
||||
)
|
||||
except:
|
||||
published_diagnostics = None
|
||||
if published_diagnostics is None:
|
||||
continue
|
||||
|
||||
existing_warning_identities = before_snapshot.warning_identities_by_before_path.get(
|
||||
edited_file_path.before_relative_path, set()
|
||||
)
|
||||
new_warning_identities: set[DiagnosticIdentity] = set()
|
||||
|
||||
for diagnostic in published_diagnostics:
|
||||
diagnostic_identity = DiagnosticIdentity.from_diagnostic(diagnostic)
|
||||
if diagnostic_identity in existing_warning_identities or diagnostic_identity in new_warning_identities:
|
||||
continue
|
||||
new_warning_identities.add(diagnostic_identity)
|
||||
|
||||
diagnostic_start = diagnostic["range"]["start"]
|
||||
owner_symbol = symbol_retriever.find_diagnostic_owner_symbol(
|
||||
relative_file_path=edited_file_path.after_relative_path,
|
||||
line=diagnostic_start["line"],
|
||||
column=diagnostic_start["character"],
|
||||
)
|
||||
name_path = owner_symbol.get_name_path() if owner_symbol is not None else "<file>"
|
||||
grouped_diagnostics.add(edited_file_path.after_relative_path, name_path, diagnostic)
|
||||
|
||||
self._grouped_diagnostics = grouped_diagnostics
|
||||
|
||||
def get_grouped_diagnostics(self) -> GroupedDiagnostics:
|
||||
return self._grouped_diagnostics
|
||||
@@ -28,11 +28,28 @@ 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"
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_AL_EXTENSION_VERSION = "18.0.2242655"
|
||||
INITIAL_AL_EXTENSION_SHA256 = "3971995e61a59dc4fcce4a65053072a67991ed624a16635c4f2911f12564b2b9"
|
||||
DEFAULT_AL_EXTENSION_VERSION = "18.0.2242655"
|
||||
DEFAULT_AL_EXTENSION_SHA256 = "3971995e61a59dc4fcce4a65053072a67991ed624a16635c4f2911f12564b2b9"
|
||||
|
||||
|
||||
def _al_extension_sha(version: str) -> str | None:
|
||||
if version == INITIAL_AL_EXTENSION_VERSION:
|
||||
return INITIAL_AL_EXTENSION_SHA256
|
||||
if version == DEFAULT_AL_EXTENSION_VERSION:
|
||||
return DEFAULT_AL_EXTENSION_SHA256
|
||||
return None
|
||||
|
||||
|
||||
def _al_extension_dirname(version: str) -> str:
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
return "al-extension" if version == INITIAL_AL_EXTENSION_VERSION else f"al-extension-{version}"
|
||||
|
||||
|
||||
AL_EXTENSION_ALLOWED_HOSTS = ("marketplace.visualstudio.com",)
|
||||
|
||||
|
||||
@@ -131,21 +148,13 @@ class ALLanguageServer(SolidLanguageServer):
|
||||
return path.replace("\\", "/")
|
||||
|
||||
@classmethod
|
||||
def _download_al_extension(cls, url: str, target_dir: str) -> bool:
|
||||
def _download_al_extension(cls, url: str, target_dir: str, expected_sha256: str | None) -> bool:
|
||||
"""
|
||||
Download and extract the AL extension from VS Code marketplace.
|
||||
|
||||
The VS Code marketplace packages extensions as .vsix files (which are ZIP archives).
|
||||
This method downloads the VSIX file and extracts it to get the language server binaries.
|
||||
|
||||
Args:
|
||||
logger: Logger for tracking download progress
|
||||
url: VS Code marketplace URL for the AL extension
|
||||
target_dir: Directory where the extension will be extracted
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
|
||||
Note:
|
||||
The download includes progress tracking and proper user-agent headers
|
||||
to ensure compatibility with the VS Code marketplace.
|
||||
@@ -158,7 +167,7 @@ class ALLanguageServer(SolidLanguageServer):
|
||||
url,
|
||||
target_dir,
|
||||
"zip",
|
||||
expected_sha256=AL_EXTENSION_SHA256 if url == AL_EXTENSION_URL else None,
|
||||
expected_sha256=expected_sha256,
|
||||
allowed_hosts=AL_EXTENSION_ALLOWED_HOSTS,
|
||||
)
|
||||
log.info("AL extension extracted successfully")
|
||||
@@ -231,8 +240,10 @@ class ALLanguageServer(SolidLanguageServer):
|
||||
elif env_path:
|
||||
log.warning(f"AL_EXTENSION_PATH set but directory not found: {env_path}")
|
||||
|
||||
# Check default download location
|
||||
default_path = os.path.join(cls.ls_resources_dir(solidlsp_settings), "al-extension", "extension")
|
||||
# Check the resolved-version download location (versioned for non-INITIAL, legacy "al-extension" for INITIAL)
|
||||
al_settings = solidlsp_settings.get_ls_specific_settings(Language.AL)
|
||||
al_extension_version = al_settings.get("al_extension_version", DEFAULT_AL_EXTENSION_VERSION)
|
||||
default_path = os.path.join(cls.ls_resources_dir(solidlsp_settings), _al_extension_dirname(al_extension_version), "extension")
|
||||
if os.path.exists(default_path):
|
||||
log.debug(f"Found AL extension in default location: {default_path}")
|
||||
return default_path
|
||||
@@ -255,9 +266,9 @@ class ALLanguageServer(SolidLanguageServer):
|
||||
Path to installed extension or None if download failed
|
||||
|
||||
"""
|
||||
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_version = al_settings.get("al_extension_version", DEFAULT_AL_EXTENSION_VERSION)
|
||||
al_extension_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), _al_extension_dirname(al_extension_version))
|
||||
al_extension_url = (
|
||||
"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/"
|
||||
f"vsextensions/al/{al_extension_version}/vspackage"
|
||||
@@ -265,7 +276,7 @@ class ALLanguageServer(SolidLanguageServer):
|
||||
|
||||
log.info(f"Downloading AL extension from: {al_extension_url}")
|
||||
|
||||
if cls._download_al_extension(al_extension_url, al_extension_dir):
|
||||
if cls._download_al_extension(al_extension_url, al_extension_dir, _al_extension_sha(al_extension_version)):
|
||||
extension_path = os.path.join(al_extension_dir, "extension")
|
||||
if os.path.exists(extension_path):
|
||||
log.info("AL extension downloaded and installed successfully")
|
||||
|
||||
@@ -20,6 +20,12 @@ from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_ANSIBLE_LANGUAGE_SERVER_VERSION = "1.2.3"
|
||||
DEFAULT_ANSIBLE_LANGUAGE_SERVER_VERSION = "1.2.3"
|
||||
|
||||
|
||||
def _deep_merge(base: dict, override: dict) -> None:
|
||||
"""Recursively merge *override* into *base*, modifying *base* in-place."""
|
||||
@@ -162,7 +168,9 @@ 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")
|
||||
ansible_language_server_version = self._custom_settings.get(
|
||||
"ansible_language_server_version", DEFAULT_ANSIBLE_LANGUAGE_SERVER_VERSION
|
||||
)
|
||||
npm_registry = self._custom_settings.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
@@ -180,8 +188,13 @@ class AnsibleLanguageServer(SolidLanguageServer):
|
||||
]
|
||||
)
|
||||
|
||||
# install ansible-language-server if not already installed
|
||||
ansible_ls_dir = os.path.join(self._ls_resources_dir, "ansible-lsp")
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
ls_dirname = (
|
||||
"ansible-lsp"
|
||||
if ansible_language_server_version == INITIAL_ANSIBLE_LANGUAGE_SERVER_VERSION
|
||||
else f"ansible-lsp-{ansible_language_server_version}"
|
||||
)
|
||||
ansible_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
ansible_executable_path = os.path.join(ansible_ls_dir, "node_modules", ".bin", "ansible-language-server")
|
||||
|
||||
# handle Windows executable extension
|
||||
|
||||
@@ -18,12 +18,72 @@ from solidlsp.ls import (
|
||||
SolidLanguageServer,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ShellCheck binary release info; download directly from upstream GitHub releases rather than
|
||||
# relying on the `shellcheck` npm wrapper (which lazily downloads on first invocation and
|
||||
# turned the bash LS install into a fragile, network-dependent step).
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_BASH_LANGUAGE_SERVER_VERSION = "5.6.0"
|
||||
DEFAULT_BASH_LANGUAGE_SERVER_VERSION = "5.6.0"
|
||||
|
||||
# ShellCheck binary path already encodes _SHELLCHECK_VERSION (see _shellcheck_binary_path),
|
||||
# so version bumps trigger reinstall correctly without further intervention.
|
||||
_SHELLCHECK_VERSION = "0.10.0"
|
||||
_SHELLCHECK_RELEASE_BASE = f"https://github.com/koalaman/shellcheck/releases/download/v{_SHELLCHECK_VERSION}"
|
||||
_SHELLCHECK_ALLOWED_HOSTS = (
|
||||
"github.com",
|
||||
"objects.githubusercontent.com",
|
||||
"release-assets.githubusercontent.com",
|
||||
)
|
||||
# Per-platform archive metadata: tar.xz on POSIX (extracts to shellcheck-v<ver>/shellcheck),
|
||||
# zip on Windows (extracts to shellcheck.exe at archive root).
|
||||
_SHELLCHECK_DEPENDENCIES: dict[PlatformId, dict[str, str]] = {
|
||||
PlatformId.LINUX_x64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.linux.x86_64.tar.xz",
|
||||
"sha256": "6c881ab0698e4e6ea235245f22832860544f17ba386442fe7e9d629f8cbedf87",
|
||||
},
|
||||
PlatformId.LINUX_arm64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.linux.aarch64.tar.xz",
|
||||
"sha256": "324a7e89de8fa2aed0d0c28f3dab59cf84c6d74264022c00c22af665ed1a09bb",
|
||||
},
|
||||
PlatformId.OSX_x64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.darwin.x86_64.tar.xz",
|
||||
"sha256": "ef27684f23279d112d8ad84e0823642e43f838993bbb8c0963db9b58a90464c2",
|
||||
},
|
||||
PlatformId.OSX_arm64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.darwin.aarch64.tar.xz",
|
||||
"sha256": "bbd2f14826328eee7679da7221f2bc3afb011f6a928b848c80c321f6046ddf81",
|
||||
},
|
||||
PlatformId.WIN_x64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.zip",
|
||||
"sha256": "eb6cd53a54ea97a56540e9d296ce7e2fa68715aa507ff23574646c1e12b2e143",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _shellcheck_install_dir(bash_ls_dir: str) -> str:
|
||||
return os.path.join(bash_ls_dir, "shellcheck")
|
||||
|
||||
|
||||
def _shellcheck_binary_path(bash_ls_dir: str) -> str:
|
||||
"""
|
||||
Returns the path to the extracted ShellCheck binary. POSIX archives extract under
|
||||
``shellcheck-v<ver>/shellcheck``; the Windows zip drops ``shellcheck.exe`` at archive root.
|
||||
"""
|
||||
install_dir = _shellcheck_install_dir(bash_ls_dir)
|
||||
if os.name == "nt":
|
||||
return os.path.join(install_dir, "shellcheck.exe")
|
||||
return os.path.join(install_dir, f"shellcheck-v{_SHELLCHECK_VERSION}", "shellcheck")
|
||||
|
||||
|
||||
class BashLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
|
||||
@@ -53,37 +113,37 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Setup runtime dependencies for Bash Language Server and return the command to start the server.
|
||||
"""
|
||||
# Verify both node and npm are installed
|
||||
# verify node + npm are available for the bash-language-server install
|
||||
is_node_installed = shutil.which("node") is not None
|
||||
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
|
||||
is_npm_installed = shutil.which("npm") is not None
|
||||
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
|
||||
bash_language_server_version = self._custom_settings.get("bash_language_server_version", "5.6.0")
|
||||
bash_language_server_version = self._custom_settings.get("bash_language_server_version", DEFAULT_BASH_LANGUAGE_SERVER_VERSION)
|
||||
npm_registry = self._custom_settings.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="bash-language-server",
|
||||
description="bash-language-server package",
|
||||
command=build_npm_install_command("bash-language-server", bash_language_server_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Install bash-language-server if not already installed
|
||||
bash_ls_dir = os.path.join(self._ls_resources_dir, "bash-lsp")
|
||||
bash_executable_path = os.path.join(bash_ls_dir, "node_modules", ".bin", "bash-language-server")
|
||||
|
||||
# Handle Windows executable extension
|
||||
bash_ls_dir = self._resolve_bash_ls_dir(bash_language_server_version)
|
||||
managed_bin_dir = os.path.join(bash_ls_dir, "node_modules", ".bin")
|
||||
bash_executable_path = os.path.join(managed_bin_dir, "bash-language-server")
|
||||
if os.name == "nt":
|
||||
bash_executable_path += ".cmd"
|
||||
|
||||
# install bash-language-server via npm
|
||||
if not os.path.exists(bash_executable_path):
|
||||
log.info(f"Bash Language Server executable not found at {bash_executable_path}. Installing...")
|
||||
deps.install(bash_ls_dir)
|
||||
log.info("Bash language server dependencies installed successfully")
|
||||
bls_deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="bash-language-server",
|
||||
description="bash-language-server package",
|
||||
command=build_npm_install_command("bash-language-server", bash_language_server_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
log.info("Installing bash-language-server...")
|
||||
bls_deps.install(bash_ls_dir)
|
||||
|
||||
# install ShellCheck binary directly from upstream releases for the current platform
|
||||
self._install_shellcheck_if_missing(bash_ls_dir)
|
||||
|
||||
if not os.path.exists(bash_executable_path):
|
||||
raise FileNotFoundError(
|
||||
@@ -92,6 +152,59 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
|
||||
return bash_executable_path
|
||||
|
||||
@staticmethod
|
||||
def _install_shellcheck_if_missing(bash_ls_dir: str) -> None:
|
||||
"""
|
||||
Downloads and extracts the platform-appropriate ShellCheck release into
|
||||
``${bash_ls_dir}/shellcheck`` if the binary is not already present.
|
||||
"""
|
||||
binary_path = _shellcheck_binary_path(bash_ls_dir)
|
||||
if os.path.exists(binary_path):
|
||||
return
|
||||
|
||||
install_dir = _shellcheck_install_dir(bash_ls_dir)
|
||||
os.makedirs(install_dir, exist_ok=True)
|
||||
|
||||
release = _SHELLCHECK_DEPENDENCIES.get(PlatformUtils.get_platform_id())
|
||||
if release is None:
|
||||
raise RuntimeError(f"ShellCheck has no upstream binary release for platform {PlatformUtils.get_platform_id().value}")
|
||||
|
||||
archive_type = "zip" if os.name == "nt" else "xztar"
|
||||
log.info(f"Downloading ShellCheck v{_SHELLCHECK_VERSION} for {PlatformUtils.get_platform_id().value}")
|
||||
FileUtils.download_and_extract_archive_verified(
|
||||
release["url"],
|
||||
install_dir,
|
||||
archive_type,
|
||||
expected_sha256=release["sha256"],
|
||||
allowed_hosts=_SHELLCHECK_ALLOWED_HOSTS,
|
||||
)
|
||||
|
||||
if not os.path.exists(binary_path):
|
||||
raise FileNotFoundError(f"ShellCheck binary not found at {binary_path} after extraction; archive layout may have changed.")
|
||||
|
||||
# ensure the binary is executable on POSIX (zip extraction does not preserve perms)
|
||||
if os.name != "nt":
|
||||
current = os.stat(binary_path).st_mode
|
||||
os.chmod(binary_path, current | 0o111)
|
||||
|
||||
def create_launch_command_env(self) -> dict[str, str]:
|
||||
bash_language_server_version = self._custom_settings.get("bash_language_server_version", DEFAULT_BASH_LANGUAGE_SERVER_VERSION)
|
||||
bash_ls_dir = self._resolve_bash_ls_dir(bash_language_server_version)
|
||||
managed_bin_dir = os.path.join(bash_ls_dir, "node_modules", ".bin")
|
||||
return {
|
||||
"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", ""),
|
||||
"SHELLCHECK_PATH": _shellcheck_binary_path(bash_ls_dir),
|
||||
}
|
||||
|
||||
def _resolve_bash_ls_dir(self, bash_language_server_version: str) -> str:
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
ls_dirname = (
|
||||
"bash-lsp"
|
||||
if bash_language_server_version == INITIAL_BASH_LANGUAGE_SERVER_VERSION
|
||||
else f"bash-lsp-{bash_language_server_version}"
|
||||
)
|
||||
return os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
return [core_path, "start"]
|
||||
|
||||
|
||||
@@ -34,6 +34,39 @@ class ClangdLanguageServer(SolidLanguageServer):
|
||||
(default: the bundled Serena version).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _determine_log_level(line: str) -> int:
|
||||
"""
|
||||
Classify a clangd stderr line using clangd's explicit level prefix.
|
||||
|
||||
See `clang::clangd::Logger::indicator` for details:
|
||||
https://clang.llvm.org/extra/doxygen/classclang_1_1clangd_1_1Logger.html
|
||||
|
||||
Clangd emits each log record prefixed by a single indicator character
|
||||
followed by a timestamp in square brackets, e.g. ``I[12:27:16.234]``.
|
||||
|
||||
The indicators are ``D`` (Debug), ``I`` (Info), ``E`` (Error) and
|
||||
``F`` (Fatal). Continuation lines of multi-line records carry no
|
||||
prefix and are treated as informational.
|
||||
|
||||
Without this override, the base implementation scans the line for
|
||||
the substrings ``error`` and ``exception``, which produces false
|
||||
positives on clangd's reconstructed compile commands in some cases
|
||||
(e.g. ``-DNO_EXCEPTIONS``, ``-fno-exceptions``).
|
||||
"""
|
||||
# classify by clangd's level indicator character
|
||||
if len(line) >= 2 and line[1] == "[":
|
||||
indicator = line[0]
|
||||
if indicator in ("E", "F"):
|
||||
return logging.ERROR
|
||||
if indicator == "I":
|
||||
return logging.INFO
|
||||
if indicator == "D":
|
||||
return logging.DEBUG
|
||||
|
||||
# continuation line or non-prefixed output: default to INFO, do not keyword-scan
|
||||
return logging.INFO
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""
|
||||
Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
|
||||
@@ -21,7 +21,35 @@ from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
CLOJURE_LSP_VERSION = "2026.02.20-16.08.58"
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_CLOJURE_LSP_VERSION = "2026.02.20-16.08.58"
|
||||
INITIAL_CLOJURE_LSP_SHA256_BY_PLATFORM = {
|
||||
"osx-arm64": "a14d4db074f665378214e2dc888472e186c228dfa065c777b0534bfda5571669",
|
||||
"osx-x64": "5507434c27104ab816e096d3336d8191641de8a65b57d76afb585d07167a3cf2",
|
||||
"linux-arm64": "f8f09fa07dd4b6743b5c57270ccf1ee5cdbc5fca09dbca8b6a3b22705b5da4e1",
|
||||
"linux-x64": "52e8bf4fd4cf171df0a3077c8bb5a3bf598d4c621e94b4876dab943a61267309",
|
||||
"win-x64": "817b1271288817c954fb9e595278b1f25003827ce31f8785f253dc4ac911041f",
|
||||
}
|
||||
DEFAULT_CLOJURE_LSP_VERSION = "2026.02.20-16.08.58"
|
||||
DEFAULT_CLOJURE_LSP_SHA256_BY_PLATFORM = {
|
||||
"osx-arm64": "a14d4db074f665378214e2dc888472e186c228dfa065c777b0534bfda5571669",
|
||||
"osx-x64": "5507434c27104ab816e096d3336d8191641de8a65b57d76afb585d07167a3cf2",
|
||||
"linux-arm64": "f8f09fa07dd4b6743b5c57270ccf1ee5cdbc5fca09dbca8b6a3b22705b5da4e1",
|
||||
"linux-x64": "52e8bf4fd4cf171df0a3077c8bb5a3bf598d4c621e94b4876dab943a61267309",
|
||||
"win-x64": "817b1271288817c954fb9e595278b1f25003827ce31f8785f253dc4ac911041f",
|
||||
}
|
||||
|
||||
|
||||
def _clojure_lsp_sha(version: str, platform_key: str) -> str | None:
|
||||
if version == INITIAL_CLOJURE_LSP_VERSION:
|
||||
return INITIAL_CLOJURE_LSP_SHA256_BY_PLATFORM[platform_key]
|
||||
if version == DEFAULT_CLOJURE_LSP_VERSION:
|
||||
return DEFAULT_CLOJURE_LSP_SHA256_BY_PLATFORM[platform_key]
|
||||
return None
|
||||
|
||||
|
||||
CLOJURE_LSP_ALLOWED_HOSTS = (
|
||||
"github.com",
|
||||
"release-assets.githubusercontent.com",
|
||||
@@ -58,7 +86,6 @@ class ClojureLSP(SolidLanguageServer):
|
||||
by Serena (default: the bundled Serena version).
|
||||
"""
|
||||
|
||||
CLOJURE_LSP_VERSION = CLOJURE_LSP_VERSION
|
||||
CLOJURE_LSP_ALLOWED_HOSTS = CLOJURE_LSP_ALLOWED_HOSTS
|
||||
|
||||
@override
|
||||
@@ -69,7 +96,6 @@ class ClojureLSP(SolidLanguageServer):
|
||||
@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(
|
||||
@@ -78,7 +104,7 @@ class ClojureLSP(SolidLanguageServer):
|
||||
platform_id="osx-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="clojure-lsp",
|
||||
sha256="a14d4db074f665378214e2dc888472e186c228dfa065c777b0534bfda5571669" if default_version else None,
|
||||
sha256=_clojure_lsp_sha(version, "osx-arm64"),
|
||||
allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -87,7 +113,7 @@ class ClojureLSP(SolidLanguageServer):
|
||||
platform_id="osx-x64",
|
||||
archive_type="zip",
|
||||
binary_name="clojure-lsp",
|
||||
sha256="5507434c27104ab816e096d3336d8191641de8a65b57d76afb585d07167a3cf2" if default_version else None,
|
||||
sha256=_clojure_lsp_sha(version, "osx-x64"),
|
||||
allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -96,7 +122,7 @@ class ClojureLSP(SolidLanguageServer):
|
||||
platform_id="linux-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="clojure-lsp",
|
||||
sha256="f8f09fa07dd4b6743b5c57270ccf1ee5cdbc5fca09dbca8b6a3b22705b5da4e1" if default_version else None,
|
||||
sha256=_clojure_lsp_sha(version, "linux-arm64"),
|
||||
allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -105,7 +131,7 @@ class ClojureLSP(SolidLanguageServer):
|
||||
platform_id="linux-x64",
|
||||
archive_type="zip",
|
||||
binary_name="clojure-lsp",
|
||||
sha256="52e8bf4fd4cf171df0a3077c8bb5a3bf598d4c621e94b4876dab943a61267309" if default_version else None,
|
||||
sha256=_clojure_lsp_sha(version, "linux-x64"),
|
||||
allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -114,7 +140,7 @@ class ClojureLSP(SolidLanguageServer):
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="clojure-lsp.exe",
|
||||
sha256="817b1271288817c954fb9e595278b1f25003827ce31f8785f253dc4ac911041f" if default_version else None,
|
||||
sha256=_clojure_lsp_sha(version, "win-x64"),
|
||||
allowed_hosts=CLOJURE_LSP_ALLOWED_HOSTS,
|
||||
),
|
||||
]
|
||||
@@ -143,16 +169,22 @@ 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()
|
||||
clojure_lsp_version = self._custom_settings.get("clojure_lsp_version", ClojureLSP.CLOJURE_LSP_VERSION)
|
||||
clojure_lsp_version = self._custom_settings.get("clojure_lsp_version", DEFAULT_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)
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
install_dir = (
|
||||
self._ls_resources_dir
|
||||
if clojure_lsp_version == INITIAL_CLOJURE_LSP_VERSION
|
||||
else os.path.join(self._ls_resources_dir, f"clojure-lsp-{clojure_lsp_version}")
|
||||
)
|
||||
clojurelsp_executable_path = deps.binary_path(install_dir)
|
||||
if not os.path.exists(clojurelsp_executable_path):
|
||||
log.info(
|
||||
f"Downloading and extracting clojure-lsp from {dependency.url} to {self._ls_resources_dir}",
|
||||
f"Downloading and extracting clojure-lsp from {dependency.url} to {install_dir}",
|
||||
)
|
||||
deps.install(self._ls_resources_dir)
|
||||
deps.install(install_dir)
|
||||
if not os.path.exists(clojurelsp_executable_path):
|
||||
raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}")
|
||||
os.chmod(clojurelsp_executable_path, 0o755)
|
||||
|
||||
@@ -206,6 +206,10 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
scripts. If you have a custom .NET installation, ensure 'dotnet' is available in PATH with version 10 or higher.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""
|
||||
Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly.
|
||||
|
||||
@@ -18,6 +18,34 @@ log = logging.getLogger(__name__)
|
||||
|
||||
DART_ALLOWED_HOSTS = ("storage.googleapis.com",)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_DART_SDK_VERSION = "3.7.1"
|
||||
INITIAL_DART_SDK_SHA256_BY_PLATFORM = {
|
||||
"linux-x64": "2813959e7d9650334015b927cc533f5beadfbf7fa48248beec471f8942a0ee71",
|
||||
"win-x64": "f56c03122e17abe5be1429eee0a975fb8ed511b6731ec90c6475992d3dee4ea5",
|
||||
"win-arm64": "fada411c6538d0ac24c35d6360767241f1298f64cbc5e88716387d54757a105a",
|
||||
"osx-x64": "a2765917b6ae49d1ac119553df9584989f9c441a46e8f18c129ba52489658d2e",
|
||||
"osx-arm64": "f57c25163092bac818f8ca6250a0d8b2c56344c6a075a1bd7c60da7ac28b32a4",
|
||||
}
|
||||
DEFAULT_DART_SDK_VERSION = "3.7.1"
|
||||
DEFAULT_DART_SDK_SHA256_BY_PLATFORM = {
|
||||
"linux-x64": "2813959e7d9650334015b927cc533f5beadfbf7fa48248beec471f8942a0ee71",
|
||||
"win-x64": "f56c03122e17abe5be1429eee0a975fb8ed511b6731ec90c6475992d3dee4ea5",
|
||||
"win-arm64": "fada411c6538d0ac24c35d6360767241f1298f64cbc5e88716387d54757a105a",
|
||||
"osx-x64": "a2765917b6ae49d1ac119553df9584989f9c441a46e8f18c129ba52489658d2e",
|
||||
"osx-arm64": "f57c25163092bac818f8ca6250a0d8b2c56344c6a075a1bd7c60da7ac28b32a4",
|
||||
}
|
||||
|
||||
|
||||
def _dart_sdk_sha(version: str, platform_key: str) -> str | None:
|
||||
if version == INITIAL_DART_SDK_VERSION:
|
||||
return INITIAL_DART_SDK_SHA256_BY_PLATFORM[platform_key]
|
||||
if version == DEFAULT_DART_SDK_VERSION:
|
||||
return DEFAULT_DART_SDK_SHA256_BY_PLATFORM[platform_key]
|
||||
return None
|
||||
|
||||
|
||||
class DartLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -49,7 +77,7 @@ 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")
|
||||
dart_sdk_version = dart_settings.get("dart_sdk_version", DEFAULT_DART_SDK_VERSION)
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
@@ -59,7 +87,7 @@ class DartLanguageServer(SolidLanguageServer):
|
||||
platform_id="linux-x64",
|
||||
archive_type="zip",
|
||||
binary_name="dart-sdk/bin/dart",
|
||||
sha256="2813959e7d9650334015b927cc533f5beadfbf7fa48248beec471f8942a0ee71" if dart_sdk_version == "3.7.1" else None,
|
||||
sha256=_dart_sdk_sha(dart_sdk_version, "linux-x64"),
|
||||
allowed_hosts=DART_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -69,7 +97,7 @@ class DartLanguageServer(SolidLanguageServer):
|
||||
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,
|
||||
sha256=_dart_sdk_sha(dart_sdk_version, "win-x64"),
|
||||
allowed_hosts=DART_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -79,7 +107,7 @@ class DartLanguageServer(SolidLanguageServer):
|
||||
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,
|
||||
sha256=_dart_sdk_sha(dart_sdk_version, "win-arm64"),
|
||||
allowed_hosts=DART_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -89,7 +117,7 @@ class DartLanguageServer(SolidLanguageServer):
|
||||
platform_id="osx-x64",
|
||||
archive_type="zip",
|
||||
binary_name="dart-sdk/bin/dart",
|
||||
sha256="a2765917b6ae49d1ac119553df9584989f9c441a46e8f18c129ba52489658d2e" if dart_sdk_version == "3.7.1" else None,
|
||||
sha256=_dart_sdk_sha(dart_sdk_version, "osx-x64"),
|
||||
allowed_hosts=DART_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -99,13 +127,18 @@ class DartLanguageServer(SolidLanguageServer):
|
||||
platform_id="osx-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="dart-sdk/bin/dart",
|
||||
sha256="f57c25163092bac818f8ca6250a0d8b2c56344c6a075a1bd7c60da7ac28b32a4" if dart_sdk_version == "3.7.1" else None,
|
||||
sha256=_dart_sdk_sha(dart_sdk_version, "osx-arm64"),
|
||||
allowed_hosts=DART_ALLOWED_HOSTS,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
dart_ls_dir = cls.ls_resources_dir(solidlsp_settings)
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
dart_ls_dir = (
|
||||
cls.ls_resources_dir(solidlsp_settings)
|
||||
if dart_sdk_version == INITIAL_DART_SDK_VERSION
|
||||
else os.path.join(cls.ls_resources_dir(solidlsp_settings), f"dart-sdk-{dart_sdk_version}")
|
||||
)
|
||||
dart_executable_path = deps.binary_path(dart_ls_dir)
|
||||
|
||||
if not os.path.exists(dart_executable_path):
|
||||
|
||||
@@ -7,9 +7,12 @@ import hashlib
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
from pathlib import PurePath
|
||||
from pathlib import Path, PurePath
|
||||
from time import sleep
|
||||
from typing import cast
|
||||
|
||||
@@ -18,6 +21,7 @@ from overrides import override
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.ls_types import UnifiedSymbolInformation
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation
|
||||
@@ -28,42 +32,121 @@ 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 = {
|
||||
INTELLICODE_ALLOWED_HOSTS = (
|
||||
"visualstudioexptteam.gallery.vsassets.io",
|
||||
"marketplace.visualstudio.com",
|
||||
"download.visualstudio.microsoft.com",
|
||||
)
|
||||
|
||||
# Version pinning convention (read this before bumping anything below):
|
||||
#
|
||||
# INITIAL_* — the very first version we shipped runtime-dependency support for, paired with
|
||||
# its SHA. NEVER edited. The legacy unversioned install directory (e.g. "vscode-java/")
|
||||
# is reserved exclusively for this version, so users who installed under the original
|
||||
# layout keep their existing cache forever.
|
||||
#
|
||||
# DEFAULT_* — the version (and matching SHA) used when the user does not override via
|
||||
# custom_settings. THIS is what we edit to upgrade: copy the new version + new SHA over
|
||||
# the existing DEFAULT_* literals, leave INITIAL_* alone.
|
||||
#
|
||||
# Resolution at install time:
|
||||
# resolved_version = custom_settings.get("..._version", DEFAULT_*_VERSION)
|
||||
# - resolved == INITIAL_* -> legacy unversioned dir, SHA = INITIAL_*_SHA256
|
||||
# - resolved == DEFAULT_* -> versioned dir "{name}-{resolved}", SHA = DEFAULT_*_SHA256
|
||||
# - any other version -> versioned dir "{name}-{resolved}", SHA verification skipped
|
||||
#
|
||||
# Consequences of this scheme:
|
||||
# - Bumping DEFAULT_* makes new installs land in a new versioned subdir => no silent reuse
|
||||
# of stale binaries (the bug this scheme exists to fix).
|
||||
# - Previously-downloaded versions are not lost: a user can request any past version via
|
||||
# custom_settings and the matching cached subdir is picked up without re-downloading.
|
||||
# - INITIAL_* and DEFAULT_* hold identical literals when first introduced; they diverge on
|
||||
# the first DEFAULT_* bump and stay independent thereafter.
|
||||
INITIAL_VSCODE_JAVA_VERSION = "1.42.0-561"
|
||||
INITIAL_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"
|
||||
DEFAULT_VSCODE_JAVA_VERSION = "1.42.0-561"
|
||||
DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM = {
|
||||
"osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933",
|
||||
"osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f",
|
||||
"linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1",
|
||||
"linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9",
|
||||
"win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3",
|
||||
}
|
||||
|
||||
INITIAL_INTELLICODE_VERSION = "1.2.30"
|
||||
INITIAL_INTELLICODE_SHA256 = "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251"
|
||||
DEFAULT_INTELLICODE_VERSION = "1.2.30"
|
||||
DEFAULT_INTELLICODE_SHA256 = "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251"
|
||||
|
||||
# Mapping from Serena's platform identifiers to upstream JDTLS config_<platform> directory names
|
||||
JDTLS_CONFIG_DIR_BY_PLATFORM = {
|
||||
"osx-arm64": "config_mac_arm",
|
||||
"darwin-arm64": "config_mac_arm",
|
||||
"osx-x64": "config_mac",
|
||||
"linux-arm64": "config_linux_arm",
|
||||
"linux-x64": "config_linux",
|
||||
"win-x64": "config_win",
|
||||
}
|
||||
|
||||
# Minimum supported JDK version for running JDTLS itself
|
||||
JDTLS_MIN_JDK_VERSION = 21
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RuntimeDependencyPaths:
|
||||
"""
|
||||
Stores the paths to the runtime dependencies of EclipseJDTLS
|
||||
Stores the paths to the runtime dependencies of EclipseJDTLS.
|
||||
|
||||
In the default mode (vscode-java VSIX), all paths are populated.
|
||||
In the upstream-jdtls mode (when ``jdtls_path`` and ``lombok_path`` are set),
|
||||
fields that have no upstream equivalent (gradle distribution, IntelliCode bundle)
|
||||
are set to None.
|
||||
"""
|
||||
|
||||
gradle_path: str
|
||||
lombok_jar_path: str
|
||||
jre_path: str
|
||||
jre_home_path: str
|
||||
jdtls_launcher_jar_path: str
|
||||
jdtls_readonly_config_path: str
|
||||
intellicode_jar_path: str
|
||||
intellisense_members_path: str
|
||||
lombok_jar_path: str
|
||||
gradle_path: str | None = None
|
||||
intellicode_jar_path: str | None = None
|
||||
intellisense_members_path: str | None = None
|
||||
|
||||
|
||||
class EclipseJDTLS(SolidLanguageServer):
|
||||
r"""
|
||||
The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class
|
||||
|
||||
Two installation modes are supported:
|
||||
|
||||
1. **Default vscode-java VSIX mode** (no extra config required) — Serena downloads the platform-specific
|
||||
vscode-java VSIX bundle (~500 MB: JDTLS + bundled JRE 21 + Lombok + IntelliCode), Gradle distribution
|
||||
and IntelliCode VSIX from public hosts. Suitable when public network access is available.
|
||||
|
||||
2. **Upstream JDTLS mode** (activated by setting both ``jdtls_path`` and ``lombok_path``) — uses an
|
||||
existing JDTLS installation and the system JDK. Nothing is downloaded. Suitable for restricted-network
|
||||
environments. Requires JDK 21+ available via ``java_home`` setting / ``JAVA_HOME`` env / PATH.
|
||||
Maven projects work out of the box (m2e bundled in JDTLS uses Maven Embedder); Gradle projects
|
||||
need ``./gradlew`` in the project or a system-installed Gradle (Buildship default discovery).
|
||||
|
||||
You can configure the following options in ls_specific_settings (in serena_config.yml):
|
||||
Upstream JDTLS mode (mutually exclusive group — set both to activate):
|
||||
- jdtls_path: Path to upstream JDTLS root (containing plugins/ and config_<platform>/).
|
||||
Get via 'brew install jdtls' or extract jdt-language-server-*.tar.gz from
|
||||
https://download.eclipse.org/jdtls/snapshots/.
|
||||
- lombok_path: Path to lombok jar (e.g. ~/.m2/repository/org/projectlombok/lombok/<ver>/lombok-<ver>.jar
|
||||
or download from https://projectlombok.org/downloads/).
|
||||
|
||||
Optional in upstream-jdtls mode:
|
||||
- java_home: Path to JDK 21+ home directory. Falls back to JAVA_HOME env, then 'which java'.
|
||||
|
||||
General settings (apply in both modes):
|
||||
- maven_user_settings: Path to Maven settings.xml file (default: ~/.m2/settings.xml)
|
||||
- gradle_user_home: Path to Gradle user home directory (default: ~/.gradle)
|
||||
- gradle_wrapper_enabled: Whether to use the project's Gradle wrapper (default: false)
|
||||
@@ -77,7 +160,16 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
- 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:
|
||||
Example configuration for upstream JDTLS mode (no downloads, suitable for offline/corporate):
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
java:
|
||||
jdtls_path: "/opt/homebrew/Cellar/jdtls/1.50.0/libexec" # or extracted tar.gz path
|
||||
lombok_path: "/Users/me/.m2/repository/org/projectlombok/lombok/1.18.36/lombok-1.18.36.jar"
|
||||
# java_home: "/opt/homebrew/opt/openjdk@21" # optional, JAVA_HOME env / PATH used otherwise
|
||||
```
|
||||
|
||||
Example configuration for default vscode-java VSIX mode (auto-download):
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
java:
|
||||
@@ -98,6 +190,10 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
```
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""
|
||||
Creates a new EclipseJDTLS instance initializing the language server settings appropriately.
|
||||
@@ -154,15 +250,60 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
) -> RuntimeDependencyPaths:
|
||||
"""
|
||||
Setup runtime dependencies for EclipseJDTLS and return the paths.
|
||||
|
||||
Two modes are supported:
|
||||
|
||||
* **Upstream JDTLS mode** (activated when both ``jdtls_path`` and ``lombok_path`` are set
|
||||
in ``ls_specific_settings.java``): uses an existing JDTLS installation (e.g. via
|
||||
``brew install jdtls`` or extracted ``jdt-language-server-*.tar.gz``) and the system JDK.
|
||||
Nothing is downloaded by Serena. Suitable for restricted-network/corporate environments.
|
||||
* **Default vscode-java VSIX mode** (activated otherwise): downloads the platform-specific
|
||||
vscode-java VSIX bundle (containing JDTLS, bundled JRE 21, Lombok, IntelliCode), Gradle
|
||||
distribution and IntelliCode VSIX from public hosts. Original behaviour, unchanged.
|
||||
"""
|
||||
jdtls_path = custom_settings.get("jdtls_path")
|
||||
lombok_path = custom_settings.get("lombok_path")
|
||||
if jdtls_path or lombok_path:
|
||||
# both must be set together to activate upstream mode
|
||||
if not (jdtls_path and lombok_path):
|
||||
raise SolidLSPException(
|
||||
"Both 'jdtls_path' and 'lombok_path' must be set together in "
|
||||
"ls_specific_settings.java to use the upstream JDTLS mode. "
|
||||
"Set both, or remove both to use the default vscode-java VSIX mode."
|
||||
)
|
||||
return EclipseJDTLS.DependencyProvider._setup_from_existing_install(str(jdtls_path), str(lombok_path), custom_settings)
|
||||
|
||||
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_version = custom_settings.get("vscode_java_version", DEFAULT_VSCODE_JAVA_VERSION)
|
||||
vscode_java_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}"
|
||||
intellicode_version = custom_settings.get("intellicode_version", "1.2.30")
|
||||
intellicode_version = custom_settings.get("intellicode_version", DEFAULT_INTELLICODE_VERSION)
|
||||
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"
|
||||
|
||||
# install-dir name per the version-pinning convention (see module-level block):
|
||||
# INITIAL -> legacy unversioned dir; everything else -> "{name}-{resolved}" subdir
|
||||
vscode_java_dirname = (
|
||||
"vscode-java" if vscode_java_version == INITIAL_VSCODE_JAVA_VERSION else f"vscode-java-{vscode_java_version}"
|
||||
)
|
||||
intellicode_dirname = (
|
||||
"intellicode" if intellicode_version == INITIAL_INTELLICODE_VERSION else f"intellicode-{intellicode_version}"
|
||||
)
|
||||
|
||||
# SHA is only known for our two pinned versions (INITIAL and current DEFAULT);
|
||||
# for any other user-supplied version we skip verification (returns None)
|
||||
def vscode_java_sha(platform_key: str) -> str | None:
|
||||
if vscode_java_version == INITIAL_VSCODE_JAVA_VERSION:
|
||||
return INITIAL_VSCODE_JAVA_SHA256_BY_PLATFORM[platform_key]
|
||||
if vscode_java_version == DEFAULT_VSCODE_JAVA_VERSION:
|
||||
return DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM[platform_key]
|
||||
return None
|
||||
|
||||
def intellicode_sha() -> str | None:
|
||||
if intellicode_version == INITIAL_INTELLICODE_VERSION:
|
||||
return INITIAL_INTELLICODE_SHA256
|
||||
if intellicode_version == DEFAULT_INTELLICODE_VERSION:
|
||||
return DEFAULT_INTELLICODE_SHA256
|
||||
return None
|
||||
|
||||
runtime_dependencies: dict[str, dict[str, dict[str, object]]] = {
|
||||
"gradle": {
|
||||
@@ -178,15 +319,15 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"darwin-arm64": {
|
||||
"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,
|
||||
"relative_extraction_path": vscode_java_dirname,
|
||||
"sha256": vscode_java_sha("osx-arm64"),
|
||||
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
|
||||
},
|
||||
"osx-arm64": {
|
||||
"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,
|
||||
"relative_extraction_path": vscode_java_dirname,
|
||||
"sha256": vscode_java_sha("osx-arm64"),
|
||||
"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",
|
||||
@@ -197,8 +338,8 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"osx-x64": {
|
||||
"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,
|
||||
"relative_extraction_path": vscode_java_dirname,
|
||||
"sha256": vscode_java_sha("osx-x64"),
|
||||
"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",
|
||||
@@ -209,8 +350,8 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"linux-arm64": {
|
||||
"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,
|
||||
"relative_extraction_path": vscode_java_dirname,
|
||||
"sha256": vscode_java_sha("linux-arm64"),
|
||||
"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",
|
||||
@@ -221,8 +362,8 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"linux-x64": {
|
||||
"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,
|
||||
"relative_extraction_path": vscode_java_dirname,
|
||||
"sha256": vscode_java_sha("linux-x64"),
|
||||
"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",
|
||||
@@ -233,8 +374,8 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"win-x64": {
|
||||
"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,
|
||||
"relative_extraction_path": vscode_java_dirname,
|
||||
"sha256": vscode_java_sha("win-x64"),
|
||||
"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",
|
||||
@@ -248,8 +389,8 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"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,
|
||||
"relative_extraction_path": intellicode_dirname,
|
||||
"sha256": intellicode_sha(),
|
||||
"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",
|
||||
@@ -346,11 +487,255 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
intellisense_members_path=intellisense_members_path,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _setup_from_existing_install(
|
||||
jdtls_path: str, lombok_path: str, custom_settings: SolidLSPSettings.CustomLSSettings
|
||||
) -> RuntimeDependencyPaths:
|
||||
"""
|
||||
Builds RuntimeDependencyPaths from an already-installed upstream JDTLS distribution
|
||||
and the system-installed JDK. No downloads are performed.
|
||||
|
||||
:param jdtls_path: absolute path to the JDTLS root (containing ``plugins/`` and ``config_<platform>/``)
|
||||
:param lombok_path: absolute path to the Lombok jar (mandatory; agent is always attached)
|
||||
:param custom_settings: language-server-specific settings from ls_specific_settings.java
|
||||
:return: populated RuntimeDependencyPaths with gradle/intellicode fields set to None
|
||||
"""
|
||||
# validate jdtls_path structure (root + plugins dir)
|
||||
jdtls_root = Path(jdtls_path)
|
||||
if not jdtls_root.is_dir():
|
||||
raise SolidLSPException(
|
||||
f"Provided jdtls_path '{jdtls_path}' is not an existing directory.\n"
|
||||
f"Fix: extract jdt-language-server-*.tar.gz from "
|
||||
f"https://download.eclipse.org/jdtls/snapshots/ or run 'brew install jdtls', "
|
||||
f"then set ls_specific_settings.java.jdtls_path to the extracted directory."
|
||||
)
|
||||
plugins_dir = jdtls_root / "plugins"
|
||||
if not plugins_dir.is_dir():
|
||||
raise SolidLSPException(
|
||||
f"Invalid jdtls_path '{jdtls_path}': 'plugins/' directory not found.\n"
|
||||
f"Expected upstream JDTLS layout (plugins/, config_<platform>/, features/) at the root. "
|
||||
f"If you pointed at a vscode-java extension, use '<extension>/server' instead."
|
||||
)
|
||||
|
||||
# resolve the main equinox launcher jar (excluding platform-specific native fragments)
|
||||
launcher_jar = EclipseJDTLS.DependencyProvider._resolve_launcher_jar(plugins_dir)
|
||||
|
||||
# resolve the platform-specific config directory under jdtls_root
|
||||
config_dir = EclipseJDTLS.DependencyProvider._resolve_config_dir(jdtls_root)
|
||||
|
||||
# validate lombok jar exists
|
||||
if not Path(lombok_path).is_file():
|
||||
raise SolidLSPException(
|
||||
f"Provided lombok_path '{lombok_path}' does not exist or is not a file.\n"
|
||||
f"Fix: download lombok jar from https://projectlombok.org/downloads/ or use the one "
|
||||
f"from your local Maven cache (~/.m2/repository/org/projectlombok/lombok/<version>/lombok-<version>.jar)."
|
||||
)
|
||||
|
||||
# resolve system JDK (priority: java_home setting -> JAVA_HOME env -> which java),
|
||||
# interrogate the JVM for its real java.home and validate version >= 21
|
||||
jre_home_path, jre_path = EclipseJDTLS.DependencyProvider._resolve_system_jdk(custom_settings)
|
||||
|
||||
log.info(
|
||||
f"Using upstream JDTLS at '{jdtls_path}' with system JDK '{jre_home_path}'. "
|
||||
f"Launcher: {launcher_jar.name}; config: {config_dir.name}; lombok: {lombok_path}"
|
||||
)
|
||||
|
||||
return RuntimeDependencyPaths(
|
||||
jre_path=jre_path,
|
||||
jre_home_path=jre_home_path,
|
||||
jdtls_launcher_jar_path=str(launcher_jar),
|
||||
jdtls_readonly_config_path=str(config_dir),
|
||||
lombok_jar_path=lombok_path,
|
||||
gradle_path=None,
|
||||
intellicode_jar_path=None,
|
||||
intellisense_members_path=None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_launcher_jar(plugins_dir: Path) -> Path:
|
||||
"""
|
||||
Locates the main Equinox launcher jar in JDTLS's ``plugins/`` directory, excluding
|
||||
platform-specific native fragments like ``org.eclipse.equinox.launcher.cocoa.macosx.*``.
|
||||
|
||||
:return: path to the main launcher jar (e.g. ``org.eclipse.equinox.launcher_1.7.0.v....jar``)
|
||||
"""
|
||||
# main launcher matches: org.eclipse.equinox.launcher_<digit>...jar (single underscore,
|
||||
# immediately followed by version digits — fragments have additional dotted segments).
|
||||
pattern = re.compile(r"^org\.eclipse\.equinox\.launcher_\d.*\.jar$")
|
||||
matches = sorted(p for p in plugins_dir.glob("org.eclipse.equinox.launcher_*.jar") if pattern.match(p.name))
|
||||
if not matches:
|
||||
raise SolidLSPException(
|
||||
f"No main Equinox launcher jar found in '{plugins_dir}'. "
|
||||
f"Expected file like 'org.eclipse.equinox.launcher_<version>.jar'. "
|
||||
f"Verify the JDTLS extraction is complete and not corrupted."
|
||||
)
|
||||
# if multiple versions are present (rare), pick the highest by name
|
||||
return matches[-1]
|
||||
|
||||
@staticmethod
|
||||
def _resolve_config_dir(jdtls_root: Path) -> Path:
|
||||
"""
|
||||
Locates the platform-specific OSGi configuration directory inside the JDTLS root.
|
||||
|
||||
:return: path to ``config_<platform>/`` directory matching the current OS/arch
|
||||
"""
|
||||
platform_id = PlatformUtils.get_platform_id().value
|
||||
config_dir_name = JDTLS_CONFIG_DIR_BY_PLATFORM.get(platform_id)
|
||||
if config_dir_name is None:
|
||||
raise SolidLSPException(
|
||||
f"Unsupported platform '{platform_id}' for upstream JDTLS mode. "
|
||||
f"Supported platforms: {sorted(set(JDTLS_CONFIG_DIR_BY_PLATFORM.values()))}."
|
||||
)
|
||||
config_dir = jdtls_root / config_dir_name
|
||||
if not config_dir.is_dir():
|
||||
raise SolidLSPException(
|
||||
f"Config directory '{config_dir}' not found. "
|
||||
f"This JDTLS distribution does not support platform '{platform_id}'. "
|
||||
f"Verify you downloaded the correct tar.gz for your OS/architecture."
|
||||
)
|
||||
return config_dir
|
||||
|
||||
@staticmethod
|
||||
def _resolve_system_jdk(custom_settings: SolidLSPSettings.CustomLSSettings) -> tuple[str, str]:
|
||||
"""
|
||||
Resolves the system-installed JDK home and ``java`` executable, validates the version.
|
||||
|
||||
The ``java`` executable is located by priority: ``java_home`` setting ->
|
||||
``JAVA_HOME`` env var -> ``java`` in PATH. The actual JDK home directory and
|
||||
major version are then discovered by querying the JVM itself via
|
||||
``java -XshowSettings:properties -version`` — this is the single source of truth
|
||||
and works correctly even when the locator is a system stub (e.g. ``/usr/bin/java``
|
||||
on macOS, which delegates to ``/usr/libexec/java_home`` under the hood and does not
|
||||
resolve to the real JDK home via simple path traversal).
|
||||
|
||||
:return: (jdk_home_directory, java_executable_path)
|
||||
"""
|
||||
# locate a java executable to interrogate
|
||||
java_exe_name = "java.exe" if platform.system() == "Windows" else "java"
|
||||
java_exe: str | None = None
|
||||
source: str
|
||||
|
||||
if explicit_home := custom_settings.get("java_home"):
|
||||
candidate = str(Path(explicit_home) / "bin" / java_exe_name)
|
||||
if not os.path.exists(candidate):
|
||||
raise SolidLSPException(
|
||||
f"java_home='{explicit_home}' is invalid: '{candidate}' does not exist. "
|
||||
f"Set ls_specific_settings.java.java_home to a JDK home that contains bin/{java_exe_name}."
|
||||
)
|
||||
java_exe = candidate
|
||||
source = f"java_home setting ({explicit_home})"
|
||||
elif env_home := os.environ.get("JAVA_HOME"):
|
||||
candidate = str(Path(env_home) / "bin" / java_exe_name)
|
||||
if os.path.exists(candidate):
|
||||
java_exe = candidate
|
||||
source = f"JAVA_HOME env ({env_home})"
|
||||
else:
|
||||
log.warning(f"JAVA_HOME='{env_home}' invalid (no '{candidate}'), falling back to PATH.")
|
||||
|
||||
if java_exe is None:
|
||||
java_in_path = shutil.which("java")
|
||||
if java_in_path is None:
|
||||
raise SolidLSPException(
|
||||
"Could not locate a Java installation for JDTLS. "
|
||||
"Set ls_specific_settings.java.java_home, set JAVA_HOME environment variable, "
|
||||
f"or ensure 'java' is on PATH. Required: JDK {JDTLS_MIN_JDK_VERSION}+."
|
||||
)
|
||||
java_exe = java_in_path
|
||||
source = f"PATH ({java_in_path})"
|
||||
|
||||
# interrogate the JVM for its real java.home and version (single source of truth)
|
||||
real_jdk_home, major_version = EclipseJDTLS.DependencyProvider._inspect_java(java_exe)
|
||||
|
||||
# validate version
|
||||
if major_version < JDTLS_MIN_JDK_VERSION:
|
||||
raise SolidLSPException(
|
||||
f"JDTLS requires JDK {JDTLS_MIN_JDK_VERSION}+ but '{java_exe}' is JDK {major_version} "
|
||||
f"(located via {source}, java.home={real_jdk_home}). "
|
||||
f"Install a newer JDK and update ls_specific_settings.java.java_home or JAVA_HOME."
|
||||
)
|
||||
log.info(f"Resolved JDK {major_version} via {source}; java.home={real_jdk_home}; java_exe={java_exe}.")
|
||||
|
||||
# prefer to use bin/java from the *real* JDK home (so JDTLS subprocesses that read JAVA_HOME
|
||||
# find a consistent layout); only fall back to the original locator if the real-home variant
|
||||
# is missing for some reason.
|
||||
real_java = str(Path(real_jdk_home) / "bin" / java_exe_name)
|
||||
if os.path.exists(real_java):
|
||||
java_exe = real_java
|
||||
|
||||
return real_jdk_home, java_exe
|
||||
|
||||
@staticmethod
|
||||
def _inspect_java(java_exe: str) -> tuple[str, int]:
|
||||
"""
|
||||
Runs ``java -XshowSettings:properties -version`` and parses ``java.home`` and the
|
||||
major version from the output. This is the most reliable cross-platform way to
|
||||
discover the JDK home (works around macOS ``/usr/bin/java`` stub issue).
|
||||
|
||||
:return: (java_home_directory_reported_by_jvm, major_version)
|
||||
"""
|
||||
try:
|
||||
# both -XshowSettings:properties and -version write to stderr by convention
|
||||
result = subprocess.run(
|
||||
[java_exe, "-XshowSettings:properties", "-version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise SolidLSPException(f"Failed to run '{java_exe} -XshowSettings:properties -version': {exc}") from exc
|
||||
|
||||
output = (result.stderr or "") + "\n" + (result.stdout or "")
|
||||
|
||||
# parse java.home from " java.home = /path/to/jdk"
|
||||
home_match = re.search(r"java\.home\s*=\s*(.+)", output)
|
||||
if not home_match:
|
||||
raise SolidLSPException(
|
||||
f"Could not parse java.home from '{java_exe}' output. "
|
||||
f"This usually means '{java_exe}' is not a valid JDK installation. "
|
||||
f"First lines of output: {output.strip().splitlines()[:5]}"
|
||||
)
|
||||
real_home = home_match.group(1).strip()
|
||||
|
||||
# parse major version from version line: 'openjdk version "21.0.2"' or 'java version "21.0.2"'
|
||||
version_match = re.search(r'version "(\d+)(?:\.\d+)*"', output)
|
||||
if not version_match:
|
||||
raise SolidLSPException(
|
||||
f"Could not parse Java version from '{java_exe}' output. "
|
||||
f"Required: JDK {JDTLS_MIN_JDK_VERSION}+. "
|
||||
f"First lines of output: {output.strip().splitlines()[:5]}"
|
||||
)
|
||||
major = int(version_match.group(1))
|
||||
return real_home, major
|
||||
|
||||
@staticmethod
|
||||
def _compute_workspace_hash(
|
||||
repository_root_path: str,
|
||||
jdtls_launcher_jar_path: str,
|
||||
custom_settings: SolidLSPSettings.CustomLSSettings,
|
||||
) -> str:
|
||||
"""
|
||||
Compute the JDTLS workspace directory name.
|
||||
|
||||
Default mode hashes the project path only — preserves backwards compatibility with
|
||||
workspaces created before upstream-JDTLS support. Upstream mode (when ``jdtls_path``
|
||||
is set) mixes in the launcher path so switching between default and upstream
|
||||
installations (or between different upstream JDTLS versions) lands in a separate
|
||||
ws_dir and avoids stale OSGi configs blocking startup.
|
||||
"""
|
||||
if custom_settings.get("jdtls_path"):
|
||||
ws_hash_input = (repository_root_path + "|" + jdtls_launcher_jar_path).encode()
|
||||
else:
|
||||
ws_hash_input = repository_root_path.encode()
|
||||
return hashlib.md5(ws_hash_input).hexdigest()
|
||||
|
||||
def create_launch_command(self) -> list[str]:
|
||||
# ws_dir is the workspace directory for the EclipseJDTLS server.
|
||||
# Use a deterministic hash of the project path so the workspace
|
||||
# (and its cached index) can be reused across restarts.
|
||||
project_hash = hashlib.md5(self._repository_root_path.encode()).hexdigest()
|
||||
project_hash = EclipseJDTLS.DependencyProvider._compute_workspace_hash(
|
||||
self._repository_root_path,
|
||||
self.runtime_dependency_paths.jdtls_launcher_jar_path,
|
||||
self._custom_settings,
|
||||
)
|
||||
ws_dir = str(
|
||||
PurePath(
|
||||
self._solidlsp_settings.ls_resources_dir,
|
||||
@@ -820,8 +1205,15 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
}
|
||||
|
||||
initialize_params["initializationOptions"]["workspaceFolders"] = [repo_uri] # type: ignore
|
||||
bundles = [self.runtime_dependency_paths.intellicode_jar_path]
|
||||
initialize_params["initializationOptions"]["bundles"] = bundles # type: ignore
|
||||
|
||||
# IntelliCode bundle: only attached in default vscode-java VSIX mode.
|
||||
# In upstream-jdtls mode (jdtls_path set) we don't ship IntelliCode — agentic Serena workflows
|
||||
# don't use completion ranking, so the bundle would be inert dead weight.
|
||||
if self.runtime_dependency_paths.intellicode_jar_path is not None:
|
||||
initialize_params["initializationOptions"]["bundles"] = [self.runtime_dependency_paths.intellicode_jar_path] # type: ignore
|
||||
else:
|
||||
initialize_params["initializationOptions"]["bundles"] = [] # type: ignore
|
||||
|
||||
initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [ # type: ignore
|
||||
{"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True}
|
||||
]
|
||||
@@ -832,7 +1224,10 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
assert os.path.exists(runtime["path"]), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist"
|
||||
|
||||
gradle_settings = initialize_params["initializationOptions"]["settings"]["java"]["import"]["gradle"] # type: ignore
|
||||
gradle_settings["home"] = self.runtime_dependency_paths.gradle_path
|
||||
# In upstream-jdtls mode we don't ship a Gradle distribution — Buildship will use the project's
|
||||
# ./gradlew wrapper or a system-installed Gradle via its standard discovery rules.
|
||||
if self.runtime_dependency_paths.gradle_path is not None:
|
||||
gradle_settings["home"] = self.runtime_dependency_paths.gradle_path
|
||||
gradle_settings["java"] = {"home": gradle_java_home if gradle_java_home is not None else self.runtime_dependency_paths.jre_path}
|
||||
return cast(InitializeParams, initialize_params)
|
||||
|
||||
@@ -899,17 +1294,22 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
|
||||
self.server.notify.workspace_did_change_configuration({"settings": initialize_params["initializationOptions"]["settings"]}) # type: ignore
|
||||
|
||||
self._intellicode_enable_command_available.wait()
|
||||
# IntelliCode enablement is only relevant in the default vscode-java VSIX mode where the
|
||||
# IntelliCode bundle is shipped. In upstream-jdtls mode it's absent and the
|
||||
# 'java.intellicode.enable' command will never be registered, so we skip the wait/call.
|
||||
if self.runtime_dependency_paths.intellicode_jar_path is not None:
|
||||
self._intellicode_enable_command_available.wait()
|
||||
|
||||
java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path
|
||||
assert os.path.exists(java_intellisense_members_path)
|
||||
intellicode_enable_result = self.server.send.execute_command(
|
||||
{
|
||||
"command": "java.intellicode.enable",
|
||||
"arguments": [True, java_intellisense_members_path],
|
||||
}
|
||||
)
|
||||
assert intellicode_enable_result
|
||||
java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path
|
||||
assert java_intellisense_members_path is not None
|
||||
assert os.path.exists(java_intellisense_members_path)
|
||||
intellicode_enable_result = self.server.send.execute_command(
|
||||
{
|
||||
"command": "java.intellicode.enable",
|
||||
"arguments": [True, java_intellisense_members_path],
|
||||
}
|
||||
)
|
||||
assert intellicode_enable_result
|
||||
|
||||
if not self._service_ready_event.is_set():
|
||||
log.info("Waiting for service to be ready ...")
|
||||
|
||||
@@ -11,6 +11,7 @@ import threading
|
||||
from overrides import override
|
||||
from sensai.util.logging import LogTime
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language, LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
@@ -21,6 +22,14 @@ from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_in
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_ELM_LANGUAGE_SERVER_VERSION = "2.8.0"
|
||||
DEFAULT_ELM_LANGUAGE_SERVER_VERSION = "2.8.0"
|
||||
INITIAL_ELM_COMPILER_VERSION = "0.19.1-6"
|
||||
DEFAULT_ELM_COMPILER_VERSION = "0.19.1-6"
|
||||
|
||||
|
||||
class ElmLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -32,9 +41,10 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
Creates an ElmLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
elm_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
|
||||
managed_bin_dir = os.path.join(self.ls_resources_dir(solidlsp_settings), "elm-lsp", "node_modules", ".bin")
|
||||
|
||||
# Resolve ELM_HOME to absolute path if it's set to a relative path
|
||||
env = {}
|
||||
env = {"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", "")}
|
||||
elm_home = os.environ.get("ELM_HOME")
|
||||
if elm_home:
|
||||
if not os.path.isabs(elm_home):
|
||||
@@ -65,9 +75,15 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Setup runtime dependencies for Elm Language Server and return the command to start the server.
|
||||
"""
|
||||
# Check if elm-language-server is already installed globally
|
||||
elm_config = solidlsp_settings.get_ls_specific_settings(Language.ELM)
|
||||
elm_language_server_version = elm_config.get("elm_language_server_version", DEFAULT_ELM_LANGUAGE_SERVER_VERSION)
|
||||
elm_compiler_version = elm_config.get("elm_compiler_version", DEFAULT_ELM_COMPILER_VERSION)
|
||||
npm_registry = elm_config.get("npm_registry")
|
||||
|
||||
# Check if elm-language-server and elm are already installed globally
|
||||
system_elm_ls = shutil.which("elm-language-server")
|
||||
if system_elm_ls:
|
||||
system_elm = shutil.which("elm")
|
||||
if system_elm_ls and system_elm:
|
||||
log.info(f"Found system-installed elm-language-server at {system_elm_ls}")
|
||||
return [system_elm_ls, "--stdio"]
|
||||
|
||||
@@ -76,10 +92,6 @@ 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(
|
||||
@@ -88,16 +100,30 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
command=build_npm_install_command("@elm-tooling/elm-language-server", elm_language_server_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="elm",
|
||||
description="Elm compiler package",
|
||||
command=build_npm_install_command("elm", elm_compiler_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Install elm-language-server if not already installed
|
||||
elm_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "elm-lsp")
|
||||
# legacy unversioned dir reserved for INITIAL pair; any other version combination goes into a versioned subdir
|
||||
is_initial = (
|
||||
elm_language_server_version == INITIAL_ELM_LANGUAGE_SERVER_VERSION and elm_compiler_version == INITIAL_ELM_COMPILER_VERSION
|
||||
)
|
||||
ls_dirname = "elm-lsp" if is_initial else f"elm-lsp-{elm_language_server_version}-{elm_compiler_version}"
|
||||
elm_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), ls_dirname)
|
||||
elm_ls_executable_path = os.path.join(elm_ls_dir, "node_modules", ".bin", "elm-language-server")
|
||||
if not os.path.exists(elm_ls_executable_path):
|
||||
log.info(f"Elm Language Server executable not found at {elm_ls_executable_path}. Installing...")
|
||||
with LogTime("Installation of Elm language server dependencies", logger=log):
|
||||
deps.install(elm_ls_dir)
|
||||
elif not system_elm:
|
||||
log.info("Elm compiler not found on PATH. Installing a managed Elm compiler...")
|
||||
with LogTime("Installation of Elm compiler dependency", logger=log):
|
||||
deps.install(elm_ls_dir)
|
||||
|
||||
if not os.path.exists(elm_ls_executable_path):
|
||||
raise FileNotFoundError(
|
||||
@@ -128,6 +154,7 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
@@ -203,3 +230,26 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
@override
|
||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
||||
return 1.0
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=-1,
|
||||
timeout=10.0,
|
||||
allow_cached=True,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
@@ -11,7 +11,7 @@ import shutil
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls import DocumentSymbols, LSPConstants, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
@@ -220,6 +220,7 @@ class FortranLanguageServer(SolidLanguageServer):
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"rangeFormatting": {"dynamicRegistration": True},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
@@ -284,3 +285,34 @@ class FortranLanguageServer(SolidLanguageServer):
|
||||
|
||||
# Fortran Language Server is ready after initialization
|
||||
log.info("Fortran Language Server initialization complete")
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(uri)
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
self.server.notify.did_save_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(True),
|
||||
allow_cached=True,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
@@ -22,7 +22,12 @@ from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
FSAUTOCOMPLETE_VERSION = "0.83.0"
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_FSAUTOCOMPLETE_VERSION = "0.83.0"
|
||||
DEFAULT_FSAUTOCOMPLETE_VERSION = "0.83.0"
|
||||
FSAUTOCOMPLETE_VERSION = DEFAULT_FSAUTOCOMPLETE_VERSION
|
||||
|
||||
|
||||
class FSharpLanguageServer(SolidLanguageServer):
|
||||
@@ -69,7 +74,7 @@ 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)
|
||||
fsautocomplete_version = fsharp_settings.get("fsautocomplete_version", DEFAULT_FSAUTOCOMPLETE_VERSION)
|
||||
dotnet_exe = DotNETUtil("8.0", allow_higher_version=True).get_dotnet_path_or_raise()
|
||||
|
||||
RuntimeDependencyCollection(
|
||||
@@ -84,7 +89,9 @@ class FSharpLanguageServer(SolidLanguageServer):
|
||||
)
|
||||
|
||||
# Install FsAutoComplete if not already installed
|
||||
fsharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "fsharp-lsp")
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
ls_dirname = "fsharp-lsp" if fsautocomplete_version == INITIAL_FSAUTOCOMPLETE_VERSION else f"fsharp-lsp-{fsautocomplete_version}"
|
||||
fsharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), ls_dirname)
|
||||
fsautocomplete_path = os.path.join(fsharp_ls_dir, "fsautocomplete")
|
||||
|
||||
# Handle Windows executable extension
|
||||
|
||||
@@ -23,6 +23,10 @@ class Gopls(SolidLanguageServer):
|
||||
Provides Go specific instantiation of the LanguageServer class using gopls.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
return True
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
# For Go projects, we should ignore:
|
||||
|
||||
@@ -18,13 +18,34 @@ 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 = {
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_VSCODE_JAVA_VERSION = "1.42.0-561"
|
||||
INITIAL_VSCODE_JAVA_SHA256_BY_PLATFORM = {
|
||||
"win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3",
|
||||
"linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9",
|
||||
"linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1",
|
||||
"osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f",
|
||||
"osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933",
|
||||
}
|
||||
DEFAULT_VSCODE_JAVA_VERSION = "1.42.0-561"
|
||||
DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM = {
|
||||
"win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3",
|
||||
"linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9",
|
||||
"linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1",
|
||||
"osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f",
|
||||
"osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933",
|
||||
}
|
||||
|
||||
|
||||
def _vscode_java_sha(version: str, platform_key: str) -> str | None:
|
||||
if version == INITIAL_VSCODE_JAVA_VERSION:
|
||||
return INITIAL_VSCODE_JAVA_SHA256_BY_PLATFORM[platform_key]
|
||||
if version == DEFAULT_VSCODE_JAVA_VERSION:
|
||||
return DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM[platform_key]
|
||||
return None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -93,7 +114,7 @@ class GroovyLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
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_version = groovy_settings.get("vscode_java_version", DEFAULT_VSCODE_JAVA_VERSION)
|
||||
vscode_java_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}"
|
||||
|
||||
# Verify platform support
|
||||
@@ -128,35 +149,35 @@ class GroovyLanguageServer(SolidLanguageServer):
|
||||
"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,
|
||||
"sha256": _vscode_java_sha(vscode_java_version, "win-x64"),
|
||||
},
|
||||
"linux-x64": {
|
||||
"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,
|
||||
"sha256": _vscode_java_sha(vscode_java_version, "linux-x64"),
|
||||
},
|
||||
"linux-arm64": {
|
||||
"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,
|
||||
"sha256": _vscode_java_sha(vscode_java_version, "linux-arm64"),
|
||||
},
|
||||
"osx-x64": {
|
||||
"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,
|
||||
"sha256": _vscode_java_sha(vscode_java_version, "osx-x64"),
|
||||
},
|
||||
"osx-arm64": {
|
||||
"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,
|
||||
"sha256": _vscode_java_sha(vscode_java_version, "osx-arm64"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -174,7 +195,9 @@ class GroovyLanguageServer(SolidLanguageServer):
|
||||
static_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "groovy_language_server")
|
||||
os.makedirs(static_dir, exist_ok=True)
|
||||
|
||||
java_dir = os.path.join(static_dir, "java")
|
||||
# legacy unversioned "java" dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
java_dirname = "java" if vscode_java_version == INITIAL_VSCODE_JAVA_VERSION else f"java-{vscode_java_version}"
|
||||
java_dir = os.path.join(static_dir, java_dirname)
|
||||
os.makedirs(java_dir, exist_ok=True)
|
||||
|
||||
java_home_path = os.path.join(java_dir, java_home_relative_path)
|
||||
|
||||
@@ -27,6 +27,22 @@ from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_VSHAXE_VERSION = "2.34.2"
|
||||
INITIAL_VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2"
|
||||
DEFAULT_VSHAXE_VERSION = "2.34.2"
|
||||
DEFAULT_VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2"
|
||||
|
||||
|
||||
def _vshaxe_sha(version: str) -> str | None:
|
||||
if version == INITIAL_VSHAXE_VERSION:
|
||||
return INITIAL_VSHAXE_SHA256
|
||||
if version == DEFAULT_VSHAXE_VERSION:
|
||||
return DEFAULT_VSHAXE_SHA256
|
||||
return None
|
||||
|
||||
|
||||
class HaxeLanguageServer(SolidLanguageServer):
|
||||
"""Haxe language server integration using vshaxe/haxe-language-server.
|
||||
@@ -58,8 +74,6 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
# Downloaded from Open VSX (not the VS Code Marketplace) because Open VSX
|
||||
# provides stable versioned URLs and SHA256 checksums for integrity verification.
|
||||
_DEFAULT_VSHAXE_VERSION = "2.34.2"
|
||||
_DEFAULT_VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2"
|
||||
|
||||
@override
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
@@ -77,7 +91,9 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
return vscode_server_path
|
||||
|
||||
# 3. Check resource dir / download from Open VSX
|
||||
haxe_ls_dir = os.path.join(self._ls_resources_dir, "haxe-lsp")
|
||||
version = self._custom_settings.get("version", DEFAULT_VSHAXE_VERSION)
|
||||
ls_dirname = "haxe-lsp" if version == INITIAL_VSHAXE_VERSION else f"haxe-lsp-{version}"
|
||||
haxe_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
server_js_path = os.path.join(haxe_ls_dir, "bin", "server.js")
|
||||
if os.path.exists(server_js_path):
|
||||
log.info(f"Found Haxe Language Server at {server_js_path}")
|
||||
@@ -92,7 +108,6 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
" 3. Set ls_path in serena_config.yml under ls_specific_settings.haxe"
|
||||
)
|
||||
|
||||
version = self._custom_settings.get("version", self._DEFAULT_VSHAXE_VERSION)
|
||||
downloaded_path = self._download_from_open_vsx(haxe_ls_dir, version)
|
||||
if downloaded_path:
|
||||
return downloaded_path
|
||||
@@ -129,16 +144,17 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
vsix_path = os.path.join(tempfile.gettempdir(), "vshaxe.vsix")
|
||||
urllib.request.urlretrieve(download_url, vsix_path)
|
||||
|
||||
# Verify SHA256 checksum only for the default (pinned) version
|
||||
if version == cls._DEFAULT_VSHAXE_VERSION:
|
||||
# Verify SHA256 checksum only when the resolved version is one of our pinned ones (INITIAL or current DEFAULT)
|
||||
expected_sha = _vshaxe_sha(version)
|
||||
if expected_sha is not None:
|
||||
sha256 = hashlib.sha256()
|
||||
with open(vsix_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
sha256.update(chunk)
|
||||
if sha256.hexdigest().lower() != cls._DEFAULT_VSHAXE_SHA256:
|
||||
if sha256.hexdigest().lower() != expected_sha:
|
||||
os.remove(vsix_path)
|
||||
raise RuntimeError(
|
||||
f"SHA256 checksum mismatch for vshaxe VSIX. Expected {cls._DEFAULT_VSHAXE_SHA256}, "
|
||||
f"SHA256 checksum mismatch for vshaxe VSIX. Expected {expected_sha}, "
|
||||
f"got {sha256.hexdigest()}. The file may be corrupted or tampered with."
|
||||
)
|
||||
log.info("SHA256 checksum verified")
|
||||
@@ -215,7 +231,14 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True},
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": True,
|
||||
"versionSupport": False,
|
||||
"tagSupport": {"valueSet": [1, 2]},
|
||||
"codeDescriptionSupport": True,
|
||||
"dataSupport": True,
|
||||
},
|
||||
"synchronization": {"dynamicRegistration": True, "didSave": True},
|
||||
"completion": {"completionItem": {"snippetSupport": True}},
|
||||
"definition": {},
|
||||
"references": {},
|
||||
|
||||
@@ -25,15 +25,32 @@ from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# GitHub release version to download when not installed locally
|
||||
_DEFAULT_VERSION = "1.3.1"
|
||||
_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 = {
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# _INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# _DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
_INITIAL_VERSION = "1.3.1"
|
||||
_INITIAL_HLSL_SHA256_BY_ASSET = {
|
||||
"shader-language-server-x86_64-pc-windows-msvc.zip": "49081c5547ddde1b8b3b17295282a80ddacbca1d6f5dcd834e2788c02bafa997",
|
||||
"shader-language-server-x86_64-unknown-linux-gnu.zip": "61710df7ca17a2d063b598936c57c56c49fbf837707a1aa886f9b0193a35be3c",
|
||||
"shader-language-server-aarch64-pc-windows-msvc.zip": "a3b3799affe2cad27652e788376b46fe76e1a6c2ce45946a486dcb26c9091412",
|
||||
}
|
||||
_DEFAULT_VERSION = "1.3.1"
|
||||
_DEFAULT_HLSL_SHA256_BY_ASSET = {
|
||||
"shader-language-server-x86_64-pc-windows-msvc.zip": "49081c5547ddde1b8b3b17295282a80ddacbca1d6f5dcd834e2788c02bafa997",
|
||||
"shader-language-server-x86_64-unknown-linux-gnu.zip": "61710df7ca17a2d063b598936c57c56c49fbf837707a1aa886f9b0193a35be3c",
|
||||
"shader-language-server-aarch64-pc-windows-msvc.zip": "a3b3799affe2cad27652e788376b46fe76e1a6c2ce45946a486dcb26c9091412",
|
||||
}
|
||||
|
||||
|
||||
def _hlsl_sha(version: str, asset_name: str) -> str | None:
|
||||
if version == _INITIAL_VERSION:
|
||||
return _INITIAL_HLSL_SHA256_BY_ASSET.get(asset_name)
|
||||
if version == _DEFAULT_VERSION:
|
||||
return _DEFAULT_HLSL_SHA256_BY_ASSET.get(asset_name)
|
||||
return None
|
||||
|
||||
|
||||
class HlslLanguageServer(SolidLanguageServer):
|
||||
@@ -78,9 +95,7 @@ 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,
|
||||
sha256=_hlsl_sha(version, "shader-language-server-x86_64-pc-windows-msvc.zip"),
|
||||
allowed_hosts=_HLSL_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -90,9 +105,7 @@ 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,
|
||||
sha256=_hlsl_sha(version, "shader-language-server-x86_64-unknown-linux-gnu.zip"),
|
||||
allowed_hosts=_HLSL_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -102,9 +115,7 @@ 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,
|
||||
sha256=_hlsl_sha(version, "shader-language-server-aarch64-pc-windows-msvc.zip"),
|
||||
allowed_hosts=_HLSL_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -139,7 +150,9 @@ class HlslLanguageServer(SolidLanguageServer):
|
||||
"See https://github.com/antaalt/shader-sense for more details."
|
||||
)
|
||||
|
||||
install_dir = os.path.join(self._ls_resources_dir, "shader-language-server")
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
ls_dirname = "shader-language-server" if version == _INITIAL_VERSION else f"shader-language-server-{version}"
|
||||
install_dir = os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
executable_path = deps.binary_path(install_dir)
|
||||
|
||||
if not os.path.exists(executable_path):
|
||||
|
||||
@@ -21,6 +21,12 @@ from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_in
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_INTELEPHENSE_VERSION = "1.14.4"
|
||||
DEFAULT_INTELEPHENSE_VERSION = "1.14.4"
|
||||
|
||||
|
||||
class Intelephense(SolidLanguageServer):
|
||||
"""
|
||||
@@ -59,11 +65,12 @@ 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")
|
||||
intelephense_version = self._custom_settings.get("intelephense_version", DEFAULT_INTELEPHENSE_VERSION)
|
||||
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")
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
ls_dirname = "php-lsp" if intelephense_version == INITIAL_INTELEPHENSE_VERSION else f"php-lsp-{intelephense_version}"
|
||||
intelephense_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
os.makedirs(intelephense_ls_dir, exist_ok=True)
|
||||
intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense")
|
||||
if not os.path.exists(intelephense_executable_path):
|
||||
|
||||
@@ -17,6 +17,12 @@ from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_JSON_LANGUAGE_SERVER_VERSION = "1.3.4"
|
||||
DEFAULT_JSON_LANGUAGE_SERVER_VERSION = "1.3.4"
|
||||
|
||||
|
||||
class JsonLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -54,7 +60,7 @@ class JsonLanguageServer(SolidLanguageServer):
|
||||
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."
|
||||
|
||||
json_language_server_version = self._custom_settings.get("json_language_server_version", "1.3.4")
|
||||
json_language_server_version = self._custom_settings.get("json_language_server_version", DEFAULT_JSON_LANGUAGE_SERVER_VERSION)
|
||||
npm_registry = self._custom_settings.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
@@ -68,7 +74,13 @@ class JsonLanguageServer(SolidLanguageServer):
|
||||
]
|
||||
)
|
||||
|
||||
json_ls_dir = os.path.join(self._ls_resources_dir, "json-lsp")
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
ls_dirname = (
|
||||
"json-lsp"
|
||||
if json_language_server_version == INITIAL_JSON_LANGUAGE_SERVER_VERSION
|
||||
else f"json-lsp-{json_language_server_version}"
|
||||
)
|
||||
json_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
json_executable_path = os.path.join(json_ls_dir, "node_modules", ".bin", "vscode-json-languageserver")
|
||||
|
||||
if os.name == "nt":
|
||||
|
||||
@@ -10,7 +10,7 @@ from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticTag, InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
@@ -115,6 +115,21 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
"""Define language-specific directories to ignore for Julia projects."""
|
||||
return super().is_ignored_dirname(dirname) or dirname in [".julia", "build", "dist"]
|
||||
|
||||
@override
|
||||
def _supports_pull_diagnostics(self) -> bool:
|
||||
# LanguageServer.jl raises an unhandled error on textDocument/diagnostic which crashes
|
||||
# the server process. Force the published-diagnostics path instead.
|
||||
return False
|
||||
|
||||
@override
|
||||
def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float:
|
||||
# LanguageServer.jl needs significant warm-up after init before it emits the first
|
||||
# publishDiagnostics: workspace/configuration round-trip + initial linter pass typically
|
||||
# land 13-15s after the file is opened in a cold LS. Module-scoped fixtures usually have
|
||||
# the cache pre-populated and return immediately, so this ceiling only kicks in for cold
|
||||
# runs (standalone test invocations, CI cold start).
|
||||
return 30.0
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Julia Language Server.
|
||||
@@ -125,8 +140,19 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"capabilities": {
|
||||
"workspace": {"workspaceFolders": True},
|
||||
# workspace.configuration MUST be true: LanguageServer.jl pulls all julia.lint.*
|
||||
# settings via workspace/configuration after initialize. Without it, the server
|
||||
# skips that codepath and runlinter stays disabled, so no diagnostics ever arrive.
|
||||
"workspace": {"workspaceFolders": True, "configuration": True},
|
||||
"textDocument": {
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": True,
|
||||
"versionSupport": False,
|
||||
"tagSupport": {"valueSet": [DiagnosticTag.Unnecessary, DiagnosticTag.Deprecated]},
|
||||
"codeDescriptionSupport": True,
|
||||
"dataSupport": True,
|
||||
},
|
||||
"synchronization": {"dynamicRegistration": True, "didSave": True},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentSymbol": {"dynamicRegistration": True},
|
||||
@@ -150,6 +176,19 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
def window_log_message(msg: dict) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
|
||||
def workspace_configuration(params: dict) -> list[Any]:
|
||||
"""
|
||||
Respond to LanguageServer.jl's workspace/configuration pull request.
|
||||
|
||||
The server requests a flat list of julia.* settings (in particular julia.lint.run,
|
||||
position 11 in the response). Returning ``None`` for each entry causes the server to
|
||||
apply its built-in defaults — including ``runlinter = true`` — which is what we want
|
||||
for diagnostics to be produced.
|
||||
"""
|
||||
items = params.get("items", []) if isinstance(params, dict) else []
|
||||
return [None for _ in items]
|
||||
|
||||
self.server.on_request("workspace/configuration", workspace_configuration)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
@@ -166,4 +205,10 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
assert "documentSymbolProvider" in init_response["capabilities"]
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# nudge the server to pull config: LanguageServer.jl only invokes request_julia_config
|
||||
# from its workspace/didChangeConfiguration handler, so without this notification the
|
||||
# server never asks us for julia.lint.run and runlinter stays disabled (no diagnostics).
|
||||
self.server.notify.workspace_did_change_configuration({"settings": {}})
|
||||
|
||||
log.info("Julia Language Server is initialized and ready.")
|
||||
@@ -41,16 +41,36 @@ log = logging.getLogger(__name__)
|
||||
# -Xmx2G: 2GB heap is sufficient for most projects; override via ls_specific_settings for large codebases
|
||||
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 = {
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_KOTLIN_LSP_VERSION = "261.13587.0"
|
||||
INITIAL_KOTLIN_LSP_SHA256_BY_SUFFIX = {
|
||||
"win-x64": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad",
|
||||
"linux-x64": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec",
|
||||
"linux-aarch64": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651",
|
||||
"mac-x64": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3",
|
||||
"mac-aarch64": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c",
|
||||
}
|
||||
DEFAULT_KOTLIN_LSP_VERSION = "261.13587.0"
|
||||
DEFAULT_KOTLIN_LSP_SHA256_BY_SUFFIX = {
|
||||
"win-x64": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad",
|
||||
"linux-x64": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec",
|
||||
"linux-aarch64": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651",
|
||||
"mac-x64": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3",
|
||||
"mac-aarch64": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c",
|
||||
}
|
||||
|
||||
|
||||
def _kotlin_lsp_sha(version: str, kotlin_suffix: str) -> str | None:
|
||||
if version == INITIAL_KOTLIN_LSP_VERSION:
|
||||
return INITIAL_KOTLIN_LSP_SHA256_BY_SUFFIX.get(kotlin_suffix)
|
||||
if version == DEFAULT_KOTLIN_LSP_VERSION:
|
||||
return DEFAULT_KOTLIN_LSP_SHA256_BY_SUFFIX.get(kotlin_suffix)
|
||||
return None
|
||||
|
||||
|
||||
# Platform-specific Kotlin LSP download suffixes
|
||||
PLATFORM_KOTLIN_SUFFIX = {
|
||||
@@ -108,8 +128,14 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
kotlin_suffix = PLATFORM_KOTLIN_SUFFIX.get(platform_id.value)
|
||||
assert kotlin_suffix, f"Unsupported platform for Kotlin LSP: {platform_id.value}"
|
||||
|
||||
# Setup paths for dependencies
|
||||
static_dir = os.path.join(self._ls_resources_dir, "kotlin_language_server")
|
||||
# Setup paths for dependencies; legacy unversioned dir reserved for INITIAL only
|
||||
kotlin_lsp_version = self._custom_settings.get("kotlin_lsp_version", DEFAULT_KOTLIN_LSP_VERSION)
|
||||
ls_dirname = (
|
||||
"kotlin_language_server"
|
||||
if kotlin_lsp_version == INITIAL_KOTLIN_LSP_VERSION
|
||||
else f"kotlin_language_server-{kotlin_lsp_version}"
|
||||
)
|
||||
static_dir = os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
os.makedirs(static_dir, exist_ok=True)
|
||||
|
||||
# Setup Kotlin Language Server
|
||||
@@ -117,11 +143,8 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
kotlin_script = os.path.join(static_dir, kotlin_script_name)
|
||||
|
||||
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]
|
||||
expected_sha256 = _kotlin_lsp_sha(kotlin_lsp_version, kotlin_suffix)
|
||||
log.info("Downloading Kotlin Language Server...")
|
||||
FileUtils.download_and_extract_archive_verified(
|
||||
kotlin_url,
|
||||
|
||||
@@ -124,6 +124,7 @@ class Lean4LanguageServer(SolidLanguageServer):
|
||||
"documentationFormat": ["markdown", "plaintext"],
|
||||
},
|
||||
},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
|
||||
@@ -26,15 +26,42 @@ 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 = {
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_LUA_LS_VERSION = "3.15.0"
|
||||
INITIAL_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",
|
||||
}
|
||||
DEFAULT_LUA_LS_VERSION = "3.15.0"
|
||||
DEFAULT_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",
|
||||
}
|
||||
|
||||
|
||||
def _lua_ls_sha(version: str, asset_name: str) -> str | None:
|
||||
if version == INITIAL_LUA_LS_VERSION:
|
||||
return INITIAL_LUA_LS_SHA256_BY_ASSET.get(asset_name)
|
||||
if version == DEFAULT_LUA_LS_VERSION:
|
||||
return DEFAULT_LUA_LS_SHA256_BY_ASSET.get(asset_name)
|
||||
return None
|
||||
|
||||
|
||||
def _lua_ls_install_dir(ls_resources_dir: str, version: str) -> Path:
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
if version == INITIAL_LUA_LS_VERSION:
|
||||
return Path(ls_resources_dir) / "lua"
|
||||
return Path(ls_resources_dir) / f"lua-{version}"
|
||||
|
||||
|
||||
class LuaLanguageServer(SolidLanguageServer):
|
||||
@@ -67,7 +94,9 @@ class LuaLanguageServer(SolidLanguageServer):
|
||||
]
|
||||
|
||||
if solidlsp_settings is not None:
|
||||
ls_resource_dir = Path(LuaLanguageServer.ls_resources_dir(solidlsp_settings)) / "lua"
|
||||
lua_settings = solidlsp_settings.get_ls_specific_settings(Language.LUA)
|
||||
lua_ls_version = lua_settings.get("lua_language_server_version", DEFAULT_LUA_LS_VERSION)
|
||||
ls_resource_dir = _lua_ls_install_dir(LuaLanguageServer.ls_resources_dir(solidlsp_settings), lua_ls_version)
|
||||
possible_paths.extend(
|
||||
[
|
||||
ls_resource_dir / "bin" / "lua-language-server",
|
||||
@@ -93,7 +122,7 @@ class LuaLanguageServer(SolidLanguageServer):
|
||||
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)
|
||||
lua_ls_version = lua_settings.get("lua_language_server_version", DEFAULT_LUA_LS_VERSION)
|
||||
system = platform.system()
|
||||
machine = platform.machine().lower()
|
||||
|
||||
@@ -122,7 +151,7 @@ class LuaLanguageServer(SolidLanguageServer):
|
||||
|
||||
download_url = f"https://github.com/LuaLS/lua-language-server/releases/download/{lua_ls_version}/{download_name}"
|
||||
|
||||
install_dir = Path(LuaLanguageServer.ls_resources_dir(solidlsp_settings)) / "lua"
|
||||
install_dir = _lua_ls_install_dir(LuaLanguageServer.ls_resources_dir(solidlsp_settings), lua_ls_version)
|
||||
install_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
log.info("Downloading lua-language-server from %s", download_url)
|
||||
@@ -131,7 +160,7 @@ class LuaLanguageServer(SolidLanguageServer):
|
||||
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,
|
||||
expected_sha256=_lua_ls_sha(lua_ls_version, download_name),
|
||||
allowed_hosts=LUA_LS_ALLOWED_HOSTS,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,15 +37,34 @@ from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
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 = {
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_LUAU_LSP_VERSION = "1.63.0"
|
||||
INITIAL_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",
|
||||
}
|
||||
DEFAULT_LUAU_LSP_VERSION = "1.63.0"
|
||||
DEFAULT_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",
|
||||
}
|
||||
|
||||
|
||||
def _luau_lsp_sha(version: str, asset_name: str) -> str | None:
|
||||
if version == INITIAL_LUAU_LSP_VERSION:
|
||||
return INITIAL_LUAU_LSP_SHA256_BY_ASSET.get(asset_name)
|
||||
if version == DEFAULT_LUAU_LSP_VERSION:
|
||||
return DEFAULT_LUAU_LSP_SHA256_BY_ASSET.get(asset_name)
|
||||
return None
|
||||
|
||||
|
||||
# Luau built-in docs CDN
|
||||
LUAU_DOCS_URL = "https://luau-lsp.pages.dev/api-docs/luau-en-us.json"
|
||||
@@ -98,14 +117,19 @@ class LuauLanguageServer(SolidLanguageServer):
|
||||
return cmd
|
||||
|
||||
def _download_luau_lsp(self) -> str:
|
||||
install_dir = Path(self._ls_resources_dir)
|
||||
luau_lsp_version = self._custom_settings.get("luau_lsp_version", DEFAULT_LUAU_LSP_VERSION)
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
install_dir = (
|
||||
Path(self._ls_resources_dir)
|
||||
if luau_lsp_version == INITIAL_LUAU_LSP_VERSION
|
||||
else Path(self._ls_resources_dir) / f"luau-lsp-{luau_lsp_version}"
|
||||
)
|
||||
install_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
binary_path = self._find_existing_binary(install_dir)
|
||||
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}"
|
||||
|
||||
@@ -114,7 +138,7 @@ class LuauLanguageServer(SolidLanguageServer):
|
||||
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,
|
||||
expected_sha256=_luau_lsp_sha(luau_lsp_version, asset_name),
|
||||
allowed_hosts=LUAU_LSP_ALLOWED_HOSTS,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,6 +28,34 @@ log = logging.getLogger(__name__)
|
||||
|
||||
MARKSMAN_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_MARKSMAN_VERSION = "2024-12-18"
|
||||
INITIAL_MARKSMAN_SHA256_BY_PLATFORM = {
|
||||
"linux-x64": "b9cb666c643dfd9b699811fdfc445ed4c56be65c1d878c21d46847f0d7b0e475",
|
||||
"linux-arm64": "b8d6972a56f3f9b7bbbf7c77ef8998e3b66fa82fb03c01398e224144486c9e73",
|
||||
"osx-x64": "7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83",
|
||||
"osx-arm64": "7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83",
|
||||
"win-x64": "39de9df039c8b0d627ac5918a9d8792ad20fc49e2461d1f5c906975c016799ec",
|
||||
}
|
||||
DEFAULT_MARKSMAN_VERSION = "2024-12-18"
|
||||
DEFAULT_MARKSMAN_SHA256_BY_PLATFORM = {
|
||||
"linux-x64": "b9cb666c643dfd9b699811fdfc445ed4c56be65c1d878c21d46847f0d7b0e475",
|
||||
"linux-arm64": "b8d6972a56f3f9b7bbbf7c77ef8998e3b66fa82fb03c01398e224144486c9e73",
|
||||
"osx-x64": "7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83",
|
||||
"osx-arm64": "7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83",
|
||||
"win-x64": "39de9df039c8b0d627ac5918a9d8792ad20fc49e2461d1f5c906975c016799ec",
|
||||
}
|
||||
|
||||
|
||||
def _marksman_sha(version: str, platform_key: str) -> str | None:
|
||||
if version == INITIAL_MARKSMAN_VERSION:
|
||||
return INITIAL_MARKSMAN_SHA256_BY_PLATFORM[platform_key]
|
||||
if version == DEFAULT_MARKSMAN_VERSION:
|
||||
return DEFAULT_MARKSMAN_SHA256_BY_PLATFORM[platform_key]
|
||||
return None
|
||||
|
||||
|
||||
class Marksman(SolidLanguageServer):
|
||||
"""
|
||||
@@ -39,12 +67,9 @@ class Marksman(SolidLanguageServer):
|
||||
"""
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
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(
|
||||
@@ -53,7 +78,7 @@ class Marksman(SolidLanguageServer):
|
||||
platform_id="linux-x64",
|
||||
archive_type="binary",
|
||||
binary_name="marksman",
|
||||
sha256="b9cb666c643dfd9b699811fdfc445ed4c56be65c1d878c21d46847f0d7b0e475" if default_version else None,
|
||||
sha256=_marksman_sha(version, "linux-x64"),
|
||||
allowed_hosts=MARKSMAN_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -62,7 +87,7 @@ class Marksman(SolidLanguageServer):
|
||||
platform_id="linux-arm64",
|
||||
archive_type="binary",
|
||||
binary_name="marksman",
|
||||
sha256="b8d6972a56f3f9b7bbbf7c77ef8998e3b66fa82fb03c01398e224144486c9e73" if default_version else None,
|
||||
sha256=_marksman_sha(version, "linux-arm64"),
|
||||
allowed_hosts=MARKSMAN_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -71,7 +96,7 @@ class Marksman(SolidLanguageServer):
|
||||
platform_id="osx-x64",
|
||||
archive_type="binary",
|
||||
binary_name="marksman",
|
||||
sha256="7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83" if default_version else None,
|
||||
sha256=_marksman_sha(version, "osx-x64"),
|
||||
allowed_hosts=MARKSMAN_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -80,7 +105,7 @@ class Marksman(SolidLanguageServer):
|
||||
platform_id="osx-arm64",
|
||||
archive_type="binary",
|
||||
binary_name="marksman",
|
||||
sha256="7e18803966231a33ee107d0d26f69b41f2f0dc1332c52dd9729c2e29fb77be83" if default_version else None,
|
||||
sha256=_marksman_sha(version, "osx-arm64"),
|
||||
allowed_hosts=MARKSMAN_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -89,7 +114,7 @@ class Marksman(SolidLanguageServer):
|
||||
platform_id="win-x64",
|
||||
archive_type="binary",
|
||||
binary_name="marksman.exe",
|
||||
sha256="39de9df039c8b0d627ac5918a9d8792ad20fc49e2461d1f5c906975c016799ec" if default_version else None,
|
||||
sha256=_marksman_sha(version, "win-x64"),
|
||||
allowed_hosts=MARKSMAN_ALLOWED_HOSTS,
|
||||
),
|
||||
]
|
||||
@@ -97,11 +122,16 @@ class Marksman(SolidLanguageServer):
|
||||
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
"""Setup runtime dependencies for marksman and return the command to start the server."""
|
||||
marksman_version = self._custom_settings.get("marksman_version", self.DEFAULT_MARKSMAN_VERSION)
|
||||
marksman_version = self._custom_settings.get("marksman_version", DEFAULT_MARKSMAN_VERSION)
|
||||
deps = self._runtime_dependencies(marksman_version)
|
||||
dependency = deps.get_single_dep_for_current_platform()
|
||||
|
||||
marksman_ls_dir = self._ls_resources_dir
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
marksman_ls_dir = (
|
||||
self._ls_resources_dir
|
||||
if marksman_version == INITIAL_MARKSMAN_VERSION
|
||||
else os.path.join(self._ls_resources_dir, f"marksman-{marksman_version}")
|
||||
)
|
||||
marksman_executable_path = deps.binary_path(marksman_ls_dir)
|
||||
if not os.path.exists(marksman_executable_path):
|
||||
log.info(
|
||||
|
||||
@@ -50,11 +50,29 @@ log = logging.getLogger(__name__)
|
||||
# Environment variable for MATLAB installation path
|
||||
MATLAB_PATH_ENV_VAR = "MATLAB_PATH"
|
||||
|
||||
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",)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_MATLAB_EXTENSION_VERSION = "1.3.9"
|
||||
INITIAL_MATLAB_EXTENSION_SHA256 = "1da3add2c3a593fa0ebcdf1d15231faee8014de10f549c36915ab9d4f18390f2"
|
||||
DEFAULT_MATLAB_EXTENSION_VERSION = "1.3.9"
|
||||
DEFAULT_MATLAB_EXTENSION_SHA256 = "1da3add2c3a593fa0ebcdf1d15231faee8014de10f549c36915ab9d4f18390f2"
|
||||
|
||||
|
||||
def _matlab_extension_sha(version: str) -> str | None:
|
||||
if version == INITIAL_MATLAB_EXTENSION_VERSION:
|
||||
return INITIAL_MATLAB_EXTENSION_SHA256
|
||||
if version == DEFAULT_MATLAB_EXTENSION_VERSION:
|
||||
return DEFAULT_MATLAB_EXTENSION_SHA256
|
||||
return None
|
||||
|
||||
|
||||
def _matlab_extension_dirname(version: str) -> str:
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
return "matlab-extension" if version == INITIAL_MATLAB_EXTENSION_VERSION else f"matlab-extension-{version}"
|
||||
|
||||
|
||||
class MatlabLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -102,20 +120,12 @@ class MatlabLanguageServer(SolidLanguageServer):
|
||||
self._matlab_path: str | None = None
|
||||
|
||||
@classmethod
|
||||
def _download_matlab_extension(cls, url: str, target_dir: str) -> bool:
|
||||
def _download_matlab_extension(cls, url: str, target_dir: str, expected_sha256: str | None) -> bool:
|
||||
"""
|
||||
Download and extract the MATLAB extension from VS Code marketplace.
|
||||
|
||||
The VS Code marketplace packages extensions as .vsix files (which are ZIP archives).
|
||||
This method downloads the VSIX file and extracts it to get the language server.
|
||||
|
||||
Args:
|
||||
url: VS Code marketplace URL for the MATLAB extension
|
||||
target_dir: Directory where the extension will be extracted
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
|
||||
"""
|
||||
try:
|
||||
log.info(f"Downloading MATLAB extension from {url}")
|
||||
@@ -124,7 +134,7 @@ class MatlabLanguageServer(SolidLanguageServer):
|
||||
url,
|
||||
target_dir,
|
||||
"zip",
|
||||
expected_sha256=MATLAB_EXTENSION_SHA256 if url == MATLAB_EXTENSION_URL else None,
|
||||
expected_sha256=expected_sha256,
|
||||
allowed_hosts=MATLAB_EXTENSION_ALLOWED_HOSTS,
|
||||
)
|
||||
log.info("MATLAB extension extracted successfully")
|
||||
@@ -155,8 +165,9 @@ class MatlabLanguageServer(SolidLanguageServer):
|
||||
elif env_path:
|
||||
log.warning(f"MATLAB_EXTENSION_PATH set but directory not found: {env_path}")
|
||||
|
||||
# Check default download location
|
||||
default_path = os.path.join(self._ls_resources_dir, "matlab-extension", "extension")
|
||||
# Check the resolved-version download location (versioned for non-INITIAL, legacy "matlab-extension" for INITIAL)
|
||||
matlab_extension_version = self._custom_settings.get("matlab_extension_version", DEFAULT_MATLAB_EXTENSION_VERSION)
|
||||
default_path = os.path.join(self._ls_resources_dir, _matlab_extension_dirname(matlab_extension_version), "extension")
|
||||
if os.path.exists(default_path):
|
||||
log.debug(f"Found MATLAB extension in default location: {default_path}")
|
||||
return default_path
|
||||
@@ -182,8 +193,8 @@ class MatlabLanguageServer(SolidLanguageServer):
|
||||
Path to installed extension or None if download failed
|
||||
|
||||
"""
|
||||
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_version = self._custom_settings.get("matlab_extension_version", DEFAULT_MATLAB_EXTENSION_VERSION)
|
||||
matlab_extension_dir = os.path.join(self._ls_resources_dir, _matlab_extension_dirname(matlab_extension_version))
|
||||
matlab_extension_url = (
|
||||
"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/MathWorks/"
|
||||
f"vsextensions/language-matlab/{matlab_extension_version}/vspackage"
|
||||
@@ -191,7 +202,7 @@ class MatlabLanguageServer(SolidLanguageServer):
|
||||
|
||||
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, _matlab_extension_sha(matlab_extension_version)):
|
||||
extension_path = os.path.join(matlab_extension_dir, "extension")
|
||||
if os.path.exists(extension_path):
|
||||
log.info("MATLAB extension downloaded and installed successfully")
|
||||
|
||||
@@ -242,7 +242,7 @@ class OcamlLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
ocaml_ok = self._ocaml_version >= (5, 2, 0)
|
||||
lsp_ok = self._lsp_version >= self.MIN_LSP_VERSION_FOR_CROSS_FILE_REFS
|
||||
return ocaml_ok and lsp_ok
|
||||
return ocaml_ok and lsp_ok and self._index_built
|
||||
|
||||
@staticmethod
|
||||
def _build_ocaml_index_static(repository_root_path: str) -> bool:
|
||||
@@ -340,6 +340,14 @@ class OcamlLanguageServer(SolidLanguageServer):
|
||||
"configuration": True,
|
||||
},
|
||||
"textDocument": {
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": True,
|
||||
"versionSupport": False,
|
||||
"tagSupport": {"valueSet": [1, 2]},
|
||||
"codeDescriptionSupport": True,
|
||||
"dataSupport": True,
|
||||
},
|
||||
"diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False},
|
||||
"synchronization": {
|
||||
"dynamicRegistration": True,
|
||||
"willSave": True,
|
||||
|
||||
@@ -22,6 +22,11 @@ from solidlsp.settings import SolidLSPSettings
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
OMNISHARP_ALLOWED_HOSTS = ("roslynomnisharp.blob.core.windows.net", "download.visualstudio.microsoft.com")
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_OMNISHARP_VERSION = "1.39.10"
|
||||
INITIAL_RAZOR_OMNISHARP_VERSION = "7.0.0-preview.23363.1"
|
||||
DEFAULT_OMNISHARP_VERSION = "1.39.10"
|
||||
DEFAULT_RAZOR_OMNISHARP_VERSION = "7.0.0-preview.23363.1"
|
||||
|
||||
@@ -168,11 +173,11 @@ class OmniSharp(SolidLanguageServer):
|
||||
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:
|
||||
if omnisharp_version not in (INITIAL_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:
|
||||
if razor_omnisharp_version not in (INITIAL_RAZOR_OMNISHARP_VERSION, DEFAULT_RAZOR_OMNISHARP_VERSION):
|
||||
dependency["integrity"] = None
|
||||
|
||||
assert platform_id in [
|
||||
@@ -207,7 +212,9 @@ class OmniSharp(SolidLanguageServer):
|
||||
assert "OmniSharp" in runtime_dependencies
|
||||
assert "RazorOmnisharp" in runtime_dependencies
|
||||
|
||||
omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "OmniSharp")
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
omnisharp_dirname = "OmniSharp" if omnisharp_version == INITIAL_OMNISHARP_VERSION else f"OmniSharp-{omnisharp_version}"
|
||||
omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), omnisharp_dirname)
|
||||
if not os.path.exists(omnisharp_ls_dir):
|
||||
os.makedirs(omnisharp_ls_dir)
|
||||
FileUtils.download_and_extract_archive_verified(
|
||||
@@ -221,7 +228,10 @@ class OmniSharp(SolidLanguageServer):
|
||||
assert os.path.exists(omnisharp_executable_path)
|
||||
os.chmod(omnisharp_executable_path, 0o755)
|
||||
|
||||
razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RazorOmnisharp")
|
||||
razor_dirname = (
|
||||
"RazorOmnisharp" if razor_omnisharp_version == INITIAL_RAZOR_OMNISHARP_VERSION else f"RazorOmnisharp-{razor_omnisharp_version}"
|
||||
)
|
||||
razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), razor_dirname)
|
||||
if not os.path.exists(razor_omnisharp_ls_dir):
|
||||
os.makedirs(razor_omnisharp_ls_dir)
|
||||
FileUtils.download_and_extract_archive_verified(
|
||||
|
||||
@@ -69,7 +69,34 @@ from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
PASLS_VERSION = "v0.2.0"
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_PASLS_VERSION = "v0.2.0"
|
||||
INITIAL_PASLS_SHA256_BY_PLATFORM = {
|
||||
"linux-x64": "517259395b0a385a5e848cf48b967645a984be3dd456118bc08771283a822a5b",
|
||||
"linux-arm64": "cb4986941cfdcf9cb74ece6bbb53a443390a908e880108a37f4ccf82b2d6c502",
|
||||
"osx-x64": "0abfcd98f63f77dba74094339a40d4407b69317c1c77b13a26b9b7dbdfd885f1",
|
||||
"osx-arm64": "d4c2411e406af96ceae12b11e77fdb0c684ca15a68bfd8b4f9c6fe1fbdf515a7",
|
||||
"win-x64": "1493c31552e6f90a59800b2d44669e01fc4551d3647f3cea5dd105a9f6bc73e5",
|
||||
}
|
||||
DEFAULT_PASLS_VERSION = "v0.2.0"
|
||||
DEFAULT_PASLS_SHA256_BY_PLATFORM = {
|
||||
"linux-x64": "517259395b0a385a5e848cf48b967645a984be3dd456118bc08771283a822a5b",
|
||||
"linux-arm64": "cb4986941cfdcf9cb74ece6bbb53a443390a908e880108a37f4ccf82b2d6c502",
|
||||
"osx-x64": "0abfcd98f63f77dba74094339a40d4407b69317c1c77b13a26b9b7dbdfd885f1",
|
||||
"osx-arm64": "d4c2411e406af96ceae12b11e77fdb0c684ca15a68bfd8b4f9c6fe1fbdf515a7",
|
||||
"win-x64": "1493c31552e6f90a59800b2d44669e01fc4551d3647f3cea5dd105a9f6bc73e5",
|
||||
}
|
||||
PASLS_VERSION = DEFAULT_PASLS_VERSION
|
||||
|
||||
|
||||
def _pasls_sha(version: str, platform_key: str) -> str | None:
|
||||
if version == INITIAL_PASLS_VERSION:
|
||||
return INITIAL_PASLS_SHA256_BY_PLATFORM.get(platform_key)
|
||||
if version == DEFAULT_PASLS_VERSION:
|
||||
return DEFAULT_PASLS_SHA256_BY_PLATFORM.get(platform_key)
|
||||
return None
|
||||
|
||||
|
||||
class PascalLanguageServer(SolidLanguageServer):
|
||||
@@ -629,7 +656,12 @@ class PascalLanguageServer(SolidLanguageServer):
|
||||
log.info(f"Found pasls in PATH: {pasls_in_path}")
|
||||
return quote_windows_path(pasls_in_path)
|
||||
|
||||
pasls_dir = cls.ls_resources_dir(solidlsp_settings)
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
pasls_dir = (
|
||||
cls.ls_resources_dir(solidlsp_settings)
|
||||
if pasls_version == INITIAL_PASLS_VERSION
|
||||
else os.path.join(cls.ls_resources_dir(solidlsp_settings), f"pasls-{pasls_version}")
|
||||
)
|
||||
os.makedirs(pasls_dir, exist_ok=True)
|
||||
|
||||
# Clean up old files from previous sessions
|
||||
@@ -647,7 +679,7 @@ class PascalLanguageServer(SolidLanguageServer):
|
||||
platform_id="linux-x64",
|
||||
archive_type="gztar",
|
||||
binary_name="pasls",
|
||||
sha256="517259395b0a385a5e848cf48b967645a984be3dd456118bc08771283a822a5b",
|
||||
sha256=_pasls_sha(pasls_version, "linux-x64"),
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="PascalLanguageServer",
|
||||
@@ -656,7 +688,7 @@ class PascalLanguageServer(SolidLanguageServer):
|
||||
platform_id="linux-arm64",
|
||||
archive_type="gztar",
|
||||
binary_name="pasls",
|
||||
sha256="cb4986941cfdcf9cb74ece6bbb53a443390a908e880108a37f4ccf82b2d6c502",
|
||||
sha256=_pasls_sha(pasls_version, "linux-arm64"),
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="PascalLanguageServer",
|
||||
@@ -665,7 +697,7 @@ class PascalLanguageServer(SolidLanguageServer):
|
||||
platform_id="osx-x64",
|
||||
archive_type="zip",
|
||||
binary_name="pasls",
|
||||
sha256="0abfcd98f63f77dba74094339a40d4407b69317c1c77b13a26b9b7dbdfd885f1",
|
||||
sha256=_pasls_sha(pasls_version, "osx-x64"),
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="PascalLanguageServer",
|
||||
@@ -674,7 +706,7 @@ class PascalLanguageServer(SolidLanguageServer):
|
||||
platform_id="osx-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="pasls",
|
||||
sha256="d4c2411e406af96ceae12b11e77fdb0c684ca15a68bfd8b4f9c6fe1fbdf515a7",
|
||||
sha256=_pasls_sha(pasls_version, "osx-arm64"),
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="PascalLanguageServer",
|
||||
@@ -683,7 +715,7 @@ class PascalLanguageServer(SolidLanguageServer):
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="pasls.exe",
|
||||
sha256="1493c31552e6f90a59800b2d44669e01fc4551d3647f3cea5dd105a9f6bc73e5",
|
||||
sha256=_pasls_sha(pasls_version, "win-x64"),
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -777,6 +809,14 @@ class PascalLanguageServer(SolidLanguageServer):
|
||||
if value:
|
||||
initialization_options[var] = value
|
||||
|
||||
initialization_options.update(
|
||||
{
|
||||
"checkSyntax": True,
|
||||
"publishDiagnostics": True,
|
||||
"showSyntaxErrors": True,
|
||||
}
|
||||
)
|
||||
|
||||
initialize_params = {
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
@@ -831,6 +871,7 @@ class PascalLanguageServer(SolidLanguageServer):
|
||||
},
|
||||
"formatting": {"dynamicRegistration": True},
|
||||
"rangeFormatting": {"dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
|
||||
@@ -20,11 +20,24 @@ from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
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")
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_PHPACTOR_VERSION = "2025.12.21.1"
|
||||
INITIAL_PHPACTOR_PHAR_SHA256 = "53bbe9625cd9b5e9b394bc2f595fbad13dbbe6dfc96950c56dea3b5d9a246cc3"
|
||||
DEFAULT_PHPACTOR_VERSION = "2025.12.21.1"
|
||||
DEFAULT_PHPACTOR_PHAR_SHA256 = "53bbe9625cd9b5e9b394bc2f595fbad13dbbe6dfc96950c56dea3b5d9a246cc3"
|
||||
|
||||
|
||||
def _phpactor_sha(version: str) -> str | None:
|
||||
if version == INITIAL_PHPACTOR_VERSION:
|
||||
return INITIAL_PHPACTOR_PHAR_SHA256
|
||||
if version == DEFAULT_PHPACTOR_VERSION:
|
||||
return DEFAULT_PHPACTOR_PHAR_SHA256
|
||||
return None
|
||||
|
||||
|
||||
class PhpactorServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -48,7 +61,7 @@ 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_version = self._custom_settings.get("phpactor_version", DEFAULT_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")
|
||||
@@ -68,15 +81,20 @@ class PhpactorServer(SolidLanguageServer):
|
||||
else:
|
||||
log.warning("Could not parse PHP version from output. Continuing anyway.")
|
||||
|
||||
phpactor_phar_path = os.path.join(self._ls_resources_dir, "phpactor.phar")
|
||||
# legacy unversioned phar at root reserved for INITIAL; every other version goes into a versioned subdir
|
||||
if phpactor_version == INITIAL_PHPACTOR_VERSION:
|
||||
phar_dir = self._ls_resources_dir
|
||||
else:
|
||||
phar_dir = os.path.join(self._ls_resources_dir, f"phpactor-{phpactor_version}")
|
||||
phpactor_phar_path = os.path.join(phar_dir, "phpactor.phar")
|
||||
if not os.path.exists(phpactor_phar_path):
|
||||
os.makedirs(self._ls_resources_dir, exist_ok=True)
|
||||
os.makedirs(phar_dir, exist_ok=True)
|
||||
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,
|
||||
expected_sha256=_phpactor_sha(phpactor_version),
|
||||
allowed_hosts=PHPACTOR_ALLOWED_HOSTS,
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ 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).
|
||||
- psscriptanalyzer_version: Override the pinned PSScriptAnalyzer version
|
||||
saved into the bundled PowerShell Editor Services module path.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -12,6 +14,7 @@ import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
from collections.abc import Hashable
|
||||
@@ -19,7 +22,8 @@ from pathlib import Path
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import LSPConstants, RawDocumentSymbol, SolidLanguageServer
|
||||
from solidlsp.ls_config import Language, LanguageServerConfig
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
from solidlsp.ls_utils import FileUtils
|
||||
@@ -30,9 +34,32 @@ from solidlsp.settings import SolidLSPSettings
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# PowerShell Editor Services version to download
|
||||
PSES_VERSION = "4.4.0"
|
||||
PSES_SHA256 = "690b91092989a0f66e6f43986166aaef69d64b559a9fda51feed882e1103fbcc"
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_PSES_VERSION = "4.4.0"
|
||||
INITIAL_PSES_SHA256 = "690b91092989a0f66e6f43986166aaef69d64b559a9fda51feed882e1103fbcc"
|
||||
DEFAULT_PSES_VERSION = "4.4.0"
|
||||
DEFAULT_PSES_SHA256 = "690b91092989a0f66e6f43986166aaef69d64b559a9fda51feed882e1103fbcc"
|
||||
|
||||
|
||||
def _pses_sha(version: str) -> str | None:
|
||||
if version == INITIAL_PSES_VERSION:
|
||||
return INITIAL_PSES_SHA256
|
||||
if version == DEFAULT_PSES_VERSION:
|
||||
return DEFAULT_PSES_SHA256
|
||||
return None
|
||||
|
||||
|
||||
def _pses_install_dir(ls_resources_dir: str, version: str) -> Path:
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
if version == INITIAL_PSES_VERSION:
|
||||
return Path(ls_resources_dir) / "powershell"
|
||||
return Path(ls_resources_dir) / f"powershell-{version}"
|
||||
|
||||
|
||||
PSES_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
|
||||
PSSCRIPTANALYZER_VERSION = "1.25.0"
|
||||
|
||||
|
||||
class PowerShellLanguageServer(SolidLanguageServer):
|
||||
@@ -94,7 +121,9 @@ class PowerShellLanguageServer(SolidLanguageServer):
|
||||
@classmethod
|
||||
def _get_pses_path(cls, solidlsp_settings: SolidLSPSettings) -> str | None:
|
||||
"""Get the path to PowerShell Editor Services installation."""
|
||||
install_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "powershell"
|
||||
ps_settings = solidlsp_settings.get_ls_specific_settings(Language.POWERSHELL)
|
||||
pses_version = ps_settings.get("pses_version", DEFAULT_PSES_VERSION)
|
||||
install_dir = _pses_install_dir(cls.ls_resources_dir(solidlsp_settings), pses_version)
|
||||
start_script = install_dir / "PowerShellEditorServices" / "Start-EditorServices.ps1"
|
||||
|
||||
if start_script.exists():
|
||||
@@ -106,13 +135,13 @@ class PowerShellLanguageServer(SolidLanguageServer):
|
||||
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)
|
||||
pses_version = ps_settings.get("pses_version", DEFAULT_PSES_VERSION)
|
||||
download_url = (
|
||||
f"https://github.com/PowerShell/PowerShellEditorServices/releases/download/v{pses_version}/PowerShellEditorServices.zip"
|
||||
)
|
||||
|
||||
# Create installation directory
|
||||
install_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "powershell"
|
||||
# Create installation directory; legacy unversioned dir reserved for INITIAL only
|
||||
install_dir = _pses_install_dir(cls.ls_resources_dir(solidlsp_settings), pses_version)
|
||||
install_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
log.info(f"Downloading PowerShell Editor Services from {download_url}...")
|
||||
@@ -120,7 +149,7 @@ class PowerShellLanguageServer(SolidLanguageServer):
|
||||
download_url,
|
||||
str(install_dir),
|
||||
"zip",
|
||||
expected_sha256=PSES_SHA256 if pses_version == PSES_VERSION else None,
|
||||
expected_sha256=_pses_sha(pses_version),
|
||||
allowed_hosts=PSES_ALLOWED_HOSTS,
|
||||
)
|
||||
|
||||
@@ -157,6 +186,29 @@ class PowerShellLanguageServer(SolidLanguageServer):
|
||||
|
||||
# The bundled modules path is the directory containing PowerShellEditorServices
|
||||
bundled_modules_path = str(Path(pses_path).parent)
|
||||
psscriptanalyzer_version = solidlsp_settings.get_ls_specific_settings(Language.POWERSHELL).get(
|
||||
"psscriptanalyzer_version", PSSCRIPTANALYZER_VERSION
|
||||
)
|
||||
psscriptanalyzer_path = Path(bundled_modules_path) / "PSScriptAnalyzer" / psscriptanalyzer_version
|
||||
if not psscriptanalyzer_path.exists():
|
||||
log.info(f"PSScriptAnalyzer {psscriptanalyzer_version} not found. Installing...")
|
||||
subprocess.run(
|
||||
[
|
||||
pwsh_path,
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
(
|
||||
"Save-Module "
|
||||
"-Name PSScriptAnalyzer "
|
||||
f"-RequiredVersion '{psscriptanalyzer_version}' "
|
||||
f"-Path '{bundled_modules_path}' "
|
||||
"-Force "
|
||||
"-ErrorAction Stop"
|
||||
),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
return pwsh_path, pses_path, bundled_modules_path
|
||||
|
||||
@@ -345,3 +397,35 @@ class PowerShellLanguageServer(SolidLanguageServer):
|
||||
self.server_ready.set()
|
||||
else:
|
||||
log.info("PowerShell Editor Services initialization complete")
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
published_uri = self._get_published_diagnostics_uri(uri)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(published_uri)
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
self.server.notify.did_save_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=published_uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(True),
|
||||
allow_cached=True,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
@@ -155,6 +155,52 @@ class PyrightServer(SolidLanguageServer):
|
||||
self.found_source_files = True
|
||||
self.analysis_complete.set()
|
||||
|
||||
def handle_pyright_progress_notification(progress_kind: str, params: object | None) -> None:
|
||||
"""Tracks Pyright-specific progress notifications.
|
||||
|
||||
Pyright can emit custom progress notifications instead of only using
|
||||
``$/progress``. Handling them avoids noisy unhandled-method warnings
|
||||
and provides an additional signal that initial analysis has quiesced.
|
||||
"""
|
||||
# normalizing the notification payload
|
||||
message_text = ""
|
||||
percentage: object | None = None
|
||||
if isinstance(params, dict):
|
||||
raw_message = params.get("message")
|
||||
message_text = "" if raw_message is None else str(raw_message)
|
||||
percentage = params.get("percentage")
|
||||
elif params is not None:
|
||||
message_text = str(params)
|
||||
|
||||
progress_label = f"{message_text} ({percentage}%)" if percentage is not None else message_text
|
||||
|
||||
# logging the progress transition
|
||||
if progress_kind == "begin":
|
||||
log.info("Pyright progress started: %s", progress_label)
|
||||
return
|
||||
|
||||
if progress_kind == "report":
|
||||
log.debug("Pyright progress update: %s", progress_label)
|
||||
return
|
||||
|
||||
log.info("Pyright progress finished: %s", progress_label)
|
||||
self.analysis_complete.set()
|
||||
|
||||
def pyright_begin_progress(params: object | None) -> None:
|
||||
"""Handles the ``pyright/beginProgress`` notification."""
|
||||
# delegating to the shared progress handler
|
||||
handle_pyright_progress_notification("begin", params)
|
||||
|
||||
def pyright_report_progress(params: object | None) -> None:
|
||||
"""Handles the ``pyright/reportProgress`` notification."""
|
||||
# delegating to the shared progress handler
|
||||
handle_pyright_progress_notification("report", params)
|
||||
|
||||
def pyright_end_progress(params: object | None) -> None:
|
||||
"""Handles the ``pyright/endProgress`` notification."""
|
||||
# delegating to the shared progress handler
|
||||
handle_pyright_progress_notification("end", params)
|
||||
|
||||
def check_experimental_status(params: dict) -> None:
|
||||
"""
|
||||
Also listen for experimental/serverStatus as a backup signal
|
||||
@@ -170,6 +216,9 @@ class PyrightServer(SolidLanguageServer):
|
||||
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("pyright/beginProgress", pyright_begin_progress)
|
||||
self.server.on_notification("pyright/reportProgress", pyright_report_progress)
|
||||
self.server.on_notification("pyright/endProgress", pyright_end_progress)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
self.server.on_notification("experimental/serverStatus", check_experimental_status)
|
||||
|
||||
@@ -26,6 +26,10 @@ class RustAnalyzer(SolidLanguageServer):
|
||||
Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _determine_log_level(line: str) -> int:
|
||||
"""Classify rust-analyzer stderr output to avoid false-positive errors."""
|
||||
|
||||
@@ -7,19 +7,31 @@ import glob
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import threading
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, LSPConstants, 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__)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_SOLIDITY_LANGUAGE_SERVER_VERSION = "0.8.4"
|
||||
DEFAULT_SOLIDITY_LANGUAGE_SERVER_VERSION = "0.8.4"
|
||||
INITIAL_FORGE_VERSION = "1.5.1"
|
||||
DEFAULT_FORGE_VERSION = "1.5.1"
|
||||
|
||||
|
||||
class SolidityLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -69,7 +81,10 @@ 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")
|
||||
solidity_language_server_version = self._custom_settings.get(
|
||||
"solidity_language_server_version", DEFAULT_SOLIDITY_LANGUAGE_SERVER_VERSION
|
||||
)
|
||||
forge_version = self._custom_settings.get("forge_version", DEFAULT_FORGE_VERSION)
|
||||
npm_registry = self._custom_settings.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
@@ -84,17 +99,29 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
),
|
||||
platform_id="any",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="forge",
|
||||
description="Foundry forge CLI",
|
||||
# the @foundry-rs/forge meta-package's postinstall uses POSIX env-var
|
||||
# syntax that fails on Windows cmd.exe, so we install the platform-specific
|
||||
# subpackage directly to bypass it
|
||||
command=build_npm_install_command(self._get_forge_npm_package(), forge_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
solidity_ls_dir = os.path.join(self._ls_resources_dir, "solidity-lsp")
|
||||
solidity_executable_path = os.path.join(solidity_ls_dir, "node_modules", ".bin", "nomicfoundation-solidity-language-server")
|
||||
solidity_ls_dir = self._resolve_solidity_ls_dir(solidity_language_server_version, forge_version)
|
||||
managed_bin_dir = os.path.join(solidity_ls_dir, "node_modules", ".bin")
|
||||
solidity_executable_path = os.path.join(managed_bin_dir, "nomicfoundation-solidity-language-server")
|
||||
forge_executable_path = os.path.join(managed_bin_dir, "forge")
|
||||
|
||||
if os.name == "nt":
|
||||
solidity_executable_path += ".cmd"
|
||||
forge_executable_path += ".cmd"
|
||||
|
||||
if not os.path.exists(solidity_executable_path):
|
||||
log.info(f"Solidity Language Server executable not found at {solidity_executable_path}. Installing...")
|
||||
if not os.path.exists(solidity_executable_path) or not os.path.exists(forge_executable_path):
|
||||
log.info("Solidity Language Server dependencies missing. Installing...")
|
||||
deps.install(solidity_ls_dir)
|
||||
log.info("Solidity language server dependencies installed successfully.")
|
||||
|
||||
@@ -106,6 +133,45 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
|
||||
return solidity_executable_path
|
||||
|
||||
@staticmethod
|
||||
def _get_forge_npm_package() -> str:
|
||||
"""
|
||||
Returns the platform-specific @foundry-rs forge npm subpackage name. Installing
|
||||
the meta-package fails on Windows due to a POSIX-only postinstall script.
|
||||
"""
|
||||
machine = platform.machine().lower()
|
||||
if machine in ("x86_64", "amd64"):
|
||||
arch = "amd64"
|
||||
elif machine in ("arm64", "aarch64"):
|
||||
arch = "arm64"
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported architecture for foundry forge: {machine}")
|
||||
|
||||
if os.name == "nt":
|
||||
if arch != "amd64":
|
||||
raise RuntimeError(f"foundry forge does not publish a Windows {arch} npm package")
|
||||
return "@foundry-rs/forge-win32-amd64"
|
||||
if platform.system() == "Darwin":
|
||||
return f"@foundry-rs/forge-darwin-{arch}"
|
||||
return f"@foundry-rs/forge-linux-{arch}"
|
||||
|
||||
def create_launch_command_env(self) -> dict[str, str]:
|
||||
solidity_language_server_version = self._custom_settings.get(
|
||||
"solidity_language_server_version", DEFAULT_SOLIDITY_LANGUAGE_SERVER_VERSION
|
||||
)
|
||||
forge_version = self._custom_settings.get("forge_version", DEFAULT_FORGE_VERSION)
|
||||
solidity_ls_dir = self._resolve_solidity_ls_dir(solidity_language_server_version, forge_version)
|
||||
managed_bin_dir = os.path.join(solidity_ls_dir, "node_modules", ".bin")
|
||||
return {"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", "")}
|
||||
|
||||
def _resolve_solidity_ls_dir(self, solidity_language_server_version: str, forge_version: str) -> str:
|
||||
# legacy unversioned dir reserved for INITIAL pair; any other combination goes into a versioned subdir
|
||||
is_initial = (
|
||||
solidity_language_server_version == INITIAL_SOLIDITY_LANGUAGE_SERVER_VERSION and forge_version == INITIAL_FORGE_VERSION
|
||||
)
|
||||
ls_dirname = "solidity-lsp" if is_initial else f"solidity-lsp-{solidity_language_server_version}-{forge_version}"
|
||||
return os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
return [core_path, "--stdio"]
|
||||
|
||||
@@ -148,6 +214,7 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
"dynamicRegistration": True,
|
||||
"contentFormat": ["markdown", "plaintext"], # type: ignore[list-item]
|
||||
},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
@@ -224,3 +291,83 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
log.info("No .sol files found; skipping indexing wait")
|
||||
|
||||
log.info("Solidity language server initialization complete")
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
uri = self._get_validation_document_uri(relative_file_path)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(uri)
|
||||
|
||||
absolute_path = pathlib.Path(self.repository_root_path, relative_file_path)
|
||||
contents = absolute_path.read_text(encoding=self._encoding)
|
||||
lines = contents.split("\n")
|
||||
end_position = {
|
||||
"line": len(lines) - 1,
|
||||
"character": len(lines[-1]),
|
||||
}
|
||||
|
||||
self.server.notify.did_open_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
LSPConstants.LANGUAGE_ID: self.language_id,
|
||||
LSPConstants.VERSION: 0,
|
||||
LSPConstants.TEXT: contents,
|
||||
}
|
||||
}
|
||||
)
|
||||
try:
|
||||
self.server.notify.did_change_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
LSPConstants.VERSION: 1,
|
||||
},
|
||||
LSPConstants.CONTENT_CHANGES: [ # type: ignore
|
||||
{
|
||||
LSPConstants.RANGE: {
|
||||
"start": {"line": 0, "character": 0},
|
||||
"end": end_position,
|
||||
},
|
||||
"text": contents,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(True),
|
||||
allow_cached=True,
|
||||
)
|
||||
finally:
|
||||
self.server.notify.did_close_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
|
||||
def _get_validation_document_uri(self, relative_file_path: str) -> str:
|
||||
absolute_path = pathlib.Path(self.repository_root_path, relative_file_path)
|
||||
|
||||
if os.name != "nt":
|
||||
return absolute_path.as_uri()
|
||||
|
||||
path = absolute_path.as_posix()
|
||||
if len(path) >= 2 and path[1] == ":":
|
||||
path = f"{path[0].lower()}%3A{path[2:]}"
|
||||
|
||||
return f"file:///{path.lstrip('/')}"
|
||||
@@ -24,15 +24,24 @@ from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
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
|
||||
# To update: download each release file and run: sha256sum <filename>
|
||||
TAPLO_SHA256_CHECKSUMS: dict[str, str] = {
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
# To update: download each release file and run: sha256sum <filename>, copy values into DEFAULT_*.
|
||||
INITIAL_TAPLO_VERSION = "0.10.0"
|
||||
INITIAL_TAPLO_SHA256_CHECKSUMS: dict[str, str] = {
|
||||
"taplo-windows-x86_64.zip": "1615eed140039bd58e7089109883b1c434de5d6de8f64a993e6e8c80ca57bdf9",
|
||||
"taplo-windows-x86.zip": "b825701daab10dcfc0251e6d668cd1a9c0e351e7f6762dd20844c3f3f3553aa0",
|
||||
"taplo-darwin-x86_64.gz": "898122cde3a0b1cd1cbc2d52d3624f23338218c91b5ddb71518236a4c2c10ef2",
|
||||
"taplo-darwin-aarch64.gz": "713734314c3e71894b9e77513c5349835eefbd52908445a0d73b0c7dc469347d",
|
||||
"taplo-linux-x86_64.gz": "8fe196b894ccf9072f98d4e1013a180306e17d244830b03986ee5e8eabeb6156",
|
||||
"taplo-linux-aarch64.gz": "033681d01eec8376c3fd38fa3703c79316f5e14bb013d859943b60a07bccdcc3",
|
||||
"taplo-linux-armv7.gz": "6b728896afe2573522f38b8e668b1ff40eb5928fd9d6d0c253ecae508274d417",
|
||||
}
|
||||
DEFAULT_TAPLO_VERSION = "0.10.0"
|
||||
DEFAULT_TAPLO_SHA256_CHECKSUMS: dict[str, str] = {
|
||||
"taplo-windows-x86_64.zip": "1615eed140039bd58e7089109883b1c434de5d6de8f64a993e6e8c80ca57bdf9",
|
||||
"taplo-windows-x86.zip": "b825701daab10dcfc0251e6d668cd1a9c0e351e7f6762dd20844c3f3f3553aa0",
|
||||
"taplo-darwin-x86_64.gz": "898122cde3a0b1cd1cbc2d52d3624f23338218c91b5ddb71518236a4c2c10ef2",
|
||||
@@ -43,7 +52,15 @@ TAPLO_SHA256_CHECKSUMS: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
def _get_taplo_download_url(version: str = TAPLO_VERSION) -> tuple[str, str]:
|
||||
def _taplo_sha(version: str, archive_filename: str) -> str | None:
|
||||
if version == INITIAL_TAPLO_VERSION:
|
||||
return INITIAL_TAPLO_SHA256_CHECKSUMS.get(archive_filename)
|
||||
if version == DEFAULT_TAPLO_VERSION:
|
||||
return DEFAULT_TAPLO_SHA256_CHECKSUMS.get(archive_filename)
|
||||
return None
|
||||
|
||||
|
||||
def _get_taplo_download_url(version: str = DEFAULT_TAPLO_VERSION) -> tuple[str, str]:
|
||||
"""
|
||||
Get the appropriate Taplo download URL for the current platform.
|
||||
|
||||
@@ -130,11 +147,11 @@ class TaploServer(SolidLanguageServer):
|
||||
log.info(f"Using system-installed Taplo at: {system_taplo}")
|
||||
return system_taplo
|
||||
|
||||
# Setup local installation directory
|
||||
taplo_dir = os.path.join(self._ls_resources_dir, "taplo")
|
||||
# Setup local installation directory; legacy unversioned dir reserved for INITIAL only
|
||||
taplo_version = self._custom_settings.get("taplo_version", DEFAULT_TAPLO_VERSION)
|
||||
ls_dirname = "taplo" if taplo_version == INITIAL_TAPLO_VERSION else f"taplo-{taplo_version}"
|
||||
taplo_dir = os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
os.makedirs(taplo_dir, exist_ok=True)
|
||||
|
||||
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)
|
||||
|
||||
@@ -158,12 +175,13 @@ class TaploServer(SolidLanguageServer):
|
||||
return [core_path, "lsp", "stdio"]
|
||||
|
||||
@classmethod
|
||||
def _download_taplo(cls, install_dir: str, executable_path: str, version: str = TAPLO_VERSION) -> None:
|
||||
def _download_taplo(cls, install_dir: str, executable_path: str, version: str = DEFAULT_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:
|
||||
# only verify the SHA when the resolved version is one of our pinned ones (INITIAL or current DEFAULT)
|
||||
expected_hash = _taplo_sha(version, archive_filename)
|
||||
if expected_hash is None and version in (INITIAL_TAPLO_VERSION, DEFAULT_TAPLO_VERSION):
|
||||
raise RuntimeError(f"No SHA256 checksum configured for Taplo archive: {archive_filename}")
|
||||
|
||||
try:
|
||||
|
||||
@@ -18,6 +18,34 @@ log = logging.getLogger(__name__)
|
||||
|
||||
TERRAFORM_LS_ALLOWED_HOSTS = ("releases.hashicorp.com",)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_TERRAFORM_LS_VERSION = "0.36.5"
|
||||
INITIAL_TERRAFORM_LS_SHA256_BY_PLATFORM = {
|
||||
"osx-arm64": "fee8743aa71fe2d8b0b9b91283b844cfa57d58457306a62e53a8f38d143cec8c",
|
||||
"osx-x64": "17c5c480f8eec7e528292565f1c05d5097a41edf7ef8ee2a9f3a18d288a1415a",
|
||||
"linux-arm64": "724f45029f32d02d88b1952c7d1526c59fc8cd5dae49e31b9fed676a83f6cae7",
|
||||
"linux-x64": "37e645cc54fd03e863157e2a3e773e7a5ff1d6cb3d045e4c20860cac1f550a44",
|
||||
"win-x64": "a9223462cac9e1c0e6ba33043fbf9fb4483609b6970b5681a6306b04366698ec",
|
||||
}
|
||||
DEFAULT_TERRAFORM_LS_VERSION = "0.36.5"
|
||||
DEFAULT_TERRAFORM_LS_SHA256_BY_PLATFORM = {
|
||||
"osx-arm64": "fee8743aa71fe2d8b0b9b91283b844cfa57d58457306a62e53a8f38d143cec8c",
|
||||
"osx-x64": "17c5c480f8eec7e528292565f1c05d5097a41edf7ef8ee2a9f3a18d288a1415a",
|
||||
"linux-arm64": "724f45029f32d02d88b1952c7d1526c59fc8cd5dae49e31b9fed676a83f6cae7",
|
||||
"linux-x64": "37e645cc54fd03e863157e2a3e773e7a5ff1d6cb3d045e4c20860cac1f550a44",
|
||||
"win-x64": "a9223462cac9e1c0e6ba33043fbf9fb4483609b6970b5681a6306b04366698ec",
|
||||
}
|
||||
|
||||
|
||||
def _terraform_ls_sha(version: str, platform_key: str) -> str | None:
|
||||
if version == INITIAL_TERRAFORM_LS_VERSION:
|
||||
return INITIAL_TERRAFORM_LS_SHA256_BY_PLATFORM[platform_key]
|
||||
if version == DEFAULT_TERRAFORM_LS_VERSION:
|
||||
return DEFAULT_TERRAFORM_LS_SHA256_BY_PLATFORM[platform_key]
|
||||
return None
|
||||
|
||||
|
||||
class TerraformLS(SolidLanguageServer):
|
||||
"""
|
||||
@@ -99,7 +127,7 @@ class TerraformLS(SolidLanguageServer):
|
||||
"""
|
||||
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")
|
||||
terraform_ls_version = terraform_settings.get("terraform_ls_version", DEFAULT_TERRAFORM_LS_VERSION)
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
@@ -110,7 +138,7 @@ class TerraformLS(SolidLanguageServer):
|
||||
platform_id="osx-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="terraform-ls",
|
||||
sha256="fee8743aa71fe2d8b0b9b91283b844cfa57d58457306a62e53a8f38d143cec8c" if terraform_ls_version == "0.36.5" else None,
|
||||
sha256=_terraform_ls_sha(terraform_ls_version, "osx-arm64"),
|
||||
allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -120,7 +148,7 @@ class TerraformLS(SolidLanguageServer):
|
||||
platform_id="osx-x64",
|
||||
archive_type="zip",
|
||||
binary_name="terraform-ls",
|
||||
sha256="17c5c480f8eec7e528292565f1c05d5097a41edf7ef8ee2a9f3a18d288a1415a" if terraform_ls_version == "0.36.5" else None,
|
||||
sha256=_terraform_ls_sha(terraform_ls_version, "osx-x64"),
|
||||
allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -130,7 +158,7 @@ class TerraformLS(SolidLanguageServer):
|
||||
platform_id="linux-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="terraform-ls",
|
||||
sha256="724f45029f32d02d88b1952c7d1526c59fc8cd5dae49e31b9fed676a83f6cae7" if terraform_ls_version == "0.36.5" else None,
|
||||
sha256=_terraform_ls_sha(terraform_ls_version, "linux-arm64"),
|
||||
allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -140,7 +168,7 @@ class TerraformLS(SolidLanguageServer):
|
||||
platform_id="linux-x64",
|
||||
archive_type="zip",
|
||||
binary_name="terraform-ls",
|
||||
sha256="37e645cc54fd03e863157e2a3e773e7a5ff1d6cb3d045e4c20860cac1f550a44" if terraform_ls_version == "0.36.5" else None,
|
||||
sha256=_terraform_ls_sha(terraform_ls_version, "linux-x64"),
|
||||
allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
@@ -150,17 +178,23 @@ class TerraformLS(SolidLanguageServer):
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="terraform-ls.exe",
|
||||
sha256="a9223462cac9e1c0e6ba33043fbf9fb4483609b6970b5681a6306b04366698ec" if terraform_ls_version == "0.36.5" else None,
|
||||
sha256=_terraform_ls_sha(terraform_ls_version, "win-x64"),
|
||||
allowed_hosts=TERRAFORM_LS_ALLOWED_HOSTS,
|
||||
),
|
||||
]
|
||||
)
|
||||
dependency = deps.get_single_dep_for_current_platform()
|
||||
|
||||
terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir(solidlsp_settings))
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
install_dir = (
|
||||
cls.ls_resources_dir(solidlsp_settings)
|
||||
if terraform_ls_version == INITIAL_TERRAFORM_LS_VERSION
|
||||
else os.path.join(cls.ls_resources_dir(solidlsp_settings), f"terraform-ls-{terraform_ls_version}")
|
||||
)
|
||||
terraform_ls_executable_path = deps.binary_path(install_dir)
|
||||
if not os.path.exists(terraform_ls_executable_path):
|
||||
log.info(f"Downloading terraform-ls from {dependency.url}")
|
||||
deps.install(cls.ls_resources_dir(solidlsp_settings))
|
||||
deps.install(install_dir)
|
||||
|
||||
assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}"
|
||||
|
||||
|
||||
@@ -23,6 +23,14 @@ from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_in
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_TYPESCRIPT_VERSION = "5.9.3"
|
||||
DEFAULT_TYPESCRIPT_VERSION = "5.9.3"
|
||||
INITIAL_TYPESCRIPT_LANGUAGE_SERVER_VERSION = "5.1.3"
|
||||
DEFAULT_TYPESCRIPT_LANGUAGE_SERVER_VERSION = "5.1.3"
|
||||
|
||||
# Platform-specific imports
|
||||
if os.name != "nt": # Unix-like systems
|
||||
import pwd
|
||||
@@ -67,6 +75,10 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
- typescript_language_server_version: Version of typescript-language-server to install (default: "5.1.3")
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
return True
|
||||
|
||||
# Safety timeout for $/progress-based indexing wait. Normally the event fires
|
||||
# well within this window; the timeout is only hit if the server never sends progress.
|
||||
INDEXING_PROGRESS_TIMEOUT = 15.0 if os.name == "nt" else 10.0
|
||||
@@ -85,7 +97,10 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
self.server_ready = threading.Event()
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
|
||||
# Progress tracking for $/progress notifications (project indexing, etc.)
|
||||
# tracking asynchronous diagnostics publication
|
||||
self._published_diagnostics_timeout = 5.0
|
||||
|
||||
# tracking project indexing progress
|
||||
self._progress_lock = threading.Lock()
|
||||
self._active_progress_tokens: set[str] = set()
|
||||
self._indexing_complete = threading.Event()
|
||||
@@ -147,8 +162,10 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
|
||||
# Get version settings from ls_specific_settings or use defaults
|
||||
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")
|
||||
typescript_version = language_specific_config.get("typescript_version", DEFAULT_TYPESCRIPT_VERSION)
|
||||
typescript_language_server_version = language_specific_config.get(
|
||||
"typescript_language_server_version", DEFAULT_TYPESCRIPT_LANGUAGE_SERVER_VERSION
|
||||
)
|
||||
npm_registry = language_specific_config.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
@@ -174,38 +191,19 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
is_npm_installed = shutil.which("npm") is not None
|
||||
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
|
||||
|
||||
# Install typescript and typescript-language-server if not already installed or version mismatch
|
||||
tsserver_ls_dir = os.path.join(self._ls_resources_dir, "ts-lsp")
|
||||
# legacy unversioned dir reserved for INITIAL pair; any other version combination goes into a versioned subdir
|
||||
is_initial = (
|
||||
typescript_version == INITIAL_TYPESCRIPT_VERSION
|
||||
and typescript_language_server_version == INITIAL_TYPESCRIPT_LANGUAGE_SERVER_VERSION
|
||||
)
|
||||
ls_dirname = "ts-lsp" if is_initial else f"ts-lsp-{typescript_version}-{typescript_language_server_version}"
|
||||
tsserver_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server")
|
||||
|
||||
# Check if installation is needed based on executable AND version
|
||||
version_file = os.path.join(tsserver_ls_dir, ".installed_version")
|
||||
expected_version = f"{typescript_version}_{typescript_language_server_version}"
|
||||
|
||||
needs_install = False
|
||||
if not os.path.exists(tsserver_executable_path):
|
||||
log.info(f"Typescript Language Server executable not found at {tsserver_executable_path}.")
|
||||
needs_install = True
|
||||
elif os.path.exists(version_file):
|
||||
with open(version_file) as f:
|
||||
installed_version = f.read().strip()
|
||||
if installed_version != expected_version:
|
||||
log.info(
|
||||
f"TypeScript Language Server version mismatch: installed={installed_version}, expected={expected_version}. Reinstalling..."
|
||||
)
|
||||
needs_install = True
|
||||
else:
|
||||
# No version file exists, assume old installation needs refresh
|
||||
log.info("TypeScript Language Server version file not found. Reinstalling to ensure correct version...")
|
||||
needs_install = True
|
||||
|
||||
if needs_install:
|
||||
log.info("Installing TypeScript Language Server dependencies...")
|
||||
log.info(f"Typescript Language Server executable not found at {tsserver_executable_path}. Installing...")
|
||||
with LogTime("Installation of TypeScript language server dependencies", logger=log):
|
||||
deps.install(tsserver_ls_dir)
|
||||
# Write version marker file
|
||||
with open(version_file, "w") as f:
|
||||
f.write(expected_version)
|
||||
log.info("TypeScript language server dependencies installed successfully")
|
||||
|
||||
if not os.path.exists(tsserver_executable_path):
|
||||
@@ -217,6 +215,15 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
return [core_path, "--stdio"]
|
||||
|
||||
def _get_language_id_for_file(self, relative_file_path: str) -> str:
|
||||
# JSX is parsed as TS without this, which silently truncates symbol
|
||||
# ranges at the first multi-line JSX expression.
|
||||
if relative_file_path.endswith(".tsx"):
|
||||
return "typescriptreact"
|
||||
if relative_file_path.endswith(".jsx"):
|
||||
return "javascriptreact"
|
||||
return self.language_id
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the TypeScript Language Server.
|
||||
@@ -239,9 +246,11 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
"signatureHelp": {"dynamicRegistration": True},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True, "prepareSupport": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"configuration": True,
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"symbol": {"dynamicRegistration": True},
|
||||
},
|
||||
@@ -284,6 +293,10 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
def execute_client_command_handler(params: dict) -> list:
|
||||
return []
|
||||
|
||||
def configuration_handler(params: dict) -> list:
|
||||
items = params.get("items", [])
|
||||
return [{} for _ in items]
|
||||
|
||||
def do_nothing(params: dict) -> None:
|
||||
return
|
||||
|
||||
@@ -343,6 +356,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
self._indexing_complete.set()
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_request("workspace/configuration", configuration_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_request("window/workDoneProgress/create", work_done_progress_create)
|
||||
@@ -427,6 +441,20 @@ class TypeScriptLanguageServer(SolidLanguageServer):
|
||||
else:
|
||||
log.warning("Additional workspace indexing did not complete within timeout; proceeding anyway")
|
||||
|
||||
@override
|
||||
def _get_published_diagnostics_uri(self, request_uri: str) -> str:
|
||||
if os.name != "nt" or not request_uri.startswith("file:///"):
|
||||
return request_uri
|
||||
|
||||
path_part = request_uri[len("file:///") :]
|
||||
if len(path_part) >= 2 and path_part[0].isalpha() and path_part[1] == ":":
|
||||
return f"file:///{path_part[0].lower()}%3A{path_part[2:]}"
|
||||
return request_uri
|
||||
|
||||
@override
|
||||
def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float:
|
||||
return self._published_diagnostics_timeout
|
||||
|
||||
@override
|
||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
||||
return 2
|
||||
|
||||
@@ -24,6 +24,12 @@ from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_in
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_VTSLS_VERSION = "0.2.9"
|
||||
DEFAULT_VTSLS_VERSION = "0.2.9"
|
||||
|
||||
|
||||
class VtsLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -73,7 +79,7 @@ class VtsLanguageServer(SolidLanguageServer):
|
||||
]
|
||||
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")
|
||||
vtsls_version = vts_config.get("vtsls_version", DEFAULT_VTSLS_VERSION)
|
||||
npm_registry = vts_config.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
@@ -86,7 +92,9 @@ class VtsLanguageServer(SolidLanguageServer):
|
||||
),
|
||||
]
|
||||
)
|
||||
vts_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "vts-lsp")
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
ls_dirname = "vts-lsp" if vtsls_version == INITIAL_VTSLS_VERSION else f"vts-lsp-{vtsls_version}"
|
||||
vts_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), ls_dirname)
|
||||
vts_executable_path = os.path.join(vts_ls_dir, "vtsls")
|
||||
|
||||
# Verify both node and npm are installed
|
||||
|
||||
@@ -426,6 +426,18 @@ class VueLanguageServer(SolidLanguageServer):
|
||||
with self._ts_server.open_file(relative_file_path):
|
||||
return self._ts_server.request_rename_symbol_edit(relative_file_path, line, column, new_name)
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
self._ensure_ls_operational()
|
||||
assert self._ts_server is not None
|
||||
return self._ts_server.request_text_document_diagnostics(relative_file_path, start_line, end_line, min_severity)
|
||||
|
||||
def _forward_edit_to_ts_server_if_needed(self, relative_file_path: str, edit_fn: Callable[[], object]) -> None:
|
||||
"""
|
||||
Calls ``edit_fn`` on the TypeScript server if the file is open there.
|
||||
@@ -598,6 +610,7 @@ class VueLanguageServer(SolidLanguageServer):
|
||||
"signatureHelp": {"dynamicRegistration": True},
|
||||
"codeAction": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True, "prepareSupport": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
|
||||
@@ -17,6 +17,12 @@ from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
INITIAL_YAML_LANGUAGE_SERVER_VERSION = "1.19.2"
|
||||
DEFAULT_YAML_LANGUAGE_SERVER_VERSION = "1.19.2"
|
||||
|
||||
|
||||
class YamlLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -66,7 +72,7 @@ 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")
|
||||
yaml_language_server_version = self._custom_settings.get("yaml_language_server_version", DEFAULT_YAML_LANGUAGE_SERVER_VERSION)
|
||||
npm_registry = self._custom_settings.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
@@ -80,8 +86,13 @@ class YamlLanguageServer(SolidLanguageServer):
|
||||
]
|
||||
)
|
||||
|
||||
# Install yaml-language-server if not already installed
|
||||
yaml_ls_dir = os.path.join(self._ls_resources_dir, "yaml-lsp")
|
||||
# legacy unversioned dir reserved for INITIAL; every other version goes into a versioned subdir
|
||||
ls_dirname = (
|
||||
"yaml-lsp"
|
||||
if yaml_language_server_version == INITIAL_YAML_LANGUAGE_SERVER_VERSION
|
||||
else f"yaml-lsp-{yaml_language_server_version}"
|
||||
)
|
||||
yaml_ls_dir = os.path.join(self._ls_resources_dir, ls_dirname)
|
||||
yaml_executable_path = os.path.join(yaml_ls_dir, "node_modules", ".bin", "yaml-language-server")
|
||||
|
||||
# Handle Windows executable extension
|
||||
|
||||
@@ -132,6 +132,7 @@ class ZigLanguageServer(SolidLanguageServer):
|
||||
"dynamicRegistration": True,
|
||||
"contentFormat": ["markdown", "plaintext"],
|
||||
},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
|
||||
+399
-6
@@ -13,8 +13,8 @@ from collections.abc import Hashable, Iterator
|
||||
from contextlib import contextmanager
|
||||
from copy import copy
|
||||
from pathlib import Path, PurePath
|
||||
from time import perf_counter, sleep
|
||||
from typing import Self, Union, cast
|
||||
from time import monotonic, perf_counter, sleep
|
||||
from typing import Any, Self, Union, cast
|
||||
|
||||
import pathspec
|
||||
from sensai.util.pickle import getstate, load_pickle
|
||||
@@ -487,10 +487,14 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
:param config: the global SolidLSP configuration.
|
||||
:param repository_root_path: the root path of the repository.
|
||||
:param process_launch_info: (DEPRECATED - implement _create_dependency_provider instead)
|
||||
:param process_launch_info: (DEPRECATED: pass None and implement _create_dependency_provider instead)
|
||||
the command used to start the actual language server.
|
||||
The command must pass appropriate flags to the binary, so that it runs in the stdio mode,
|
||||
as opposed to HTTP, TCP modes supported by some language servers.
|
||||
:param language_id: The language identifier which will be passed to the language server in the `textDocument/didOpen`
|
||||
notification by default.
|
||||
If the language server uses multiple language identifiers, it must override the method `get_language_id_for_file`
|
||||
to provide the appropriate identifier for each type of file.
|
||||
:param cache_version_raw_document_symbols: the version, for caching, of the raw document symbols coming
|
||||
from this specific language server. This should be incremented by subclasses calling this constructor
|
||||
whenever the format of the raw document symbols changes (typically because the language server
|
||||
@@ -511,6 +515,10 @@ class SolidLanguageServer(ABC):
|
||||
self.language_id = language_id
|
||||
self.open_file_buffers: dict[str, LSPFileBuffer] = {}
|
||||
self.language = Language(language_id)
|
||||
self._published_diagnostics: dict[str, list[ls_types.Diagnostic]] = {}
|
||||
self._published_diagnostics_generation_by_uri: dict[str, int] = {}
|
||||
self._published_diagnostics_generation = 0
|
||||
self._published_diagnostics_condition = threading.Condition()
|
||||
|
||||
# initialise symbol caches
|
||||
self.cache_dir = Path(self._solidlsp_settings.project_data_path) / self.CACHE_FOLDER_NAME / self.language_id
|
||||
@@ -550,6 +558,7 @@ class SolidLanguageServer(ABC):
|
||||
logger=logging_fn,
|
||||
start_independent_lsp_process=config.start_independent_lsp_process,
|
||||
)
|
||||
self.server.on_any_notification(self._observe_server_notification)
|
||||
|
||||
# Set up the pathspec matcher for the ignored paths
|
||||
# for all absolute paths in ignored_paths, convert them to relative paths
|
||||
@@ -590,6 +599,388 @@ class SolidLanguageServer(ABC):
|
||||
_seen.add(additional_workspace_abs_path)
|
||||
self._additional_workspace_abs_paths.append(additional_workspace_abs_path)
|
||||
|
||||
def _observe_server_notification(self, method: str, params: Any) -> None:
|
||||
"""
|
||||
Observe notifications sent by the language server.
|
||||
|
||||
This is used for generic cross-language bookkeeping that must work independently of
|
||||
language-specific notification handlers.
|
||||
"""
|
||||
if method == "textDocument/publishDiagnostics":
|
||||
self._store_published_diagnostics(params)
|
||||
|
||||
def _store_published_diagnostics(self, params: Any) -> None:
|
||||
"""
|
||||
Store diagnostics received through ``textDocument/publishDiagnostics``.
|
||||
"""
|
||||
if not isinstance(params, dict):
|
||||
return
|
||||
|
||||
uri = params.get("uri")
|
||||
diagnostics = params.get("diagnostics")
|
||||
if not isinstance(uri, str) or not isinstance(diagnostics, list):
|
||||
return
|
||||
|
||||
normalized_diagnostics: list[ls_types.Diagnostic] = []
|
||||
for diagnostic in diagnostics:
|
||||
if not isinstance(diagnostic, dict):
|
||||
continue
|
||||
if "message" not in diagnostic or "range" not in diagnostic:
|
||||
continue
|
||||
|
||||
normalized_diagnostic: ls_types.Diagnostic = {
|
||||
"uri": uri,
|
||||
"message": diagnostic["message"],
|
||||
"range": diagnostic["range"],
|
||||
}
|
||||
severity = diagnostic.get("severity")
|
||||
if isinstance(severity, int):
|
||||
normalized_diagnostic["severity"] = ls_types.DiagnosticSeverity(severity)
|
||||
|
||||
code = diagnostic.get("code")
|
||||
if isinstance(code, int | str):
|
||||
normalized_diagnostic["code"] = code
|
||||
|
||||
if "source" in diagnostic:
|
||||
normalized_diagnostic["source"] = diagnostic["source"]
|
||||
normalized_diagnostics.append(ls_types.Diagnostic(**normalized_diagnostic))
|
||||
|
||||
# canonicalize the key so lookups using URIs produced by pathlib.Path.as_uri() match
|
||||
# what servers publish (e.g. file:///c%3A/... or file:///c:/... vs. file:///C:/...)
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
|
||||
with self._published_diagnostics_condition:
|
||||
self._published_diagnostics_generation += 1
|
||||
self._published_diagnostics[key] = normalized_diagnostics
|
||||
self._published_diagnostics_generation_by_uri[key] = self._published_diagnostics_generation
|
||||
self._published_diagnostics_condition.notify_all()
|
||||
|
||||
@staticmethod
|
||||
def _canonicalize_published_diagnostics_uri(uri: str) -> str:
|
||||
"""
|
||||
Canonicalizes a ``file://`` URI so that diagnostics published by language servers
|
||||
and lookups based on ``pathlib.Path.as_uri()`` agree on the same key.
|
||||
|
||||
On Windows, servers may publish under ``file:///c%3A/...`` or ``file:///c:/...`` while
|
||||
``pathlib.Path.as_uri()`` produces ``file:///C:/...``. The canonical form uses an
|
||||
upper-case drive letter and a plain colon.
|
||||
"""
|
||||
if os.name != "nt" or not uri.startswith("file:///"):
|
||||
return uri
|
||||
|
||||
# extract the segment after "file:///" up to the next slash and look for a drive letter
|
||||
prefix = "file:///"
|
||||
rest = uri[len(prefix) :]
|
||||
slash = rest.find("/")
|
||||
head = rest if slash < 0 else rest[:slash]
|
||||
tail = "" if slash < 0 else rest[slash:]
|
||||
|
||||
if (len(head) >= 2 and head[0].isalpha() and head[1] == ":") or (
|
||||
len(head) >= 4 and head[0].isalpha() and head[1:4].lower() == "%3a"
|
||||
):
|
||||
head = head[0].upper() + ":"
|
||||
else:
|
||||
return uri
|
||||
|
||||
return prefix + head + tail
|
||||
|
||||
def _get_published_diagnostics_generation(self, uri: str) -> int:
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
with self._published_diagnostics_condition:
|
||||
return self._published_diagnostics_generation_by_uri.get(key, -1)
|
||||
|
||||
def _wait_for_published_diagnostics(
|
||||
self,
|
||||
uri: str,
|
||||
after_generation: int,
|
||||
timeout: float,
|
||||
) -> list[ls_types.Diagnostic] | None:
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
deadline = perf_counter() + timeout
|
||||
with self._published_diagnostics_condition:
|
||||
while True:
|
||||
current_generation = self._published_diagnostics_generation_by_uri.get(key, -1)
|
||||
if current_generation > after_generation:
|
||||
return list(self._published_diagnostics.get(key, []))
|
||||
|
||||
remaining_timeout = deadline - perf_counter()
|
||||
if remaining_timeout <= 0:
|
||||
return None
|
||||
self._published_diagnostics_condition.wait(timeout=remaining_timeout)
|
||||
|
||||
def _get_cached_published_diagnostics(self, uri: str) -> list[ls_types.Diagnostic] | None:
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
with self._published_diagnostics_condition:
|
||||
diagnostics = self._published_diagnostics.get(key)
|
||||
if diagnostics is None:
|
||||
return None
|
||||
return list(diagnostics)
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_matches_range(diagnostic: ls_types.Diagnostic, start_line: int, end_line: int) -> bool:
|
||||
diagnostic_start_line = diagnostic["range"]["start"]["line"]
|
||||
diagnostic_end_line = diagnostic["range"]["end"]["line"]
|
||||
|
||||
# normalize inverted ranges: some servers (e.g. Julia's JuliaSyntax.jl for unterminated
|
||||
# expressions) emit diagnostics whose end position lies before the start position.
|
||||
# treat such ranges as a single span covering both lines.
|
||||
lo = min(diagnostic_start_line, diagnostic_end_line)
|
||||
hi = max(diagnostic_start_line, diagnostic_end_line)
|
||||
|
||||
# when end_line < 0 the caller imposes no upper bound; only enforce the lower bound.
|
||||
if end_line < 0:
|
||||
return hi >= start_line
|
||||
return lo <= end_line and hi >= start_line
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_matches_min_severity(diagnostic: ls_types.Diagnostic, min_severity: int) -> bool:
|
||||
severity = diagnostic.get("severity")
|
||||
if severity is None:
|
||||
return True
|
||||
return int(severity) <= min_severity
|
||||
|
||||
@classmethod
|
||||
def _filter_diagnostics(
|
||||
cls,
|
||||
diagnostics: list[ls_types.Diagnostic],
|
||||
start_line: int,
|
||||
end_line: int,
|
||||
min_severity: int,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
diagnostics = [d for d in diagnostics if cls._diagnostic_matches_range(d, start_line, end_line)]
|
||||
diagnostics = [d for d in diagnostics if cls._diagnostic_matches_min_severity(d, min_severity)]
|
||||
return diagnostics
|
||||
|
||||
def _validate_text_document_diagnostics_request(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int,
|
||||
end_line: int,
|
||||
min_severity: int,
|
||||
) -> str:
|
||||
if not self.server_started:
|
||||
log.error("request_text_document_diagnostics called before Language Server started")
|
||||
raise SolidLSPException("Language Server not started")
|
||||
if start_line < 0:
|
||||
raise ValueError(f"start_line must be non-negative, got {start_line}")
|
||||
if end_line != -1 and end_line < start_line:
|
||||
raise ValueError(f"end_line must be -1 or >= start_line, got {end_line} < {start_line}")
|
||||
if min_severity not in {1, 2, 3, 4}:
|
||||
raise ValueError(f"min_severity must be one of 1, 2, 3, 4, got {min_severity}")
|
||||
return pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
|
||||
|
||||
def get_published_diagnostics_generation(self, relative_file_path: str) -> int:
|
||||
"""
|
||||
Get the generation number for the latest published diagnostics of a file.
|
||||
|
||||
:param relative_file_path: The relative path of the file.
|
||||
:return: the generation number, or ``-1`` if none were published yet.
|
||||
"""
|
||||
uri = pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
|
||||
return self._get_published_diagnostics_generation(uri)
|
||||
|
||||
def get_cached_published_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic] | None:
|
||||
"""
|
||||
Get cached diagnostics received through ``textDocument/publishDiagnostics``.
|
||||
|
||||
:param relative_file_path: The relative path of the file to retrieve diagnostics for.
|
||||
:param start_line: the first 0-based line to include in the result.
|
||||
:param end_line: the last 0-based line to include in the result. ``-1`` means no upper bound.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
Diagnostics with lower-or-equal numeric severity are returned.
|
||||
:return: the cached diagnostics, or ``None`` if no diagnostics were published yet.
|
||||
"""
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
diagnostics = self._get_cached_published_diagnostics(uri)
|
||||
if diagnostics is None:
|
||||
return None
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
|
||||
def request_published_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
after_generation: int = -1,
|
||||
timeout: float = 2.5,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
allow_cached: bool = True,
|
||||
) -> list[ls_types.Diagnostic] | None:
|
||||
"""
|
||||
Wait for diagnostics received through ``textDocument/publishDiagnostics`` and return them.
|
||||
|
||||
:param relative_file_path: The relative path of the file to retrieve diagnostics for.
|
||||
:param after_generation: only return diagnostics published after this generation. ``-1`` accepts the next publication.
|
||||
:param timeout: the maximum time to wait for a newer publication.
|
||||
:param start_line: the first 0-based line to include in the result.
|
||||
:param end_line: the last 0-based line to include in the result. ``-1`` means no upper bound.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
Diagnostics with lower-or-equal numeric severity are returned.
|
||||
:param allow_cached: whether to fall back to the current cached diagnostics if no newer publication arrives in time.
|
||||
:return: the published diagnostics, or ``None`` if no diagnostics are available.
|
||||
"""
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
published_uri = self._get_published_diagnostics_uri(uri)
|
||||
diagnostics: list[ls_types.Diagnostic] | None = None
|
||||
|
||||
# keeping the document open
|
||||
with self.open_file(relative_file_path):
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=published_uri,
|
||||
after_generation=after_generation,
|
||||
timeout=timeout,
|
||||
allow_cached=allow_cached,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return None
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
|
||||
def _get_published_diagnostics_uri(self, request_uri: str) -> str:
|
||||
"""
|
||||
Gets the URI under which published diagnostics should be looked up.
|
||||
"""
|
||||
return request_uri
|
||||
|
||||
def _supports_pull_diagnostics(self) -> bool:
|
||||
"""
|
||||
Whether the language server handles ``textDocument/diagnostic`` (LSP 3.17 pull
|
||||
diagnostics) gracefully. Subclasses should override and return ``False`` when the
|
||||
underlying server does not implement pull diagnostics, especially when sending
|
||||
the request would terminate the server (e.g. LanguageServer.jl raises an error
|
||||
for unknown methods that crashes the process).
|
||||
"""
|
||||
return True
|
||||
|
||||
def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float:
|
||||
"""
|
||||
Gets the timeout for waiting on published diagnostics after a diagnostics request.
|
||||
"""
|
||||
return 2.5
|
||||
|
||||
def _accept_published_diagnostics(self, diagnostics: list[ls_types.Diagnostic]) -> bool:
|
||||
"""
|
||||
Determines whether a published diagnostics payload should satisfy the current wait.
|
||||
"""
|
||||
return bool(diagnostics)
|
||||
|
||||
def _wait_for_relevant_published_diagnostics(
|
||||
self,
|
||||
uri: str,
|
||||
after_generation: int,
|
||||
timeout: float,
|
||||
allow_cached: bool = True,
|
||||
) -> list[ls_types.Diagnostic] | None:
|
||||
"""
|
||||
Waits for a published diagnostics payload that is relevant for the current request.
|
||||
"""
|
||||
deadline = monotonic() + timeout
|
||||
current_after_generation = after_generation
|
||||
|
||||
while True:
|
||||
remaining_timeout = deadline - monotonic()
|
||||
if remaining_timeout <= 0:
|
||||
break
|
||||
|
||||
diagnostics = self._wait_for_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=current_after_generation,
|
||||
timeout=remaining_timeout,
|
||||
)
|
||||
if diagnostics is None:
|
||||
break
|
||||
if self._accept_published_diagnostics(diagnostics):
|
||||
return diagnostics
|
||||
current_after_generation = self._get_published_diagnostics_generation(uri)
|
||||
|
||||
if allow_cached:
|
||||
diagnostics = self._get_cached_published_diagnostics(uri)
|
||||
if diagnostics is not None and self._accept_published_diagnostics(diagnostics):
|
||||
return diagnostics
|
||||
|
||||
return None
|
||||
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> 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
|
||||
:param start_line: the first 0-based line to include in the result.
|
||||
:param end_line: the last 0-based line to include in the result. `-1` means no upper bound.
|
||||
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
|
||||
Diagnostics with lower-or-equal numeric severity are returned.
|
||||
|
||||
:return: A list of diagnostics for the file
|
||||
"""
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
published_uri = self._get_published_diagnostics_uri(uri)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(published_uri)
|
||||
ret: list[ls_types.Diagnostic] | None = None
|
||||
pull_diagnostics_failed = False
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
response: Any = None
|
||||
# only send pull diagnostics when the server actually supports it; some servers
|
||||
# (e.g. Julia's LanguageServer.jl) hard-error and crash the process on unknown methods
|
||||
if self._supports_pull_diagnostics():
|
||||
try:
|
||||
response = self.server.send.text_document_diagnostic(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
except SolidLSPException as ex:
|
||||
log.debug("Falling back to published diagnostics for %s due to pull-diagnostics error: %s", relative_file_path, ex)
|
||||
response = None
|
||||
pull_diagnostics_failed = True
|
||||
|
||||
if response is not None:
|
||||
assert isinstance(response, dict), (
|
||||
f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
|
||||
)
|
||||
ret = []
|
||||
for item in response["items"]: # type: ignore
|
||||
new_item: ls_types.Diagnostic = {
|
||||
"uri": uri,
|
||||
"severity": item["severity"],
|
||||
"message": item["message"],
|
||||
"range": item["range"],
|
||||
"code": item.get("code"), # type: ignore
|
||||
}
|
||||
if "source" in item:
|
||||
new_item["source"] = item["source"]
|
||||
ret.append(ls_types.Diagnostic(**new_item))
|
||||
|
||||
if not ret:
|
||||
published_diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=published_uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(pull_diagnostics_failed),
|
||||
)
|
||||
if published_diagnostics is not None:
|
||||
ret = published_diagnostics
|
||||
|
||||
if ret is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(ret, start_line, end_line, min_severity)
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
"""
|
||||
Creates the dependency provider for this language server.
|
||||
@@ -870,10 +1261,12 @@ class SolidLanguageServer(ABC):
|
||||
pass
|
||||
|
||||
def _get_language_id_for_file(self, relative_file_path: str) -> str:
|
||||
"""Return the language ID for a file.
|
||||
"""
|
||||
Determines the language identifier to pass to the language server for the given file,
|
||||
particularly `textDocument/didOpen` requests.
|
||||
|
||||
Override in subclasses to return file-specific language IDs.
|
||||
Default implementation returns self.language_id.
|
||||
Override this method in subclasses to return file-specific language identifiers.
|
||||
The default implementation returns the main identifier passed at construction (self.language_id).
|
||||
"""
|
||||
return self.language_id
|
||||
|
||||
|
||||
@@ -207,6 +207,12 @@ class Language(str, Enum):
|
||||
case _:
|
||||
return 2
|
||||
|
||||
def supports_implementation_request(self) -> bool:
|
||||
"""
|
||||
Return whether the default language server for this language supports ``textDocument/implementation``.
|
||||
"""
|
||||
return self.get_ls_class().supports_implementation_request()
|
||||
|
||||
def get_source_fn_matcher(self) -> FilenameMatcher:
|
||||
match self:
|
||||
case self.PYTHON | self.PYTHON_JEDI | self.PYTHON_TY:
|
||||
|
||||
@@ -150,6 +150,7 @@ class LanguageServerProcess:
|
||||
self._pending_requests: dict[Any, Request] = {}
|
||||
self.on_request_handlers: dict[str, Callable[[Any], Any]] = {}
|
||||
self.on_notification_handlers: dict[str, Callable[[Any], None]] = {}
|
||||
self._notification_observers: list[Callable[[str, Any], None]] = []
|
||||
self._trace_log_fn = logger
|
||||
self.tasks: dict[int, Any] = {}
|
||||
self.task_counter = 0
|
||||
@@ -510,6 +511,12 @@ class LanguageServerProcess:
|
||||
"""
|
||||
self.on_notification_handlers[method] = cb
|
||||
|
||||
def on_any_notification(self, cb: Callable[[str, Any], None]) -> None:
|
||||
"""
|
||||
Register an observer that is invoked for every notification received from the server.
|
||||
"""
|
||||
self._notification_observers.append(cb)
|
||||
|
||||
def _response_handler(self, response: StringDict) -> None:
|
||||
"""
|
||||
Handle the response received from the server for a request, using the id to determine the request
|
||||
@@ -561,6 +568,16 @@ class LanguageServerProcess:
|
||||
"""
|
||||
method = response.get("method", "")
|
||||
params = response.get("params")
|
||||
|
||||
for observer in self._notification_observers:
|
||||
try:
|
||||
observer(method, params)
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception as ex:
|
||||
if not self._is_shutting_down:
|
||||
log.error("Error handling notification observer for method '%s': %s", method, ex, exc_info=ex)
|
||||
|
||||
handler = self.on_notification_handlers.get(method)
|
||||
if not handler:
|
||||
log.warning("Unhandled method '%s'", method)
|
||||
|
||||
@@ -347,7 +347,7 @@ class Diagnostic(TypedDict):
|
||||
""" The severity of the diagnostic. """
|
||||
message: str
|
||||
""" The diagnostic message. """
|
||||
code: str
|
||||
code: NotRequired[str | int]
|
||||
""" The code of the diagnostic. """
|
||||
source: NotRequired[str]
|
||||
""" The source of the diagnostic, e.g. the name of the tool that produced it. """
|
||||
|
||||
@@ -124,6 +124,14 @@ class TextUtils:
|
||||
end_idx = TextUtils.get_index_from_line_col(text, end_line, end_col)
|
||||
return text[start_idx:end_idx]
|
||||
|
||||
@staticmethod
|
||||
def get_text_in_lines_range(text: str, start_line: int, end_line: int) -> str:
|
||||
"""
|
||||
Returns the text encompassed by the given start and end lines (inclusive).
|
||||
"""
|
||||
lines = text.splitlines(keepends=True)
|
||||
return "".join(lines[start_line : end_line + 1])
|
||||
|
||||
|
||||
class PathUtils:
|
||||
"""
|
||||
|
||||
@@ -131,11 +131,14 @@ class StopLoopException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def create_message(payload: PayloadLike) -> tuple[bytes, bytes, bytes]:
|
||||
def create_message(payload: PayloadLike) -> tuple[bytes, bytes]:
|
||||
body = json.dumps(payload, check_circular=False, ensure_ascii=False, separators=(",", ":")).encode(ENCODING)
|
||||
# NOTE: only Content-Length is sent. Content-Type is optional per the LSP
|
||||
# spec (default: application/vscode-jsonrpc; charset=utf-8), and Godot's
|
||||
# GDScript LSP parser only handles Content-Length — any additional header
|
||||
# makes it silently drop the message.
|
||||
return (
|
||||
f"Content-Length: {len(body)}\r\n".encode(ENCODING),
|
||||
"Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n".encode(ENCODING),
|
||||
f"Content-Length: {len(body)}\r\n\r\n".encode(ENCODING),
|
||||
body,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil as _sh
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
@@ -41,10 +42,13 @@ _LANGUAGE_REPO_ALIASES: dict[Language, Language] = {
|
||||
Language.CPP_CCLS: Language.CPP,
|
||||
Language.PHP_PHPACTOR: Language.PHP,
|
||||
Language.PYTHON_JEDI: Language.PYTHON,
|
||||
Language.PYTHON_TY: Language.PYTHON,
|
||||
Language.RUBY_SOLARGRAPH: Language.RUBY,
|
||||
Language.PYTHON_TY: Language.PYTHON,
|
||||
}
|
||||
|
||||
PYTHON_LANGUAGE_BACKENDS = [Language.PYTHON, Language.PYTHON_TY]
|
||||
|
||||
|
||||
def get_repo_path(language: Language) -> Path:
|
||||
repo_language = _LANGUAGE_REPO_ALIASES.get(language, language)
|
||||
@@ -334,3 +338,60 @@ def language_tests_enabled(language: Language) -> bool:
|
||||
:return: True if tests for the language are enabled, False otherwise
|
||||
"""
|
||||
return language not in _disabled_languages
|
||||
|
||||
|
||||
def language_supports_implementation(language: Language) -> bool:
|
||||
return language.supports_implementation_request()
|
||||
|
||||
|
||||
def languages_supporting_implementation(*languages: Language) -> list[Language]:
|
||||
return [language for language in languages if language_supports_implementation(language)]
|
||||
|
||||
|
||||
_VERIFIED_IMPLEMENTATION_LANGUAGES = {
|
||||
Language.CSHARP,
|
||||
Language.GO,
|
||||
Language.JAVA,
|
||||
Language.RUST,
|
||||
Language.TYPESCRIPT,
|
||||
}
|
||||
|
||||
|
||||
def language_has_verified_implementation_support(language: Language) -> bool:
|
||||
"""
|
||||
True only for languages where the server advertises implementation support and
|
||||
the repo fixtures contain a verified working go-to-implementation scenario.
|
||||
"""
|
||||
return language in _VERIFIED_IMPLEMENTATION_LANGUAGES and language_supports_implementation(language)
|
||||
|
||||
|
||||
def find_identifier_position(file_path: Path, identifier: str) -> tuple[int, int] | None:
|
||||
pattern = re.compile(r"\b" + re.escape(identifier) + r"\b")
|
||||
with file_path.open(encoding="utf-8") as f:
|
||||
for line_idx, line in enumerate(f):
|
||||
match = pattern.search(line)
|
||||
if match:
|
||||
return line_idx, match.start()
|
||||
return None
|
||||
|
||||
|
||||
def find_identifier_pos(
|
||||
file_path: Path,
|
||||
identifier: str,
|
||||
occurrence_index: int = 0,
|
||||
column_offset: int = 0,
|
||||
) -> tuple[int, int] | None:
|
||||
if occurrence_index < 0:
|
||||
raise ValueError("occurrence_index must be non-negative")
|
||||
if column_offset < 0:
|
||||
raise ValueError("column_offset must be non-negative")
|
||||
|
||||
pattern = re.compile(r"\b" + re.escape(identifier) + r"\b")
|
||||
current_index = 0
|
||||
with file_path.open(encoding="utf-8") as f:
|
||||
for line_idx, line in enumerate(f):
|
||||
for match in pattern.finditer(line):
|
||||
if current_index == occurrence_index:
|
||||
return line_idx, match.start() + column_offset
|
||||
current_index += 1
|
||||
return None
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
if then
|
||||
echo "$missingGreeting"
|
||||
fi
|
||||
@@ -0,0 +1,9 @@
|
||||
(ns test-app.diagnostics-sample)
|
||||
|
||||
(defn broken-factory []
|
||||
missing-greeting)
|
||||
|
||||
(defn broken-consumer []
|
||||
(let [value (broken-factory)]
|
||||
(println value)
|
||||
missing-consumer-value))
|
||||
@@ -8,5 +8,10 @@
|
||||
"directory": ".",
|
||||
"command": "g++ -std=c++17 -I . -c b.cpp",
|
||||
"file": "b.cpp"
|
||||
},
|
||||
{
|
||||
"directory": ".",
|
||||
"command": "g++ -std=c++17 -I . -c diagnostics_sample.cpp",
|
||||
"file": "diagnostics_sample.cpp"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
int brokenFactory() {
|
||||
return missingGreeting;
|
||||
}
|
||||
|
||||
int brokenConsumer() {
|
||||
int value = brokenFactory();
|
||||
return value + missingConsumerValue;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
def broken_factory : String
|
||||
missingGreeting
|
||||
end
|
||||
|
||||
broken_consumer = missingConsumerValue
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace TestProject
|
||||
{
|
||||
public static class DiagnosticsSample
|
||||
{
|
||||
public static string BrokenFactory()
|
||||
{
|
||||
return missingGreeting;
|
||||
}
|
||||
|
||||
public static void BrokenConsumer()
|
||||
{
|
||||
string value = BrokenFactory();
|
||||
Console.WriteLine(value);
|
||||
Console.WriteLine(missingConsumerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using TestProject.Services;
|
||||
|
||||
namespace TestProject
|
||||
{
|
||||
@@ -7,10 +8,13 @@ namespace TestProject
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Hello, World!");
|
||||
|
||||
|
||||
var calculator = new Calculator();
|
||||
int result = calculator.Add(5, 3);
|
||||
Console.WriteLine($"5 + 3 = {result}");
|
||||
|
||||
IGreeter greeter = new ConsoleGreeter();
|
||||
Console.WriteLine(greeter.FormatGreeting("World"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,4 +44,4 @@ namespace TestProject
|
||||
return (double)a / b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace TestProject.Services
|
||||
{
|
||||
public class ConsoleGreeter : IGreeter
|
||||
{
|
||||
public string FormatGreeting(string name)
|
||||
{
|
||||
return $"Hello, {name}!";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TestProject.Services
|
||||
{
|
||||
public interface IGreeter
|
||||
{
|
||||
string FormatGreeting(string name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
String brokenFactory() {
|
||||
return 'broken'
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
defmodule DiagnosticsSample do
|
||||
def broken_factory do
|
||||
missing_greeting
|
||||
end
|
||||
|
||||
def broken_consumer do
|
||||
value = broken_factory()
|
||||
{value, missing_consumer_value}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
module DiagnosticsSample exposing (brokenFactory)
|
||||
|
||||
brokenFactory : String
|
||||
brokenFactory =
|
||||
"unterminated
|
||||
@@ -0,0 +1,5 @@
|
||||
-module(diagnostics_sample).
|
||||
-export([broken_factory/0, broken_consumer/0]).
|
||||
|
||||
broken_factory() -> MissingGreeting.
|
||||
broken_consumer() -> MissingConsumerValue.
|
||||
@@ -0,0 +1,5 @@
|
||||
program diagnostics_sample
|
||||
implicit none
|
||||
integer :: duplicated_value
|
||||
real :: duplicated_value
|
||||
end program diagnostics_sample
|
||||
Loaded 100 of 216 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user