Merge remote-tracking branch 'origin/main' into tool-desc-update

This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-07-15 20:18:26 +02:00
commit 755de59f16
133 files changed
+6136 -2023

No files matched your search

+26 -3
View File
@@ -64,9 +64,10 @@ jobs:
env:
MARKERS_JVM: "java or csharp or fsharp or kotlin or scala or groovy or clojure or bsl"
MARKERS_NATIVE: "cpp or rust or go or zig or pascal or swift" # swift is macOS-only (skips elsewhere via the guard)
# other-langs: grab-bag + scripting languages + haskell. Haskell (GHC/HLS) is installed only on
# Linux -- it's too slow/expensive to build on the macOS runner -- and skips on Windows+macOS via the guard.
MARKERS_OTHER_LANGS: "ruby or php or lua or luau or bash or powershell or elixir or erlang or dart or haxe or haskell or terraform or rego or ansible or yaml or toml or markdown or latex or crystal or cue or fortran or ada or matlab or systemverilog or hlsl or msl or al"
# other-langs: grab-bag + scripting languages + haskell + qml. Haskell (GHC/HLS) and qml (qmlls) are
# installed only on Linux -- too slow/expensive to build on the macOS runner (Haskell), or not worth
# the cost of a cross-OS Qt pull (qmlls) -- and skip on Windows+macOS via the central conftest guard.
MARKERS_OTHER_LANGS: "ruby or php or lua or luau or bash or powershell or elixir or erlang or dart or haxe or haskell or terraform or rego or ansible or yaml or toml or markdown or latex or crystal or cue or fortran or ada or matlab or systemverilog or hlsl or msl or al or qml"
# niche: slow, mostly-cached toolchains. ocaml + haskell are the two slowest -- kept in separate
# batches (ocaml here, haskell in other-langs). nix + perl skip on Windows via the guard.
MARKERS_NICHE: "julia or r or perl or lean4 or nix or ocaml"
@@ -418,6 +419,28 @@ jobs:
mv regal.exe "$HOME/bin/"
echo "$HOME/bin" >> $GITHUB_PATH
fi
- name: Install qmlls (QML Language Server)
# qmlls needs Qt 6.6+ for textDocument/documentSymbol (the feature these tests assert); Ubuntu
# noble's apt only has Qt 6.4.2, whose qmlls returns -32601 "method not found" for it. So install
# the official standalone build (TheQtCompanyRnD/qmlls-workflow, linked from the Qt docs) -- an 8 MB
# single binary built from recent Qt dev. Restricted to Linux (a cross-OS Qt pull isn't worth it);
# QML skips on Windows/macOS via the conftest guard. libodbc2 is its only non-bundled runtime dep.
if: matrix.batch == 'other-langs' && runner.os == 'Linux'
shell: bash
run: |
QMLLS_VERSION="0.7"
QMLLS_DIR="$HOME/.qmlls"
mkdir -p "$QMLLS_DIR"
curl -fsSL -o /tmp/qmlls.zip "https://github.com/TheQtCompanyRnD/qmlls-workflow/releases/download/${QMLLS_VERSION}/qmllanguageserver-linux-x64-${QMLLS_VERSION}.zip"
unzip -o /tmp/qmlls.zip -d "$QMLLS_DIR" >/dev/null
rm /tmp/qmlls.zip
chmod +x "$QMLLS_DIR/qmlls"
# libodbc.so.2 is qmlls's only non-bundled runtime dep; install only if absent on the image.
ldconfig -p | grep -q 'libodbc.so.2' || { sudo apt-get update && sudo apt-get install -y libodbc2; }
# Fail loudly if any shared lib is still unresolved (e.g. a missing runtime dep).
missing="$(ldd "$QMLLS_DIR/qmlls" | grep 'not found' || true)"
[ -z "$missing" ] || { echo "ERROR: qmlls has unresolved runtime libs:"; echo "$missing"; exit 1; }
echo "$QMLLS_DIR" >> "$GITHUB_PATH"
- name: Cache Free Pascal Compiler
if: matrix.batch == 'native'
id: cache-fpc
@@ -33,51 +33,78 @@ To implement a new language server using the DependencyProvider pattern:
```
The resource dir that is passed is the directory in which installed dependencies should be stored!
**Base Classes:**
**Base Classes** (choose the most specific one that fits):
- **`LanguageServerDependencyProviderSinglePath`** - For language servers with a single core dependency (e.g., an executable or JAR file)
- Provides automatic support for the `ls_path` custom setting, allowing users to override the core dependency path (if they have it installed it themselves)
- **`LanguageServerDependencyProviderUvx`** - For language servers distributed as a PyPI package, run on demand
via `uvx` / `uv x` (no installation step to implement)
- Simply instantiate it with the package name, pinned default version, entrypoint (console script) and optional `extra_args`;
the version can be overridden by the user via the configured `version_setting_key` custom setting
- Reference implementation: `PyrightServer`
- **`LanguageServerDependencyProviderBaseCommand`** - For the common case where the
launch command is constructed from a *base command* (which the user can override via custom settings; handled generically)
- Implement `_create_default_base_command()` to return the default base command (executable + args), downloading/installing
dependencies beforehand if necessary
- Implement `_create_launch_command_from_base_command(base_command)` to add any further arguments, producing the
final launch command
- **`LanguageServerDependencyProviderSinglePath`** - Alternative to inheriting from `...BaseCommand` directly for the case
of a single core dependency (e.g., an executable or JAR file); use when the single path is not used directly as a base command
otherwise inherit from `...BaseCommand` directly
- Implement `_get_or_install_core_dependency()` to return the path to the core dependency, downloading/installing it automatically if necessary
- Implement `_create_launch_command(core_path)` to build the full command from the core path
- Reference implementations: `TypeScriptLanguageServer`, `Intelephense`, `ClojureLSP`, `ClangdLanguageServer`, `PyrightServer`
- Reference implementations: `TypeScriptLanguageServer`, `Intelephense`, `ClojureLSP`, `ClangdLanguageServer`
- **`LanguageServerDependencyProvider`** - The base class, which can be directly inherited from for complex cases with multiple dependencies or custom setup
- Implement `create_launch_command()` directly
- **`LanguageServerDependencyProvider`** - The root base class, for complex cases with multiple dependencies or custom setup
- Implement `create_launch_command()` directly (note: no automatic support for user-level launch command overrides in this case)
- Reference implementations: `EclipseJDTLS`, `CSharpLanguageServer`, `MatlabLanguageServer`
**Implementation Pointers::**
- When returning the command, prefer the list-based representation for robustness
- Override `create_launch_command_env` if the launch command needs environment variables to be set (defaults to `{}` in the base implementation)
You should look at at least one existing implementation of each base class to understand how they work.
### 1.2 LSP Initialization
Override initialization methods if needed:
Override `_create_base_initialize_params` to provide server-specific initialization
parameters. The common keys — `processId`, `rootPath`, `rootUri`, `clientInfo` and
`workspaceFolders` — are set centrally by the `InitializeParamsBuilder` (see
`src/solidlsp/initialize_params.py`), so your override MUST NOT set them. Just return
the server-specific settings (typically `capabilities` and `initializationOptions`):
```python
def _get_initialize_params(self) -> InitializeParams:
"""Return language-specific initialization parameters."""
def _create_base_initialize_params(self) -> dict:
"""Return language-specific initialization parameters (server-specific keys only)."""
return {
"processId": os.getpid(),
"rootUri": PathUtils.path_to_uri(self.repository_root_path),
"capabilities": {
# Language-specific capabilities
}
},
# "initializationOptions": {...}, # if the server needs them
}
def _start_server(self):
"""Start the language server with custom handlers."""
# Set up notification handlers
self.server.on_notification("window/logMessage", self._handle_log_message)
# Start server and initialize
# Start server and initialize. Do NOT call _create_base_initialize_params directly;
# _create_initialize_params() wraps it with the builder to add the common keys.
self.server.start()
init_response = self.server.send.initialize(self._get_initialize_params())
init_response = self.server.send.initialize(self._create_initialize_params())
self.server.notify.initialized({})
```
Notes:
- The builder resolves `workspaceFolders` from the language server config (indexed
folders + `ls_additional_workspace_folders`); don't build the folder list yourself.
- To send a folder list nested inside `initializationOptions` (some servers, e.g.
`EclipseJDTLS`/`KotlinLanguageServer`, need this), set it explicitly there — only the
*top-level* `workspaceFolders` is builder-managed.
- To suppress the top-level `workspaceFolders` entirely, override
`_create_initialize_params_builder` and construct `DefaultInitializeParamsBuilder`
with `set_workspace_folders=False`.
After `_start_server` returns, the language server should be fully operational.
If the server requires that one waits for certain notifications or responses before being ready, implement that logic here.
For an example, see `EclipseJDTLS._start_server`.
+56 -26
View File
@@ -1,26 +1,31 @@
# the name by which the project can be referenced within Serena
# the name by which the project can be referenced within Serena/when chatting with the LLM.
project_name: "serena"
# list of languages for which language servers are started; choose from:
# al ansible bash clojure cpp
# cpp_ccls crystal csharp csharp_omnisharp dart
# elixir elm erlang fortran fsharp
# list of languages for which language servers are started (LSP backend only); choose from:
# ada al angular ansible bash
# bsl clojure cpp cpp_ccls crystal
# csharp csharp_omnisharp cue dart elixir
# elm erlang fortran fsharp gdscript
# 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.)
# html java json julia kotlin
# latex lean4 lua luau markdown
# matlab msl nix ocaml pascal
# perl php php_phpactor php_phpantom powershell
# python python_jedi python_pyrefly python_ty r
# rego ruby ruby_solargraph rust scala
# scss solidity svelte swift systemverilog
# terraform toml typescript typescript_vts vue
# yaml zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# 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.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
@@ -73,7 +78,7 @@ initial_prompt: |
Comments:
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
clearly defines *what* it is. Any details then follow in subsequent sentences.
Pull requests:
Read `mem:creating_pull_requests` when asked to participate in the creation of a pull request.
@@ -136,8 +141,8 @@ ignored_memory_patterns: []
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
ls_specific_settings: {}
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
@@ -145,13 +150,38 @@ ls_specific_settings: {}
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
added_modes:
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
# the command runs in the project root directory and is only executed if the project is trusted
# (see trusted_project_path_patterns in the global configuration).
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
# example: activation_command: "npx nx run-many -t build"
activation_command:
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
# must be a positive number.
activation_command_timeout: 180.0
# list of workspace folder paths (LSP backend only).
# These folders will be used to build up Serena's symbol index.
# Paths must be within the project root and should thus be relative to the project root.
# Furthermore, the paths should not be filtered by ignore settings.
# Default setting: The entire project root folder (".") is considered.
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
# ls_workspace_folders:
# - "./subproject1"
# - "./subproject2"
ls_workspace_folders:
- "."
# list of additional workspace folder paths for cross-package reference support.
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries.
# Currently supported for: TypeScript.
# symbols and references across package boundaries, but these folders are not indexed by Serena,
# i.e. the respective symbols will not be found using Serena's symbol search tool.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
additional_workspace_folders: []
# - "../sibling-package"
# - "../shared-lib"
ls_additional_workspace_folders: []
+64 -1
View File
@@ -3,9 +3,20 @@
Status of the `main` branch. Changes prior to the next official version change will appear here.
* General:
- Speed up MCP startup when auto-creating projects at startup (e.g. using `--project-from-cwd`) by
determining the project's languages in a background thread #1683
- Fix: in `glob_to_regex` / `search_text(is_glob=True)`, a `?` wildcard matched two characters instead
of one (it emitted `..` rather than `.`); it now matches a single character, consistent with the
`?` semantics documented for `glob_match` and the `test_??.py` example in `search_text`'s docstring.
- Add notion of trusted projects via new global configuration setting `trusted_project_path_patterns`.
Current effects:
- `ls_specific_settings` defined in project configurations will only be applied for trusted projects
- `ls_specific_settings` defined in project configurations will only be applied for trusted projects
- `activation_command` (and `activation_command_timeout`) defined in project configurations will only
be executed for trusted projects: an optional shell command run in the project root before the
language backend initialises (e.g. to generate source files a language server needs to index).
Exit code is the primary completion signal; `activation_command_timeout` (default 180s) is a safety
backstop — on expiry the process is killed and activation continues. Failures and timeouts are
logged but do not abort activation.
- Fix: context or mode argument referencing a known name (e.g. `--context anitgravity`) could result in
incorrect file access if a corresponding local file existed (e.g. `./antigravity` binary);
file access is now guarded with path detection (file ending or path separator must be present)
@@ -18,6 +29,16 @@ Status of the `main` branch. Changes prior to the next official version change w
- Update prompts/instructions: Serena instructions manual, modes (editing, interactive)
- Allow structured tool output to be configured on a per-context basis, disabling it for Claude Code
(which does not correctly unpack structured output) #1042
- Fix: Project-specific filtering of files for source files ignored the language backend.
The check is really only possible for LSP.
- Fix: File system permission errors during gitignore scanning were not caught #1624
- During project creation, language composition percentages are now computed relative to the total number
of recognised source files instead of all files, i.e. unrecognised files are ignored in the percentage
computation.
- Fix: Use LSP-compliant line splitting ("\n", "\r\n" and "\r" can define line breaks); previously only "\n" considered
- Fix: Apply consistent line splitting across tools, uniformly applying the LSP splitting semantics;
affects `search_text` used by `search_for_pattern` tool (reported in #1684)
- Fix: glob pattern expansion in `expand_braces` did not terminate with empty or unbalanced braces #1690
* CLI:
- Fix `--project-from-cwd` hijacking git worktrees nested under a Serena project. `find_project_root`
@@ -36,24 +57,64 @@ Status of the `main` branch. Changes prior to the next official version change w
- Make tool call errors surface explicitly as errors at the MCP protocol level
* Language Servers:
- Fix: `SolidLanguageServer.is_ignored_path` raised `FileNotFoundError` for paths that are not present
on disk, crashing symbol tools when a language server reported locations of generated files
(e.g. JDTLS reporting Lombok-generated classes under `target/classes`). Missing paths are now
classified by the usual ignore rules, and symbol locations resolving to non-existent files are skipped.
- Solidity: fix diagnostics intermittently coming back empty on slow or cold environments (macOS/Windows CI).
- Perl: expose `file_filter` and `ignore_dirs` via `ls_specific_settings["perl"]`, so projects with
non-standard extensions (e.g. `.cgi`, `.psgi`) can make those files visible to Perl::LanguageServer.
Configured extensions are also synced into the Perl source-file matcher (now a cached singleton),
keeping `find_symbol` / symbol indexing consistent with the LS; the matcher is reset on every
language server activation so one project's reconfiguration does not leak into the next.
`FilenameMatcher` gains `add_extensions` / `reset` methods. Defaults are unchanged. #1449
- C/C++ (clangd): improve support and documentation for Unreal Engine 5 projects.
- HLSL (`shader-language-server`): pass `--locked` to `cargo install` when building from source
on macOS (and in the manual-install instructions), honoring the crate's packaged `Cargo.lock`.
Without it, fresh dependency resolution pulled in shader-sense 1.4.0, which no longer compiles
against the pinned shader_language_server 1.3.1, breaking the macOS CI job.
- `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`.
Enables Yarn PnP setups with `typescript.tsdk` pointing at the Yarn-generated SDK.
- TypeScript/VTS: disable automatic typing acquisition during initialization (no network
downloads at startup) and replace the fixed 2-second cross-file reference wait with
event-based `$/progress` indexing tracking (configurable `indexing_timeout`, default 30s)
- C#: minor fixes in Omnisharp and Roslyn that prevented startup on some systems #1617
- `SvelteLanguageServer`: Fix diagnostics requests for TypeScript/JavaScript files incorrectly being
processed by the Svelte LS instead of the TypeScript LS.
- `SvelteLanguageServer`: Fix document-symbol requests for TypeScript/JavaScript files returning empty
results in svelte-only mode (`languages: [svelte]`. #1552
- Svelte + TypeScript: make companion TypeScript server raise on readiness and indexing timeouts (instead
of silent "proceeding anyway"), so that partial indexing surfaces as a clear failure instead of flaky/wrong
cross-file results; add configurable timeouts (`server_ready_timeout`, `indexing_timeout`)
and overridable timeout hooks (base TS stays permissive). Svelte test fixture now uses `npm ci` +
committed `package-lock.json`.
- `JuliaLanguageServer`: Fix the stdio MCP server exiting right after `initialize` ("tools fetch failed")
when `julia` is enabled. #1577
- `Java`: invalidate JDTLS workspace cache when Java import settings change #1576
- `Java`: use `JAVA_HOME` for Gradle import when `use_system_java_home` is enabled and `gradle_java_home`
is unset. #1657
- `Java`: stop hard-ignoring directories named `target`/`build`/`bin`/`out`/`classes`/`dist`/`lib` in
`EclipseJDTLS`. These are all valid Java package identifiers, so ignoring them by name hid legitimate
source from the symbol tools even when they were not gitignored. Removed the hardcoded override; real
build output is already excluded via `.gitignore`. #1645
- Improve quoting of arguments in shell executions
- Add **LaTeX** support (experimental) via [texlab](https://github.com/latex-lsp/texlab).
- Add **QML** support via Qt's [`qmlls`](https://doc.qt.io/qt-6/qtqml-tool-qmlls.html) language
server (requires Qt 6 with `qmlls`/`qmlls6` on PATH). #1381
- PHP: add support for PHPantom as alternative to the already supported PHP LS #1554.
- Add new launch command customization options: `ls_args`, `ls_extra_args` and `ls_base_cmd`
- Add new configuration option `ls_workspace_folders` to allow indexed source folders to be specified
explicitly. In monorepos, this allows the set of indexed folders to be restricted to a subset of
the repository. #1627
- Rename configuration option `additional_workspace_folders` to `ls_additional_workspace_folders`
and support the option across all language servers (previously limited to TypeScript).
- `Pyright`: bump timeout for waiting for initial analysis from 5s to 60s.
* JetBrains:
- Add configuration option `jetbrains_launch_command`, allowing Serena to spawn IDE instances automatically
upon project activation
- Fix: `jet_brains_list_inspections` failed when only default parameters were used #1615
- Fix: `jet_brains_run_inspections` returned incorrectly transformed data (malforming snake-case conversion applied to all keys)
* Dashboard:
- Make list of trusted hosts configurable, fixing host validation introduced in v1.5.2 allowing only
@@ -69,6 +130,8 @@ Status of the `main` branch. Changes prior to the next official version change w
* Memories:
- Make memory iteration follow symbolic links
- Fix: a memory name that was absolute (e.g. `/etc/cron.d/backdoor`) or contained empty path
segments could write outside the `memories` folder.
* Dependencies:
- Add dependency `oslex`
@@ -130,6 +130,8 @@ Some languages require additional installations or setup steps, as noted.
[pyrefly](https://github.com/facebook/pyrefly) (language `python_pyrefly`),
[Jedi](https://github.com/palotas/jedi-language-server) (language `python_jedi`);
ty and pyrefly require `uv`/`uvx` in PATH)
* **QML**
(requires Qt 6, provides `qmlls` or `qmlls6` on PATH; see the [Qt qmlls documentation](https://doc.qt.io/qt-6/qtqml-tool-qmlls.html))
* **R**
(requires installation of the `languageserver` R package)
* **Ruby**
+1 -1
View File
@@ -397,7 +397,7 @@ Add the `serena` MCP server configuration
"command": "serena",
"args": [
"start-mcp-server",
"--context=claude-desktop"
"--context=desktop-app"
]
}
}
+8 -34
View File
@@ -13,7 +13,7 @@ setting up a project with Serena typically involves the following steps:
4. **Working on coding tasks**: Using Serena to help you with actual coding tasks in the project
(project-creation-indexing)=
## Project Creation & Indexing
## Project Creation
Project creation is the process of defining fundamental project settings that are relevant to Serena's operation.
@@ -38,7 +38,7 @@ To explicitly create a project, use the following command while in the project d
* You can immediately index the project after creation with `--index`.
(project-config)=
#### Project Configuration
## Project Configuration
After creation, you can adjust the project settings in the generated `.serena/project.yml` file
within the project directory.
@@ -51,8 +51,9 @@ The file allows you to configure ...
* the encoding used in source files
* ignore rules
* write access
* [additional workspace folders](additional-workspace-folders) for cross-package reference support in monorepos
* the list of workspace folders to be processed by language servers (when using the LSP backend)
* an initial prompt that shall be passed to the LLM whenever the project is activated
* a shell command to run upon project activation (prior to language backend initialisation)
* the set of tools and modes to use for the project
* and some other settings.
@@ -60,44 +61,17 @@ 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).
:::{note}
Many settings in project.yml *extend* or *override* settings in the global configuration file `serena_config.yml`.
Many settings in project.yml *extend* or *override* settings in [Serena's global configuration](global-config).
So use the project configuration specifically for aspects that apply only to the particular project.
:::
**Local Overrides**. The project.yml file is intended to be versioned together with the project.
**Local Overrides**. The `project.yml` file is intended to be versioned together with the project.
You can specify local overrides for the settings in a `project.local.yml` file in the same directory
(which, by default, is ignored by git).
Any keys defined therein will override the respective key in `project.yml`.
(additional-workspace-folders)=
#### Additional Workspace Folders (Cross-Package References)
In monorepos or multi-package setups, Serena's language server normally only sees symbols within the
project root. To enable cross-package references (e.g. `find_referencing_symbols` discovering usages
in sibling packages), configure `additional_workspace_folders` in your `project.yml`:
```yaml
additional_workspace_folders:
- ../shared-lib
- ../api-client
- /absolute/path/to/another-package
```
Paths can be absolute or relative to the project root. Each folder is registered as an LSP workspace
folder, and the language server will discover symbols and references across all listed packages.
**Currently supported for:** TypeScript. Other language servers will raise an error if this setting
is used with them. Support for additional languages can be added by implementing the
`_find_representative_source_file()` method in the respective language server class.
:::{note}
Each additional workspace folder adds startup time, as the language server needs to index the
additional projects. For large monorepos, consider listing only the packages you actively need
cross-references for.
:::
(indexing)=
### Indexing
## Indexing
:::{note}
Indexing is not a relevant operation when using the JetBrains plugin, as indexing is handled by the IDE.
@@ -109,7 +83,7 @@ that requires symbol information.
While in the project directory, run this command:
<serena> project index
serena project index
Indexing has to be called only once. During regular usage, Serena will automatically update the index whenever files change.
+91 -19
View File
@@ -7,7 +7,7 @@ You can disable tools, change Serena's fundamental instructions
(what we denote as the `system_prompt`), adjust the output of tools that just provide a prompt,
and even adjust tool descriptions.
Serena is configured in using a multi-layered approach:
Serena is configured using a multi-layered approach:
* **global configuration** (`serena_config.yml`, see below)
* **project configuration** (`project.yml`, see [Project Configuration](project-config))
@@ -216,31 +216,69 @@ ls_specific_settings:
```
(override-ls-path)=
#### Overriding the Language Server Path
#### Customizing the Language Server Launch Command
Most of Serena's language servers, particularly those that use a single core path for the language server (e.g. the main executable),
support overriding that path via the `ls_path` setting.
Therefore, if you have installed the language server yourself and want to use your installation
instead of Serena's managed installation, you can set the `ls_path` setting as follows:
Most of Serena's language servers construct the command that launches the language server process from
a *base command* or a *core dependency*.
For these language servers, the following settings can be used to customize the launch command:
| Setting | Description |
|--------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ls_path` (string) or `ls_base_cmd` (list) | overrides the path of the language server's core dependency (`ls_path`), e.g. its executable or a JAR file, or a base command for its execution (`ls_base_cmd`), e.g. `["npx", "-y", "/my/local/package"]`. Use this if you have installed the language server yourself and want Serena to use your installation instead of its managed installation. |
| `ls_args` (list) | overrides the internal command construction completely and simply adds `ls_args` to the base command |
| `ls_extra_args` (list) | a list of additional arguments to append to the launch command |
* If you set `ls_args`, the internal command construction (which may do more than to append arguments to a base command) is bypassed.
You can define the full launch command by providing both `ls_path`/`ls_base_cmd` and `ls_args`.
* If `ls_args` is not set, the internal command construction (which sets default arguments) is applied, and you can use `ls_path` or `ls_base_cmd` to override the path of the core dependency/the base command.
* `ls_extra_args` is always appended to the end of the launch command.
Example:
```yaml
ls_specific_settings:
<language>:
ls_path: "/path/to/language-server"
ls_extra_args: ["--log-level=debug"]
```
This is supported by all language servers deriving their dependency provider from `LanguageServerDependencyProviderSinglePath`,
and by some additional wrappers that explicitly expose `ls_path`.
These settings are supported by all language servers whose dependency provider derives from
`LanguageServerDependencyProviderBaseCommand`, and `ls_path` is additionally exposed by some implementations explicitly.
Common examples include: `ansible`, `bash`, `bsl`, `clojure`, `cpp`, `cpp_ccls`, `hlsl`, `html`, `kotlin`, `lean4`, `luau`, `markdown`, `php`,
`php_phpactor`, `python`, `rust`, `scss`, `solidity`, `systemverilog`, `toml`, `typescript`, and `yaml`.
Note: `angular` does **not** support `ls_path` — the Angular language server is part of a multi-process orchestration
(`ngserver` plus a companion TypeScript language server with the `@angular/language-service` plugin and an HTML
companion) where the dependency layout matters; use the version overrides documented in the Angular section below
to pin specific releases of the bundled stack.
If `ls_path` is set, Serena's managed download or install is bypassed for that language server.
In that case, any server-specific version or registry settings do not apply.
If a language server supports `ls_path`, setting it bypasses Serena's managed download or install for that server.
In that case, any server-specific version or registry settings only apply when `ls_path` is not set.
(override-init-options)=
#### Overriding Language Server Initialization Options
When Serena starts a language server, it sends a set of `initializationOptions` as part of the
Language Server Protocol `initialize` request. These options are constructed internally and are
tailored to each language server. In some cases, you may want to override or extend these options,
e.g. to enable a feature or to adjust a behavior that is specific to your setup.
Under the key `initializationOptions` within a language's `ls_specific_settings`, you can provide a
dictionary of options that is applied on top of the internally constructed `initializationOptions`.
The values are combined at the top level only: for each top-level key you define, your value
replaces the original value for that key exactly as given (there is no recursive/deep merge of
nested dictionaries). Internally constructed keys that you do not define are left unchanged.
* If Serena constructs `initializationOptions` for the language server, each top-level key you
provide replaces the internally constructed value for that same key, while all other internally
constructed keys are retained.
* If Serena does not construct any `initializationOptions` for the language server, your custom
options are used as-is.
Example:
```yaml
ls_specific_settings:
<language>:
initializationOptions:
someFeature:
enabled: true
```
#### AL
@@ -657,8 +695,8 @@ The following settings are supported for the Java language server:
| `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_java_home` | `null` | Path to the JDK used by Gradle. When unset, Gradle uses `JAVA_HOME` if `use_system_java_home` is enabled and `JAVA_HOME` is set; otherwise it falls back to Serena's bundled JRE. |
| `use_system_java_home` | `false` | Use the system's `JAVA_HOME` environment variable for JDTLS itself and, when `gradle_java_home` is unset, Gradle import. Enable this if your project requires a specific JDK vendor or version for Gradle's JDK checks. |
| `gradle_version` | `8.14.2` | (vscode-java mode only) Override the Gradle distribution version Serena downloads by default. |
| `vscode_java_version` | `1.54.0-923` | (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. |
@@ -833,6 +871,34 @@ Notes:
- Use the FPC compiler driver (`fpc`/`fpc.exe`), not backend compilers like `ppc386.exe`.
- These settings are passed as environment variables to the pasls process.
#### Perl
Serena uses [Perl::LanguageServer](https://metacpan.org/pod/Perl::LanguageServer) for Perl support. Install Perl and the server with `cpanm Perl::LanguageServer`; Linux and macOS only (the server does not run on Windows).
Perl::LanguageServer only indexes files whose extension is in its `perl.fileFilter` and skips directories listed in `perl.ignoreDirs`. Both are exposed below so projects with non-standard extensions (e.g. `.cgi` / `.psgi` web handlers) can make those files visible (#1449).
**Configuration:**
Configure the language server via `ls_specific_settings.perl` in `serena_config.yml`:
| Setting | Default | Description |
| -------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `file_filter` | `[".pm", ".pl", ".t"]` | File extensions (with leading dot) that Perl::LanguageServer should index, e.g. `[".pm", ".pl", ".t", ".cgi"]`. |
| `ignore_dirs` | `[".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"]` | Directory names Perl::LanguageServer should skip when indexing. |
Example configuration:
```yaml
ls_specific_settings:
perl:
file_filter: [".pm", ".pl", ".t", ".cgi", ".psgi"]
ignore_dirs: [".git", "blib", "local", "vendor", "cover_db"]
```
Notes:
- Extensions added via `file_filter` are also synced into Serena's Perl source-file matcher, so `find_symbol` and symbol indexing treat the same files as the language server. Defaults are unchanged when these keys are omitted.
- The matcher is reset on every language server activation, so one project's `file_filter` does not leak into another.
#### PHP (`Intelephense`)
Serena uses Intelephense for the `php` language key.
@@ -992,9 +1058,8 @@ Supported settings:
| `typescript_version` | `5.9.3` | Override the bundled `typescript` npm package version Serena installs when `ls_path` is not set. |
| `typescript_language_server_version` | `5.1.3` | Override the bundled `typescript-language-server` npm package version Serena installs when `ls_path` is not set. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
TypeScript supports [additional workspace folders](additional-workspace-folders) for cross-package
reference discovery. Configure `additional_workspace_folders` in `project.yml` to enable this feature.
| `indexing_timeout` | `30.0` | Timeout in seconds for waiting on tsserver's `$/progress` project-indexing signal (both at startup and before the first cross-file reference query). If indexing does not complete within this window, Serena logs a warning and proceeds anyway. Increase it for very large projects. |
| `server_ready_timeout` | `10.0` | Timeout in seconds for waiting on the server-ready signal after initialization. If the signal does not arrive within this window, Serena logs a message and proceeds anyway. |
#### Svelte
@@ -1012,8 +1077,15 @@ Supported settings:
| `typescript_language_server_version` | `5.1.3` (falls back to `ls_specific_settings.typescript.typescript_language_server_version`) | Override the `typescript-language-server` npm package version for the companion server. |
| `typescript_svelte_plugin_version` | `0.3.52` | Override the `typescript-svelte-plugin` npm package version used for `.svelte`-aware TS resolution. |
| `npm_registry` | `null` | Override the npm registry Serena uses for all managed installs. |
| `indexing_timeout` | `120.0` (falls back to `ls_specific_settings.typescript.indexing_timeout`) | Timeout in seconds for the companion TS server to finish indexing `.svelte` files. On timeout, startup fails with a diagnostic indexing-state summary instead of serving cross-file results from a partially indexed program. |
| `initialization_options_configuration` | `{}` | Deep-merge overrides for any of the ten plugin configuration sections (`svelte`, `prettier`, `emmet`, `typescript`, `javascript`, `js/ts`, `css`, `less`, `scss`, `html`). |
Unlike the plain TypeScript server, the companion is strict about readiness: it raises on server-ready and
indexing timeouts instead of proceeding with a cold or partially indexed program (which would silently degrade
cross-file renames and references). The companion reads `server_ready_timeout` and `indexing_timeout` from
`ls_specific_settings.typescript` with raised defaults (30s and 120s respectively); `ls_specific_settings.svelte.indexing_timeout`
takes precedence for the `.svelte`-file indexing wait.
All four packages are tracked via a version file; changing any version setting triggers a clean reinstall.
#### TypeScript via `vtsls`
+6 -2
View File
@@ -97,7 +97,7 @@ dev = [
"pyasn1==0.6.3",
"PyJWT==2.12.0",
]
agno = ["agno==2.5.10", "sqlalchemy==2.0.41"] # agno bumped for session state overwrite CVE
agno = ["agno==2.6.6", "sqlalchemy==2.0.41"] # agno bumped for session state overwrite CVE
google = ["google-genai==1.27.0"]
[project.urls]
@@ -173,6 +173,7 @@ format = ["_ruff_fix", "_ruff_format"]
_ty_core = "ty check src/serena src/solidlsp"
_ty_test = "ty check test --exclude test/resources"
type-check = ["_ty_core", "_ty_test"]
ty = ["type-check"]
# docs
_autogen_docs = "python docs/autogen_docs.py"
_sphinx_build = "sphinx-build -b html docs docs/_build -W --keep-going"
@@ -377,11 +378,14 @@ markers = [
"scss: language server running for SCSS / Sass / CSS (some-sass-language-server)",
"angular: language server running for Angular (uses @angular/language-server + @angular/language-service tsserver plugin)",
"ada: language server running for Ada / SPARK (uses AdaCore Ada Language Server)",
"qml: language server running for QML (uses Qt's qmlls)",
]
[tool.codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = '.git*,*.svg,*.lock,*.min.*,*test_memories_manager.py'
# We skip third-party test fixture repositories entirely (they contain
# external package lockfiles, minified code, and arbitrary dependency strings).
skip = '.git*,*.svg,*.lock,*.min.*,*test_memories_manager.py,./test/resources/repos'
check-hidden = true
ignore-regex = '\.\w+'
ignore-words-list = 'paket,EDN,als,ue'
+49 -7
View File
@@ -63,6 +63,7 @@ from serena.util.inspection import iter_subclasses
from serena.util.logging import MemoryLogHandler
from solidlsp.ls_config import Language
from solidlsp.util import subprocess_util
from solidlsp.util.subprocess_util import terminate_process_tree_with_kill_fallback
if TYPE_CHECKING:
from serena.gui_log_viewer import GuiLogViewer
@@ -640,12 +641,10 @@ class SerenaAgent:
# determine the effective language backend for this session.
# If a startup project is provided and has a per-project override, use it; otherwise use the global config.
# Since we don't want to change the toolset after startup, the language backend cannot be changed within a running Serena session
self._language_backend = self.serena_config.language_backend
if registered_project_to_activate is not None and registered_project_to_activate.project_config.language_backend is not None:
self._language_backend = registered_project_to_activate.project_config.language_backend
log.info(f"Using language backend as configured in project.yml: {self._language_backend.name}")
else:
log.info(f"Using language backend from global configuration: {self._language_backend.name}")
self._language_backend = self.serena_config.determine_language_backend(
project_config=registered_project_to_activate.project_config if registered_project_to_activate is not None else None,
log_choice=True,
)
# create the tool names mapping for prompts
self._prompt_tool_names_mapping = self._create_prompt_tool_names_mapping(self._language_backend)
@@ -1202,6 +1201,7 @@ class SerenaAgent:
self._update_active_tools()
def init_project_services() -> None:
self._run_project_activation_command(project)
self._init_active_project_language_backend()
# initialise the project's language backend in the background
@@ -1216,6 +1216,48 @@ class SerenaAgent:
return True
@staticmethod
def _run_project_activation_command(project: Project) -> None:
"""
Runs the given project's activation_command (if set and the project is trusted).
Failures are logged.
"""
activation_command = project.project_config.activation_command
if not activation_command:
return
if not project.is_trusted():
log.warning(
f"Project path {project.project_root} is not trusted, ignoring activation_command "
"from project configuration. To trust the project, modify the trusted path patterns "
"in the global configuration."
)
return
timeout = project.project_config.activation_command_timeout
cmd = subprocess_util.convert_shell_cmd(activation_command)
log.info(f"Running activation_command for project '{project.project_name}': {cmd}")
try:
with LogTime("Project activation command", logger=log):
p = subprocess.Popen(
cmd,
shell=True,
cwd=project.project_root,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
**subprocess_util.subprocess_kwargs(),
)
try:
_, stderr = p.communicate(timeout=timeout)
if p.returncode != 0:
log.error(f"activation_command for project '{project.project_name}' failed (exit {p.returncode}): {stderr.strip()}")
except subprocess.TimeoutExpired:
log.error(
f"Activation_command for project '{project.project_name}' timed out after "
f"{timeout}s; terminating process and continuing with backend initialisation."
)
terminate_process_tree_with_kill_fallback(p, terminate_timeout=5.0, process_name="activation_command")
except Exception:
log.exception(f"Unexpected error running activation_command for project '{project.project_name}'")
def _init_active_project_language_backend(self) -> None:
"""
Initialises the active project's language backend
@@ -1258,7 +1300,7 @@ class SerenaAgent:
if project_instance is not None:
log.info(f"Found registered project '{project_instance.project_name}' at path {project_instance.project_root}")
elif os.path.isdir(project_root_or_name):
project_instance = self.serena_config.add_project_from_path(project_root_or_name)
project_instance = self.serena_config.add_project_from_path(project_root_or_name, asynchronous_autogen=True)
log.info(f"Added new project {project_instance.project_name} for path {project_instance.project_root}")
if project_instance is None:
+4 -10
View File
@@ -1124,17 +1124,11 @@ class MemoryCommands(AutoRegisteringGroup):
exist at the configured location). Never auto-creates the ``.serena`` directory —
if no project configuration is found, the user is directed at ``serena project create``.
"""
from serena.project import Project
serena_config = SerenaConfig.from_config_file()
project_path = os.path.abspath(project)
try:
proj = Project.load(project_path, serena_config=serena_config, autogenerate=False)
except FileNotFoundError as e:
raise click.UsageError(
f"{e}\nNo Serena project found at {project_path}. Create one first with:\n serena project create {project_path}"
) from e
return proj.memory_manager
registered_project = serena_config.get_registered_project(project)
if registered_project is None:
raise click.UsageError(f"No Serena project found for '{project}'. Create one first.")
return registered_project.get_project_instance(serena_config).memory_manager
@staticmethod
@click.command(
+219 -88
View File
@@ -6,6 +6,7 @@ import dataclasses
import os
import re
import shutil
import threading
from collections.abc import Iterator, Sequence
from copy import deepcopy
from dataclasses import dataclass, field
@@ -298,33 +299,105 @@ Uses $projectDir and $projectFolderName as placeholders.
"""
class ProjectConfigAutoGenerationMode(Enum):
NONE = "none"
"""
no auto-generation
"""
SYNCHRONOUS = "sync"
"""
synchronous auto-generation, i.e. the configuration is fully generated before returning from the function call
"""
ASYNCHRONOUS = "async"
"""
asynchronous auto-generation, where time-consuming configuration parts (currently only the
list of programming languages) are determined in a background thread and initialised as empty
"""
def is_autogen_enabled(self):
return self != ProjectConfigAutoGenerationMode.NONE
@dataclass(kw_only=True)
class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
project_name: str
languages: list[Language]
ignored_paths: list[str] = field(default_factory=list)
additional_workspace_folders: list[str] = field(default_factory=list)
ls_workspace_folders: list[str] = field(default_factory=lambda: ["."])
ls_additional_workspace_folders: list[str] = field(default_factory=list)
read_only: bool = False
ignore_all_files_in_gitignore: bool = True
initial_prompt: str = ""
encoding: str = DEFAULT_SOURCE_FILE_ENCODING
activation_command: str | None = None
activation_command_timeout: float = 180.0
# internal fields which are not mapped to/from the configuration file (must start with "_")
_local_override_keys: list[str] = field(default_factory=list)
# class-level constants
# class-level members
SERENA_PROJECT_FILE = "project.yml"
SERENA_LOCAL_PROJECT_FILE = "project.local.yml"
FIELDS_WITHOUT_DEFAULTS = {"project_name", "languages"}
RENAMED_FIELDS = {"additional_workspace_folders": "ls_additional_workspace_folders"}
YAML_COMMENT_NORMALISATION = YamlCommentNormalisation.LEADING
"""
the comment normalisation strategy to use when loading/saving project configuration files.
The template file must match this configuration (i.e. it must use leading comments if this is set to LEADING).
"""
_async_completion_events = {}
"""
maps the object id of a ProjectConfig instance to an event which is set when the asynchronous auto-generation of
the configuration is complete (if applicable).
"""
_save_lock = threading.Lock()
def _tostring_includes(self) -> list[str]:
return ["project_name"]
@classmethod
def _determine_project_languages(cls, project_root: str, interactive: bool) -> list[Language]:
# determine languages automatically
log.info("Determining programming languages used in the project")
language_composition = determine_programming_language_composition(project_root)
log.info("Language composition: %s", language_composition)
if len(language_composition) == 0:
log.warning(
"No source files for supported language servers were found in %s. "
"Creating project with no configured languages. "
"Symbol-related tools (e.g. find_symbol, get_symbols_overview) will not work "
"when using the LSP backend. You can add languages later via the Serena dashboard "
"or by manually editing the project configuration.",
project_root,
)
languages_to_use = []
else:
# sort languages by number of files found
languages_and_percentages = sorted(
language_composition.items(), key=lambda item: (item[1], item[0].get_priority()), reverse=True
)
# find the language with the highest percentage and enable it
top_language_pair = languages_and_percentages[0]
other_language_pairs = languages_and_percentages[1:]
languages_to_use = [top_language_pair[0]]
# if in interactive mode, ask the user which other languages to enable
if len(other_language_pairs) > 0 and interactive:
print(
"Detected and enabled main language '%s' (%.2f%% of source files)." % (top_language_pair[0].value, top_language_pair[1])
)
print(f"Additionally detected {len(other_language_pairs)} other language(s).\n")
print("Note: Enable only languages you need symbolic retrieval/editing capabilities for.")
print(" Additional language servers use resources and some languages may require additional")
print(" system-level installations/configuration (see Serena documentation).")
print("\nWhich additional languages do you want to enable?")
for lang, perc in other_language_pairs:
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang.value, perc), default=False)
if enable:
languages_to_use.append(lang)
print()
log.info("Using languages: %s", languages_to_use)
return languages_to_use
@classmethod
def autogenerate(
cls,
@@ -334,6 +407,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
languages: list[Language] | None = None,
save_to_disk: bool = True,
interactive: bool = False,
asynchronous: bool = False,
) -> Self:
"""
Autogenerate a project configuration for a given project root.
@@ -345,8 +419,12 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
:param languages: the languages of the project; if None, they will be determined automatically
:param save_to_disk: whether to save the project configuration to disk
:param interactive: whether to run in interactive CLI mode, asking the user for input where appropriate
:param asynchronous: whether to run in asynchronous mode, where time-consuming configuration parts (currently only the
determination of the list of programming languages) are determined in a background thread and initialised as empty
:return: the project configuration
"""
if interactive and asynchronous:
raise ValueError("Cannot use interactive mode with asynchronous auto-generation")
project_root = Path(project_root).resolve()
if not project_root.exists():
raise FileNotFoundError(f"Project root not found: {project_root}")
@@ -354,61 +432,62 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
log.info("Project root: %s", project_root)
project_folder_name = project_root.name
project_name = project_name or project_folder_name
use_asynchronous_language_determination = False
if languages is None:
# determine languages automatically
log.info("Determining programming languages used in the project")
language_composition = determine_programming_language_composition(str(project_root))
log.info("Language composition: %s", language_composition)
if len(language_composition) == 0:
log.warning(
"No source files for supported language servers were found in %s. "
"Creating project with no configured languages. "
"Symbol-related tools (e.g. find_symbol, get_symbols_overview) will not work "
"when using the LSP backend. You can add languages later via the Serena dashboard "
"or by manually editing the project configuration.",
project_root,
)
languages_to_use: list[str] = []
if asynchronous:
use_asynchronous_language_determination = True
languages_to_use = [] # temporarily empty, will be determined in background thread
else:
# sort languages by number of files found
languages_and_percentages = sorted(
language_composition.items(), key=lambda item: (item[1], item[0].get_priority()), reverse=True
)
# find the language with the highest percentage and enable it
top_language_pair = languages_and_percentages[0]
other_language_pairs = languages_and_percentages[1:]
languages_to_use = [top_language_pair[0].value]
# if in interactive mode, ask the user which other languages to enable
if len(other_language_pairs) > 0 and interactive:
print(
"Detected and enabled main language '%s' (%.2f%% of source files)."
% (top_language_pair[0].value, top_language_pair[1])
)
print(f"Additionally detected {len(other_language_pairs)} other language(s).\n")
print("Note: Enable only languages you need symbolic retrieval/editing capabilities for.")
print(" Additional language servers use resources and some languages may require additional")
print(" system-level installations/configuration (see Serena documentation).")
print("\nWhich additional languages do you want to enable?")
for lang, perc in other_language_pairs:
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang.value, perc), default=False)
if enable:
languages_to_use.append(lang.value)
print()
log.info("Using languages: %s", languages_to_use)
determined_languages = cls._determine_project_languages(str(project_root), interactive=interactive)
languages_to_use = [l.value for l in determined_languages]
else:
languages_to_use = [lang.value for lang in languages]
config_with_comments, _ = cls._load_yaml_dict(PROJECT_TEMPLATE_FILE)
config_with_comments["project_name"] = project_name
config_with_comments["languages"] = languages_to_use
project_yml_path = serena_config.get_project_yml_location(str(project_root))
if save_to_disk:
project_yml_path = serena_config.get_project_yml_location(str(project_root))
log.info("Saving project configuration to %s", project_yml_path)
save_yaml(project_yml_path, config_with_comments)
with cls._save_lock:
save_yaml(project_yml_path, config_with_comments)
project_local_yml_path = os.path.join(os.path.dirname(project_yml_path), cls.SERENA_LOCAL_PROJECT_FILE)
shutil.copy(PROJECT_LOCAL_TEMPLATE_FILE, project_local_yml_path)
return cls._from_dict(config_with_comments, local_override_keys=[])
project_config = cls._from_dict(config_with_comments, local_override_keys=[])
# if asynchronous language determination is used, start a background thread which updates and saves the configuration
# and set an event which can be awaited to ensure that the configuration is complete
if use_asynchronous_language_determination:
event = threading.Event()
project_config._async_completion_events[id(project_config)] = event
def async_language_determination():
try:
with LogTime("Asynchronous language determination", logger=log):
project_config.languages = cls._determine_project_languages(str(project_root), interactive=False)
if save_to_disk:
project_config.save(project_yml_path)
finally:
event.set()
threading.Thread(target=async_language_determination, name="project-language-determination", daemon=True).start()
return project_config
def await_asynchronous_completion(self):
"""
Wait for the asynchronous auto-generation of the configuration to complete (if applicable), ensuring
that, in particular, the list of programming languages is complete, which may be determined asynchronously
when first creating the project configuration.
"""
event = ProjectConfig._async_completion_events.get(id(self))
if event is None:
return
if not event.is_set():
log.info("Waiting for asynchronous auto-generation of project configuration to complete ...")
event.wait()
log.info("Asynchronous auto-generation of project configuration completed.")
@classmethod
def default_project_yml_path(cls, project_root: str | Path) -> str:
@@ -441,9 +520,22 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
and `was_complete` will always be True.
"""
data = load_yaml(yml_path, comment_normalisation=comment_normalisation)
was_complete = True
# backward compatibility
# NOTE: This must also work for project.local.yml files, which may be highly incomplete
# * handle single "language" field
if "languages" not in data and "language" in data:
data["languages"] = [data["language"]]
del data["language"]
# * handle renamed fields
for old_key, new_key in cls.RENAMED_FIELDS.items():
if old_key in data and new_key not in data:
data[new_key] = data[old_key]
del data[old_key]
was_complete = False
# apply defaults
was_complete = True
if apply_defaults:
for field_info in dataclasses.fields(cls):
key = field_info.name
@@ -456,13 +548,6 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
default_value = get_dataclass_default(cls, key)
data.setdefault(key, default_value)
# backward compatibility
# NOTE: This must also work for project.local.yml files, which may be highly incomplete
# * handle single "language" field
if "languages" not in data and "language" in data:
data["languages"] = [data["language"]]
del data["language"]
# Note: Checks for validity of fields must not happen here but in _from_dict.
# Here, the data may be incomplete, because this function is also used for
# loading project.local.yml files.
@@ -478,6 +563,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
the ProjectConfig dataclass
:param local_override_keys: the list of keys that have been overridden from project.local.yml
"""
# map languages to list of enum items, checking for errors
lang_name_mapping = {"javascript": "typescript"}
languages: list[Language] = []
for language_str in data["languages"]:
@@ -493,6 +579,15 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
f"Invalid language: {orig_language_str}.\nValid language_strings are: {[l.value for l in Language]}"
) from e
# Validate activation_command_timeout
activation_command_timeout_raw = data.get("activation_command_timeout", 180.0)
try:
activation_command_timeout = float(activation_command_timeout_raw)
except (TypeError, ValueError) as e:
raise ValueError(f"activation_command_timeout must be a number, got: {activation_command_timeout_raw}") from e
if activation_command_timeout <= 0:
raise ValueError(f"activation_command_timeout must be positive, got: {activation_command_timeout}")
# Validate symbol_info_budget
symbol_info_budget_raw = data["symbol_info_budget"]
symbol_info_budget = symbol_info_budget_raw
@@ -515,7 +610,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
fixed_tools = data["fixed_tools"] or []
excluded_tools = data["excluded_tools"] or []
included_optional_tools = data["included_optional_tools"] or []
additional_workspace_folders = data.get("additional_workspace_folders") or []
additional_workspace_folders = data.get("ls_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.")
@@ -524,7 +619,8 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
project_name=data["project_name"],
languages=languages,
ignored_paths=ignored_paths,
additional_workspace_folders=additional_workspace_folders,
ls_workspace_folders=data["ls_workspace_folders"],
ls_additional_workspace_folders=additional_workspace_folders,
excluded_tools=excluded_tools,
fixed_tools=fixed_tools,
included_optional_tools=included_optional_tools,
@@ -540,6 +636,8 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
default_modes=data["default_modes"],
symbol_info_budget=symbol_info_budget,
ls_specific_settings=data.get("ls_specific_settings", {}),
activation_command=data.get("activation_command"),
activation_command_timeout=activation_command_timeout,
_local_override_keys=local_override_keys,
)
@@ -567,13 +665,18 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
return os.path.join(os.path.dirname(project_yml_path), cls.SERENA_LOCAL_PROJECT_FILE)
@classmethod
def load(cls, project_root: Path | str, serena_config: "SerenaConfig", autogenerate: bool = False) -> Self:
def load(
cls,
project_root: Path | str,
serena_config: "SerenaConfig",
autogen: ProjectConfigAutoGenerationMode = ProjectConfigAutoGenerationMode.NONE,
) -> Self:
"""
Load a ProjectConfig instance from the path to the project root.
:param project_root: the path to the project root
:param serena_config: the global Serena configuration
:param autogenerate: whether to auto-generate the configuration if it does not exist
:param autogen: the auto-generation mode to apply if the project configuration does not yet exist
"""
project_root = Path(project_root)
project_folder_name = project_root.name
@@ -582,8 +685,8 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
# auto-generate if necessary
if not os.path.exists(yaml_path):
if autogenerate:
return cls.autogenerate(project_root, serena_config)
if autogen.is_autogen_enabled():
return cls.autogenerate(project_root, serena_config, asynchronous=autogen == ProjectConfigAutoGenerationMode.ASYNCHRONOUS)
else:
raise FileNotFoundError(f"Project configuration file not found: {yaml_path}")
@@ -630,34 +733,35 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
config_path = project_yml_path
log.info("Saving updated project configuration to %s", config_path)
# get the current configuration as a dictionary
cur_dict = self._to_yaml_dict()
with self._save_lock:
# get the current configuration as a dictionary
cur_dict = self._to_yaml_dict()
# load commented map from the original file and update all non-overridden keys
config_with_comments, _ = self._load_yaml_dict(config_path, self.YAML_COMMENT_NORMALISATION)
for key in cur_dict:
if key not in self._local_override_keys:
config_with_comments[key] = cur_dict[key]
# load commented map from the original file and update all non-overridden keys
config_with_comments, _ = self._load_yaml_dict(config_path, self.YAML_COMMENT_NORMALISATION)
for key in cur_dict:
if key not in self._local_override_keys:
config_with_comments[key] = cur_dict[key]
# transfer missing comments from the template file
template_config, _ = self._load_yaml_dict(PROJECT_TEMPLATE_FILE, self.YAML_COMMENT_NORMALISATION)
transfer_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION, force_update_all=True)
# transfer missing comments from the template file
template_config, _ = self._load_yaml_dict(PROJECT_TEMPLATE_FILE, self.YAML_COMMENT_NORMALISATION)
transfer_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION, force_update_all=True)
# save project.yml
save_yaml(config_path, config_with_comments)
# save project.yml
save_yaml(config_path, config_with_comments)
# update project.local.yml to reflect overridden keys if necessary
if save_project_local_yml:
project_local_yml_path = self._project_local_yml_path(project_yml_path)
if self._local_override_keys and os.path.exists(project_local_yml_path):
log.info("Saving updated local project configuration to %s", project_local_yml_path)
local_config_with_comments, _ = self._load_yaml_dict(
project_local_yml_path, comment_normalisation=YamlCommentNormalisation.NONE, apply_defaults=False
)
for key in self._local_override_keys:
if key in cur_dict:
local_config_with_comments[key] = cur_dict[key]
save_yaml(project_local_yml_path, local_config_with_comments)
# update project.local.yml to reflect overridden keys if necessary
if save_project_local_yml:
project_local_yml_path = self._project_local_yml_path(project_yml_path)
if self._local_override_keys and os.path.exists(project_local_yml_path):
log.info("Saving updated local project configuration to %s", project_local_yml_path)
local_config_with_comments, _ = self._load_yaml_dict(
project_local_yml_path, comment_normalisation=YamlCommentNormalisation.NONE, apply_defaults=False
)
for key in self._local_override_keys:
if key in cur_dict:
local_config_with_comments[key] = cur_dict[key]
save_yaml(project_local_yml_path, local_config_with_comments)
class RegisteredProject(ToStringMixin):
@@ -694,8 +798,21 @@ class RegisteredProject(ToStringMixin):
)
@classmethod
def from_project_root(cls, project_root: str | Path, serena_config: "SerenaConfig") -> "RegisteredProject":
project_config = ProjectConfig.load(project_root, serena_config=serena_config)
def from_project_root(
cls,
project_root: str | Path,
serena_config: "SerenaConfig",
autogen: ProjectConfigAutoGenerationMode = ProjectConfigAutoGenerationMode.NONE,
) -> "RegisteredProject":
"""
Creates a RegisteredProject instance from a project root path, which must exist on disk.
:param project_root: path to an existing directory
:param serena_config: the Serena configuration
:param autogen: the auto-generation mode to use for the project configuration if it does not yet exist
:return: the RegisteredProject instance
"""
project_config = ProjectConfig.load(project_root, serena_config=serena_config, autogen=autogen)
return RegisteredProject(
project_root=str(project_root),
project_config=project_config,
@@ -1062,7 +1179,8 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
def get_registered_project(self, project_root_or_name: str, autoregister: bool = False) -> Optional[RegisteredProject]:
"""
:param project_root_or_name: path to the project root or the name of the project
:param autoregister: whether to register the project if it exists but is not registered yet
:param autoregister: whether to auto-register projects that are not yet registered in Serena's global configuration
but have an existing project configuration file. Project configuration files are never auto-generated.
:return: the registered project, or None if not found
"""
# look for project by name
@@ -1082,7 +1200,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
for project in self.projects:
if project.matches_root_path(project_root_or_name):
return project
# no registered project found; auto-register if project configuration exists
# no registered project found; optionally auto-register if a project configuration already exists
if autoregister:
config_path = self.get_project_yml_location(project_root_or_name)
if os.path.isfile(config_path):
@@ -1106,13 +1224,14 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
self.projects.append(registered_project)
self._persist_projects()
def add_project_from_path(self, project_root: Path | str) -> "Project":
def add_project_from_path(self, project_root: Path | str, asynchronous_autogen: bool = False) -> "Project":
"""
Adds a new project to the Serena configuration from a given path, auto-generating the project
with defaults if it does not exist.
Will raise a FileExistsError if a project already exists at the path.
:param project_root: the path to the project to add
:param asynchronous_autogen: whether to use asynchronous auto-generation for the project configuration
:return: the project that was added
"""
from ..project import Project
@@ -1129,7 +1248,8 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
f"Project with path {project_root} was already added with name '{already_registered_project.project_name}'."
)
project_config = ProjectConfig.load(project_root, serena_config=self, autogenerate=True)
autogen = ProjectConfigAutoGenerationMode.ASYNCHRONOUS if asynchronous_autogen else ProjectConfigAutoGenerationMode.SYNCHRONOUS
project_config = ProjectConfig.load(project_root, serena_config=self, autogen=autogen)
new_project = Project(
project_root=str(project_root),
@@ -1297,3 +1417,14 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
if glob_match(pattern, project_root_str):
return True
return False
def determine_language_backend(self, project_config: ProjectConfig | None = None, log_choice: bool = False):
language_backend = self.language_backend
if project_config and project_config.language_backend is not None:
language_backend = project_config.language_backend
if log_choice:
log.info(f"Using language backend as configured in project: {language_backend.name}")
else:
if log_choice:
log.info(f"Using language backend from global configuration: {language_backend.name}")
return language_backend
@@ -303,7 +303,15 @@ class JetBrainsPluginClient(ToStringMixin):
f"{self._plugin_version}. Ask the user to update the plugin!"
)
def _make_request(self, method: str, endpoint: str, data: Optional[dict] = None) -> dict[str, Any]:
def _make_request(self, method: str, endpoint: str, data: Optional[dict] = None, pythonify: bool = True) -> dict[str, Any]:
"""
:param method: the HTTP method to use ("GET" or "POST")
:param endpoint: the endpoint to call (e.g., "/findSymbol")
:param data: the data to send in the request body (for POST requests)
:param pythonify: whether to recursively "pythonify" the response object, converting all keys to snake_case.
This must not be enabled if *any* key contains variable data rather (and therefore isn't a well-defined DTO structure).
:return: the response as a dictionary
"""
url = f"{self._base_url}{endpoint}"
response: Response | None = None
@@ -320,7 +328,11 @@ class JetBrainsPluginClient(ToStringMixin):
# Try to parse JSON response
try:
return self._pythonify_response(response.json())
response_json = response.json()
if pythonify:
return self._pythonify_response(response_json)
else:
return response_json
except json.JSONDecodeError:
# If response is not JSON, return raw text
return {"response": response.text}
@@ -676,7 +688,7 @@ class JetBrainsPluginClient(ToStringMixin):
inspection_names: list[str] | None = None,
start_line: int | None = None,
end_line: int | None = None,
) -> jb.RunInspectionsResponse:
) -> dict:
"""
Runs IDE inspections on the given file and returns the results.
@@ -685,6 +697,9 @@ class JetBrainsPluginClient(ToStringMixin):
: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
:return: the inspection results as a dictionary.
NOTE: The response is currently *not* a well-defined DTO, because it stores variable data in keys.
Consequently, the `pythonify` option is disabled for this request, and the response is returned as-is.
"""
self._require_version_at_least(2023, 2, 14)
request_data: dict[str, Any] = {
@@ -698,7 +713,7 @@ class JetBrainsPluginClient(ToStringMixin):
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))
return self._make_request("POST", "/runInspectionsOnFile", request_data, pythonify=False)
def list_inspections(
self,
-16
View File
@@ -91,22 +91,6 @@ class TypeHierarchyResponse(TypedDict):
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
+5 -4
View File
@@ -5,7 +5,7 @@ from collections.abc import Iterator
from sensai.util.logging import LogTime
from serena.config.serena_config import SerenaPaths
from serena.config.serena_config import ProjectConfig, SerenaPaths
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.settings import SolidLSPSettings
@@ -22,25 +22,27 @@ class LanguageServerFactory:
def __init__(
self,
project_root: str,
project_config: ProjectConfig,
project_data_path: str,
encoding: str,
ignored_patterns: list[str],
ls_timeout: float | None = None,
ls_specific_settings: dict | None = None,
additional_workspace_folders: list[str] | None = None,
trace_lsp_communication: bool = False,
):
self.project_root = project_root
self.project_config = project_config
self.project_data_path = project_data_path
self.encoding = encoding
self.ignored_patterns = ignored_patterns
self.ls_timeout = ls_timeout
self.ls_specific_settings = ls_specific_settings
self.additional_workspace_folders = additional_workspace_folders or []
self.trace_lsp_communication = trace_lsp_communication
def create_language_server(self, language: Language) -> SolidLanguageServer:
ls_config = LanguageServerConfig(
workspace_folders=self.project_config.ls_workspace_folders,
additional_workspace_folders=self.project_config.ls_additional_workspace_folders,
code_language=language,
ignored_paths=self.ignored_patterns,
trace_lsp_communication=self.trace_lsp_communication,
@@ -56,7 +58,6 @@ class LanguageServerFactory:
solidlsp_dir=SerenaPaths().serena_user_home_dir,
project_data_path=self.project_data_path,
ls_specific_settings=self.ls_specific_settings or {},
additional_workspace_folders=self.additional_workspace_folders,
),
)
+33 -18
View File
@@ -147,11 +147,41 @@ class MemoryManager:
# use a callable replacement to avoid backreference interpretation of characters in ref_new
return re.subn(pattern, lambda _m: ref_new, content)
def _resolve_memory_path(self, base_dir: Path, parts: Sequence[str]) -> Path:
"""
Builds the ``*.md`` path for ``parts`` under ``base_dir``, creating any parent
subdirectories, and guarantees the result stays inside ``base_dir``.
The containment check is a defense-in-depth backstop for :meth:`get_memory_file_path`'s
up-front segment validation: even if a crafted name slipped through, the built path must
never escape the memories sandbox (which would let an agent read/write/delete arbitrary
files). The check runs *before* any directory is created, so a rejected name cannot leave
stray directories behind either. It is deliberately *lexical* (``normpath``, no symlink
resolution): directory symlinks placed inside the memories folder are a supported way to
share memories (e.g. a monorepo symlinking each submodule's memory dir), and those must
keep resolving to their targets at I/O time.
"""
filename = f"{parts[-1]}.md"
subdir = base_dir if len(parts) == 1 else base_dir.joinpath(*parts[:-1])
candidate = subdir / filename
base_norm = Path(os.path.normpath(base_dir))
if not Path(os.path.normpath(candidate)).is_relative_to(base_norm):
raise ValueError(f"Memory name resolves outside the memories directory. Got: {'/'.join(parts)}")
subdir.mkdir(parents=True, exist_ok=True)
return candidate
def get_memory_file_path(self, name: str) -> Path:
name = self._sanitize_name(name)
parts = name.split("/")
if ".." in parts:
raise ValueError(f"Memory name cannot contain '..' segments for security reasons. Got: {name}")
raise ValueError(f"Memory name cannot contain '..' segments. Got: {name}")
# Reject absolute names and empty path segments: pathlib discards the base directory when
# joined with an absolute path (e.g. "/etc/cron.d/backdoor" would reset to "/etc/cron.d"),
# letting a memory name escape the sandbox. A leading "/" produces an empty first segment.
if os.path.isabs(name) or "" in parts:
raise ValueError(f"Memory name cannot be absolute or contain empty path segments. Got: {name}")
if self._is_global(name):
if name == self.GLOBAL_TOPIC:
@@ -160,26 +190,11 @@ class MemoryManager:
)
# Strip "global/" prefix and resolve against global dir
sub_name = name[len(self.GLOBAL_TOPIC) + 1 :]
parts = sub_name.split("/")
filename = f"{parts[-1]}.md"
if len(parts) > 1:
subdir = self._global_memory_dir / "/".join(parts[:-1])
subdir.mkdir(parents=True, exist_ok=True)
return subdir / filename
return self._global_memory_dir / filename
return self._resolve_memory_path(self._global_memory_dir, sub_name.split("/"))
# Project-local memory
assert self._project_memory_dir is not None, "Project dir was not passed at initialization"
filename = f"{parts[-1]}.md"
if len(parts) > 1:
# Create subdirectory path
subdir = self._project_memory_dir / "/".join(parts[:-1])
subdir.mkdir(parents=True, exist_ok=True)
return subdir / filename
return self._project_memory_dir / filename
return self._resolve_memory_path(self._project_memory_dir, parts)
def _check_write_access(self, name: str, is_tool_context: bool) -> None:
# in tool context, memories can be read-only
+55 -20
View File
@@ -9,12 +9,15 @@ from sensai.util.logging import LogTime
from sensai.util.string import TextBuilder, ToStringMixin
from serena.config.serena_config import (
LanguageBackend,
ProjectConfig,
ProjectConfigAutoGenerationMode,
SerenaConfig,
)
from serena.ls_manager import LanguageServerFactory, LanguageServerManager
from serena.memories.memory_manager import MemoryManager
from serena.util.file_system import GitignoreParser, match_path
from serena.util.file_proxy import FileCollection
from serena.util.file_system import GitignoreParser, match_path, scan_directory
from serena.util.text_utils import MatchedConsecutiveLines, search_files
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
@@ -120,18 +123,28 @@ class Project(ToStringMixin):
def project_name(self) -> str:
return self.project_config.project_name
@property
def language_backend(self) -> LanguageBackend:
# The backend configuration is fundamentally owned by the agent, so it takes
# precedence. (Note: The agent does not necessary honour the project's choice,
# as it may be invalid.)
if self._agent is not None:
return self._agent.get_language_backend()
else:
return self.serena_config.determine_language_backend(self.project_config)
@classmethod
def load(
cls,
project_root: str | Path,
serena_config: "SerenaConfig",
autogenerate: bool = True,
autogen: ProjectConfigAutoGenerationMode = ProjectConfigAutoGenerationMode.SYNCHRONOUS,
) -> "Project":
assert serena_config is not None
project_root = Path(project_root).resolve()
if not project_root.exists():
raise FileNotFoundError(f"Project root not found: {project_root}")
project_config = ProjectConfig.load(project_root, serena_config=serena_config, autogenerate=autogenerate)
project_config = ProjectConfig.load(project_root, serena_config=serena_config, autogen=autogen)
return Project(project_root=str(project_root), project_config=project_config, serena_config=serena_config)
def save_config(self) -> None:
@@ -219,17 +232,20 @@ class Project(ToStringMixin):
log.debug(f"Path {abs_path} does not exist, skipping ignore check")
return False
# Check file extension if it's a file
is_file = os.path.isfile(abs_path)
if is_file and ignore_non_source_files:
is_file_in_supported_language = False
for language in self.project_config.languages:
fn_matcher = language.get_source_fn_matcher()
if fn_matcher.is_relevant_filename(abs_path):
is_file_in_supported_language = True
break
if not is_file_in_supported_language:
return True
# check code file restriction (depending on backend)
if ignore_non_source_files:
# apply restriction only for LSP backend, which enumerates known languages
# and therefore can determine whether a file is a source file or not
if self.language_backend.is_lsp():
if os.path.isfile(abs_path):
is_file_in_supported_language = False
for language in self.project_config.languages:
fn_matcher = language.get_source_fn_matcher()
if fn_matcher.is_relevant_filename(abs_path):
is_file_in_supported_language = True
break
if not is_file_in_supported_language:
return True
# Create normalized path for consistent handling
rel_path = Path(relative_path)
@@ -344,7 +360,7 @@ class Project(ToStringMixin):
)
return rel_file_paths
def search_source_files_for_pattern(
def search_project_files_for_pattern(
self,
pattern: str,
relative_path: str = "",
@@ -353,6 +369,7 @@ class Project(ToStringMixin):
paths_include_glob: str | None = None,
paths_exclude_glob: str | None = None,
multiline: bool = True,
code_files_only: bool = True,
) -> list[MatchedConsecutiveLines]:
"""
Search for a pattern across all (non-ignored) source files
@@ -366,12 +383,26 @@ class Project(ToStringMixin):
:param multiline: Whether to compile the regex with the DOTALL flag (``.`` matches newlines).
:return: List of matched consecutive lines with context
"""
relative_file_paths = self.gather_source_files(relative_path=relative_path)
if code_files_only:
relative_file_paths = self.gather_source_files(relative_path=relative_path)
file_collection = FileCollection.from_local_project_paths(relative_file_paths, self)
else:
abs_path = os.path.join(self.project_root, relative_path)
if os.path.isfile(abs_path):
rel_paths_to_search = [relative_path]
else:
_dirs, rel_paths_to_search = scan_directory(
path=abs_path,
recursive=True,
is_ignored_dir=self.is_ignored_path,
is_ignored_file=self.is_ignored_path,
relative_to=self.project_root,
)
file_collection = FileCollection.from_local_project_paths(rel_paths_to_search, self)
return search_files(
relative_file_paths,
file_collection,
pattern,
root_path=self.project_root,
file_reader=self.read_file,
context_lines_before=context_lines_before,
context_lines_after=context_lines_after,
paths_include_glob=paths_include_glob,
@@ -408,6 +439,10 @@ class Project(ToStringMixin):
:return: the language server manager, which is also stored in the project instance
"""
try:
# ensure that the project configuration, particularly the list of languages is complete,
# despite asynchronous first-time project configuration generation (which may not have completed yet)
self.project_config.await_asynchronous_completion()
# determine timeout to use for LS calls
tool_timeout = self.serena_config.tool_timeout
if tool_timeout is None or tool_timeout < 0:
@@ -436,12 +471,12 @@ class Project(ToStringMixin):
)
factory = LanguageServerFactory(
project_root=self.project_root,
project_config=self.project_config,
project_data_path=self._serena_data_folder,
encoding=self.project_config.encoding,
ignored_patterns=self._ignored_patterns,
ls_timeout=ls_timeout,
ls_specific_settings=ls_specific_settings,
additional_workspace_folders=self.project_config.additional_workspace_folders,
trace_lsp_communication=self.serena_config.trace_lsp_communication,
)
self.language_server_manager = LanguageServerManager.from_languages(self.project_config.languages, factory)
+47 -21
View File
@@ -1,23 +1,25 @@
# the name by which the project can be referenced within Serena
# the name by which the project can be referenced within Serena/when chatting with the LLM.
project_name: "project_name"
# list of languages for which language servers are started; choose from:
# al angular ansible bash clojure
# cpp cpp_ccls crystal csharp csharp_omnisharp
# dart elixir elm erlang fortran
# fsharp go groovy haskell haxe
# hlsl html java json julia
# kotlin lean4 lua luau markdown
# list of languages for which language servers are started (LSP backend only); choose from:
# ada al angular ansible bash
# bsl clojure cpp cpp_ccls crystal
# csharp csharp_omnisharp cue dart elixir
# elm erlang fortran fsharp gdscript
# go groovy haskell haxe hlsl
# html java json julia kotlin
# latex 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 scss solidity
# svelte 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.)
# perl php php_phpactor php_phpantom powershell
# python python_jedi python_pyrefly python_ty r
# rego ruby ruby_solargraph rust scala
# scss solidity svelte swift systemverilog
# terraform toml typescript typescript_vts vue
# yaml zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# 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.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
@@ -37,6 +39,19 @@ languages: ["python"]
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
# the command runs in the project root directory and is only executed if the project is trusted
# (see trusted_project_path_patterns in the global configuration).
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
# example: activation_command: "npx nx run-many -t build"
activation_command:
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
# must be a positive number.
activation_command_timeout: 180
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
@@ -58,16 +73,27 @@ ignore_all_files_in_gitignore: true
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
ls_specific_settings: {}
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
# list of workspace folder paths (LSP backend only).
# These folders will be used to build up Serena's symbol index.
# Paths must be within the project root and should thus be relative to the project root.
# Furthermore, the paths should not be filtered by ignore settings.
# Default setting: The entire project root folder (".") is considered.
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
# ls_workspace_folders:
# - "./subproject1"
# - "./subproject2"
ls_workspace_folders: ["."]
# list of additional workspace folder paths for cross-package reference support.
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries.
# Currently supported for: TypeScript.
# symbols and references across package boundaries, but these folders are not indexed by Serena,
# i.e. the respective symbols will not be found using Serena's symbol search tools.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
additional_workspace_folders: []
ls_additional_workspace_folders: []
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
+10 -35
View File
@@ -20,7 +20,6 @@ from serena.util.text_utils import (
ReplacementOccurrence,
expand_braces,
glob_match,
search_files,
)
@@ -595,40 +594,16 @@ class SearchForPatternTool(Tool):
if not os.path.exists(abs_path):
raise FileNotFoundError(f"Relative path {relative_path} does not exist.")
if restrict_search_to_code_files:
matches = self.project.search_source_files_for_pattern(
pattern=substring_pattern,
relative_path=relative_path,
context_lines_before=context_lines_before,
context_lines_after=context_lines_after,
paths_include_glob=paths_include_glob.strip(),
paths_exclude_glob=paths_exclude_glob.strip(),
multiline=multiline,
)
else:
if os.path.isfile(abs_path):
rel_paths_to_search = [relative_path]
else:
_dirs, rel_paths_to_search = scan_directory(
path=abs_path,
recursive=True,
is_ignored_dir=self.project.is_ignored_path,
is_ignored_file=self.project.is_ignored_path,
relative_to=self.get_project_root(),
)
# TODO (maybe): not super efficient to walk through the files again and filter if glob patterns are provided
# but it probably never matters and this version required no further refactoring
matches = search_files(
rel_paths_to_search,
substring_pattern,
context_lines_before=context_lines_before,
context_lines_after=context_lines_after,
file_reader=self.project.read_file,
root_path=self.get_project_root(),
paths_include_glob=paths_include_glob,
paths_exclude_glob=paths_exclude_glob,
multiline=multiline,
)
matches = self.project.search_project_files_for_pattern(
pattern=substring_pattern,
relative_path=relative_path,
context_lines_before=context_lines_before,
context_lines_after=context_lines_after,
paths_include_glob=paths_include_glob.strip(),
paths_exclude_glob=paths_exclude_glob.strip(),
multiline=multiline,
code_files_only=restrict_search_to_code_files,
)
# group matches by file
file_to_matches: dict[str, list[str]] = defaultdict(list)
+91
View File
@@ -0,0 +1,91 @@
import logging
import os
from abc import ABC, abstractmethod
from collections.abc import Iterator
from typing import TYPE_CHECKING, Self
if TYPE_CHECKING:
from serena.project import Project
log = logging.getLogger(__name__)
class FileProxy(ABC):
@abstractmethod
def get_contents(self) -> str:
""":return: the contents of the file as a string."""
@abstractmethod
def get_relative_path(self) -> str:
""":return: the relative path reported by Serena (actual relative path or encoded external path)"""
@abstractmethod
def is_glob_supported(self):
"""
:return: whether the proxy supports glob filtering based on its relative path
"""
class LocalProjectFileProxy(FileProxy):
def __init__(self, relative_path: str, project: "Project"):
self._relative_path = relative_path
self._project = project
def get_contents(self) -> str:
abs_path = os.path.join(self._project.project_root, self._relative_path)
with open(abs_path, encoding=self._project.project_config.encoding) as f:
return f.read()
def get_relative_path(self) -> str:
return self._relative_path
def is_glob_supported(self):
return True
class FileCollection:
def __init__(self, file_proxies: list[FileProxy]):
self._file_proxies = file_proxies
def __len__(self) -> int:
return len(self._file_proxies)
def __iter__(self) -> Iterator[FileProxy]:
return iter(self._file_proxies)
@classmethod
def from_local_project_paths(cls, relative_paths: list[str], project: "Project") -> Self:
return cls([LocalProjectFileProxy(path, project) for path in relative_paths])
def filter_glob(self, paths_include_glob: str | None = None, paths_exclude_glob: str | None = None) -> "FileCollection":
"""
Filters the collection based on the given patterns.
Note: Filtering is applied only to local project files. Other files are always retained.
:param paths_include_glob: optional glob pattern to include files from the list
:param paths_exclude_glob: optional glob pattern to exclude files from the list
:return: the filtered collection
"""
from serena.util.text_utils import GlobMatcher
if paths_include_glob is None and paths_exclude_glob is None:
return self
include_glob_matcher = GlobMatcher(paths_include_glob) if paths_include_glob else None
exclude_glob_matcher = GlobMatcher(paths_exclude_glob) if paths_exclude_glob else None
filtered_files = []
for f in self._file_proxies:
if f.is_glob_supported():
path = f.get_relative_path()
if include_glob_matcher:
if not include_glob_matcher.matches(path):
log.debug(f"Skipping {path}: does not match include pattern {paths_include_glob}")
continue
if exclude_glob_matcher:
if exclude_glob_matcher.matches(path):
log.debug(f"Skipping {path}: matches exclude pattern {paths_exclude_glob}")
continue
filtered_files.append(f)
return FileCollection(filtered_files)
+6 -1
View File
@@ -164,7 +164,12 @@ class GitignoreParser:
queue: list[str] = [self.repo_root]
def scan(abs_path: str | None) -> Iterator[str]:
for entry in os.scandir(abs_path):
try:
entries = os.scandir(abs_path)
except PermissionError as ex:
log.debug(f"Skipping unreadable directory {abs_path}: {ex}")
return
for entry in entries:
try:
if entry.is_dir(follow_symlinks=follow_symlinks):
queue.append(entry.path)
+26 -22
View File
@@ -31,36 +31,40 @@ def determine_programming_language_composition(repo_path: str) -> dict[Language,
"""
Determine the programming language composition of a repository.
:param repo_path: Path to the repository to analyze
Percentages are computed relative to the number of files that match at least
one supported language, not the total file count. This prevents files that
belong to no supported language (images, plain text, licenses, lock files, etc.)
from diluting language percentages in repositories where such files dominate.
:return: Dictionary mapping languages to percentages of files matching each language
:param repo_path: Path to the repository to analyze
:return: Dictionary mapping languages to percentages of recognised source files
matching each language (denominator = files matched by at least one language)
"""
all_files = find_all_non_ignored_files(repo_path)
if not all_files:
return {}
# Count files for each language
# collect all language matchers once
all_languages = list(Language.iter_all(include_experimental=False))
matchers = {lang: lang.get_source_fn_matcher() for lang in all_languages}
# count files per language in a single pass over the files
language_counts: dict[Language, int] = {}
total_files = len(all_files)
for language in Language.iter_all(include_experimental=False):
matcher = language.get_source_fn_matcher()
count = 0
for file_path in all_files:
# Use just the filename for matching, not the full path
filename = os.path.basename(file_path)
recognised_files = 0
for file_path in all_files:
# Use just the filename for matching, not the full path
filename = os.path.basename(file_path)
matched_any = False
for lang, matcher in matchers.items():
if matcher.is_relevant_filename(filename):
count += 1
language_counts[lang] = language_counts.get(lang, 0) + 1
matched_any = True
if matched_any:
recognised_files += 1
if count > 0:
language_counts[language] = count
if recognised_files == 0:
return {}
# Convert counts to percentages
language_percentages: dict[Language, float] = {}
for language, count in language_counts.items():
percentage = (count / total_files) * 100
language_percentages[language] = round(percentage, 2)
return language_percentages
# convert to percentages relative to recognised source files only
return {lang: round(count / recognised_files * 100, 2) for lang, count in language_counts.items()}
+59 -104
View File
@@ -1,7 +1,6 @@
import fnmatch
import hashlib
import logging
import os
import re
from collections.abc import Callable
from dataclasses import dataclass, field
@@ -11,7 +10,7 @@ from typing import Any, Literal, Self
from bs4 import BeautifulSoup
from joblib import Parallel, delayed
from serena.constants import DEFAULT_SOURCE_FILE_ENCODING
from serena.util.file_proxy import FileCollection, FileProxy
from solidlsp.ls_utils import TextUtils
log = logging.getLogger(__name__)
@@ -101,7 +100,7 @@ class MatchedConsecutiveLines:
def from_file_contents(
cls, file_contents: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0, source_file_path: str | None = None
) -> Self:
line_contents = file_contents.split("\n")
line_contents = TextUtils.split_lines(file_contents)
start_lineno = max(0, line - context_lines_before)
end_lineno = min(len(line_contents) - 1, line + context_lines_after)
text_lines: list[TextLine] = []
@@ -125,7 +124,7 @@ def glob_to_regex(glob_pat: str) -> str:
if ch == "*":
regex_parts.append(".*")
elif ch == "?":
regex_parts.append("..")
regex_parts.append(".")
elif ch == "\\":
i += 1
if i < len(glob_pat):
@@ -142,7 +141,6 @@ def search_text(
pattern: str,
content: str | None = None,
source_file_path: str | None = None,
allow_multiline_match: bool = False,
context_lines_before: int = 0,
context_lines_after: int = 0,
is_glob: bool = False,
@@ -155,8 +153,6 @@ def search_text(
:param content: The text content to search. May be None if source_file_path is provided.
:param source_file_path: Optional path to the source file. If content is None,
this has to be passed and the file will be read.
:param allow_multiline_match: Whether to search across multiple lines. Currently, the default
option (False) is very inefficient, so it is recommended to set this to True.
:param context_lines_before: Number of context lines to include before matches
:param context_lines_after: Number of context lines to include after matches
:param is_glob: If True, pattern is treated as a glob-like pattern (e.g., "*.py", "test_??.py")
@@ -173,76 +169,46 @@ def search_text(
raise ValueError("Pass either content or source_file_path")
matches = []
lines = content.splitlines()
lines = TextUtils.split_lines(content)
total_lines = len(lines)
# Convert pattern to a compiled regex if it's a string
if is_glob:
pattern = glob_to_regex(pattern)
if allow_multiline_match:
# For multiline matches, optionally use DOTALL so '.' matches newlines
flags = (re.MULTILINE | re.DOTALL) if multiline else 0
compiled_pattern = re.compile(pattern, flags)
# Search across the entire content as a single string
for match in compiled_pattern.finditer(content):
start_pos = match.start()
end_pos = match.end()
# Find the line numbers for the start and end positions
start_line_num = content[:start_pos].count("\n")
end_line_num = content[:end_pos].count("\n")
# For multiline matches, optionally use DOTALL so '.' matches newlines
flags = (re.MULTILINE | re.DOTALL) if multiline else 0
compiled_pattern = re.compile(pattern, flags)
# Search across the entire content as a single string
for match in compiled_pattern.finditer(content):
start_pos = match.start()
end_pos = match.end()
# Calculate the range of lines to include in the context
context_start = max(0, start_line_num - context_lines_before)
context_end = min(total_lines - 1, end_line_num + context_lines_after)
# Find the line numbers for the start and end positions
start_line_num = TextUtils.get_line_from_index(content, start_pos)
end_line_num = TextUtils.get_line_from_index(content, end_pos)
# Create TextLine objects for the context
context_lines = []
for line_num in range(context_start, context_end + 1):
if context_start <= line_num < start_line_num:
match_type = LineType.BEFORE_MATCH
elif end_line_num < line_num <= context_end:
match_type = LineType.AFTER_MATCH
else:
match_type = LineType.MATCH
# Calculate the range of lines to include in the context
context_start = max(0, start_line_num - context_lines_before)
context_end = min(total_lines - 1, end_line_num + context_lines_after)
context_lines.append(TextLine(line_number=line_num, line_content=lines[line_num], match_type=match_type))
# Create TextLine objects for the context
context_lines = []
for line_num in range(context_start, context_end + 1):
if context_start <= line_num < start_line_num:
match_type = LineType.BEFORE_MATCH
elif end_line_num < line_num <= context_end:
match_type = LineType.AFTER_MATCH
else:
match_type = LineType.MATCH
matches.append(MatchedConsecutiveLines(lines=context_lines, source_file_path=source_file_path))
else:
# TODO: extremely inefficient! Since we currently don't use this option in SerenaAgent or LanguageServer,
# it is not urgent to fix, but should be either improved or the option should be removed.
# Search line by line, normal compile without DOTALL
compiled_pattern = re.compile(pattern)
for i, line in enumerate(lines):
if compiled_pattern.search(line):
# Calculate the range of lines to include in the context
context_start = max(0, i - context_lines_before)
context_end = min(total_lines - 1, i + context_lines_after)
context_lines.append(TextLine(line_number=line_num, line_content=lines[line_num], match_type=match_type))
# Create TextLine objects for the context
context_lines = []
for j in range(context_start, context_end + 1):
if j < i:
match_type = LineType.BEFORE_MATCH
elif j > i:
match_type = LineType.AFTER_MATCH
else:
match_type = LineType.MATCH
context_lines.append(TextLine(line_number=j, line_content=lines[j], match_type=match_type))
matches.append(MatchedConsecutiveLines(lines=context_lines, source_file_path=source_file_path))
matches.append(MatchedConsecutiveLines(lines=context_lines, source_file_path=source_file_path))
return matches
def default_file_reader(file_path: str) -> str:
"""Reads using the default encoding."""
with open(file_path, encoding=DEFAULT_SOURCE_FILE_ENCODING) as f:
return f.read()
def expand_braces(pattern: str) -> list[str]:
"""
Expands brace patterns in a glob string.
@@ -250,18 +216,22 @@ def expand_braces(pattern: str) -> list[str]:
Handles multiple brace sets as well.
"""
patterns = [pattern]
while any("{" in p for p in patterns):
while any("{" in p or "}" in p for p in patterns):
new_patterns = []
for p in patterns:
match = re.search(r"\{([^{}]+)\}", p)
match = re.search(r"\{([^{}]*)\}", p)
if match:
options_expr = match.group(1)
if not options_expr:
raise ValueError(f"Invalid glob brace expression in {pattern!r}: empty braces are not allowed")
prefix = p[: match.start()]
suffix = p[match.end() :]
options = match.group(1).split(",")
options = options_expr.split(",")
for option in options:
new_patterns.append(f"{prefix}{option}{suffix}")
else:
new_patterns.append(p)
raise ValueError(f"Invalid glob brace expression in {pattern!r}: unmatched brace")
patterns = new_patterns
return patterns
@@ -319,11 +289,17 @@ def glob_match(pattern: str, path: str) -> bool:
return fnmatch.fnmatch(path, pattern)
class GlobMatcher:
def __init__(self, expr: str):
self._patterns = expand_braces(expr)
def matches(self, path: str) -> bool:
return any(glob_match(p, path) for p in self._patterns)
def search_files(
relative_file_paths: list[str],
file_collection: FileCollection,
pattern: str,
root_path: str = "",
file_reader: Callable[[str], str] = default_file_reader,
context_lines_before: int = 0,
context_lines_after: int = 0,
paths_include_glob: str | None = None,
@@ -333,11 +309,8 @@ def search_files(
"""
Search for a pattern in a list of files.
:param relative_file_paths: list of relative file paths in which to search
:param file_collection: the collection of files to search (will be optionally filtered by glob patterns)
:param pattern: pattern to search for
:param root_path: root path to resolve relative paths against (by default, current working directory).
:param file_reader: function to read a file, by default will just use os.open.
All files that can't be read by it will be skipped.
:param context_lines_before: number of context lines to include before matches
:param context_lines_after: number of context lines to include after matches
:param paths_include_glob: optional glob pattern to include files from the list
@@ -345,53 +318,35 @@ def search_files(
:param multiline: whether to apply multi-line matching, enabling the flags re.DOTALL and re.MULTILINE (default: True)
:return: list of MatchedConsecutiveLines objects
"""
# Pre-filter paths (done sequentially to avoid overhead)
# Use proper glob matching instead of gitignore patterns
include_patterns = expand_braces(paths_include_glob) if paths_include_glob else None
exclude_patterns = expand_braces(paths_exclude_glob) if paths_exclude_glob else None
# apply glob filter
file_collection = file_collection.filter_glob(paths_include_glob=paths_include_glob, paths_exclude_glob=paths_exclude_glob)
log.info(f"Processing {len(file_collection)} files.")
filtered_paths = []
for path in relative_file_paths:
if include_patterns:
if not any(glob_match(p, path) for p in include_patterns):
log.debug(f"Skipping {path}: does not match include pattern {paths_include_glob}")
continue
if exclude_patterns:
if any(glob_match(p, path) for p in exclude_patterns):
log.debug(f"Skipping {path}: matches exclude pattern {paths_exclude_glob}")
continue
filtered_paths.append(path)
log.info(f"Processing {len(filtered_paths)} files.")
def process_single_file(path: str) -> dict[str, Any]:
def process_single_file(file_proxy: FileProxy) -> dict[str, Any]:
"""Process a single file - this function will be parallelized."""
relative_path = file_proxy.get_relative_path()
try:
abs_path = os.path.join(root_path, path)
file_content = file_reader(abs_path)
file_content = file_proxy.get_contents()
search_results = search_text(
pattern,
content=file_content,
source_file_path=path,
allow_multiline_match=True,
source_file_path=relative_path,
context_lines_before=context_lines_before,
context_lines_after=context_lines_after,
multiline=multiline,
)
if len(search_results) > 0:
log.debug(f"Found {len(search_results)} matches in {path}")
return {"path": path, "results": search_results, "error": None}
log.debug(f"Found {len(search_results)} matches in {relative_path}")
return {"path": relative_path, "results": search_results, "error": None}
except Exception as e:
log.debug(f"Error processing {path}: {e}")
return {"path": path, "results": [], "error": str(e)}
log.debug(f"Error processing {relative_path}: {e}")
return {"path": relative_path, "results": [], "error": str(e)}
# Execute in parallel using joblib
results = Parallel(
n_jobs=-1,
backend="threading",
)(delayed(process_single_file)(path) for path in filtered_paths)
)(delayed(process_single_file)(file_proxy) for file_proxy in file_collection)
# Collect results and errors
matches = []
@@ -406,7 +361,7 @@ def search_files(
if skipped_file_error_tuples:
log.debug(f"Failed to read {len(skipped_file_error_tuples)} files: {skipped_file_error_tuples}")
log.info(f"Found {len(matches)} total matches across {len(filtered_paths)} files")
log.info(f"Found {len(matches)} total matches across {len(file_collection)} files")
return matches
+246
View File
@@ -0,0 +1,246 @@
import logging
import os
import shutil
from abc import ABC, abstractmethod
from collections.abc import Sequence
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
class LanguageServerDependencyProvider(ABC):
"""
Prepares dependencies for a language server (if any), ultimately enabling the launch command to be constructed
and optionally providing environment variables that are necessary for the execution.
"""
def __init__(self, custom_settings: SolidLSPSettings.CustomLSSettings, ls_resources_dir: str):
"""
:param custom_settings: the (user-provided) language server-specific settings
:param ls_resources_dir: the directory in which data for the language-server shall be stored
(this is already specific to the concrete language server, i.e. no further subdirectory is needed)
"""
self._custom_settings = custom_settings
self._ls_resources_dir = ls_resources_dir
@abstractmethod
def create_launch_command(self) -> list[str]:
"""
Creates the launch command for this language server, potentially downloading and installing dependencies
beforehand.
:return: the launch command as a list containing the executable and its arguments
"""
def create_launch_command_env(self) -> dict[str, str]:
"""
Provides environment variables to be set when executing the launch command.
This method is intended to be overridden by subclasses that need to set variables.
:return: a mapping for variable names to values
"""
return {}
class LanguageServerDependencyProviderBaseCommand(LanguageServerDependencyProvider, ABC):
"""
Special case of a dependency provider, where the launch command is constructed from a base command.
The user can configure aspects of the launch command in LS-specific settings:
* ``ls_base_cmd`: overrides the base command
* ``ls_path``: overrides the path of the language server's core dependency (e.g. its executable or a JAR file),
from which the base command is formed, bypassing Serena's managed installation
* ``ls_args``: overrides the arguments that are added to the base command in order to form the launch command
* ``ls_extra_args``: additional arguments to append to the launch command
"""
@abstractmethod
def _create_default_base_command(self) -> list[str]:
"""
Obtains the default base command for this language server, potentially downloading and installing dependencies
beforehand.
Note: The user can override the base command, so this will only be run if no custom base command is provided.
:return: the base command as a list containing the executable and its arguments
"""
@abstractmethod
def _create_launch_command_from_base_command(self, base_command: list[str]) -> list[str]:
"""
Adds any additional arguments to the base command to create the final launch command.
:param base_command: the base command
:return: the extended command
"""
def create_launch_command(self) -> list[str]:
# obtain base command
base_command = self._custom_settings.get("ls_base_cmd", None)
if base_command is not None and not isinstance(base_command, list):
log.warning("The 'ls_base_cmd' setting should be a list of strings. Ignoring the provided value: %s", base_command)
base_command = None
if base_command is None:
ls_path = self._custom_settings.get("ls_path", None)
if ls_path is not None:
base_command = [ls_path]
else:
# default case: base command is constructed by the provider implementation
base_command = self._create_default_base_command()
# create launch command from base command
ls_args = self._custom_settings.get("ls_args")
if ls_args is not None:
if not isinstance(ls_args, list):
log.warning("The 'ls_args' setting should be a list of strings. Ignoring the provided value: %s", ls_args)
ls_args = None
if ls_args is not None:
cmd = list(base_command) + ls_args
else:
cmd = self._create_launch_command_from_base_command(list(base_command))
# add user-provided extra arguments (if any)
ls_extra_args = self._custom_settings.get("ls_extra_args", [])
if ls_extra_args:
if not isinstance(ls_extra_args, list):
log.warning("The 'ls_extra_args' setting should be a list of strings. Ignoring the provided value: %s", ls_extra_args)
else:
cmd = cmd + ls_extra_args
return cmd
class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProviderBaseCommand, ABC):
"""
Special case of a dependency provider, where there is a single core dependency which provides
the basis for the launch command.
The core dependency's path can be overridden by the user in LS-specific settings (SerenaConfig)
via the key "ls_path". If the user provides the key, the specified path is used directly.
Otherwise, the provider implementation is called to get or install the core dependency.
Note: The inheritance from the BaseCommand class serves to allow user overrides to be handled
centrally, but implementations of this class do not necessarily follow the principle that
the base command is strictly extended.
Yet incompatibility arises only if (a) the user overrides the base command with a command that
has arguments, and (b) the implementation of this class does not construct the launch command
by appending arguments to the core dependency's path.
"""
@abstractmethod
def _get_or_install_core_dependency(self) -> str:
"""
Gets the language server's core path, potentially installing dependencies beforehand.
:return: the core dependency's path (e.g. executable, jar, etc.)
"""
@abstractmethod
def _create_launch_command(self, core_path: str) -> list[str]:
"""
:param core_path: path to the core dependency
:return: the launch command as a list containing the executable and its arguments
"""
def _create_default_base_command(self) -> list[str]:
# We treat the core path as the only element of the base command,
# noting that the construction of the launch command from the base command
# is not necessarily to append arguments only.
core_path = self._get_or_install_core_dependency()
return [core_path]
def _create_launch_command_from_base_command(self, base_command: list[str]) -> list[str]:
core_path = base_command[0]
cmd = self._create_launch_command(core_path)
if len(base_command) == 1:
# This is the regular case, where the base command consists only of the core
# dependency's path, and the launch command is constructed from it.
return cmd
else:
# In this case, the user has overridden the base command via LS-specific settings
# with a command that has arguments and therefore does not map cleanly to
# the single path assumption. However, in special cases where the full command
# was constructed by appending arguments to the core dependency's path,
# we simply assume that the provided base command can be substituted.
if cmd[0] == core_path:
return base_command + cmd[1:]
else:
raise ValueError("Language server base launch command with arguments unsupported")
class LanguageServerDependencyProviderUvx(LanguageServerDependencyProviderBaseCommand):
"""
Dependency provider for language servers distributed as a PyPI package, run on demand via ``uvx`` / ``uv x``.
The pinned package version can be overridden by the user via the LS-specific setting given by
``version_setting_key``. Alternatively, the LS-specific setting "ls_path" can be set to the path of an
already-installed language server executable, in which case it is launched directly, bypassing uv entirely.
"""
DEFAULT_UVX_PYTHON_VERSION = "3.13"
def __init__(
self,
custom_settings: "SolidLSPSettings.CustomLSSettings",
ls_resources_dir: str,
*,
package: str,
entrypoint: str,
default_version: str,
version_setting_key: str,
extra_args: Sequence[str] = (),
):
"""
:param package: the PyPI package name (e.g. ``"pyright"``)
:param entrypoint: the console script provided by the package (e.g. ``"pyright-langserver"``)
:param default_version: the package version to pin unless overridden
:param version_setting_key: the LS-specific setting key through which the user can override the version
:param extra_args: arguments appended after the entrypoint (e.g. ``("--stdio",)``)
"""
super().__init__(custom_settings, ls_resources_dir)
self._package = package
self._entrypoint = entrypoint
self._default_version = default_version
self._version_setting_key = version_setting_key
self._extra_args = tuple(extra_args)
@staticmethod
def _build_uvx_base_command(
package: str,
version: str,
entrypoint: str,
python_version: str = DEFAULT_UVX_PYTHON_VERSION,
) -> list[str]:
"""Build a command that runs a pinned PyPI package's console script on demand via ``uvx`` / ``uv x``.
Resolution order:
1. Prefer ``uvx`` (env var ``UVX`` or PATH lookup).
2. Fall back to ``uv x`` if only ``uv`` is on PATH.
3. Raise ``RuntimeError`` if neither is available.
:param package: PyPI package name (e.g. ``"pyright"``).
:param version: Pinned package version.
:param entrypoint: Console script provided by the package (e.g. ``"pyright-langserver"``).
:param python_version: Python interpreter version passed via ``-p`` (uv will fetch it if missing).
"""
base_args = ["-p", python_version, "--from", f"{package}=={version}", entrypoint]
uvx_path = os.environ.get("UVX") or shutil.which("uvx")
if uvx_path is not None:
return [uvx_path, *base_args]
uv_path = shutil.which("uv")
if uv_path is not None:
return [uv_path, "x", *base_args]
raise RuntimeError("Could not find 'uvx' or 'uv' in PATH. Install uv (https://docs.astral.sh/uv/).")
def _create_default_base_command(self):
version = self._custom_settings.get(self._version_setting_key, self._default_version)
return self._build_uvx_base_command(self._package, version, self._entrypoint)
def _create_launch_command_from_base_command(self, base_command: list[str]) -> list[str]:
return base_command + list(self._extra_args)
+82
View File
@@ -0,0 +1,82 @@
import logging
import os
import pathlib
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Self, cast
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, WorkspaceFolder
if TYPE_CHECKING:
from solidlsp import SolidLanguageServer
log = logging.getLogger(__name__)
class InitializeParamsBuilder(ABC):
def __init__(self):
self._params = {}
def with_base_options(self, options: dict | InitializeParams) -> Self:
self._params.update(options)
return self
def _set(self, key: str, value: Any) -> None:
if key in self._params:
log.debug("Overriding existing option '%s' with new value: %s (old value: %s)", key, value, self._params[key])
self._params[key] = value
@abstractmethod
def _apply_updates(self) -> None:
"""
Applies implementation-specific updates to the options.
"""
def build(self) -> InitializeParams:
self._apply_updates()
return cast(InitializeParams, self._params)
class DefaultInitializeParamsBuilder(InitializeParamsBuilder):
def __init__(self, ls: "SolidLanguageServer", set_workspace_folders: bool = True):
super().__init__()
self._ls = ls
self._set_workspace_folders = set_workspace_folders
@staticmethod
def _create_workspace_folder_entry(path: str) -> WorkspaceFolder:
abs_path = os.path.abspath(path)
return {"uri": pathlib.Path(abs_path).as_uri(), "name": os.path.basename(abs_path)}
def _apply_updates(self):
root_abs_path = self._ls.repository_root_path
self._set("processId", os.getpid())
self._set("rootPath", root_abs_path)
self._set("rootUri", pathlib.Path(root_abs_path).as_uri())
self._set("clientInfo", {"name": "Serena"})
if self._set_workspace_folders:
abs_workspace_paths = self._ls.config.get_absolute_workspace_folders(root_abs_path)
log.info("Workspace folders: %s", abs_workspace_paths)
workspace_folders = [self._create_workspace_folder_entry(abs_path) for abs_path in abs_workspace_paths]
additional_abs_workspace_paths = [
p for p in self._ls.config.get_absolute_additional_workspace_folders(root_abs_path) if p not in abs_workspace_paths
]
if additional_abs_workspace_paths:
log.info("Additional workspace folders (not indexed by SolidLSP): %s", additional_abs_workspace_paths)
workspace_folders.extend([self._create_workspace_folder_entry(abs_path) for abs_path in additional_abs_workspace_paths])
self._set("workspaceFolders", workspace_folders)
init_options_key = "initializationOptions"
custom_init_options = self._ls.custom_settings.get(init_options_key, None)
if custom_init_options:
if not isinstance(custom_init_options, dict):
log.error("Custom initialization options should be a dictionary, but got %s. Ignoring.", type(custom_init_options).__name__)
else:
log.info("Applying custom initialization options: %s", custom_init_options)
if init_options_key in self._params:
self._params[init_options_key].update(custom_init_options)
else:
self._params[init_options_key] = custom_init_options
log.info("Final initialization options: %s", self._params[init_options_key])
@@ -9,15 +9,12 @@ pragmas/aspects in source rather than file extension, so a single
import logging
import os
import pathlib
import threading
from typing import cast
from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -155,15 +152,9 @@ class AdaLanguageServer(SolidLanguageServer):
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
def _get_initialize_params(self) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Returns the initialization params for the Ada Language Server."""
repository_absolute_path = self.repository_root_path
root_uri = pathlib.Path(repository_absolute_path).as_uri()
result = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"workspace": {
"applyEdit": True,
@@ -188,9 +179,8 @@ class AdaLanguageServer(SolidLanguageServer):
},
"initializationOptions": {},
"trace": "off",
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
return cast(InitializeParams, result)
return result
def _start_server(self) -> None:
"""Start the Ada Language Server process and complete the LSP handshake."""
@@ -213,7 +203,7 @@ class AdaLanguageServer(SolidLanguageServer):
log.info("Starting ada_language_server process")
self.server.start()
initialize_params = self._get_initialize_params()
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -7,7 +7,6 @@ You can pass the following entries in ``ls_specific_settings["al"]``:
import logging
import os
import pathlib
import platform
import re
import stat
@@ -448,20 +447,12 @@ class ALLanguageServer(SolidLanguageServer):
return None
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> dict:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the AL Language Server.
"""
# Ensure we have an absolute path for URI generation
repository_path = pathlib.Path(repository_absolute_path).resolve()
root_uri = repository_path.as_uri()
# AL requires extensive capabilities based on VS Code trace
initialize_params = {
"processId": os.getpid(),
"rootPath": str(repository_path),
"rootUri": root_uri,
"capabilities": {
"workspace": {
"applyEdit": True,
@@ -512,7 +503,6 @@ class ALLanguageServer(SolidLanguageServer):
},
},
"trace": "verbose",
"workspaceFolders": [{"uri": root_uri, "name": repository_path.name}],
}
return initialize_params
@@ -556,12 +546,12 @@ class ALLanguageServer(SolidLanguageServer):
self.server.start()
# Send initialize request
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to AL LSP server and awaiting response")
# Send initialize and wait for response
resp = self.server.send_request("initialize", initialize_params)
resp = self.server.send_request("initialize", dict(initialize_params))
if resp is None:
raise RuntimeError("AL Language Server initialization failed - no response")
@@ -70,7 +70,7 @@ from solidlsp.language_servers.typescript_language_server import (
from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, SymbolInformation
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -166,8 +166,8 @@ class AngularTypeScriptServer(TypeScriptLanguageServer):
)
@override
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
params = super()._get_initialize_params(repository_absolute_path)
def _create_base_initialize_params(self) -> dict:
params = super()._create_base_initialize_params()
# Load @angular/language-service as a tsserver plugin via typescript-language-server's
# initializationOptions.plugins API (the same API Vue uses for @vue/typescript-plugin).
params["initializationOptions"] = {
@@ -502,8 +502,7 @@ class AngularLanguageServer(SolidLanguageServer):
else:
log.debug("Found @angular/core at %s", found)
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
root_uri = pathlib.Path(repository_absolute_path).as_uri()
def _create_base_initialize_params(self) -> dict:
params: dict = {
"locale": "en",
"capabilities": {
@@ -531,17 +530,8 @@ class AngularLanguageServer(SolidLanguageServer):
"tsProbeLocations": [os.path.join(self._install_dir, "node_modules")],
"forceStrictTemplates": False,
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return params # type: ignore[return-value]
return params
@override
def _start_server(self) -> None:
@@ -580,7 +570,7 @@ class AngularLanguageServer(SolidLanguageServer):
try:
log.info("Starting Angular language server (ngserver)")
self.server.start()
init_params = self._get_initialize_params(self.repository_root_path)
init_params = self._create_initialize_params()
init_response = self.server.send.initialize(init_params)
log.debug("Angular LS initialize response: %s", init_response)
self.server.notify.initialized({})
@@ -6,7 +6,6 @@ Contains various configurations and settings specific to Ansible YAML files (pla
import fnmatch
import logging
import os
import pathlib
import shutil
from typing import Any, ClassVar
@@ -15,7 +14,6 @@ from overrides import override
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -217,14 +215,12 @@ class AnsibleLanguageServer(SolidLanguageServer):
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "--stdio"]
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Returns the initialize params for the Ansible Language Server.
Reads shortcut keys and the ``ansible_settings`` dict from ``_custom_settings``
to build ``initializationOptions``.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
# default ansible settings, populated from shortcut keys
ansible_settings: dict[str, Any] = {
"ansible": {
@@ -282,18 +278,9 @@ class AnsibleLanguageServer(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {"ansible": ansible_settings},
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""Starts the Ansible Language Server, waits for the server to be ready."""
@@ -325,7 +312,7 @@ class AnsibleLanguageServer(SolidLanguageServer):
log.info("Starting Ansible server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -5,7 +5,6 @@ Contains various configurations and settings specific to Bash scripting.
import logging
import os
import pathlib
import shutil
import threading
@@ -19,7 +18,6 @@ from solidlsp.ls import (
)
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__)
@@ -208,12 +206,10 @@ class BashLanguageServer(SolidLanguageServer):
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "start"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Bash Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -237,17 +233,8 @@ class BashLanguageServer(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""
@@ -283,7 +270,7 @@ class BashLanguageServer(SolidLanguageServer):
log.info("Starting Bash server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -14,7 +14,6 @@ You can configure the following options in ls_specific_settings (in serena_confi
import logging
import os
import pathlib
import re
import shutil
import subprocess
@@ -27,7 +26,6 @@ from solidlsp.ls import (
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__)
@@ -166,10 +164,8 @@ class BSLLanguageServer(SolidLanguageServer):
_verify_java_available()
return ["java", "-jar", core_path]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
root_uri = pathlib.Path(repository_absolute_path).as_uri()
return { # type: ignore[return-value]
def _create_base_initialize_params(self) -> dict:
return {
"locale": "en",
"capabilities": {
"textDocument": {
@@ -179,9 +175,9 @@ class BSLLanguageServer(SolidLanguageServer):
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))}, # type: ignore
"symbolKind": {"valueSet": list(range(1, 27))},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, # type: ignore
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"rename": {"dynamicRegistration": True, "prepareSupport": True},
},
"workspace": {
@@ -190,12 +186,6 @@ class BSLLanguageServer(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{"uri": root_uri, "name": os.path.basename(repository_absolute_path)},
],
}
def _start_server(self) -> None:
@@ -216,7 +206,7 @@ class BSLLanguageServer(SolidLanguageServer):
log.info("Starting BSL language server process")
self.server.start()
init_params = self._get_initialize_params(self.repository_root_path)
init_params = self._create_initialize_params()
init_response = self.server.send.initialize(init_params)
log.debug("BSL LSP initialize response: %s", init_response)
@@ -29,10 +29,8 @@ https://github.com/MaskRay/ccls
"""
import logging
import os
import pathlib
import threading
from typing import Any, cast
from typing import Any
from overrides import override
@@ -43,7 +41,6 @@ from solidlsp.ls import (
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__)
@@ -105,12 +102,10 @@ class CCLS(SolidLanguageServer):
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the ccls Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -123,18 +118,9 @@ class CCLS(SolidLanguageServer):
},
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
# ccls supports initializationOptions but none are required for basic functionality
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -154,7 +140,7 @@ class CCLS(SolidLanguageServer):
log.info("Starting ccls server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to ccls and awaiting response")
self.server.send.initialize(initialize_params)
@@ -2,10 +2,9 @@ import hashlib
import json
import logging
import os
import pathlib
import threading
from collections.abc import Hashable
from typing import Any, cast
from typing import Any
from overrides import override
@@ -13,7 +12,6 @@ from solidlsp.language_servers.common import UE_IGNORED_DIRNAMES
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, ProcessLaunchInfo, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection, is_unreal_engine_project
@@ -315,12 +313,10 @@ class ClangdLanguageServer(SolidLanguageServer):
# which is required for finding cross-file references
return [core_path, "--background-index"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the clangd Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -336,18 +332,9 @@ class ClangdLanguageServer(SolidLanguageServer):
},
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -403,7 +390,7 @@ class ClangdLanguageServer(SolidLanguageServer):
log.info("Starting Clangd server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+3 -11
View File
@@ -9,13 +9,11 @@ import re
import shutil
import subprocess
import threading
from typing import cast
from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -308,10 +306,8 @@ class ClojureLSP(SolidLanguageServer):
log.info(f"clojure-lsp source-paths scanned from project descriptors: {scanned}")
return scanned
def _get_initialize_params(self) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Returns the init params for clojure-lsp."""
repository_absolute_path = self.repository_root_path
root_uri = pathlib.Path(repository_absolute_path).as_uri()
source_paths = self._resolve_source_paths()
initialization_options: dict = {"dependency-scheme": "jar", "text-document-sync-kind": "incremental"}
@@ -319,9 +315,6 @@ class ClojureLSP(SolidLanguageServer):
initialization_options["source-paths"] = source_paths
result = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"workspace": {
"applyEdit": True,
@@ -344,9 +337,8 @@ class ClojureLSP(SolidLanguageServer):
},
"initializationOptions": initialization_options,
"trace": "off",
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
return cast(InitializeParams, result)
return result
def _start_server(self) -> None:
def register_capability_handler(params: dict) -> None:
@@ -389,7 +381,7 @@ class ClojureLSP(SolidLanguageServer):
log.info("Starting clojure-lsp server process")
self.server.start()
initialize_params = self._get_initialize_params()
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
-37
View File
@@ -4,7 +4,6 @@ import logging
import os
import pathlib
import platform
import shutil
import subprocess
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass, replace
@@ -151,42 +150,6 @@ class RuntimeDependencyCollection:
)
DEFAULT_UVX_PYTHON_VERSION = "3.13"
def build_uvx_launch_command(
package: str,
version: str,
entrypoint: str,
extra_args: Sequence[str] = (),
python_version: str = DEFAULT_UVX_PYTHON_VERSION,
) -> list[str]:
"""Build a command that runs a pinned PyPI package's console script on demand via ``uvx`` / ``uv x``.
Resolution order:
1. Prefer ``uvx`` (env var ``UVX`` or PATH lookup).
2. Fall back to ``uv x`` if only ``uv`` is on PATH.
3. Raise ``RuntimeError`` if neither is available.
:param package: PyPI package name (e.g. ``"pyright"``).
:param version: Pinned package version.
:param entrypoint: Console script provided by the package (e.g. ``"pyright-langserver"``).
:param extra_args: Arguments appended after the entrypoint (e.g. ``("--stdio",)``).
:param python_version: Python interpreter version passed via ``-p`` (uv will fetch it if missing).
"""
base_args = ["-p", python_version, "--from", f"{package}=={version}", entrypoint, *extra_args]
uvx_path = os.environ.get("UVX") or shutil.which("uvx")
if uvx_path is not None:
return [uvx_path, *base_args]
uv_path = shutil.which("uv")
if uv_path is not None:
return [uv_path, "x", *base_args]
raise RuntimeError("Could not find 'uvx' or 'uv' in PATH. Install uv (https://docs.astral.sh/uv/).")
def build_npm_install_command(package_name: str, version: str, registry: str | None = None) -> list[str]:
"""Build a pinned npm install command for a package in a Serena-managed install directory."""
command = ["npm", "install", "--prefix", "./"]
@@ -3,14 +3,11 @@ Provides Crystal specific instantiation of the LanguageServer class using Crysta
"""
import logging
import os
import pathlib
import shutil
import time
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -83,12 +80,10 @@ class CrystalLanguageServer(SolidLanguageServer):
log.info(f"Waiting {remaining_delay:.1f}s for Crystalline to compile the project")
time.sleep(remaining_delay)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Return the initialize params for the Crystal language server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -119,17 +114,8 @@ class CrystalLanguageServer(SolidLanguageServer):
"configuration": True,
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore[return-value]
return initialize_params
def _start_server(self) -> None:
"""Start the Crystal language server process."""
@@ -150,7 +136,7 @@ class CrystalLanguageServer(SolidLanguageServer):
log.info("Starting Crystal language server (Crystalline) process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -26,7 +26,7 @@ from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_types import Hover
from solidlsp.ls_utils import FileUtils, PathUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
from solidlsp.lsp_protocol_handler.lsp_types import InitializeResult
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -486,60 +486,51 @@ class CSharpLanguageServer(SolidLanguageServer):
except Exception as e:
raise SolidLSPException(f"Failed to download package {package_name} version {package_version} from NuGet.org: {e}") from e
def _get_initialize_params(self) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Microsoft.CodeAnalysis.LanguageServer.
"""
root_uri = PathUtils.path_to_uri(self.repository_root_path)
root_name = os.path.basename(self.repository_root_path)
return cast(
InitializeParams,
{
"workspaceFolders": [{"uri": root_uri, "name": root_name}],
"processId": os.getpid(),
"rootPath": self.repository_root_path,
"rootUri": root_uri,
"capabilities": {
"window": {
"workDoneProgress": True,
"showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}},
"showDocument": {"support": True},
return {
"capabilities": {
"window": {
"workDoneProgress": True,
"showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}},
"showDocument": {"support": True},
},
"workspace": {
"applyEdit": True,
"workspaceEdit": {"documentChanges": True},
"didChangeConfiguration": {"dynamicRegistration": True},
"didChangeWatchedFiles": {"dynamicRegistration": True},
"symbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"workspace": {
"applyEdit": True,
"workspaceEdit": {"documentChanges": True},
"didChangeConfiguration": {"dynamicRegistration": True},
"didChangeWatchedFiles": {"dynamicRegistration": True},
"symbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
"executeCommand": {"dynamicRegistration": True},
"configuration": True,
"workspaceFolders": True,
"workDoneProgress": True,
},
"textDocument": {
"synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"signatureHelp": {
"dynamicRegistration": True,
"signatureInformation": {
"documentationFormat": ["markdown", "plaintext"],
"parameterInformation": {"labelOffsetSupport": True},
},
"executeCommand": {"dynamicRegistration": True},
"configuration": True,
"workspaceFolders": True,
"workDoneProgress": True,
},
"textDocument": {
"synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"signatureHelp": {
"dynamicRegistration": True,
"signatureInformation": {
"documentationFormat": ["markdown", "plaintext"],
"parameterInformation": {"labelOffsetSupport": True},
},
},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
"hierarchicalDocumentSymbolSupport": True,
},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
"hierarchicalDocumentSymbolSupport": True,
},
},
},
)
}
def _start_server(self) -> None:
indexing_complete = threading.Event()
@@ -672,7 +663,7 @@ class CSharpLanguageServer(SolidLanguageServer):
raise SolidLSPException(f"Failed to start C# language server: {e}")
# Send initialization
initialize_params = self._get_initialize_params()
initialize_params = self._create_initialize_params()
log.info("Sending initialize request to language server")
try:
@@ -7,15 +7,12 @@ from __future__ import annotations
import logging
import os
import pathlib
import threading
from typing import cast
from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -184,15 +181,9 @@ class CueLanguageServer(SolidLanguageServer):
# cue's LSP mode is activated via the `lsp` subcommand; it speaks LSP over stdio.
return [core_path, "lsp"]
def _get_initialize_params(self) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Returns the init params for ``cue lsp``."""
repository_absolute_path = self.repository_root_path
root_uri = pathlib.Path(repository_absolute_path).as_uri()
result = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"workspace": {
"applyEdit": True,
@@ -221,9 +212,8 @@ class CueLanguageServer(SolidLanguageServer):
},
"initializationOptions": {},
"trace": "off",
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
return cast(InitializeParams, result)
return result
def _start_server(self) -> None:
def register_capability_handler(params: dict) -> None:
@@ -252,7 +242,7 @@ class CueLanguageServer(SolidLanguageServer):
log.info("Starting cue lsp server process")
self.server.start()
initialize_params = self._get_initialize_params()
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -1,8 +1,6 @@
import logging
import os
import pathlib
from collections.abc import Hashable
from typing import cast
from overrides import override
@@ -11,7 +9,6 @@ from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from ..ls_config import Language, LanguageServerConfig
from ..lsp_protocol_handler.lsp_types import InitializeParams
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
@@ -149,12 +146,10 @@ class DartLanguageServer(SolidLanguageServer):
return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2"
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Dart Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"capabilities": {
"textDocument": {
@@ -171,18 +166,9 @@ class DartLanguageServer(SolidLanguageServer):
"allowOpenUri": False,
},
"trace": "verbose",
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": pathlib.Path(repository_absolute_path).as_uri(),
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -212,7 +198,7 @@ class DartLanguageServer(SolidLanguageServer):
log.info("Starting dart-language-server server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.debug("Sending initialize request to dart-language-server")
init_response = self.server.send_request("initialize", initialize_params) # type: ignore
log.info(f"Received initialize response from dart-language-server: {init_response}")
+47 -52
View File
@@ -25,7 +25,7 @@ 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
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, SymbolInformation
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -180,8 +180,10 @@ class EclipseJDTLS(SolidLanguageServer):
- 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)
- gradle_java_home: Path to JDK for Gradle (default: null, uses bundled JRE)
- use_system_java_home: Whether to use the system's JAVA_HOME for JDTLS itself (default: false)
- gradle_java_home: Path to JDK for Gradle (default: null, falls back to JAVA_HOME when
use_system_java_home is true, then the bundled JRE)
- use_system_java_home: Whether to use the system's JAVA_HOME for JDTLS itself and, when
gradle_java_home is unset, Gradle import (default: false)
- jdtls_xmx: Maximum heap size for the JDTLS server JVM (default: "3G")
- jdtls_xms: Initial heap size for the JDTLS server JVM (default: "100m")
- intellicode_xmx: Maximum heap size for the IntelliCode embedded JVM (default: "1G")
@@ -217,7 +219,7 @@ class EclipseJDTLS(SolidLanguageServer):
# gradle_user_home: 'C:\\Users\\YourName\\.gradle' # Windows (use single quotes!)
gradle_wrapper_enabled: true # set to true for projects with custom plugins/repositories
gradle_java_home: "/path/to/jdk" # set to override Gradle's JDK
use_system_java_home: true # set to true to use system JAVA_HOME for JDTLS
use_system_java_home: true # set to true to use system JAVA_HOME for JDTLS and Gradle fallback
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
@@ -252,24 +254,6 @@ class EclipseJDTLS(SolidLanguageServer):
ls_resources_dir = self.ls_resources_dir(self._solidlsp_settings)
return self.DependencyProvider(self._custom_settings, ls_resources_dir, self._solidlsp_settings, self.repository_root_path)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
# Ignore common Java build directories from different build tools:
# - Maven: target
# - Gradle: build, .gradle
# - Eclipse: bin, .settings
# - IntelliJ IDEA: out, .idea
# - General: classes, dist, lib
return super().is_ignored_dirname(dirname) or dirname in [
"target", # Maven
"build", # Gradle
"bin", # Eclipse
"out", # IntelliJ IDEA
"classes", # General
"dist", # General
"lib", # General
]
class DependencyProvider(LanguageServerDependencyProvider):
def __init__(
self,
@@ -904,15 +888,47 @@ class EclipseJDTLS(SolidLanguageServer):
log.info(f"Using bundled JRE for JDTLS: {java_home}")
return {"syntaxserver": "false", "JAVA_HOME": java_home}
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
def _resolve_gradle_java_home(self) -> str:
"""
Resolve the Java home configured for JDTLS Gradle import settings.
:return: Java home selected from explicit Gradle override, system ``JAVA_HOME`` when enabled, or bundled runtime.
:raises FileNotFoundError: If an explicitly configured ``gradle_java_home`` path does not exist.
"""
# explicit Gradle override...
gradle_java_home = self._custom_settings.get("gradle_java_home")
if gradle_java_home is not None:
if not os.path.exists(gradle_java_home):
error_msg = (
f"Gradle Java home not found: {gradle_java_home}. "
f"Fix: update path in ~/.serena/serena_config.yml (ls_specific_settings -> java -> gradle_java_home), "
f"or remove the setting to use JAVA_HOME or the bundled JRE fallback"
)
log.error(error_msg)
raise FileNotFoundError(error_msg)
log.info(f"Using Gradle Java home from custom location: {gradle_java_home}")
return gradle_java_home
# system JAVA_HOME fallback...
use_system_java_home = self._custom_settings.get("use_system_java_home", False)
if use_system_java_home:
system_java_home = os.environ.get("JAVA_HOME")
if system_java_home:
log.info(f"Using system JAVA_HOME for Gradle: {system_java_home}")
return system_java_home
log.warning("use_system_java_home is set but JAVA_HOME is not set in environment, falling back to bundled JRE for Gradle")
# bundled runtime fallback...
log.info(f"Using bundled JRE for Gradle: {self.runtime_dependency_paths.jre_path}")
return self.runtime_dependency_paths.jre_path
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize parameters for the EclipseJDTLS server.
"""
# Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available
if not os.path.isabs(repository_absolute_path):
repository_absolute_path = os.path.abspath(repository_absolute_path)
repo_uri = pathlib.Path(repository_absolute_path).as_uri()
repo_uri = pathlib.Path(self.repository_root_path).as_uri()
# Load user's Maven and Gradle configuration paths from ls_specific_settings["java"]
@@ -979,25 +995,11 @@ class EclipseJDTLS(SolidLanguageServer):
f"Gradle wrapper {'enabled' if gradle_wrapper_enabled else 'disabled'} (configurable via ls_specific_settings -> java -> gradle_wrapper_enabled)"
)
# Gradle Java home: default to None, which means the bundled JRE is used
gradle_java_home = self._custom_settings.get("gradle_java_home")
if gradle_java_home is not None:
if not os.path.exists(gradle_java_home):
error_msg = (
f"Gradle Java home not found: {gradle_java_home}. "
f"Fix: update path in ~/.serena/serena_config.yml (ls_specific_settings -> java -> gradle_java_home), "
f"or remove the setting to use the bundled JRE"
)
log.error(error_msg)
raise FileNotFoundError(error_msg)
log.info(f"Using Gradle Java home from custom location: {gradle_java_home}")
else:
log.info(f"Using bundled JRE for Gradle: {self.runtime_dependency_paths.jre_path}")
# Gradle Java home: explicit setting, system JAVA_HOME when requested, then bundled runtime.
gradle_java_home = self._resolve_gradle_java_home()
initialize_params = {
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": pathlib.Path(repository_absolute_path).as_uri(),
"capabilities": {
"workspace": {
"applyEdit": True,
@@ -1290,13 +1292,6 @@ class EclipseJDTLS(SolidLanguageServer):
},
},
"trace": "verbose",
"processId": os.getpid(),
"workspaceFolders": [
{
"uri": repo_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
initialize_params["initializationOptions"]["workspaceFolders"] = [repo_uri]
@@ -1323,8 +1318,8 @@ class EclipseJDTLS(SolidLanguageServer):
# ./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)
gradle_settings["java"] = {"home": gradle_java_home}
return initialize_params
def _start_server(self) -> None:
"""
@@ -1377,7 +1372,7 @@ class EclipseJDTLS(SolidLanguageServer):
log.info("Starting EclipseJDTLS server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -5,14 +5,13 @@ import stat
import subprocess
import threading
from collections.abc import Hashable
from typing import Any, cast
from typing import Any
from overrides import override
from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -256,19 +255,12 @@ class ElixirTools(SolidLanguageServer):
return None
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Expert Language Server.
"""
# Ensure the path is absolute
abs_path = os.path.abspath(repository_absolute_path)
root_uri = pathlib.Path(abs_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"locale": "en",
"rootPath": abs_path,
"rootUri": root_uri,
"initializationOptions": {
"mix_env": "dev",
"mix_target": "host",
@@ -319,10 +311,9 @@ class ElixirTools(SolidLanguageServer):
"workDoneProgress": True,
},
},
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""Start Expert server process"""
@@ -381,7 +372,7 @@ class ElixirTools(SolidLanguageServer):
log.debug("Starting Expert server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.debug("Sending initialize request to Expert")
init_response = self.server.send.initialize(initialize_params)
@@ -4,7 +4,6 @@ Provides Elm specific instantiation of the LanguageServer class. Contains variou
import logging
import os
import pathlib
import shutil
import threading
@@ -14,7 +13,6 @@ 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
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -131,13 +129,10 @@ class ElmLanguageServer(SolidLanguageServer):
)
return [elm_ls_executable_path, "--stdio"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Elm Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -169,17 +164,8 @@ class ElmLanguageServer(SolidLanguageServer):
"skipInstallPackageConfirmation": True,
"onlyUpdateDiagnosticsOnSave": False,
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore[return-value]
return initialize_params
def _start_server(self) -> None:
"""
@@ -204,7 +190,7 @@ class ElmLanguageServer(SolidLanguageServer):
log.info("Starting Elm server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -3,8 +3,6 @@ Fortran Language Server implementation using fortls.
"""
import logging
import os
import pathlib
import re
from overrides import override
@@ -19,7 +17,6 @@ from solidlsp.ls import (
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__)
@@ -190,10 +187,8 @@ class FortranLanguageServer(SolidLanguageServer):
version_setting_key="fortls_version",
)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Initialize params for Fortran Language Server."""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -231,17 +226,8 @@ class FortranLanguageServer(SolidLanguageServer):
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore[return-value]
return initialize_params
def _start_server(self) -> None:
"""Start Fortran Language Server process."""
@@ -264,7 +250,7 @@ class FortranLanguageServer(SolidLanguageServer):
log.info("Starting Fortran Language Server (fortls) process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request to Fortran Language Server")
init_response = self.server.send.initialize(initialize_params)
@@ -4,7 +4,6 @@ Provides F# specific instantiation of the LanguageServer class.
import logging
import os
import pathlib
import shutil
import threading
from pathlib import Path
@@ -16,7 +15,6 @@ from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependenc
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -129,17 +127,11 @@ class FSharpLanguageServer(SolidLanguageServer):
# FsAutoComplete uses --lsp flag for LSP mode
return f"{fsautocomplete_path} --adaptive-lsp-server-enabled --project-graph-enabled --use-fcs-transparent-compiler"
def _get_initialize_params(self) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the F# Language Server.
"""
root_uri = pathlib.Path(self.repository_root_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"rootPath": self.repository_root_path,
"rootUri": root_uri,
"workspaceFolders": [{"name": "workspace", "uri": root_uri}],
"capabilities": {
"workspace": {
"applyEdit": True,
@@ -269,7 +261,7 @@ class FSharpLanguageServer(SolidLanguageServer):
"trace": "off",
}
return initialize_params # type: ignore
return initialize_params
def _get_dotnet_root(self) -> str:
"""
@@ -362,7 +354,7 @@ class FSharpLanguageServer(SolidLanguageServer):
raise SolidLSPException(f"Failed to start F# language server: {e}")
# Send initialization
initialize_params = self._get_initialize_params()
initialize_params = self._create_initialize_params()
log.info("Sending initialize request to F# language server")
try:
@@ -8,14 +8,12 @@ The editor must be open with its built-in language server enabled (default).
import logging
import os
import pathlib
from collections.abc import Callable
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_process import LanguageServerInterface, TCPConnectionInfo, TCPLanguageServer
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo, StringDict
from solidlsp.lsp_protocol_handler.server import StringDict
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -49,7 +47,7 @@ class GodotLanguageServer(SolidLanguageServer):
self._configured_request_timeout: float | None = None
# Dummy ProcessLaunchInfo — _create_language_server_interface() ignores it
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=""), "gdscript", solidlsp_settings)
super().__init__(config, repository_root_path, None, "gdscript", solidlsp_settings)
def set_request_timeout(self, timeout: float | None) -> None:
"""Cap the timeout at the value configured in ls_specific_settings, if set."""
@@ -75,11 +73,7 @@ class GodotLanguageServer(SolidLanguageServer):
pass
return None
def _create_language_server_interface(
self,
process_launch_info: ProcessLaunchInfo,
logging_fn: Callable[[str, str, StringDict | str], None] | None,
) -> LanguageServerInterface:
def _create_language_server_interface(self, logging_fn: Callable[[str, str, StringDict | str], None] | None) -> LanguageServerInterface:
settings = self._custom_settings or {}
port = settings.get("port", DEFAULT_GODOT_LS_PORT)
request_timeout = settings.get("request_timeout", DEFAULT_GODOT_REQUEST_TIMEOUT)
@@ -93,14 +87,8 @@ class GodotLanguageServer(SolidLanguageServer):
request_timeout=request_timeout,
)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
root_uri = pathlib.Path(repository_absolute_path).as_uri()
def _create_base_initialize_params(self) -> dict:
params = {
"processId": os.getpid(),
"rootUri": root_uri,
"rootPath": repository_absolute_path,
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -125,7 +113,7 @@ class GodotLanguageServer(SolidLanguageServer):
},
},
}
return params # type: ignore
return params
def _start_server(self) -> None:
def do_nothing(params: dict) -> None:
@@ -143,7 +131,7 @@ class GodotLanguageServer(SolidLanguageServer):
log.info("Connecting to Godot LSP at %s:%d", self._conn_info.host, self._conn_info.port)
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending LSP initialize request to Godot")
self.server.send.initialize(initialize_params)
self.server.notify.initialized({})
+4 -16
View File
@@ -2,16 +2,14 @@ import hashlib
import json
import logging
import os
import pathlib
import subprocess
from collections.abc import Hashable
from typing import Any, cast
from typing import Any
from overrides import override
from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -103,11 +101,10 @@ class Gopls(SolidLanguageServer):
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), "go", solidlsp_settings)
self.request_id = 0
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Go Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params: dict = {
"locale": "en",
"capabilities": {
@@ -122,15 +119,6 @@ class Gopls(SolidLanguageServer):
},
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
# Apply gopls-specific settings via initializationOptions
@@ -149,7 +137,7 @@ class Gopls(SolidLanguageServer):
log.debug("Applying gopls settings via initializationOptions: keys=%s", list(gopls_settings.keys()))
initialize_params["initializationOptions"] = gopls_settings
return cast(InitializeParams, initialize_params)
return initialize_params
def _validate_gopls_settings_dict(self, gopls_settings: object) -> dict:
if not isinstance(gopls_settings, dict):
@@ -233,7 +221,7 @@ class Gopls(SolidLanguageServer):
log.info("Starting gopls server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -5,13 +5,11 @@ Provides Groovy specific instantiation of the LanguageServer class. Contains var
import dataclasses
import logging
import os
import pathlib
import shlex
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -244,19 +242,11 @@ class GroovyLanguageServer(SolidLanguageServer):
" Ensure the JAR file is available at the configured path\n"
)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Groovy Language Server.
"""
if not os.path.isabs(repository_absolute_path):
repository_absolute_path = os.path.abspath(repository_absolute_path)
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"clientInfo": {"name": "Serena Groovy Client", "version": "1.0.0"},
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"textDocument": {
"synchronization": {"dynamicRegistration": True, "didSave": True},
@@ -282,15 +272,8 @@ class GroovyLanguageServer(SolidLanguageServer):
}
},
},
"processId": os.getpid(),
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""
@@ -316,7 +299,7 @@ class GroovyLanguageServer(SolidLanguageServer):
log.info("Starting Groovy server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -4,7 +4,6 @@ Provides Haskell specific instantiation of the LanguageServer class. Contains va
import logging
import os
import pathlib
import shutil
import time
from typing import Any
@@ -13,7 +12,6 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -107,14 +105,11 @@ class HaskellLanguageServer(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["dist", "dist-newstyle", ".stack-work", ".cabal-sandbox"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Haskell Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"clientInfo": {"name": "Serena", "version": "0.1.0"},
"locale": "en",
"capabilities": {
"workspace": {
@@ -299,17 +294,8 @@ class HaskellLanguageServer(SolidLanguageServer):
}
},
"trace": "verbose",
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""
@@ -364,7 +350,7 @@ class HaskellLanguageServer(SolidLanguageServer):
log.info("Starting Haskell Language Server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -22,7 +22,7 @@ from solidlsp.ls import (
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_types import Hover
from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticSeverity, InitializeParams
from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticSeverity
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -202,13 +202,11 @@ class HaxeLanguageServer(SolidLanguageServer):
"dump",
]
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Return initialize params for the Haxe Language Server.
displayArguments are resolved from user-configured buildFile or auto-discovered .hxml files.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
# 1. Check for user-configured .hxml path
configured_build_file = self._custom_settings.get("buildFile")
if configured_build_file:
@@ -216,7 +214,7 @@ class HaxeLanguageServer(SolidLanguageServer):
display_arguments = [configured_build_file]
else:
# 2. Auto-discover .hxml files recursively
display_arguments = self._discover_hxml_file(repository_absolute_path)
display_arguments = self._discover_hxml_file(self.repository_root_path)
init_options: dict = {"displayArguments": display_arguments}
rename_source_folders = self._custom_settings.get("renameSourceFolders")
@@ -258,17 +256,8 @@ class HaxeLanguageServer(SolidLanguageServer):
},
},
"initializationOptions": init_options,
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore[return-value]
return initialize_params
@staticmethod
def _discover_hxml_file(repository_absolute_path: str) -> list[str]:
@@ -377,7 +366,7 @@ class HaxeLanguageServer(SolidLanguageServer):
log.info("Starting Haxe server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
self.server.send.initialize(initialize_params)
@@ -5,9 +5,8 @@ Supports HLSL, GLSL, and WGSL shader file formats.
import logging
import os
import pathlib
import shutil
from typing import Any, cast
from typing import Any
from overrides import override
@@ -17,7 +16,6 @@ from solidlsp.ls import (
SolidLanguageServer,
)
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -83,7 +81,7 @@ class HlslLanguageServer(SolidLanguageServer):
base_url = f"{_GITHUB_RELEASE_BASE}/{tag}"
# macOS has no pre-built binaries; build from source via cargo install
cargo_install_cmd = ["cargo", "install", "shader_language_server", "--version", version, "--root", "."]
cargo_install_cmd = ["cargo", "install", "shader_language_server", "--version", version, "--locked", "--root", "."]
deps = RuntimeDependencyCollection(
[
@@ -143,7 +141,7 @@ class HlslLanguageServer(SolidLanguageServer):
raise FileNotFoundError(
"shader-language-server is not installed and no auto-install is available for your platform.\n"
"Please install it using one of the following methods:\n"
" cargo: cargo install shader_language_server\n"
" cargo: cargo install shader_language_server --locked\n"
" GitHub: Download from https://github.com/antaalt/shader-sense/releases\n"
"On macOS, install the Rust toolchain (https://rustup.rs) and Serena will build from source automatically.\n"
"See https://github.com/antaalt/shader-sense for more details."
@@ -167,13 +165,8 @@ class HlslLanguageServer(SolidLanguageServer):
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "--stdio"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
root_uri = pathlib.Path(repository_absolute_path).as_uri()
def _create_base_initialize_params(self) -> dict:
initialize_params = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"locale": "en",
"capabilities": {
"textDocument": {
@@ -207,9 +200,8 @@ class HlslLanguageServer(SolidLanguageServer):
"configuration": True,
},
},
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
return cast(InitializeParams, initialize_params)
return initialize_params
@override
def _start_server(self) -> None:
@@ -237,7 +229,7 @@ class HlslLanguageServer(SolidLanguageServer):
log.info("Starting shader-language-server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request")
init_response = self.server.send.initialize(initialize_params)
+4 -15
View File
@@ -4,7 +4,6 @@ Provides PHP specific instantiation of the LanguageServer class using Intelephen
import logging
import os
import pathlib
import shutil
from time import sleep
@@ -13,7 +12,7 @@ from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, InitializeParams, LocationLink
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink
from solidlsp.settings import SolidLSPSettings
from ..lsp_protocol_handler import lsp_types
@@ -110,11 +109,10 @@ class Intelephense(SolidLanguageServer):
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialization params for the Intelephense Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -135,15 +133,6 @@ class Intelephense(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
initialization_options = {}
# Add license key if provided via environment variable
@@ -159,7 +148,7 @@ class Intelephense(SolidLanguageServer):
initialization_options["intelephense.files.maxSize"] = max_file_size
initialize_params["initializationOptions"] = initialization_options
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""Start Intelephense server process"""
@@ -180,7 +169,7 @@ class Intelephense(SolidLanguageServer):
log.info("Starting Intelephense server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+3 -19
View File
@@ -3,16 +3,12 @@ Provides Python specific instantiation of the LanguageServer class. Contains var
"""
import logging
import os
import pathlib
import threading
from typing import cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -40,18 +36,12 @@ class JediServer(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Jedi Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"clientInfo": {"name": "Serena", "version": "0.1.0"},
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": root_uri,
# Note: this is not necessarily the minimal set of capabilities...
"capabilities": {
"workspace": {
@@ -143,14 +133,8 @@ class JediServer(SolidLanguageServer):
},
},
"trace": "verbose",
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -182,7 +166,7 @@ class JediServer(SolidLanguageServer):
log.info("Starting jedi-language-server server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -5,14 +5,12 @@ Contains various configurations and settings specific to JSON files.
import logging
import os
import pathlib
import shutil
from typing import Any
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -101,12 +99,10 @@ class JsonLanguageServer(SolidLanguageServer):
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "--stdio"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the JSON Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -127,17 +123,8 @@ class JsonLanguageServer(SolidLanguageServer):
"didChangeConfiguration": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""
@@ -160,7 +147,7 @@ class JsonLanguageServer(SolidLanguageServer):
log.info("Starting JSON server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+4 -15
View File
@@ -1,6 +1,5 @@
import logging
import os
import pathlib
import platform
import shutil
import subprocess
@@ -10,7 +9,7 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticTag, InitializeParams
from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticTag
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -136,15 +135,11 @@ class JuliaLanguageServer(SolidLanguageServer):
# runs (standalone test invocations, CI cold start).
return 30.0
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Julia Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params: InitializeParams = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
initialize_params: dict = {
"capabilities": {
# workspace.configuration MUST be true: LanguageServer.jl pulls all julia.lint.*
# settings via workspace/configuration after initialize. Without it, the server
@@ -164,12 +159,6 @@ class JuliaLanguageServer(SolidLanguageServer):
"documentSymbol": {"dynamicRegistration": True},
},
},
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params
@@ -202,7 +191,7 @@ class JuliaLanguageServer(SolidLanguageServer):
log.info("Starting LanguageServer.jl server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request to Julia Language Server")
init_response = self.server.send.initialize(initialize_params)
@@ -21,7 +21,6 @@ import os
import pathlib
import stat
import threading
from typing import cast
from overrides import override
@@ -32,7 +31,6 @@ from solidlsp.ls import (
)
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -185,20 +183,13 @@ class KotlinLanguageServer(SolidLanguageServer):
env["JAVA_TOOL_OPTIONS"] = jvm_options
return env
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Kotlin Language Server.
"""
if not os.path.isabs(repository_absolute_path):
repository_absolute_path = os.path.abspath(repository_absolute_path)
root_uri = pathlib.Path(repository_absolute_path).as_uri()
root_uri = pathlib.Path(self.repository_root_path).as_uri()
initialize_params = {
"clientInfo": {"name": "Multilspy Kotlin Client", "version": "1.0.0"},
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"workspace": {
"applyEdit": True,
@@ -429,15 +420,8 @@ class KotlinLanguageServer(SolidLanguageServer):
},
},
"trace": "off",
"processId": os.getpid(),
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -503,7 +487,7 @@ class KotlinLanguageServer(SolidLanguageServer):
log.info("Starting Kotlin server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -4,17 +4,13 @@ Uses the built-in Lean 4 language server (lean --server).
"""
import logging
import os
import pathlib
import shutil
import subprocess
from typing import cast
from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, 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__)
@@ -96,16 +92,11 @@ class Lean4LanguageServer(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in [".lake", "build"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Lean 4 Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -131,14 +122,8 @@ class Lean4LanguageServer(SolidLanguageServer):
"didChangeConfiguration": {"dynamicRegistration": True},
},
},
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""Start the Lean 4 language server process."""
@@ -160,7 +145,7 @@ class Lean4LanguageServer(SolidLanguageServer):
log.info("Starting Lean 4 language server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+3 -17
View File
@@ -7,8 +7,6 @@ You can pass the following entries in ``ls_specific_settings["lua"]``:
"""
import logging
import os
import pathlib
import platform
import shutil
from collections.abc import Hashable
@@ -20,7 +18,6 @@ from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -220,12 +217,10 @@ class LuaLanguageServer(SolidLanguageServer):
return original_name
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Lua Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -270,15 +265,6 @@ class LuaLanguageServer(SolidLanguageServer):
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {
# Lua Language Server specific options
"runtime": {
@@ -304,7 +290,7 @@ class LuaLanguageServer(SolidLanguageServer):
},
},
}
return initialize_params # type: ignore[return-value]
return initialize_params
def _start_server(self) -> None:
"""Start Lua Language Server process"""
@@ -325,7 +311,7 @@ class LuaLanguageServer(SolidLanguageServer):
log.info("Starting Lua Language Server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+3 -17
View File
@@ -20,8 +20,6 @@ See: https://github.com/JohnnyMorganz/luau-lsp
"""
import logging
import os
import pathlib
import platform
import shutil
import threading
@@ -32,7 +30,6 @@ from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -270,12 +267,10 @@ class LuauLanguageServer(SolidLanguageServer):
super().__init__(config, repository_root_path, None, "luau", solidlsp_settings)
self.server_ready = threading.Event()
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Luau Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -322,20 +317,11 @@ class LuauLanguageServer(SolidLanguageServer):
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
# luau-lsp initialization options
# These can be overridden via .luaurc in the project root
"initializationOptions": {},
}
return initialize_params # type: ignore[return-value]
return initialize_params
def _start_server(self) -> None:
"""Start Luau Language Server process"""
@@ -366,7 +352,7 @@ class LuauLanguageServer(SolidLanguageServer):
log.info("Starting Luau Language Server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+5 -18
View File
@@ -5,7 +5,6 @@ Contains various configurations and settings specific to Markdown.
import logging
import os
import pathlib
from collections.abc import Hashable
from overrides import override
@@ -19,7 +18,6 @@ from solidlsp.ls import (
)
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -195,17 +193,12 @@ class Marksman(SolidLanguageServer):
return document_symbols
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Marksman Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params: InitializeParams = {
"processId": os.getpid(),
initialize_params: dict = {
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -215,9 +208,9 @@ class Marksman(SolidLanguageServer):
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))}, # type: ignore[arg-type]
"symbolKind": {"valueSet": list(range(1, 27))},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, # type: ignore[list-item]
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"codeAction": {"dynamicRegistration": True},
},
"workspace": {
@@ -226,12 +219,6 @@ class Marksman(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params
@@ -256,7 +243,7 @@ class Marksman(SolidLanguageServer):
log.info("Starting marksman server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to marksman server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -37,12 +37,12 @@ import pathlib
import platform
import shutil
import threading
from typing import Any, cast
from typing import Any
from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, InitializeParams, SymbolInformation
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, SymbolInformation
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -361,10 +361,8 @@ class MatlabLanguageServer(SolidLanguageServer):
"MATLAB_INSTALL_PATH": self.get_matlab_path(),
}
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Return the initialize params for the MATLAB Language Server."""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -394,17 +392,8 @@ class MatlabLanguageServer(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""Start the MATLAB Language Server and wait for it to be ready."""
@@ -460,7 +449,7 @@ class MatlabLanguageServer(SolidLanguageServer):
log.info("Starting MATLAB server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -8,7 +8,6 @@ and launched as a subprocess using the current Python interpreter.
import logging
import os
import pathlib
import sys
import threading
@@ -16,7 +15,6 @@ from solidlsp.ls import (
SolidLanguageServer,
)
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -46,10 +44,8 @@ class MslLanguageServer(SolidLanguageServer):
)
self.server_ready = threading.Event()
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Returns the initialize params for the mSL Language Server."""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -69,12 +65,8 @@ class MslLanguageServer(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""Starts the mSL Language Server."""
@@ -91,7 +83,7 @@ class MslLanguageServer(SolidLanguageServer):
log.info("Starting mSL server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request to mSL LSP server")
init_response = self.server.send.initialize(initialize_params)
+2 -16
View File
@@ -6,8 +6,6 @@ Note: Windows is not supported as Nix itself doesn't support Windows natively.
"""
import logging
import os
import pathlib
import platform
import shutil
import subprocess
@@ -18,7 +16,6 @@ from overrides import override
from solidlsp import ls_types
from solidlsp.ls import DocumentSymbols, 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
from solidlsp.settings import SolidLSPSettings
@@ -250,12 +247,10 @@ class NixLanguageServer(SolidLanguageServer):
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=nixd_path, cwd=repository_root_path), "nix", solidlsp_settings)
self.request_id = 0
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for nixd.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -318,15 +313,6 @@ class NixLanguageServer(SolidLanguageServer):
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {
# nixd specific options
"nixpkgs": {"expr": "import <nixpkgs> { }"},
@@ -360,7 +346,7 @@ class NixLanguageServer(SolidLanguageServer):
log.info("Starting nixd server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -5,7 +5,6 @@ Contains various configurations and settings specific to OCaml and Reason.
import logging
import os
import pathlib
import platform
import re
import shutil
@@ -18,7 +17,6 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from solidlsp.util.subprocess_util import subprocess_kwargs
@@ -321,19 +319,13 @@ class OcamlLanguageServer(SolidLanguageServer):
"""Define language-specific directories to ignore for OCaml projects."""
return super().is_ignored_dirname(dirname) or dirname in ["_build", "_opam", ".opam"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the OCaml Language Server.
Supports both OCaml and Reason.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"clientInfo": {"name": "Serena", "version": "0.1.0"},
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"workspace": {
"workspaceFolders": True,
@@ -376,14 +368,8 @@ class OcamlLanguageServer(SolidLanguageServer):
},
},
"trace": "verbose",
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore[return-value]
return initialize_params
def _start_server(self) -> None:
"""
@@ -417,7 +403,7 @@ class OcamlLanguageServer(SolidLanguageServer):
log.info("Starting OCaml LSP server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+6 -17
View File
@@ -5,7 +5,6 @@ Provides C# specific instantiation of the LanguageServer class. Contains various
import json
import logging
import os
import pathlib
import threading
from collections.abc import Iterable
@@ -15,7 +14,6 @@ from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -127,8 +125,7 @@ class OmniSharp(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Omnisharp Language Server.
"""
@@ -137,18 +134,10 @@ class OmniSharp(SolidLanguageServer):
del d["_description"]
d["processId"] = os.getpid()
assert d["rootPath"] == "$rootPath"
d["rootPath"] = repository_absolute_path
assert d["rootUri"] == "$rootUri"
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
assert d["workspaceFolders"][0]["uri"] == "$uri"
d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri()
assert d["workspaceFolders"][0]["name"] == "$name"
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
# processId, rootPath, rootUri and workspaceFolders are set centrally by the InitializeParamsBuilder
del d["rootPath"]
del d["rootUri"]
del d["workspaceFolders"]
return d
@@ -402,7 +391,7 @@ class OmniSharp(SolidLanguageServer):
log.info("Starting OmniSharp server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+3 -17
View File
@@ -49,7 +49,6 @@ import hashlib
import json
import logging
import os
import pathlib
import platform
import shutil
import tarfile
@@ -64,7 +63,6 @@ import zipfile
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, quote_windows_path
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -777,8 +775,7 @@ class PascalLanguageServer(SolidLanguageServer):
log.info(f"Using pasls at: {pasls_executable_path}")
return quote_windows_path(pasls_executable_path)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Pascal Language Server.
@@ -788,8 +785,6 @@ class PascalLanguageServer(SolidLanguageServer):
We only pass target OS/CPU in initializationOptions if explicitly set.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
# Build initializationOptions from environment variables
# pasls reads these to configure CodeTools:
# - PP: Path to FPC compiler executable
@@ -881,18 +876,9 @@ class PascalLanguageServer(SolidLanguageServer):
},
},
"initializationOptions": initialization_options,
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""
@@ -926,7 +912,7 @@ class PascalLanguageServer(SolidLanguageServer):
log.info("Starting Pascal server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -5,8 +5,6 @@ Note: Windows is not supported as Nix itself doesn't support Windows natively.
"""
import logging
import os
import pathlib
import subprocess
import time
from typing import Any
@@ -14,23 +12,34 @@ from typing import Any
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import DidChangeConfigurationParams, InitializeParams
from solidlsp.lsp_protocol_handler.lsp_types import DidChangeConfigurationParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
# Defaults handed to Perl::LanguageServer via its `perl.fileFilter` / `perl.ignoreDirs` settings.
# Exposed for override via `ls_specific_settings["perl"]`. Extensions added via `file_filter` are
# also synced into Language.PERL.get_source_fn_matcher() (a cached singleton) so Serena's symbol
# index and the language server agree on which files are Perl sources (#1449).
_DEFAULT_FILE_FILTER: list[str] = [".pm", ".pl", ".t"]
_DEFAULT_IGNORE_DIRS: list[str] = [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"]
class PerlLanguageServer(SolidLanguageServer):
"""
Provides Perl specific instantiation of the LanguageServer class using Perl::LanguageServer.
"""
# Keep in sync with Language.PERL.get_source_fn_matcher() in solidlsp/ls_config.py:
# extensions missing here are invisible to Perl::LanguageServer's project index.
_FILE_FILTER: list = [".pm", ".pl", ".t"]
You can pass the following entries in ``ls_specific_settings["perl"]``:
- file_filter: List of file extensions (with leading dot) that Perl::LanguageServer should
index, e.g. ``[".pm", ".pl", ".t", ".cgi"]``. Defaults to ``[".pm", ".pl", ".t"]``.
The same extensions are added to Serena's Perl source-file matcher, so ``find_symbol``
and symbol indexing treat them consistently (see #1449).
- ignore_dirs: Directory names Perl::LanguageServer should skip when indexing. Defaults to
``[".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"]``.
"""
@staticmethod
def _get_perl_version() -> str | None:
@@ -103,6 +112,31 @@ class PerlLanguageServer(SolidLanguageServer):
return "perl -MPerl::LanguageServer -e 'Perl::LanguageServer::run'"
@staticmethod
def _resolve_filter_settings(solidlsp_settings: SolidLSPSettings) -> tuple[list[str], list[str]]:
"""Resolve the ``fileFilter`` / ``ignoreDirs`` to hand to Perl::LanguageServer.
Reads optional overrides from ``ls_specific_settings["perl"]`` (keys ``file_filter`` and
``ignore_dirs``); falls back to the defaults otherwise. Extracted as a pure function so the
configuration plumbing can be unit-tested without starting the language server.
"""
perl_settings = solidlsp_settings.get_ls_specific_settings(Language.PERL)
file_filter = perl_settings.get("file_filter", list(_DEFAULT_FILE_FILTER))
ignore_dirs = perl_settings.get("ignore_dirs", list(_DEFAULT_IGNORE_DIRS))
return file_filter, ignore_dirs
@staticmethod
def _sync_source_fn_matcher(file_filter: list[str]) -> None:
"""Keep Serena's Perl source-file matcher in sync with the LS ``fileFilter``.
``Language.PERL.get_source_fn_matcher()`` is a ``@cache``d per-language singleton, so adding
the configured extensions here propagates to every consumer of the matcher — symbol index
traversal (``project.is_ignored_path``), the LS ignore check (``ls.is_ignored_path``), and
language composition detection. Without this, ``find_symbol`` would not surface symbols in
files whose extensions were added to ``file_filter`` (#1449).
"""
Language.PERL.get_source_fn_matcher().add_extensions(*file_filter)
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
# Setup runtime dependencies before initializing
perl_ls_cmd = self._setup_runtime_dependencies()
@@ -111,18 +145,17 @@ class PerlLanguageServer(SolidLanguageServer):
config, repository_root_path, ProcessLaunchInfo(cmd=perl_ls_cmd, cwd=repository_root_path), "perl", solidlsp_settings
)
self.request_id = 0
self._file_filter, self._ignore_dirs = self._resolve_filter_settings(solidlsp_settings)
# Sync Serena's source-file matcher with the configured extensions so find_symbol and the
# language server agree on which files are Perl sources (see #1449).
self._sync_source_fn_matcher(self._file_filter)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for Perl::LanguageServer.
Based on the expected structure from Perl::LanguageServer::Methods::_rpcreq_initialize.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -138,15 +171,9 @@ class PerlLanguageServer(SolidLanguageServer):
},
},
"initializationOptions": {},
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""Start Perl::LanguageServer process"""
@@ -166,8 +193,8 @@ class PerlLanguageServer(SolidLanguageServer):
perl_config = {
"perlInc": [self.repository_root_path, "."],
"fileFilter": self._FILE_FILTER,
"ignoreDirs": [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"],
"fileFilter": self._file_filter,
"ignoreDirs": self._ignore_dirs,
}
return [perl_config]
@@ -180,7 +207,7 @@ class PerlLanguageServer(SolidLanguageServer):
log.info("Starting Perl::LanguageServer process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -202,8 +229,8 @@ class PerlLanguageServer(SolidLanguageServer):
"settings": {
"perl": {
"perlInc": [self.repository_root_path, "."],
"fileFilter": self._FILE_FILTER,
"ignoreDirs": [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"],
"fileFilter": self._file_filter,
"ignoreDirs": self._ignore_dirs,
}
}
}
+3 -15
View File
@@ -4,7 +4,6 @@ Provides PHP specific instantiation of the LanguageServer class using Phpactor.
import logging
import os
import pathlib
import re
import shutil
import stat
@@ -15,7 +14,6 @@ from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -122,15 +120,11 @@ class PhpactorServer(SolidLanguageServer):
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialization params for the Phpactor Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -145,19 +139,13 @@ class PhpactorServer(SolidLanguageServer):
"didChangeConfiguration": {"dynamicRegistration": True},
},
},
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {
"language_server_phpstan.enabled": False,
"language_server_psalm.enabled": False,
"language_server_php_cs_fixer.enabled": False,
},
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""Start Phpactor server process."""
@@ -178,7 +166,7 @@ class PhpactorServer(SolidLanguageServer):
log.info("Starting Phpactor server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+4 -17
View File
@@ -4,11 +4,9 @@ Provides PHP specific instantiation of the LanguageServer class using PHPantom.
import logging
import os
import pathlib
import shutil
import stat
from time import sleep
from typing import cast
from overrides import override
@@ -22,7 +20,7 @@ from solidlsp.ls import (
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler import lsp_types as protocol_lsp_types
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, InitializeParams, LocationLink
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -178,12 +176,10 @@ class PHPantomServer(SolidLanguageServer):
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialization params for the PHPantom Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
# declaring client capabilities
initialize_params = {
"locale": "en",
@@ -213,17 +209,8 @@ class PHPantomServer(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""Start PHPantom server process."""
@@ -244,7 +231,7 @@ class PHPantomServer(SolidLanguageServer):
log.info("Starting PHPantom server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
# negotiating server capabilities
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
@@ -11,7 +11,6 @@ You can pass the following entries in ``ls_specific_settings["powershell"]``:
import logging
import os
import pathlib
import platform
import shutil
import subprocess
@@ -27,7 +26,6 @@ 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
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -277,12 +275,10 @@ class PowerShellLanguageServer(SolidLanguageServer):
return original_name
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the PowerShell Editor Services.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -326,17 +322,8 @@ class PowerShellLanguageServer(SolidLanguageServer):
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore[return-value]
return initialize_params
def _start_server(self) -> None:
"""
@@ -375,7 +362,7 @@ class PowerShellLanguageServer(SolidLanguageServer):
log.info("Starting PowerShell Editor Services process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -15,7 +15,7 @@ import pathlib
import sys
import threading
import time
from typing import Any, cast
from typing import Any
from typing_extensions import override
@@ -23,7 +23,7 @@ from solidlsp import ls_types
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, LSPErrorCodes
from solidlsp.lsp_protocol_handler.lsp_types import LSPErrorCodes
from solidlsp.lsp_protocol_handler.server import LSPError, PayloadLike
from solidlsp.settings import SolidLSPSettings
@@ -171,17 +171,11 @@ class PyreflyLanguageServer(SolidLanguageServer):
def _get_language_id_for_file(self, relative_file_path: str) -> str:
return "python"
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the pyrefly language server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"clientInfo": {"name": "Serena", "version": "0.1.0"},
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"window": {
"workDoneProgress": True,
@@ -216,9 +210,8 @@ class PyreflyLanguageServer(SolidLanguageServer):
"publishDiagnostics": {"relatedInformation": True},
},
},
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -280,7 +273,7 @@ class PyreflyLanguageServer(SolidLanguageServer):
self.server.start()
# retry requests that pyrefly cancels while re-indexing the workspace in the background
self._install_mutation_retry()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to pyrefly server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -3,8 +3,6 @@ Provides Python specific instantiation of the LanguageServer class. Contains var
"""
import logging
import os
import pathlib
import re
import threading
from typing import cast
@@ -13,7 +11,6 @@ from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, 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__)
@@ -27,6 +24,8 @@ class PyrightServer(SolidLanguageServer):
Contains various configurations and settings specific to Python.
"""
_TIMEOUT_FOR_INITIAL_ANALYSIS = 60.0
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a PyrightServer instance. This class is not meant to be instantiated directly.
@@ -59,16 +58,8 @@ class PyrightServer(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Pyright Language Server.
"""
# Create basic initialization parameters
def _create_base_initialize_params(self) -> dict:
initialize_params = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": pathlib.Path(repository_absolute_path).as_uri(),
"initializationOptions": {
"exclude": [
"**/__pycache__",
@@ -111,12 +102,8 @@ class PyrightServer(SolidLanguageServer):
"publishDiagnostics": {"relatedInformation": True},
},
},
"workspaceFolders": [
{"uri": pathlib.Path(repository_absolute_path).as_uri(), "name": os.path.basename(repository_absolute_path)}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -230,7 +217,7 @@ class PyrightServer(SolidLanguageServer):
self.server.start()
# Send proper initialization parameters
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to pyright server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -246,8 +233,8 @@ class PyrightServer(SolidLanguageServer):
# Wait for Pyright to complete its initial workspace analysis
# This prevents zombie processes by ensuring background tasks finish
log.info("Waiting for Pyright to complete initial workspace analysis...")
if self.analysis_complete.wait(timeout=5.0):
log.info(f"Waiting up to {self._TIMEOUT_FOR_INITIAL_ANALYSIS}s for Pyright to complete initial workspace analysis...")
if self.analysis_complete.wait(timeout=self._TIMEOUT_FOR_INITIAL_ANALYSIS):
log.info("Pyright initial analysis complete, server ready")
else:
log.warning("Timeout waiting for Pyright analysis completion, proceeding anyway")
@@ -0,0 +1,128 @@
"""
Provides QML specific instantiation of the LanguageServer class using Qt's qmlls.
"""
import logging
import shutil
from solidlsp.ls import (
LanguageServerDependencyProvider,
LanguageServerDependencyProviderSinglePath,
SolidLanguageServer,
)
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
class QmlLanguageServer(SolidLanguageServer):
"""
Provides QML specific instantiation of the LanguageServer class using qmlls.
qmlls is the official QML language server shipped with Qt 6.
It must be installed separately; see https://doc.qt.io/qt-6/qtqml-tool-qmlls.html
for installation instructions.
The dependency provider looks for ``qmlls6`` first, then falls back to ``qmlls``.
Users can override the executable via the ``ls_path`` entry in ``ls_specific_settings.qml``.
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
super().__init__(config, repository_root_path, None, "qml", solidlsp_settings)
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
"""
Discover the qmlls executable on PATH.
Tries ``qmlls6`` first (Qt 6+), then falls back to ``qmlls``.
:return: path to the qmlls executable
:raises FileNotFoundError: if qmlls is neither on PATH nor provided via ``ls_path``
"""
qmlls_binary = shutil.which("qmlls6") or shutil.which("qmlls")
if qmlls_binary is None:
raise FileNotFoundError(
"qmlls (QML language server) is not installed or not in PATH.\n"
"Please install Qt 6 and ensure 'qmlls' (or 'qmlls6') is available on your PATH.\n"
"See: https://doc.qt.io/qt-6/qtqml-tool-qmlls.html"
)
return qmlls_binary
def _create_launch_command(self, core_path: str) -> list[str]:
# qmlls communicates via stdio by default; no extra flags are required.
return [core_path]
def _create_base_initialize_params(self) -> dict:
"""
Return the language-specific initialize params for the QML language server.
``processId``, ``rootPath``, ``rootUri`` and ``workspaceFolders`` are populated by the
default ``InitializeParamsBuilder`` and must not be set here.
"""
initialize_params = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"definition": {"dynamicRegistration": True, "linkSupport": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"completion": {
"dynamicRegistration": True,
"completionItem": {
"snippetSupport": True,
"documentationFormat": ["markdown", "plaintext"],
},
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"],
},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"configuration": True,
},
},
}
return initialize_params
def _start_server(self) -> None:
"""Start the QML language server process."""
def register_capability_handler(_params: dict) -> None:
return
def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(_params: dict) -> None:
return
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
log.info("Starting QML language server (qmlls) process")
self.server.start()
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
# verify server capabilities
capabilities = init_response["capabilities"]
log.info(f"QML language server capabilities: {list(capabilities.keys())}")
self.server.notify.initialized({})
@@ -1,6 +1,4 @@
import logging
import os
import pathlib
import subprocess
from typing import Any
@@ -8,7 +6,6 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -68,10 +65,8 @@ class RLanguageServer(SolidLanguageServer):
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=r_cmd, cwd=repository_root_path), "r", solidlsp_settings)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Initialize params for R Language Server."""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -107,17 +102,8 @@ class RLanguageServer(SolidLanguageServer):
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""Start R Language Server process."""
@@ -140,7 +126,7 @@ class RLanguageServer(SolidLanguageServer):
log.info("Starting R Language Server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info(
"Sending initialize request to R Language Server",
)
+3 -19
View File
@@ -1,16 +1,12 @@
"""Regal Language Server implementation for Rego policy files."""
import logging
import os
import shutil
from typing import cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PathUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -54,20 +50,14 @@ class RegalLanguageServer(SolidLanguageServer):
solidlsp_settings,
)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Regal Language Server.
:param repository_absolute_path: Absolute path to the repository
:return: LSP initialization parameters
"""
root_uri = PathUtils.path_to_uri(repository_absolute_path)
initialize_params = {
"processId": os.getpid(),
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -89,14 +79,8 @@ class RegalLanguageServer(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"workspaceFolders": [
{
"name": os.path.basename(repository_absolute_path),
"uri": root_uri,
}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""Start Regal language server process and wait for initialization."""
@@ -117,7 +101,7 @@ class RegalLanguageServer(SolidLanguageServer):
log.info("Starting Regal language server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info(
"Sending initialize request from LSP client to LSP server and awaiting response",
+4 -8
View File
@@ -11,7 +11,6 @@ You can pass the following entries in ``ls_specific_settings["ruby"]``:
import json
import logging
import os
import pathlib
import shutil
import subprocess
import threading
@@ -20,7 +19,7 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
from solidlsp.lsp_protocol_handler.lsp_types import InitializeResult
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -329,16 +328,13 @@ class RubyLsp(SolidLanguageServer):
return base_patterns
def _get_initialize_params(self) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns ruby-lsp specific initialization parameters.
"""
exclude_patterns = self._get_ruby_exclude_patterns(self.repository_root_path)
initialize_params = {
"processId": os.getpid(),
"rootPath": self.repository_root_path,
"rootUri": pathlib.Path(self.repository_root_path).as_uri(),
"capabilities": {
"workspace": {
"workspaceEdit": {"documentChanges": True},
@@ -371,7 +367,7 @@ class RubyLsp(SolidLanguageServer):
},
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""
@@ -437,7 +433,7 @@ class RubyLsp(SolidLanguageServer):
log.info("Starting ruby-lsp server process")
self.server.start()
initialize_params = self._get_initialize_params()
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
log.info(f"Sending init params: {json.dumps(initialize_params, indent=4)}")
+27 -30
View File
@@ -9,13 +9,11 @@ import platform
import shutil
import subprocess
import threading
from typing import cast
from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, 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__)
@@ -210,14 +208,11 @@ class RustAnalyzer(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["target"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Rust Analyzer Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"clientInfo": {"name": "Visual Studio Code - Insiders", "version": "1.82.0-insider"},
"locale": "en",
"capabilities": {
"workspace": {
@@ -672,17 +667,8 @@ class RustAnalyzer(SolidLanguageServer):
"workspace": {"symbol": {"search": {"kind": "only_types", "limit": 128, "scope": "workspace"}}},
},
"trace": "verbose",
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -690,9 +676,8 @@ class RustAnalyzer(SolidLanguageServer):
"""
def register_capability_handler(params: dict) -> None:
assert "registrations" in params
for registration in params["registrations"]:
if registration["method"] == "workspace/executeCommand":
for registration in params.get("registrations", []):
if registration.get("method") == "workspace/executeCommand":
self.initialize_searcher_command_available.set()
self.resolve_main_method_available.set()
return
@@ -701,7 +686,7 @@ class RustAnalyzer(SolidLanguageServer):
# TODO: Should we wait for
# server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
# Before proceeding?
if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
if params.get("type") == "ServiceReady" and params.get("message") == "ServiceReady":
self.service_ready_event.set()
def execute_client_command_handler(params: dict) -> list:
@@ -711,7 +696,7 @@ class RustAnalyzer(SolidLanguageServer):
return
def check_experimental_status(params: dict) -> None:
if params["quiescent"] == True:
if params.get("quiescent") is True:
self.server_ready.set()
def window_log_message(msg: dict) -> None:
@@ -728,17 +713,29 @@ class RustAnalyzer(SolidLanguageServer):
log.info("Starting RustAnalyzer server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore
assert "completionProvider" in init_response["capabilities"]
assert init_response["capabilities"]["completionProvider"] == {
"resolveProvider": True,
"triggerCharacters": [":", ".", "'", "("],
"completionItem": {"labelDetailsSupport": True},
}
# Validate key server capabilities. These are sanity checks, not hard requirements:
# rust-analyzer may evolve its advertised capabilities across versions, so a mismatch is
# logged rather than asserted. (An over-strict equality check on completionProvider here
# previously crashed startup whenever upstream added or changed a completionProvider field.)
capabilities = init_response.get("capabilities", {}) if isinstance(init_response, dict) else {}
text_document_sync = capabilities.get("textDocumentSync")
change_kind = text_document_sync.get("change") if isinstance(text_document_sync, dict) else text_document_sync
if change_kind != 2:
log.warning("rust-analyzer: unexpected textDocumentSync.change=%r (expected 2 = incremental)", change_kind)
if "completionProvider" not in capabilities:
log.warning("rust-analyzer: server did not advertise a completionProvider capability")
self.server.notify.initialized({})
self.server_ready.wait()
# Wait for rust-analyzer to finish initial indexing (it emits experimental/serverStatus with
# quiescent=true when ready). Use a timeout so a server that crashes or never signals
# readiness cannot hang startup indefinitely (previously this waited with no timeout).
_SERVER_READY_TIMEOUT = 120.0
log.info("Waiting for rust-analyzer to signal readiness (quiescent)...")
if self.server_ready.wait(timeout=_SERVER_READY_TIMEOUT):
log.info("rust-analyzer ready")
else:
log.warning("rust-analyzer did not signal readiness within %.0fs; proceeding anyway", _SERVER_READY_TIMEOUT)
@@ -4,7 +4,6 @@ Provides Scala specific instantiation of the LanguageServer class. Contains vari
import logging
import os
import pathlib
import shutil
import subprocess
from enum import Enum
@@ -14,7 +13,6 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -259,17 +257,12 @@ class ScalaLanguageServer(SolidLanguageServer):
log.info("Bootstrapping metals finished.")
return [metals_executable]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Scala Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"initializationOptions": {
"compilerOptions": {
"completionCommand": None,
@@ -304,7 +297,7 @@ class ScalaLanguageServer(SolidLanguageServer):
},
"capabilities": {"textDocument": {"documentSymbol": {"hierarchicalDocumentSymbolSupport": True}}},
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""
@@ -315,7 +308,7 @@ class ScalaLanguageServer(SolidLanguageServer):
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
self.server.send.initialize(initialize_params)
self.server.notify.initialized({})
+6 -19
View File
@@ -6,7 +6,6 @@ Contains various configurations and settings specific to Ruby.
import json
import logging
import os
import pathlib
import re
import shutil
import subprocess
@@ -16,7 +15,6 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -267,18 +265,13 @@ class Solargraph(SolidLanguageServer):
return base_patterns
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Solargraph Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
exclude_patterns = Solargraph._get_ruby_exclude_patterns(repository_absolute_path)
exclude_patterns = Solargraph._get_ruby_exclude_patterns(self.repository_root_path)
initialize_params: InitializeParams = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
initialize_params = {
"initializationOptions": {
"exclude": exclude_patterns,
},
@@ -289,17 +282,11 @@ class Solargraph(SolidLanguageServer):
"textDocument": {
"documentSymbol": {
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))}, # type: ignore[arg-type]
"symbolKind": {"valueSet": list(range(1, 27))},
},
},
},
"trace": "verbose", # type: ignore[typeddict-item]
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"trace": "verbose",
}
return initialize_params
@@ -341,7 +328,7 @@ class Solargraph(SolidLanguageServer):
log.info("Starting solargraph server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
log.info(f"Sending init params: {json.dumps(initialize_params, indent=4)}")
@@ -10,7 +10,7 @@ import pathlib
import platform
import shutil
import threading
from time import sleep
from time import monotonic, sleep
from typing import Any
from overrides import override
@@ -19,7 +19,6 @@ from solidlsp import ls_types
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
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__)
@@ -41,6 +40,14 @@ class SolidityLanguageServer(SolidLanguageServer):
Requires Node.js and npm to be installed.
"""
_VALIDATION_COMPLETION_TIMEOUT = 60.0
"""upper bound for awaiting the validation-completion signal (``custom/validation-job-status``),
aligned with the indexing wait; on cold environments validation includes downloading the pinned
solc version"""
_POST_VALIDATION_PUBLISH_WAIT = 2.5
"""grace period for the diagnostics publication that accompanies the completion signal"""
@staticmethod
def _determine_log_level(line: str) -> int:
"""Suppress known non-critical stderr output from the Solidity language server."""
@@ -67,6 +74,11 @@ class SolidityLanguageServer(SolidLanguageServer):
"solidity",
solidlsp_settings,
)
self._validation_completed = threading.Event()
"""set when the language server reports an asynchronous validation run as finished
(``custom/validation-job-status``); cleared at the start of each diagnostics request"""
self._last_validation_status: Any | None = None
"""payload of the most recent ``custom/validation-job-status`` notification, kept for logging"""
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
@@ -178,21 +190,10 @@ class SolidityLanguageServer(SolidLanguageServer):
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in {"artifacts", "cache", "typechain-types"}
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Return LSP InitializeParams for the Solidity language server."""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
return { # type: ignore
return {
"locale": "en",
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"capabilities": {
"textDocument": {
"synchronization": {
@@ -208,11 +209,11 @@ class SolidityLanguageServer(SolidLanguageServer):
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))}, # type: ignore[arg-type]
"symbolKind": {"valueSet": list(range(1, 27))},
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"], # type: ignore[list-item]
"contentFormat": ["markdown", "plaintext"],
},
"publishDiagnostics": {"relatedInformation": True},
},
@@ -254,16 +255,24 @@ class SolidityLanguageServer(SolidLanguageServer):
if indexed_count[0] >= expected_count:
all_indexed.set()
def on_validation_job_status(params: Any) -> None:
# signals completion of an asynchronous validation run (a forge/solc compile);
# the payload carries no document URI, so the event is global to the server instance
self._last_validation_status = params
log.debug(f"Solidity LSP: validation job finished: {params}")
self._validation_completed.set()
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.server.on_notification("custom/file-indexed", on_file_indexed)
self.server.on_notification("custom/validation-job-status", on_validation_job_status)
log.info("Starting Solidity language server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.debug("Sending initialize request to Solidity language server")
init_response = self.server.send.initialize(initialize_params)
log.debug(f"Received initialize response from Solidity server: {init_response}")
@@ -312,6 +321,9 @@ class SolidityLanguageServer(SolidLanguageServer):
"character": len(lines[-1]),
}
# any validation that finishes after this point may satisfy the fallback wait below
self._validation_completed.clear()
self.server.notify.did_open_text_document(
{ # ty: ignore[invalid-argument-type] # dict built from LSPConstants keys; shape matches the TypedDict
LSPConstants.TEXT_DOCUMENT: {
@@ -340,12 +352,52 @@ class SolidityLanguageServer(SolidLanguageServer):
],
}
)
# fast path: wait for a non-empty diagnostics publication (warm environments)
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,
)
# fallback: validation (a forge/solc compile) runs asynchronously in the server and can
# exceed the initial wait on slow or cold environments (e.g. CI runners downloading the
# pinned solc); await its completion signal, then take the publication made for this
# request, accepting an empty result as the validated answer. The signal carries no
# document URI, so a completion belonging to another document can wake the wait early;
# re-arm and keep waiting until the deadline in that case.
if diagnostics is None:
# fast path missed; validation is still running -- await its completion signal so the
# wait below is visible in logs on slow/cold environments
log.debug(
f"Solidity diagnostics not yet published for {uri}; "
f"awaiting validation completion (up to {self._VALIDATION_COMPLETION_TIMEOUT:.0f}s)"
)
deadline = monotonic() + self._VALIDATION_COMPLETION_TIMEOUT
while diagnostics is None:
remaining = deadline - monotonic()
if remaining <= 0 or not self._validation_completed.wait(timeout=remaining):
log.warning(
f"Solidity validation completion was not signalled within "
f"{self._VALIDATION_COMPLETION_TIMEOUT:.0f}s for {uri} "
f"(last status: {self._last_validation_status}); returning without diagnostics"
)
break
diagnostics = self._wait_for_published_diagnostics(
uri=uri,
after_generation=diagnostics_before_request,
timeout=self._POST_VALIDATION_PUBLISH_WAIT,
)
if diagnostics is None:
# a completion fired but published nothing for our URI (e.g. a run for another
# document); re-arm and keep waiting so the ongoing wait stays visible
log.debug(f"Solidity validation completed without diagnostics for {uri} yet; continuing to wait")
self._validation_completed.clear()
else:
log.debug(
f"Solidity diagnostics resolved via validation completion "
f"(status={self._last_validation_status}): {len(diagnostics)} diagnostic(s)"
)
finally:
self.server.notify.did_close_text_document(
{ # ty: ignore[invalid-argument-type] # dict built from LSPConstants keys; shape matches the TypedDict
@@ -25,7 +25,6 @@ from __future__ import annotations
import logging
import os
import pathlib
import shutil
import threading
@@ -34,7 +33,6 @@ from overrides import override
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -157,9 +155,7 @@ class SomeSassLanguageServer(SolidLanguageServer):
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "--stdio"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
root_uri = pathlib.Path(repository_absolute_path).as_uri()
def _create_base_initialize_params(self) -> dict:
initialize_params: dict = {
"locale": "en",
"capabilities": {
@@ -183,17 +179,8 @@ class SomeSassLanguageServer(SolidLanguageServer):
},
},
"initializationOptions": SOMESASS_INIT_OPTIONS,
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore[return-value]
return initialize_params
@staticmethod
def _handle_workspace_configuration(params: dict) -> list[dict]:
@@ -231,7 +218,7 @@ class SomeSassLanguageServer(SolidLanguageServer):
log.info("Starting some-sass-language-server")
self.server.start()
init_params = self._get_initialize_params(self.repository_root_path)
init_params = self._create_initialize_params()
init_response = self.server.send.initialize(init_params)
log.debug("Some Sass LS initialize response: %s", init_response)
assert "completionProvider" in init_response["capabilities"], "Some Sass LSP did not advertise completionProvider"
+3 -18
View File
@@ -1,6 +1,5 @@
import logging
import os
import pathlib
import subprocess
import time
from collections.abc import Hashable
@@ -11,7 +10,6 @@ from solidlsp import ls_types
from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_types import SymbolKind
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -75,13 +73,10 @@ class SourceKitLSP(SolidLanguageServer):
return original_name.split("(", 1)[0].strip()
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Swift Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"capabilities": {
"general": {
@@ -295,7 +290,6 @@ class SourceKitLSP(SolidLanguageServer):
"workspaceFolders": True,
},
},
"clientInfo": {"name": "Visual Studio Code", "version": "1.102.2"},
"initializationOptions": {
"backgroundIndexing": True,
"backgroundPreparationMode": "enabled",
@@ -305,18 +299,9 @@ class SourceKitLSP(SolidLanguageServer):
"workspace/peekDocuments": True,
},
"locale": "en",
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore[return-value]
return initialize_params
def _start_server(self) -> None:
"""Start sourcekit-lsp server process"""
@@ -337,7 +322,7 @@ class SourceKitLSP(SolidLanguageServer):
log.info("Starting sourcekit-lsp server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -28,13 +28,18 @@ from solidlsp.ls import (
SolidLanguageServer,
)
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
TS_EXT = frozenset({".ts", ".tsx", ".mts", ".cts"})
JS_EXT = frozenset({".js", ".jsx", ".mjs", ".cjs"})
SVELTE_EXT = frozenset({".svelte"})
# cap the file listing in SvelteCompanionPreparationError so a large repo cannot bloat the message
_MAX_FAILED_FILES_IN_ERROR = 10
class SvelteCompanionPreparationError(RuntimeError):
"""Raised when the companion TypeScript server cannot be prepared deterministically."""
def _is_ts_file(uri: str) -> bool:
@@ -55,6 +60,9 @@ class SvelteTypeScriptServer(TypeScriptLanguageServer):
Spawned and owned by :class:`SvelteLanguageServer`; not instantiated directly.
"""
INDEXING_PROGRESS_TIMEOUT = 120.0
SERVER_READY_TIMEOUT = 30.0
class DependencyProvider(TypeScriptLanguageServer.DependencyProvider):
"""Returns the pre-installed typescript-language-server binary.
@@ -120,8 +128,8 @@ class SvelteTypeScriptServer(TypeScriptLanguageServer):
return "typescript"
@override
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
params = super()._get_initialize_params(repository_absolute_path)
def _create_base_initialize_params(self) -> dict:
params = super()._create_base_initialize_params()
params["initializationOptions"] = {
"plugins": [
{
@@ -143,6 +151,16 @@ class SvelteTypeScriptServer(TypeScriptLanguageServer):
self.server.on_request("workspace/configuration", workspace_configuration_handler)
super()._start_server()
@override
def _handle_server_ready_timeout(self, timeout: float) -> None:
raise TimeoutError(f"Svelte companion TypeScript server did not become ready within {timeout:.0f}s")
@override
def _handle_project_indexing_timeout(self, timeout: float) -> None:
raise TimeoutError(
f"Svelte companion TypeScript server project indexing did not complete within {timeout:.0f}s ({self.describe_indexing_state()})"
)
class SvelteLanguageServer(SolidLanguageServer):
"""
@@ -152,6 +170,9 @@ class SvelteLanguageServer(SolidLanguageServer):
* ``svelte_language_server_version``: version of ``svelte-language-server``
to install (default: ``0.18.0``).
* ``npm_registry``: optional alternative npm-compatible registry URL.
* ``indexing_timeout``: optional timeout in seconds for companion TS indexing of
Svelte files. Falls back to ``ls_specific_settings["typescript"].indexing_timeout``
or the Svelte companion default.
* ``initialization_options_configuration``: optional dict merged into
``initializeParams.initializationOptions.configuration`` (same top-level keys as in
Svelte Language Tools: ``svelte``, ``prettier``, ``typescript``, …).
@@ -314,6 +335,15 @@ class SvelteLanguageServer(SolidLanguageServer):
log.debug("Error processing svelte file %s: %s", svelte_file, exc)
return svelte_files
def _get_companion_indexing_timeout(self) -> float:
""":return: maximum seconds to wait for companion TS indexing after opening Svelte files."""
ts_settings = self._solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT)
timeout = self._custom_settings.get(
"indexing_timeout",
ts_settings.get("indexing_timeout", SvelteTypeScriptServer.INDEXING_PROGRESS_TIMEOUT),
)
return float(timeout)
def _ensure_svelte_files_indexed_on_ts_server(self) -> None:
"""Open all .svelte files on the companion TS server so the plugin includes them in the TS program.
@@ -333,6 +363,8 @@ class SvelteLanguageServer(SolidLanguageServer):
# prepare progress tracking BEFORE opening files to avoid a race
self._ts_server.expect_indexing()
failed_svelte_files = []
first_open_error: Exception | None = None
for svelte_file in svelte_files:
try:
with self._ts_server.open_file(svelte_file) as file_buffer:
@@ -340,15 +372,29 @@ class SvelteLanguageServer(SolidLanguageServer):
self._indexed_svelte_file_uris.append(file_buffer.uri)
except Exception as exc:
log.debug("Failed to open %s on companion TS server: %s", svelte_file, exc)
if first_open_error is None:
first_open_error = exc
failed_svelte_files.append(svelte_file)
if failed_svelte_files:
shown_files = sorted(failed_svelte_files)[:_MAX_FAILED_FILES_IN_ERROR]
remainder = len(failed_svelte_files) - len(shown_files)
listing = ", ".join(shown_files) + (f" and {remainder} more" if remainder else "")
raise SvelteCompanionPreparationError(
f"Failed to open {len(failed_svelte_files)} Svelte file(s) on companion TypeScript server: {listing}"
) from first_open_error
self._svelte_files_indexed = True
log.info("Svelte file indexing complete; waiting for companion TS server to finish processing")
timeout = TypeScriptLanguageServer.INDEXING_PROGRESS_TIMEOUT
if self._ts_server.wait_for_indexing(timeout=timeout):
timeout = self._get_companion_indexing_timeout()
if self._ts_server._wait_for_indexing_start_or_completion(timeout=timeout):
log.info("Companion TypeScript server finished indexing .svelte files")
else:
log.warning("Timeout (%ss) waiting for companion TS server to index .svelte files; proceeding anyway", timeout)
raise TimeoutError(
f"Companion TypeScript server did not finish indexing {len(svelte_files)} .svelte files within {timeout:.0f}s "
f"({self._ts_server.describe_indexing_state()})"
)
def _cleanup_indexed_svelte_files(self) -> None:
"""Decrement ref-counts for all .svelte files opened during indexing."""
@@ -385,13 +431,13 @@ class SvelteLanguageServer(SolidLanguageServer):
)
log.info("Starting companion SvelteTypeScriptServer")
self._ts_server.start()
log.info("Waiting for companion SvelteTypeScriptServer to be ready ...")
if not self._ts_server.server_ready.wait(timeout=30.0):
log.warning("Timeout waiting for companion SvelteTypeScriptServer; proceeding anyway")
self._ts_server.server_ready.set()
self._ts_server_started = True
log.info("Companion SvelteTypeScriptServer ready")
self._ensure_svelte_files_indexed_on_ts_server()
except (TimeoutError, SvelteCompanionPreparationError):
log.exception("Failed to prepare companion SvelteTypeScriptServer; aborting Svelte server startup")
self._stop_typescript_server()
raise
except Exception:
log.exception("Error starting companion SvelteTypeScriptServer; TS-side operations degrade to svelte LS")
self._ts_server = None
@@ -440,7 +486,7 @@ class SvelteLanguageServer(SolidLanguageServer):
self.server.notify.send_notification = send_notification_wrapped
def _get_initialize_params(self) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Svelte Language Server.
@@ -451,8 +497,6 @@ class SvelteLanguageServer(SolidLanguageServer):
The resulting dict is also stored as :attr:`_lsp_configuration` so
``workspace/configuration`` requests can be answered with real values.
"""
root_uri = pathlib.Path(self.repo_path).as_uri()
# base configuration mirroring all plugin-sections from svelte-vscode initializationOptions
lsp_config: dict[str, Any] = {
"svelte": {},
@@ -514,17 +558,8 @@ class SvelteLanguageServer(SolidLanguageServer):
"dontFilterIncompleteCompletions": True,
"configuration": lsp_config,
},
"processId": os.getpid(),
"rootPath": self.repo_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(self.repo_path),
}
],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
def window_log_message(msg: dict) -> None:
@@ -562,7 +597,7 @@ class SvelteLanguageServer(SolidLanguageServer):
self._wrap_notify_send_for_ts_js_mirror()
self.server.start()
init_params = self._get_initialize_params()
init_params = self._create_initialize_params()
init_response = self.server.send.initialize(init_params)
assert "documentSymbolProvider" in init_response["capabilities"], "Svelte LSP did not advertise documentSymbolProvider"
@@ -4,14 +4,12 @@ SystemVerilog language server using verible-verilog-ls.
import logging
import os
import pathlib
import shutil
import subprocess
from typing import Any, cast
from typing import Any
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
@@ -150,13 +148,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer):
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
root_uri = pathlib.Path(repository_absolute_path).as_uri()
def _create_base_initialize_params(self) -> dict:
initialize_params = {
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"locale": "en",
"capabilities": {
"textDocument": {
@@ -186,9 +179,8 @@ class SystemVerilogLanguageServer(SolidLanguageServer):
"didChangeConfiguration": {"dynamicRegistration": True},
},
},
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
def do_nothing(params: Any) -> None:
@@ -205,7 +197,7 @@ class SystemVerilogLanguageServer(SolidLanguageServer):
log.info("Starting verible-verilog-ls process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request")
init_response = self.server.send.initialize(initialize_params)
+4 -16
View File
@@ -18,8 +18,7 @@ from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import FileUtils, PathUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.ls_utils import FileUtils
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -208,12 +207,10 @@ class TaploServer(SolidLanguageServer):
f"Failed to download Taplo from {download_url}. Try installing manually: cargo install taplo-cli --locked"
) from e
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Taplo Language Server.
"""
root_uri = PathUtils.path_to_uri(repository_absolute_path)
initialize_params = {
"locale": "en",
"capabilities": {
@@ -236,17 +233,8 @@ class TaploServer(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""
@@ -269,7 +257,7 @@ class TaploServer(SolidLanguageServer):
log.info("Starting Taplo server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request to Taplo server")
init_response = self.server.send.initialize(initialize_params)
+4 -17
View File
@@ -1,14 +1,12 @@
import logging
import os
import shutil
from typing import cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import PathUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.ls_utils import PlatformUtils
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -219,17 +217,12 @@ class TerraformLS(SolidLanguageServer):
)
self.request_id = 0
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Terraform Language Server.
"""
root_uri = PathUtils.path_to_uri(repository_absolute_path)
result = {
"processId": os.getpid(),
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -243,14 +236,8 @@ class TerraformLS(SolidLanguageServer):
},
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
},
"workspaceFolders": [
{
"name": os.path.basename(repository_absolute_path),
"uri": root_uri,
}
],
}
return cast(InitializeParams, result)
return result
def _start_server(self) -> None:
"""Start terraform-ls server process"""
@@ -271,7 +258,7 @@ class TerraformLS(SolidLanguageServer):
log.info("Starting terraform-ls server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -5,14 +5,12 @@ texlab is downloaded as a prebuilt binary from the latex-lsp/texlab GitHub relea
import logging
import os
from typing import cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PathUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.ls_utils import PlatformUtils
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -134,15 +132,10 @@ class TexlabLanguageServer(SolidLanguageServer):
solidlsp_settings,
)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""Returns the initialize params for the texlab Language Server."""
root_uri = PathUtils.path_to_uri(repository_absolute_path)
result = {
"processId": os.getpid(),
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -159,14 +152,8 @@ class TexlabLanguageServer(SolidLanguageServer):
},
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
},
"workspaceFolders": [
{
"name": os.path.basename(repository_absolute_path),
"uri": root_uri,
}
],
}
return cast(InitializeParams, result)
return result
def _start_server(self) -> None:
"""Start the texlab server process."""
@@ -187,7 +174,7 @@ class TexlabLanguageServer(SolidLanguageServer):
log.info("Starting texlab server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to texlab and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+3 -14
View File
@@ -8,15 +8,11 @@ You can pass the following entries in ``ls_specific_settings["python_ty"]``:
"""
import logging
import os
import pathlib
from typing import cast
from typing_extensions import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, 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__)
@@ -61,17 +57,11 @@ class TyLanguageServer(SolidLanguageServer):
def _get_language_id_for_file(self, relative_file_path: str) -> str:
return "python"
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Ty language server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"processId": os.getpid(),
"clientInfo": {"name": "Serena", "version": "0.1.0"},
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"workspace": {
"workspaceEdit": {"documentChanges": True},
@@ -101,9 +91,8 @@ class TyLanguageServer(SolidLanguageServer):
"publishDiagnostics": {"relatedInformation": True},
},
},
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -130,7 +119,7 @@ class TyLanguageServer(SolidLanguageServer):
# starting and initializing the server
log.info("Starting ty language server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to ty server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -4,10 +4,10 @@ Provides TypeScript specific instantiation of the LanguageServer class. Contains
import logging
import os
import pathlib
import shutil
import threading
from typing import Any, cast
import time
from typing import Any
from overrides import override
from sensai.util.logging import LogTime
@@ -16,7 +16,6 @@ from solidlsp import ls_types
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
@@ -73,6 +72,8 @@ class TypeScriptLanguageServer(SolidLanguageServer):
You can pass the following entries in ls_specific_settings["typescript"]:
- typescript_version: Version of TypeScript to install (default: "5.9.3")
- typescript_language_server_version: Version of typescript-language-server to install (default: "5.1.3")
- indexing_timeout: float, timeout in seconds for project indexing (default: 30.0)
- server_ready_timeout: float, timeout in seconds for the server-ready signal (default: 10.0)
"""
@classmethod
@@ -81,7 +82,8 @@ class TypeScriptLanguageServer(SolidLanguageServer):
# 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
INDEXING_PROGRESS_TIMEOUT = 30.0
SERVER_READY_TIMEOUT = 10.0
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
@@ -114,6 +116,35 @@ class TypeScriptLanguageServer(SolidLanguageServer):
"""
return self._indexing_complete.wait(timeout=timeout)
def _wait_for_indexing_start_or_completion(self, timeout: float, start_grace: float | None = None) -> bool:
"""Wait until TypeScript indexing has started and drained, or provably never started.
:param timeout: Maximum seconds to wait once active indexing progress is observed.
:param start_grace: Maximum seconds to wait for progress to begin after opening files.
:return: True if indexing completed or no progress began within the grace period, False on timeout.
"""
grace = self._INDEXING_START_GRACE_S if start_grace is None else start_grace
# wait for progress to begin
progress_deadline = time.monotonic() + grace
while time.monotonic() < progress_deadline:
with self._progress_lock:
if self._active_progress_tokens:
break
if self._indexing_complete.is_set():
return True
time.sleep(0.05)
# treat absent progress as ready
with self._progress_lock:
has_active_progress = bool(self._active_progress_tokens)
if not has_active_progress:
self._indexing_complete.set()
return True
# wait for active progress to drain
return self.wait_for_indexing(timeout=timeout)
def expect_indexing(self) -> None:
"""Signal that new files are about to be opened and async indexing should be awaited.
@@ -123,6 +154,44 @@ class TypeScriptLanguageServer(SolidLanguageServer):
"""
self._indexing_complete.clear()
def describe_indexing_state(self) -> str:
""":return: compact diagnostic state for TypeScript indexing progress."""
with self._progress_lock:
active_tokens = sorted(token for token in self._active_progress_tokens if token)
complete = self._indexing_complete.is_set()
token_text = ", ".join(active_tokens) if active_tokens else "<none>"
return f"complete={complete}, active_progress_tokens={token_text}"
def _get_server_ready_timeout(self) -> float:
""":return: maximum seconds to wait for the TypeScript server-ready signal."""
return float(self._custom_settings.get("server_ready_timeout", self.SERVER_READY_TIMEOUT))
def _get_indexing_timeout(self) -> float:
""":return: maximum seconds to wait for TypeScript project indexing."""
return float(self._custom_settings.get("indexing_timeout", self.INDEXING_PROGRESS_TIMEOUT))
def _handle_server_ready_timeout(self, timeout: float) -> None:
"""Handle a TypeScript server-ready timeout.
The base TypeScript server keeps the historical permissive behavior. Strict companion
servers override this hook to fail before serving requests from a cold server.
"""
log.info("Timeout waiting for TypeScript server to become ready after %.0fs, proceeding anyway", timeout)
self.server_ready.set()
def _handle_project_indexing_timeout(self, timeout: float) -> None:
"""Handle a TypeScript project-indexing timeout.
The base TypeScript server keeps the historical permissive behavior. Strict companion
servers override this hook to fail before serving requests from a partially indexed program.
"""
log.warning(
"TypeScript project indexing did not complete within %.0fs; proceeding anyway (%s)",
timeout,
self.describe_indexing_state(),
)
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
@@ -224,13 +293,18 @@ class TypeScriptLanguageServer(SolidLanguageServer):
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.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
def _create_base_initialize_params(self) -> dict:
initialize_params = {
"locale": "en",
# Disable Automatic Type Acquisition (ATA): with ATA enabled, tsserver fetches
# @types/* packages from npm in the background during indexing, which makes startup
# slow, network-dependent, and nondeterministic (and can hang on offline/locked-down
# machines). Serena relies on the types already installed in the project instead.
"initializationOptions": {
"preferences": {
"disableAutomaticTypingAcquisition": True,
},
},
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -258,12 +332,8 @@ class TypeScriptLanguageServer(SolidLanguageServer):
"workDoneProgress": True, # Enables $/progress notifications for project loading
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": self._build_workspace_folders_param(repository_absolute_path),
}
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -366,7 +436,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
log.info("Starting TypeScript server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info(
"Sending initialize request from LSP client to LSP server and awaiting response",
@@ -382,25 +452,22 @@ class TypeScriptLanguageServer(SolidLanguageServer):
}
self.server.notify.initialized({})
if self.server_ready.wait(timeout=10.0):
server_ready_timeout = self._get_server_ready_timeout()
if self.server_ready.wait(timeout=server_ready_timeout):
log.info("TypeScript server is ready")
else:
log.info("Timeout waiting for TypeScript server to become ready, proceeding anyway")
# Fallback: assume server is ready after timeout
self.server_ready.set()
self._handle_server_ready_timeout(server_ready_timeout)
# Wait for any async project loading to complete.
# typescript-language-server may send $/progress for "Initializing JS/TS
# language features…" after initialized. If no progress is sent,
# _indexing_complete stays SET and wait() returns immediately.
indexing_timeout = self._get_indexing_timeout()
log.info("Waiting for TypeScript project indexing to complete (if async)...")
if self.wait_for_indexing(timeout=self.INDEXING_PROGRESS_TIMEOUT):
if self.wait_for_indexing(timeout=indexing_timeout):
log.info("TypeScript project indexing complete")
else:
log.warning(
"TypeScript project indexing did not complete within %.0fs; proceeding anyway",
self.INDEXING_PROGRESS_TIMEOUT,
)
self._handle_project_indexing_timeout(indexing_timeout)
self._activate_additional_workspaces()
@@ -436,10 +503,15 @@ class TypeScriptLanguageServer(SolidLanguageServer):
@override
def _wait_for_additional_workspace_indexing(self) -> None:
if self.wait_for_indexing(timeout=self.INDEXING_PROGRESS_TIMEOUT):
timeout = self._get_indexing_timeout()
if self.wait_for_indexing(timeout=timeout):
log.info("Additional workspace indexing complete")
else:
log.warning("Additional workspace indexing did not complete within timeout; proceeding anyway")
log.warning(
"Additional workspace indexing did not complete within %.0fs; proceeding anyway (%s)",
timeout,
self.describe_indexing_state(),
)
@override
def _get_published_diagnostics_uri(self, request_uri: str) -> str:
@@ -456,8 +528,27 @@ class TypeScriptLanguageServer(SolidLanguageServer):
return self._published_diagnostics_timeout
@override
def _get_wait_time_for_cross_file_referencing(self) -> float:
return 2
def _pre_open_for_cross_file_references(self) -> None:
if not self._has_waited_for_cross_file_references:
self.expect_indexing()
#: how long to wait for tsserver to *start* reporting indexing progress after didOpen;
#: matches the fixed wait the base implementation previously used
_INDEXING_START_GRACE_S = 2.0
@override
def _wait_for_cross_file_references_if_needed(self) -> None:
if self._has_waited_for_cross_file_references:
return
timeout = self._get_indexing_timeout()
if self._wait_for_indexing_start_or_completion(timeout=timeout):
log.info("TypeScript cross-file indexing complete")
else:
log.warning(
"TypeScript cross-file indexing did not complete within %.0fs; proceeding (%s)", timeout, self.describe_indexing_state()
)
self._has_waited_for_cross_file_references = True
@override
def _get_preferred_definition(self, definitions: list[ls_types.Location]) -> ls_types.Location:
@@ -20,7 +20,6 @@ from __future__ import annotations
import logging
import os
import pathlib
import shutil
import threading
@@ -29,7 +28,6 @@ from overrides import override
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -112,9 +110,7 @@ class VsCodeHtmlLanguageServer(SolidLanguageServer):
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "--stdio"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
root_uri = pathlib.Path(repository_absolute_path).as_uri()
def _create_base_initialize_params(self) -> dict:
initialize_params: dict = {
"locale": "en",
"capabilities": {
@@ -142,17 +138,8 @@ class VsCodeHtmlLanguageServer(SolidLanguageServer):
"handledSchemas": ["file"],
"provideFormatter": False,
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore[return-value]
return initialize_params
def _start_server(self) -> None:
def do_nothing(_params: dict) -> None:
@@ -169,7 +156,7 @@ class VsCodeHtmlLanguageServer(SolidLanguageServer):
log.info("Starting vscode-html-language-server")
self.server.start()
init_params = self._get_initialize_params(self.repository_root_path)
init_params = self._create_initialize_params()
init_response = self.server.send.initialize(init_params)
log.debug("HTML LS initialize response: %s", init_response)
# Sanity-check key capabilities; HTML LSP always provides documentSymbol + completion
@@ -6,17 +6,14 @@ which provides TypeScript language server functionality via VSCode's TypeScript
import logging
import os
import pathlib
import shutil
import threading
from typing import cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -141,16 +138,20 @@ class VtsLanguageServer(SolidLanguageServer):
raise ValueError(f"ls_specific_settings.typescript_vts.initialization_options must be a dict, got {type(opts).__name__}")
return opts
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the VTS Language Server.
If ``initialization_options`` is set in ``ls_specific_settings["typescript_vts"]``,
it is forwarded verbatim as LSP ``initializationOptions``.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params: dict = {
"locale": "en",
"initializationOptions": {
"preferences": {
"disableAutomaticTypingAcquisition": True,
},
},
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -172,22 +173,13 @@ class VtsLanguageServer(SolidLanguageServer):
"configuration": True, # This might be needed for vtsls
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
if self._initialization_options:
log.info("Forwarding user-provided initializationOptions to vtsls: %s", self._initialization_options)
initialize_params["initializationOptions"] = self._initialization_options
return cast(InitializeParams, initialize_params)
return initialize_params
def _start_server(self) -> None:
"""
@@ -244,7 +236,7 @@ class VtsLanguageServer(SolidLanguageServer):
log.info("Starting VTS server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -27,7 +27,7 @@ from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_types import Location
from solidlsp.ls_utils import PathUtils
from solidlsp.lsp_protocol_handler import lsp_types
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, ExecuteCommandParams, InitializeParams, SymbolInformation
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, ExecuteCommandParams, SymbolInformation
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -121,8 +121,8 @@ class VueTypeScriptServer(TypeScriptLanguageServer):
)
@override
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
params = super()._get_initialize_params(repository_absolute_path)
def _create_base_initialize_params(self) -> dict:
params = super()._create_base_initialize_params()
params["initializationOptions"] = {
"plugins": [
@@ -617,8 +617,7 @@ class VueLanguageServer(SolidLanguageServer):
return [vue_executable_path, "--stdio"], tsdk_path, ts_ls_executable_path
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
root_uri = pathlib.Path(repository_absolute_path).as_uri()
def _create_base_initialize_params(self) -> dict:
initialize_params = {
"locale": "en",
"capabilities": {
@@ -644,15 +643,6 @@ class VueLanguageServer(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {
"vue": {
"hybridMode": True,
@@ -662,7 +652,7 @@ class VueLanguageServer(SolidLanguageServer):
},
},
}
return initialize_params # type: ignore
return initialize_params
def _start_typescript_server(self) -> None:
try:
@@ -811,7 +801,7 @@ class VueLanguageServer(SolidLanguageServer):
log.info("Starting Vue server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -5,14 +5,12 @@ Contains various configurations and settings specific to YAML files.
import logging
import os
import pathlib
import shutil
from typing import Any
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -114,12 +112,10 @@ class YamlLanguageServer(SolidLanguageServer):
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "--stdio"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the YAML Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -142,15 +138,6 @@ class YamlLanguageServer(SolidLanguageServer):
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {
"yaml": {
"schemaStore": {"enable": True, "url": "https://www.schemastore.org/api/json/catalog.json"},
@@ -161,7 +148,7 @@ class YamlLanguageServer(SolidLanguageServer):
}
},
}
return initialize_params # type: ignore
return initialize_params
def _start_server(self) -> None:
"""
@@ -184,7 +171,7 @@ class YamlLanguageServer(SolidLanguageServer):
log.info("Starting YAML server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+3 -15
View File
@@ -13,7 +13,6 @@ from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -100,12 +99,10 @@ class ZigLanguageServer(SolidLanguageServer):
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd="zls", cwd=repository_root_path), "zig", solidlsp_settings)
self.request_id = 0
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Zig Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
@@ -140,15 +137,6 @@ class ZigLanguageServer(SolidLanguageServer):
"configuration": True,
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {
# ZLS specific options based on schema.json
# Critical paths for ZLS to understand the project
@@ -178,7 +166,7 @@ class ZigLanguageServer(SolidLanguageServer):
"inlay_hints_hide_redundant_param_names_last_token": False,
},
}
return initialize_params # type: ignore[return-value]
return initialize_params
def _start_server(self) -> None:
"""Start ZLS server process"""
@@ -199,7 +187,7 @@ class ZigLanguageServer(SolidLanguageServer):
log.info("Starting ZLS server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
+202 -209
View File
@@ -8,21 +8,29 @@ import shutil
import threading
from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Callable, Hashable, Iterator, Sequence
from collections.abc import Callable, Hashable, Iterator
from contextlib import contextmanager
from copy import copy
from dataclasses import dataclass
from pathlib import Path, PurePath
from time import monotonic, perf_counter, sleep
from typing import Any, Self, Union, cast
import pathspec
from sensai.util.helper import mark_used
from sensai.util.pickle import getstate, load_pickle
from sensai.util.string import ToStringMixin
from serena.util.file_system import match_path
from serena.util.text_utils import MatchedConsecutiveLines
from solidlsp import ls_types
from solidlsp.language_servers.common import build_uvx_launch_command
from solidlsp.dependency_provider import (
LanguageServerDependencyProvider,
LanguageServerDependencyProviderBaseCommand,
LanguageServerDependencyProviderSinglePath,
LanguageServerDependencyProviderUvx,
)
from solidlsp.initialize_params import DefaultInitializeParamsBuilder, InitializeParamsBuilder
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_process import LanguageServerInterface, StdioLanguageServer
@@ -36,6 +44,7 @@ from solidlsp.lsp_protocol_handler.lsp_types import (
DefinitionParams,
DocumentSymbol,
ImplementationParams,
InitializeParams,
LocationLink,
RenameParams,
SymbolInformation,
@@ -57,6 +66,14 @@ The `DocumentSymbol` is the preferred type, but the legacy type `SymbolInformati
log = logging.getLogger(__name__)
# backward compatibility (support old imports of these classes from this module)
mark_used(
LanguageServerDependencyProvider,
LanguageServerDependencyProviderBaseCommand,
LanguageServerDependencyProviderUvx,
LanguageServerDependencyProviderSinglePath,
)
_debug_enabled = log.isEnabledFor(logging.DEBUG)
"""Serves as a flag that triggers additional computation when debug logging is enabled."""
@@ -270,112 +287,6 @@ class DocumentSymbols:
return self._all_symbols, self.root_symbols
class LanguageServerDependencyProvider(ABC):
"""
Prepares dependencies for a language server (if any), ultimately enabling the launch command to be constructed
and optionally providing environment variables that are necessary for the execution.
"""
def __init__(self, custom_settings: SolidLSPSettings.CustomLSSettings, ls_resources_dir: str):
self._custom_settings = custom_settings
self._ls_resources_dir = ls_resources_dir
@abstractmethod
def create_launch_command(self) -> list[str]:
"""
Creates the launch command for this language server, potentially downloading and installing dependencies
beforehand.
:return: the launch command as a list containing the executable and its arguments
"""
def create_launch_command_env(self) -> dict[str, str]:
"""
Provides environment variables to be set when executing the launch command.
This method is intended to be overridden by subclasses that need to set variables.
:return: a mapping for variable names to values
"""
return {}
class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvider, ABC):
"""
Special case of a dependency provider, where there is a single core dependency which provides
the basis for the launch command.
The core dependency's path can be overridden by the user in LS-specific settings (SerenaConfig)
via the key "ls_path". If the user provides the key, the specified path is used directly.
Otherwise, the provider implementation is called to get or install the core dependency.
"""
@abstractmethod
def _get_or_install_core_dependency(self) -> str:
"""
Gets the language server's core path, potentially installing dependencies beforehand.
:return: the core dependency's path (e.g. executable, jar, etc.)
"""
def create_launch_command(self) -> list[str]:
path = self._custom_settings.get("ls_path", None)
if path is not None:
core_path = path
else:
core_path = self._get_or_install_core_dependency()
return self._create_launch_command(core_path)
@abstractmethod
def _create_launch_command(self, core_path: str) -> list[str]:
"""
:param core_path: path to the core dependency
:return: the launch command as a list containing the executable and its arguments
"""
class LanguageServerDependencyProviderUvx(LanguageServerDependencyProvider):
"""
Dependency provider for language servers distributed as a PyPI package, run on demand via ``uvx`` / ``uv x``.
The pinned package version can be overridden by the user via the LS-specific setting given by
``version_setting_key``. Alternatively, the LS-specific setting "ls_path" can be set to the path of an
already-installed language server executable, in which case it is launched directly, bypassing uv entirely.
"""
def __init__(
self,
custom_settings: "SolidLSPSettings.CustomLSSettings",
ls_resources_dir: str,
*,
package: str,
entrypoint: str,
default_version: str,
version_setting_key: str,
extra_args: Sequence[str] = (),
):
"""
:param package: the PyPI package name (e.g. ``"pyright"``)
:param entrypoint: the console script provided by the package (e.g. ``"pyright-langserver"``)
:param default_version: the package version to pin unless overridden
:param version_setting_key: the LS-specific setting key through which the user can override the version
:param extra_args: arguments appended after the entrypoint (e.g. ``("--stdio",)``)
"""
super().__init__(custom_settings, ls_resources_dir)
self._package = package
self._entrypoint = entrypoint
self._default_version = default_version
self._version_setting_key = version_setting_key
self._extra_args = tuple(extra_args)
def create_launch_command(self) -> list[str]:
ls_path = self._custom_settings.get("ls_path")
if ls_path is not None:
return [ls_path, *self._extra_args]
version = self._custom_settings.get(self._version_setting_key, self._default_version)
return build_uvx_launch_command(self._package, version, self._entrypoint, self._extra_args)
class SolidLanguageServer(ABC):
"""
High-level abstraction for language server interaction, which wraps the underlying low-level LSP interface
@@ -505,9 +416,8 @@ class SolidLanguageServer(ABC):
repository_root_path = os.path.abspath(repository_root_path)
ls_class = config.code_language.get_ls_class()
# For now, we assume that all language server implementations have the same signature of the constructor
# (which, unfortunately, differs from the signature of the base class).
# If this assumption is ever violated, we need branching logic here.
# All language server implementations are required to use the same signature of the constructor
# (which differs from the signature of the base class constructor).
ls = ls_class(config, repository_root_path, solidlsp_settings)
ls.set_request_timeout(timeout)
return ls
@@ -526,7 +436,7 @@ class SolidLanguageServer(ABC):
Do not instantiate this class directly. Use `LanguageServer.create` method instead.
:param config: the global SolidLSP configuration.
:param config: the language server's configuration.
:param repository_root_path: the root path of the repository.
:param process_launch_info: (DEPRECATED: pass None and implement _create_dependency_provider instead)
the command used to start the actual language server.
@@ -536,15 +446,19 @@ class SolidLanguageServer(ABC):
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 solidlsp_settings: the global SolidLSP settings
: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
improves/fixes its output).
"""
self._solidlsp_config = config
self.config = config
self._solidlsp_settings = solidlsp_settings
lang = self.get_language_enum_instance()
self._custom_settings = solidlsp_settings.get_ls_specific_settings(lang)
"""
the (user-provided) language server-specific settings
"""
self._ls_resources_dir = self.ls_resources_dir(solidlsp_settings)
log.debug(f"Custom config (LS-specific settings) for {lang}: {self._custom_settings}")
self._encoding = config.encoding
@@ -563,6 +477,11 @@ class SolidLanguageServer(ABC):
"""
identifies the language server (not to be confused with the language id passed to the language server)
"""
# The source filename matcher is a @cache'd per-language singleton. A previous project may
# have extended it (e.g. Perl's file_filter adding .cgi); reset it here so every activation
# starts from the language's default extensions, then language-server subclasses re-apply
# their own settings during the rest of __init__.
self.language.get_source_fn_matcher().reset()
self._published_diagnostics: dict[str, list[ls_types.Diagnostic]] = {}
self._published_diagnostics_generation_by_uri: dict[str, int] = {}
self._published_diagnostics_generation = 0
@@ -592,14 +511,10 @@ class SolidLanguageServer(ABC):
else:
logging_fn = None
# create the LanguageServerHandler, which provides the functionality to start the language server and communicate with it,
# preparing the launch command beforehand
# create the low-level server interface, potentially installing dependencies and launching a subprocess
self._process_launch_info: ProcessLaunchInfo | None = process_launch_info
self._dependency_provider: LanguageServerDependencyProvider | None = None
if process_launch_info is None:
self._dependency_provider = self._create_dependency_provider()
process_launch_info = self._create_process_launch_info()
log.debug(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}")
self.server = self._create_language_server_interface(process_launch_info, logging_fn)
self.server = self._create_language_server_interface(logging_fn)
"""
the low-level language server interface
"""
@@ -619,28 +534,20 @@ class SolidLanguageServer(ABC):
self._has_waited_for_cross_file_references = False
# resolving additional workspace folders
self._additional_workspace_abs_paths: list[str] = []
_seen: set[str] = set()
for additional_workspace_path in self._solidlsp_settings.additional_workspace_folders:
if not additional_workspace_path or additional_workspace_path == ".":
continue
if not os.path.isabs(additional_workspace_path):
additional_workspace_abs_path = os.path.realpath(os.path.join(self.repository_root_path, additional_workspace_path))
else:
additional_workspace_abs_path = str(Path(additional_workspace_path).resolve())
if not os.path.isdir(additional_workspace_abs_path):
log.error(
"additional_workspace_folders: skipping non-existent directory %s (resolved to %s)",
additional_workspace_abs_path,
additional_workspace_abs_path,
)
continue
if additional_workspace_abs_path in _seen:
log.info("additional_workspace_folders: skipping duplicate %s", additional_workspace_path)
continue
_seen.add(additional_workspace_abs_path)
self._additional_workspace_abs_paths.append(additional_workspace_abs_path)
self._abs_workspace_folders_indexed = self.config.get_absolute_workspace_folders(self.repository_root_path)
self._abs_workspace_folders_additional = self.config.get_absolute_additional_workspace_folders(self.repository_root_path)
"""
additional workspace folders, which are passed to the language server in the initialization request
but which are not indexed by SolidLSP, i.e. not traversed for full symbol tree
"""
self._abs_workspace_folders_all = self._abs_workspace_folders_indexed + self._abs_workspace_folders_additional
@property
def custom_settings(self) -> SolidLSPSettings.CustomLSSettings:
"""
The (user-provided) language server-specific settings.
"""
return self._custom_settings
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
"""
@@ -653,15 +560,23 @@ class SolidLanguageServer(ABC):
f"{self.__class__.__name__} must implement _create_dependency_provider() or pass process_launch_info to __init__()"
)
def _get_dependency_provider(self) -> LanguageServerDependencyProvider:
if self._dependency_provider is None:
self._dependency_provider = self._create_dependency_provider()
return self._dependency_provider
def _create_process_launch_info(self) -> ProcessLaunchInfo:
assert self._dependency_provider is not None
cmd = self._dependency_provider.create_launch_command()
env = self._dependency_provider.create_launch_command_env()
dependency_provider = self._get_dependency_provider()
cmd = dependency_provider.create_launch_command()
env = dependency_provider.create_launch_command_env()
return ProcessLaunchInfo(cmd=cmd, cwd=self.repository_root_path, env=env)
def _create_language_server_interface(
self, process_launch_info: ProcessLaunchInfo, logging_fn: Callable[[str, str, StringDict | str], None] | None
) -> LanguageServerInterface:
def _get_process_launch_info(self) -> ProcessLaunchInfo:
if self._process_launch_info is None:
self._process_launch_info = self._create_process_launch_info()
return self._process_launch_info
def _create_language_server_interface(self, logging_fn: Callable[[str, str, StringDict | str], None] | None) -> LanguageServerInterface:
"""
Creates the low-level language server interface for LSP communication.
@@ -670,16 +585,18 @@ class SolidLanguageServer(ABC):
The default implementation creates a StdioLanguageServer, but subclasses can override this method to create a different type
of interface if needed.
:param process_launch_info: process launch information
:param logging_fn: the trace logging function
:return: the interface
"""
language_id = self.language_id
process_launch_info = self._get_process_launch_info()
log.debug(f"Creating language server instance with {language_id=} and {process_launch_info}")
return StdioLanguageServer(
process_launch_info,
language=self.language,
determine_log_level=self._determine_log_level,
logger=logging_fn,
start_independent_lsp_process=self._solidlsp_config.start_independent_lsp_process,
start_independent_lsp_process=self.config.start_independent_lsp_process,
)
# --- diagnostics-related functions ---
@@ -1081,6 +998,48 @@ class SolidLanguageServer(ABC):
"""Check if a relative path traverses outside the workspace root via '..' components."""
return ".." in PurePath(relative_file_path).parts
@dataclass
class PathWorkspaceStatus:
"""
Represents the status of a path with respect to the language server's workspace folders.
"""
ls: "SolidLanguageServer"
resolve_abs_path: Path
is_in_workspace_folder: bool
workspace_root: str | None = None
"""
the workspace root the path is within/relative to, if any
"""
@classmethod
def from_abs_resolved_path(cls, path: Path, ls: "SolidLanguageServer") -> "SolidLanguageServer.PathWorkspaceStatus":
"""
:param path: an absolute, resolved Path instance
:param ls: the language server instance
"""
for workspace in ls._abs_workspace_folders_all:
if path.is_relative_to(workspace):
return SolidLanguageServer.PathWorkspaceStatus(
ls=ls, is_in_workspace_folder=True, workspace_root=workspace, resolve_abs_path=path
)
return SolidLanguageServer.PathWorkspaceStatus(ls=ls, is_in_workspace_folder=False, resolve_abs_path=path)
@classmethod
def from_relative_path(cls, relative_path: str, ls: "SolidLanguageServer") -> "SolidLanguageServer.PathWorkspaceStatus":
"""
:param relative_path: a relative path from the repository root
:param ls: the language server instance
"""
return cls.from_abs_resolved_path(pathlib.Path(ls.repository_root_path, relative_path).resolve(), ls)
def check_within_workspace_or_raise(self):
if not self.is_in_workspace_folder:
raise ValueError(
f"Path {self.resolve_abs_path} is outside of configured workspaces. "
f"Configured workspaces: {self.ls._abs_workspace_folders_all}."
)
def _resolve_file_uri(self, relative_file_path: str) -> str:
"""Construct a canonical file URI from a relative path.
@@ -1090,36 +1049,9 @@ class SolidLanguageServer(ABC):
p = pathlib.Path(os.path.join(self.repository_root_path, relative_file_path))
if self._path_contains_dots(relative_file_path):
p = p.resolve()
is_outside_of_configured_workspaces = True
configured_workspaces = [*self._additional_workspace_abs_paths, self.repository_root_path]
for workspace in configured_workspaces:
if p.is_relative_to(workspace):
is_outside_of_configured_workspaces = False
break
if is_outside_of_configured_workspaces:
raise ValueError(
f"Path {relative_file_path} contains '..' segments and is outside of configured workspaces. "
f"Configured workspaces: {configured_workspaces}. Resolved path: {p}."
)
self.PathWorkspaceStatus.from_abs_resolved_path(p, self).check_within_workspace_or_raise()
return p.as_uri()
def _build_workspace_folders_param(self, repository_absolute_path: str) -> list[dict[str, str]]:
"""Build the ``workspaceFolders`` list for LSP initialization.
Returns a list containing the primary workspace folder followed by any
additional workspace folders configured in settings.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
folders: list[dict[str, str]] = [
{"uri": root_uri, "name": os.path.basename(repository_absolute_path)},
]
for abs_folder in self._additional_workspace_abs_paths:
folder_uri = pathlib.Path(abs_folder).as_uri()
folders.append({"uri": folder_uri, "name": os.path.basename(abs_folder)})
if len(folders) > 1:
log.info("LSP multi-root workspace: %d folders", len(folders))
return folders
def _activate_additional_workspaces(self) -> None:
"""Open a representative file from each additional workspace folder to
trigger project loading in the language server.
@@ -1133,7 +1065,7 @@ class SolidLanguageServer(ABC):
enable this feature; the default implementation raises ``NotImplementedError``.
"""
opened_count = 0
for additional_workspace in self._additional_workspace_abs_paths:
for additional_workspace in self._abs_workspace_folders_additional:
source_file = self._find_representative_source_file(additional_workspace)
if source_file is None:
log.warning("No source file found in additional workspace folder: %s", additional_workspace)
@@ -1224,26 +1156,27 @@ class SolidLanguageServer(ABC):
Determine if a path should be ignored based on file type
and ignore patterns.
Missing paths are classified without raising, since language servers may report
locations of generated files that are not present on disk (e.g. compiled classes
under build output). A missing path is treated as a file if its name has a suffix
and as a directory otherwise; directory-only ignore patterns (e.g. ``build/``)
match only existing directories.
:param relative_path: Relative path to check
:param ignore_unsupported_files: whether files that are not supported source files should be ignored
:return: True if the path should be ignored, False otherwise
"""
abs_path = os.path.join(self.repository_root_path, relative_path)
if not os.path.exists(abs_path):
raise FileNotFoundError(f"File {abs_path} not found, the ignore check cannot be performed")
# Check file extension if it's a file
is_file = os.path.isfile(abs_path)
if is_file and ignore_unsupported_files:
fn_matcher = self.get_source_fn_matcher()
if not fn_matcher.is_relevant_filename(abs_path):
return True
# Create normalized path for consistent handling
rel_path = Path(relative_path)
# Check each part of the path against always fulfilled ignore conditions
# determine whether the path is a file: from the filesystem if the path exists, lexically otherwise
exists = os.path.exists(abs_path)
if not exists:
log.debug("Path %s does not exist; the ignore check is performed on the path itself", abs_path)
is_file = os.path.isfile(abs_path) if exists else bool(rel_path.suffix)
# check each directory component against always-ignored dirnames
dir_parts = rel_path.parts
if is_file:
dir_parts = dir_parts[:-1]
@@ -1253,6 +1186,12 @@ class SolidLanguageServer(ABC):
if self.is_ignored_dirname(part):
return True
# apply the unsupported-extension rule to files
if is_file and ignore_unsupported_files:
fn_matcher = self.get_source_fn_matcher()
if not fn_matcher.is_relevant_filename(abs_path):
return True
return match_path(relative_path, self.get_ignore_spec(), root_path=self.repository_root_path)
@contextmanager
@@ -1457,6 +1396,9 @@ class SolidLanguageServer(ABC):
self._ensure_server_started()
t0 = perf_counter() if _debug_enabled else None
# arm indexing tracking before didOpen so that the subsequent wait can
# observe the indexing progress triggered by opening the file
self.language_server._pre_open_for_cross_file_references()
with self.language_server.open_file(self.relative_file_path):
self.language_server._wait_for_cross_file_references_if_needed()
try:
@@ -1521,6 +1463,12 @@ class SolidLanguageServer(ABC):
)
return None
# skip locations of generated files that are not present on disk
# (language servers may report e.g. compiled classes under build output)
if not os.path.exists(abs_path):
log.info("%s found symbol at non-existent path: %s", self.request_name, abs_path)
return None
if self.skip_ignored_paths and self.language_server.is_ignored_path(rel_path_str):
log.info("%s found symbol in ignored path: %s", self.request_name, rel_path_str)
return None
@@ -1608,6 +1556,14 @@ class SolidLanguageServer(ABC):
},
)
def _pre_open_for_cross_file_references(self) -> None:
"""Called just before open_file() for requests that may need cross-file indexing.
Override to pre-arm async indexing tracking before sending didOpen (e.g. clear
a threading.Event so a subsequent wait for indexing blocks correctly).
The base implementation is a no-op.
"""
def _wait_for_cross_file_references_if_needed(self) -> None:
if not self._has_waited_for_cross_file_references:
# Some LS require waiting for a while before they can return accurate cross-file results.
@@ -2052,24 +2008,17 @@ class SolidLanguageServer(ABC):
If a directory is passed, all files within this directory will be considered.
:return: A list of root symbols representing the top-level packages/modules in the project.
"""
if within_relative_path is not None:
within_abs_path = os.path.join(self.repository_root_path, within_relative_path)
if not os.path.exists(within_abs_path):
raise FileNotFoundError(f"File or directory not found: {within_abs_path}")
if os.path.isfile(within_abs_path):
if self.is_ignored_path(within_relative_path):
log.error("You passed a file explicitly, but it is ignored. This is probably an error. File: %s", within_relative_path)
return []
else:
root_nodes = self.request_document_symbols(within_relative_path).root_symbols
return root_nodes
# Helper function to recursively process directories
def process_directory(rel_dir_path: str) -> list[ls_types.UnifiedSymbolInformation]:
abs_dir_path = self.repository_root_path if rel_dir_path == "." else os.path.join(self.repository_root_path, rel_dir_path)
def process_directory(abs_dir_path: str) -> list[ls_types.UnifiedSymbolInformation]:
abs_dir_path = os.path.realpath(abs_dir_path)
if self.is_ignored_path(str(Path(abs_dir_path).relative_to(self.repository_root_path))):
rel_dir_path: str | None
try:
rel_dir_path = str(Path(abs_dir_path).relative_to(self.repository_root_path))
except ValueError: # not relative to repository root
rel_dir_path = None
if rel_dir_path and self.is_ignored_path(rel_dir_path):
log.debug("Skipping directory: %s (because it should be ignored)", rel_dir_path)
return []
@@ -2116,7 +2065,7 @@ class SolidLanguageServer(ABC):
continue
if os.path.isdir(contained_dir_or_file_abs_path):
child_symbols = process_directory(contained_dir_or_file_rel_path)
child_symbols = process_directory(contained_dir_or_file_abs_path)
package_symbol["children"].extend(child_symbols)
for child in child_symbols:
child["parent"] = package_symbol
@@ -2166,9 +2115,23 @@ class SolidLanguageServer(ABC):
return result
# Start from the root or the specified directory
start_rel_path = within_relative_path or "."
return process_directory(start_rel_path)
if within_relative_path:
within_abs_path = os.path.join(self.repository_root_path, within_relative_path)
if not os.path.exists(within_abs_path):
raise FileNotFoundError(f"File or directory not found: {within_abs_path}")
if self.is_ignored_path(within_relative_path):
raise ValueError(f"Explicitly requested symbols in '{within_relative_path}' while the path is ignored")
if os.path.isfile(within_abs_path):
root_nodes = self.request_document_symbols(within_relative_path).root_symbols
return root_nodes
else:
self.PathWorkspaceStatus.from_abs_resolved_path(Path(within_abs_path).resolve(), self).check_within_workspace_or_raise()
return process_directory(within_abs_path)
else:
full_result = []
for root in self.config.get_absolute_workspace_folders(self.repository_root_path):
full_result.extend(process_directory(root))
return full_result
@staticmethod
def _get_range_from_file_content(file_content: str) -> ls_types.Range:
@@ -3156,3 +3119,33 @@ class SolidLanguageServer(ABC):
def is_running(self) -> bool:
return self.server.is_running()
def _create_initialize_params_builder(self) -> InitializeParamsBuilder:
return DefaultInitializeParamsBuilder(self)
@abstractmethod
def _create_base_initialize_params(self) -> dict | InitializeParams:
"""
Subclasses should override this method to provide server-specific InitializeParams settings,
which provide the basis for the InitializeParams object sent to the language server during initialization.
The returned dictionary is passed to the builder constructed by _create_initialize_params_builder()
in order to create the final InitializeParams object.
The default builder implementation already sets the following keys, so implementations of this method
should not set them:
- processId
- rootPath
- rootUri
- clientInfo
- workspaceFolders
:return: the base InitializeParams settings
"""
def _create_initialize_params(self) -> InitializeParams:
"""
Create the InitializeParams object to send to the language server during initialization.
"""
return self._create_initialize_params_builder().with_base_options(self._create_base_initialize_params()).build()
+104 -3
View File
@@ -2,15 +2,21 @@
Configuration objects for language servers
"""
import logging
import os
import re
from collections.abc import Iterable
from dataclasses import dataclass, field
from enum import Enum
from functools import cache
from pathlib import Path
from typing import TYPE_CHECKING, Self
if TYPE_CHECKING:
from solidlsp import SolidLanguageServer
log = logging.getLogger(__name__)
class FilenameMatcher:
def __init__(self, *file_extensions: str, case_sensitive: bool = True) -> None:
@@ -20,6 +26,38 @@ class FilenameMatcher:
"""
self._file_extensions = list(set(file_extensions)) if case_sensitive else list(set(ext.lower() for ext in file_extensions))
self._case_sensitive = case_sensitive
# Snapshot of the initial configuration, used by ``reset``. Relevant for matchers that are
# per-language singletons (``Language.get_source_fn_matcher`` is ``@cache``d): extensions added
# via ``add_extensions`` for one project must not leak into the next, so the singleton is reset
# to this snapshot at every language server initialisation.
self._initial_file_extensions = list(self._file_extensions)
def reset(self) -> None:
"""
Restore the matcher to its initial set of extensions (as provided at construction).
Undoes any extensions added via :meth:`add_extensions`. Intended for the per-language
singleton matchers, which are reset at the start of every language server initialisation so
that a previous project's reconfiguration does not leak into a newly activated one.
"""
self._file_extensions = list(self._initial_file_extensions)
def add_extensions(self, *file_extensions: str) -> None:
"""
Add further file extensions to this matcher (idempotent).
This is intended for matchers that are per-language singletons, i.e. those returned by
:meth:`Language.get_source_fn_matcher` (which is ``@cache``d): extensions that a user
configures for a language server (e.g. ``.cgi`` for Perl) can be added here so that every
consumer of the matcher — symbol index traversal, ignore checks, language composition —
treats the same set of files as sources, staying in sync with the language server.
:param file_extensions: the additional file extensions, e.g. ``.cgi``
"""
for ext in file_extensions:
norm = ext if self._case_sensitive else ext.lower()
if norm not in self._file_extensions:
self._file_extensions.append(norm)
def is_relevant_filename(self, fn: str) -> bool:
if not self._case_sensitive:
@@ -34,7 +72,7 @@ class FilenameMatcher:
a *complete* extension — i.e. the extension must either end the string or be followed
by a non-extension-character (anything other than a letter, digit, or underscore).
"""
if self._case_sensitive:
if not self._case_sensitive:
string = string.lower()
for ext in self._file_extensions:
if re.search(rf"{re.escape(ext)}(?:\W|$)", string):
@@ -135,6 +173,11 @@ class Language(str, Enum):
The editor must already be running with its built-in LSP enabled (default).
Supports .gd and .gdscript files.
"""
QML = "qml"
"""QML language server using Qt's qmlls (or qmlls6).
Supports .qml files. Requires Qt 6 installation providing qmlls on PATH.
See https://doc.qt.io/qt-6/qtqml-tool-qmlls.html
"""
# Experimental or deprecated Language Servers
TYPESCRIPT_VTS = "typescript_vts"
"""Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls"""
@@ -305,6 +348,8 @@ class Language(str, Enum):
"""
return self.get_ls_class().supports_implementation_request()
# NOTE: Caching results in a singleton per enum item, which is a precondition for persistent configuration of the matcher.
@cache
def get_source_fn_matcher(self) -> FilenameMatcher:
match self:
case self.PYTHON | self.PYTHON_JEDI | self.PYTHON_TY | self.PYTHON_PYREFLY:
@@ -517,6 +562,8 @@ class Language(str, Enum):
return FilenameMatcher(".ads", ".adb", ".ada", case_sensitive=False)
case self.GDSCRIPT:
return FilenameMatcher(".gd", ".gdscript")
case self.QML:
return FilenameMatcher(".qml")
case self.HTML:
return FilenameMatcher(".html", ".htm")
case self.SCSS:
@@ -796,6 +843,10 @@ class Language(str, Enum):
from solidlsp.language_servers.godot_language_server import GodotLanguageServer
return GodotLanguageServer
case self.QML:
from solidlsp.language_servers.qml_language_server import QmlLanguageServer
return QmlLanguageServer
case self.HTML:
from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer
@@ -826,13 +877,28 @@ class Language(str, Enum):
raise ValueError(f"Unhandled language server class: {ls_class}")
@dataclass
@dataclass(frozen=True)
class LanguageServerConfig:
"""
Configuration parameters
Configuration parameters for a language server instance
"""
code_language: Language
"""
defines the language server to use
"""
workspace_folders: list[str] = field(default_factory=lambda: ["."])
"""
list of workspace folders to be used by the language server and to be fully indexed by SolidLSP.
Paths can either be absolute or relative to the project root.
These folders must be descendants of the project root.
"""
additional_workspace_folders: list[str] = field(default_factory=list)
"""
list of additional workspace folders to be passed to the language server, but which are not to be indexed by SolidLSP.
Paths can either be absolute or relative to the project root.
These folders can potentially be outside of the project root, e.g. for cross-package reference support.
"""
trace_lsp_communication: bool = False
start_independent_lsp_process: bool = True
ignored_paths: list[str] = field(default_factory=list)
@@ -845,3 +911,38 @@ class LanguageServerConfig:
import inspect
return cls(**{k: v for k, v in env.items() if k in inspect.signature(cls).parameters})
@staticmethod
def _absolute_workspace_folders(folders: list[str], project_root: str) -> list[str]:
abs_workspace_folders = []
for path in folders:
if os.path.isabs(path):
abs_path = str(Path(path).resolve())
else:
abs_path = os.path.realpath(os.path.join(project_root, path))
if not os.path.exists(abs_path):
log.error("Workspace folder does not exist: %s; skipping", abs_path)
continue
if abs_path in abs_workspace_folders:
log.warning("Duplicate workspace folder: %s; skipping", abs_path)
continue
abs_workspace_folders.append(abs_path)
return abs_workspace_folders
def get_absolute_workspace_folders(self, project_root: str) -> list[str]:
"""
Get the absolute paths of the workspace folders, resolving relative paths against the project root.
:param project_root: The root path of the project
:return: List of absolute workspace folder paths
"""
return self._absolute_workspace_folders(self.workspace_folders, project_root)
def get_absolute_additional_workspace_folders(self, project_root: str) -> list[str]:
"""
Get the absolute paths of the additional workspace folders, resolving relative paths against the project root.
:param project_root: The root path of the project
:return: List of absolute additional workspace folder paths
"""
return self._absolute_workspace_folders(self.additional_workspace_folders, project_root)
+204 -37
View File
@@ -31,6 +31,124 @@ class InvalidTextLocationError(Exception):
pass
class TextStepper:
def __init__(self, chars: str):
self._chars = chars
self._len = len(chars)
self.line = 0
"""
the current 0-based line index
"""
self.col = 0
"""
the current 0-based column index
"""
self.idx = 0
"""
the current 0-based index in the full text.
The index specifies the next character to be processed, i.e. if this is
the length of the text, then the end of the text has been reached.
It specifies a location in the same way as a cursor insertion position:
cursor at the very beginning (idx=0) means insert before the first character.
"""
self.is_newline = False
"""
whether the last step processed a full line ending in a line break
"""
self.prev_line_start_idx = 0
"""
start index of the last fully processed line (inclusive)
"""
self.prev_line_end_idx = 0
"""
end of the last fully processed line (exclusive), excluding newline characters
"""
self.line_start_idx = 0
"""
start of the current, not yet completed line (inclusive)
"""
def _get_char(self, idx: int) -> str | None:
if idx < 0 or idx >= self._len:
return None
return self._chars[idx]
def step_line(self) -> bool:
"""
Processes the next line in the text, advancing past the next newline sequence or,
if no further newline is present, to the end of the text.
:return: True if processing was possible, False if the end of the text had already been reached
(idx not advanced)
"""
if self.idx >= self._len:
return False
# find the next newline sequence
# Note: LSP defines that a newline is given by either "\n", "\r\n", or "\r" on its own
# Reference: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocuments
# The search for "\r" is bounded by the position of the next "\n".
lf_idx = self._chars.find("\n", self.idx)
cr_idx = self._chars.find("\r", self.idx, lf_idx if lf_idx != -1 else self._len)
if cr_idx != -1:
newline_start_idx = cr_idx
newline_end_idx = cr_idx + 2 if self._get_char(cr_idx + 1) == "\n" else cr_idx + 1
elif lf_idx != -1:
newline_start_idx = lf_idx
newline_end_idx = lf_idx + 1
else:
newline_start_idx = None
newline_end_idx = None
# advance past the newline sequence or, in its absence, consume the trailing line
if newline_start_idx is not None and newline_end_idx is not None:
self.idx = newline_end_idx
self.line += 1
self.col = 0
self.is_newline = True
self.prev_line_start_idx = self.line_start_idx
self.prev_line_end_idx = newline_start_idx
self.line_start_idx = newline_end_idx
else:
self.idx = self._len
self.col = self._len - self.line_start_idx
self.is_newline = False
return True
def process_all(self):
"""
Processes all characters in the text, updating the line and column numbers accordingly.
"""
while self.step_line():
pass
def _get_last_line(self, with_end: bool) -> str:
"""
Returns the last line processed, optionally including the newline character(s) at the end
"""
start_idx = self.prev_line_start_idx
end_idx = self.prev_line_end_idx if not with_end else self.line_start_idx
return self._chars[start_idx:end_idx]
def process_all_gather_lines(self, with_ends: bool) -> list[str]:
"""
Processes all characters in the text and returns a list of lines
:param with_ends: whether to include the newline character(s) at the end of each line
:return: the list of lines
"""
lines = []
while self.step_line():
if self.is_newline:
lines.append(self._get_last_line(with_end=with_ends))
# add the last line (which was not followed by a newline), even if empty
last_line = self._chars[self.line_start_idx :]
lines.append(last_line)
return lines
class TextUtils:
"""
Utilities for text operations.
@@ -39,54 +157,84 @@ class TextUtils:
@staticmethod
def get_line_col_from_index(text: str, index: int) -> tuple[int, int]:
"""
Returns the zero-indexed line and column number of the given index in the given text
:param text: the text in which the index is to be located
:param index: the 0-based index in the text
:return: a tuple (0-based line number, 0-based column number) corresponding to the index in the text
"""
l = 0
c = 0
idx = 0
while idx < index:
if text[idx] == "\n":
l += 1
c = 0
else:
c += 1
idx += 1
# step over full lines; once the line containing the index has been processed, compute
# the position from the difference to the respective line's start index
text_stepper = TextStepper(text)
while text_stepper.step_line():
if text_stepper.idx > index:
if text_stepper.is_newline:
# position was stepped over as part of the previous line
if index > text_stepper.prev_line_end_idx:
# edge case: the index points into a multi-character newline sequence ("\r\n");
# map this to the beginning of the following line
return text_stepper.line, 0
return text_stepper.line - 1, index - text_stepper.prev_line_start_idx
else:
# position was stepped over as part of the current line (which was not followed by a newline)
return text_stepper.line, index - text_stepper.line_start_idx
return l, c
# handle the case where the end of the text was reached without stepping past the index
if index > text_stepper.idx:
raise InvalidTextLocationError(f"{index=}")
return text_stepper.line, text_stepper.col
@classmethod
def get_line_from_index(cls, text: str, index: int) -> int:
"""
:param text: the text in which the index is to be located
:param index: the 0-based index in the text
:return: the 0-based line number corresponding to the index in the text
"""
return cls.get_line_col_from_index(text, index)[0]
@staticmethod
def get_index_from_line_col(text: str, line: int, col: int) -> int:
"""
Returns the index of the given zero-indexed line and column number in the given text
:param text: the text in which the coordinates are to be located
:param line: the 0-based line number
:param col: the 0-based column number
:return: the corresponding 0-based index in the text
"""
idx = 0
while line > 0:
if idx >= len(text):
raise InvalidTextLocationError
if text[idx] == "\n":
line -= 1
idx += 1
idx += col
return idx
# step over full lines until the requested line is reached
text_stepper = TextStepper(text)
while text_stepper.line < line:
if not text_stepper.step_line():
raise InvalidTextLocationError("{line=}, {col=}")
return text_stepper.line_start_idx + col
@staticmethod
def _get_updated_position_from_line_and_column_and_edit(l: int, c: int, text_to_be_inserted: str) -> tuple[int, int]:
"""
Utility function to get the position of the cursor after inserting text at a given line and column.
:param l: the 0-based line number before the edit
:param c: the 0-based column number before the edit
:param text_to_be_inserted: the text that was inserted at the given position
:return: the updated 0-based line and column numbers after the edit (end of insertion)
"""
num_newlines_in_gen_text = text_to_be_inserted.count("\n")
if num_newlines_in_gen_text > 0:
l += num_newlines_in_gen_text
c = len(text_to_be_inserted.split("\n")[-1])
text_stepper = TextStepper(text_to_be_inserted)
text_stepper.process_all()
if text_stepper.line > 0:
l += text_stepper.line
c = text_stepper.col
else:
c += len(text_to_be_inserted)
return (l, c)
c += text_stepper.col
return l, c
@staticmethod
def delete_text_between_positions(text: str, start_line: int, start_col: int, end_line: int, end_col: int) -> tuple[str, str]:
"""
Deletes the text between the given start and end positions.
Returns the modified text and the deleted text.
:param text: the original text
:param start_line: the 0-based line number of the start position
:param start_col: the 0-based column number of the start position
:param end_line: the 0-based line number of the end position
:param end_col: the 0-based column number of the end position
:return: a tuple containing the modified text and the deleted text
"""
del_start_idx = TextUtils.get_index_from_line_col(text, start_line, start_col)
del_end_idx = TextUtils.get_index_from_line_col(text, end_line, end_col)
@@ -98,18 +246,29 @@ class TextUtils:
@staticmethod
def insert_text_at_position(text: str, line: int, col: int, text_to_be_inserted: str) -> tuple[str, int, int]:
"""
Inserts the given text at the given line and column.
Returns the modified text and the new line and column.
Inserts the given text at the given position and returns the
:param text: the original text
:param line: the 0-based line number where the text should be inserted
:param col: the 0-based column number where the text should be inserted
:param text_to_be_inserted: the text to be inserted
:return: a tuple containing the modified text, the updated line number, and the updated column number
(position after the inserted text)
"""
try:
change_index = TextUtils.get_index_from_line_col(text, line, col)
except InvalidTextLocationError:
num_lines_in_text = text.count("\n") + 1
text_stepper = TextStepper(text)
text_stepper.process_all()
num_lines_in_text = text_stepper.line + 1
max_line = num_lines_in_text - 1
if line == max_line + 1 and col == 0: # trying to insert at new line after full text
# insert at end, adding missing newline
# insert at end, adding missing newline and adjusting insertion position
# to the actual end coordinates of the text
change_index = len(text)
text_to_be_inserted = "\n" + text_to_be_inserted
line = text_stepper.line
col = text_stepper.col
else:
raise
new_text = text[:change_index] + text_to_be_inserted + text[change_index:]
@@ -125,14 +284,22 @@ 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:
@classmethod
def get_text_in_lines_range(cls, 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)
lines = cls.split_lines(text, with_ends=True)
return "".join(lines[start_line : end_line + 1])
@staticmethod
def split_lines(text: str, with_ends: bool = False) -> list[str]:
"""
Splits the given text into lines, optionally including the newline character(s) at the end of each line.
"""
text_stepper = TextStepper(text)
return text_stepper.process_all_gather_lines(with_ends=with_ends)
class PathUtils:
"""
+6 -7
View File
@@ -18,6 +18,12 @@ log = logging.getLogger(__name__)
@dataclass
class SolidLSPSettings:
"""
Configures SolidLSP-specific data storage as well as global settings.
Note: Server instance-specific settings belong in LanguageServerConfig, not here.
"""
solidlsp_dir: str = str(pathlib.Path.home() / ".solidlsp")
"""
Path to the directory in which to store global Solid-LSP data (which is not project-specific)
@@ -34,13 +40,6 @@ class SolidLSPSettings:
Have a look at the docstring of the constructors of the corresponding LS implementations within solidlsp to see which options are available.
No documentation on options means no options are available.
"""
additional_workspace_folders: list[str] = field(default_factory=list)
"""
Additional workspace folder paths for cross-package reference support.
Paths can be absolute or relative to the project root. Each folder is added as a workspace
folder in the LSP initialization, enabling language servers to discover symbols and references
across package boundaries (e.g. in monorepos).
"""
def __post_init__(self) -> None:
os.makedirs(str(self.solidlsp_dir), exist_ok=True)
+18 -2
View File
@@ -66,6 +66,7 @@ def _create_ls(
ignored_paths: list[str] | None = None,
trace_lsp_communication: bool = False,
ls_specific_settings: dict[Language, dict[str, Any]] | None = None,
workspace_folders: list[str] | None = None,
additional_workspace_folders: list[str] | None = None,
solidlsp_dir: Path | None = None,
) -> SolidLanguageServer:
@@ -79,6 +80,8 @@ def _create_ls(
code_language=language,
ignored_paths=ignored_paths,
trace_lsp_communication=trace_lsp_communication,
workspace_folders=workspace_folders or ["."],
additional_workspace_folders=additional_workspace_folders or [],
)
effective_solidlsp_dir = solidlsp_dir if solidlsp_dir is not None else SerenaPaths().serena_user_home_dir
project_data_path = os.path.join(repo_path, SERENA_MANAGED_DIR_NAME)
@@ -89,7 +92,6 @@ def _create_ls(
solidlsp_dir=effective_solidlsp_dir,
project_data_path=project_data_path,
ls_specific_settings=ls_specific_settings or {},
additional_workspace_folders=additional_workspace_folders or [],
),
)
@@ -101,11 +103,19 @@ def start_ls_context(
ignored_paths: list[str] | None = None,
trace_lsp_communication: bool = False,
ls_specific_settings: dict[Language, dict[str, Any]] | None = None,
workspace_folders: list[str] | None = None,
additional_workspace_folders: list[str] | None = None,
solidlsp_dir: Path | None = None,
) -> Iterator[SolidLanguageServer]:
ls = _create_ls(
language, repo_path, ignored_paths, trace_lsp_communication, ls_specific_settings, additional_workspace_folders, solidlsp_dir
language,
repo_path,
ignored_paths,
trace_lsp_communication,
ls_specific_settings,
workspace_folders,
additional_workspace_folders,
solidlsp_dir,
)
log.info(f"Starting language server for {language} {repo_path}")
with ls.start_server_context():
@@ -244,6 +254,7 @@ Flag indicating whether the tests are running in the GitHub CI environment.
is_windows = platform.system() == "Windows"
is_macos = platform.system() == "Darwin"
is_linux = platform.system() == "Linux"
_LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = {
@@ -397,6 +408,11 @@ def _determine_disabled_languages() -> list[Language]:
result.append(Language.REGO)
if _sh.which("elm") is None and not is_ci:
result.append(Language.ELM)
# qmlls is installed (standalone build; see pytest.yml) only on the Ubuntu other-langs CI batch. It is
# expected there, so a missing binary on Linux CI is NOT skipped here -- the test runs and fails loudly,
# catching a CI setup regression. On Windows/macOS CI (never installed) and off-CI without the binary it skips.
if (_sh.which("qmlls6") is None and _sh.which("qmlls") is None) and not (is_ci and is_linux):
result.append(Language.QML)
# === 3. Disabled wherever the precondition is missing (including on CI) ===
# 3a. Platform precondition: these language servers have no native Windows support.
@@ -0,0 +1,14 @@
import QtQuick 2.15
Rectangle {
id: root
width: 200
height: root.width
color: "green"
Text {
id: label
text: root.width + "x" + root.height
anchors.centerIn: parent
}
}
@@ -0,0 +1,23 @@
import QtQuick 2.15
Item {
width: 400
height: 300
CustomComponent {
x: 50
y: 50
}
Rectangle {
y: 180
width: 200
height: 100
color: "yellow"
Text {
text: "Another shape"
anchors.centerIn: parent
}
}
}
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff. Show more