mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 10:19:02 +00:00
Merge remote-tracking branch 'origin/main' into tool-desc-update
This commit is contained in:
commit
755de59f16
133 files changed
+6136
-2023
No files matched your search
@@ -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
@@ -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
@@ -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**
|
||||
|
||||
@@ -397,7 +397,7 @@ Add the `serena` MCP server configuration
|
||||
"command": "serena",
|
||||
"args": [
|
||||
"start-mcp-server",
|
||||
"--context=claude-desktop"
|
||||
"--context=desktop-app"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 **.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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({})
|
||||
|
||||
@@ -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,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,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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",
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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
Reference in new issue
Block a user