mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 20:15:56 +00:00
Merge branch 'main' into features/move_and_safe_delete
# Conflicts: # src/serena/util/text_utils.py
This commit is contained in:
commit
94b118dcdc
25 files changed
+1616
-959
No files matched your search
+33
-17
@@ -8,10 +8,10 @@ project_name: "serena"
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# powershell python python_jedi r rego
|
||||
# ruby ruby_solargraph rust scala swift
|
||||
# terraform toml typescript typescript_vts vue
|
||||
# yaml zig
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# swift terraform toml typescript typescript_vts
|
||||
# vue yaml zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
@@ -20,8 +20,8 @@ project_name: "serena"
|
||||
# - For JavaScript, use typescript
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# - csharp: Requires the presence of a .sln file in the project folder.
|
||||
# - pascal: Requires Free Pascal Compiler (fpc) and optionally Lazarus.
|
||||
# Some languages require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
@@ -32,17 +32,19 @@ languages:
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
|
||||
# list of additional paths to ignore in all projects
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# list of tool names to exclude.
|
||||
# This extends the existing exclusions (e.g. from the global configuration)
|
||||
#
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
@@ -83,11 +85,10 @@ read_only: false
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||
# This extends the existing inclusions (e.g. from the global configuration).
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: |
|
||||
@@ -106,11 +107,11 @@ initial_prompt: |
|
||||
Comments:
|
||||
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
|
||||
clearly defines *what* it is. Any details then follow in subsequent sentences.
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: utf-8
|
||||
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
@@ -147,6 +148,21 @@ language_backend:
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
# line ending convention for file writes: unset (use global setting, "lf", "crlf", or "native" (platform default)
|
||||
# If not undefined, overrides the global setting in serena_config.yml
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||
line_ending:
|
||||
|
||||
# list of regex patterns for memories to completely ignore.
|
||||
# Matching memories will not appear in list_memories or activate_project output
|
||||
# and cannot be accessed via read_memory or write_memory.
|
||||
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
# Example: ["_archive/.*", "_episodes/.*"]
|
||||
ignored_memory_patterns: []
|
||||
|
||||
# advanced configuration option allowing to configure language server-specific options.
|
||||
# Maps the language key to the options.
|
||||
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
||||
# No documentation on options means no options are available.
|
||||
ls_specific_settings: {}
|
||||
@@ -147,6 +147,9 @@ you would run
|
||||
|
||||
and then configure your client to connect to `http://localhost:9121/mcp`.
|
||||
|
||||
By default, only connections from localhost are allowed; pass the `--host <listen_address>` option to configure
|
||||
the listen address and allow remote connections if needed (but be aware of the security implications of doing so).
|
||||
|
||||
**When to use.** Note that Serena is a stateful MCP server, and only one coding project can be active at a time.
|
||||
Therefore, starting a single Serena instance and connecting it to multiple clients is only
|
||||
appropriate if all clients will be working on the same project.
|
||||
|
||||
@@ -498,6 +498,10 @@ The following settings are supported for the Java language server:
|
||||
| `gradle_version` | `8.14.2` | Override the Gradle distribution version Serena downloads by default. |
|
||||
| `vscode_java_version` | `1.42.0-561` | Override the bundled `vscode-java` runtime bundle version Serena downloads by default. |
|
||||
| `intellicode_version` | `1.2.30` | Override the IntelliCode VSIX version Serena downloads by default. |
|
||||
| `jdtls_xmx` | `3G` | Maximum heap size for the JDTLS server JVM. |
|
||||
| `jdtls_xms` | `100m` | Initial heap size for the JDTLS server JVM. |
|
||||
| `intellicode_xmx` | `1G` | Maximum heap size for the IntelliCode embedded JVM. |
|
||||
| `intellicode_xms` | `100m` | Initial heap size for the IntelliCode embedded JVM. |
|
||||
|
||||
Note:
|
||||
- When overriding `vscode_java_version`, Serena still assumes that the downloaded runtime bundle keeps the same internal
|
||||
|
||||
@@ -1,29 +1,77 @@
|
||||
# Security Considerations
|
||||
|
||||
Security and protection against supply chain attacks are important to us, and we take this topic seriously.
|
||||
Security is important to us, and we take this topic seriously.
|
||||
|
||||
Serena comes in two main variants with different security characteristics:
|
||||
## Serena's Assumptions
|
||||
|
||||
- the **JetBrains-based variant**, which integrates with a running JetBrains IDE, and
|
||||
- the **language-server-based variant** (the free variant), which can automatically acquire language-server dependencies on demand.
|
||||
The current security model for Serena assumes:
|
||||
|
||||
## JetBrains Variant
|
||||
- the local machine is trusted,
|
||||
- the MCP client (i.e. the LLM) is trusted,
|
||||
- the code repository being worked on is trusted,
|
||||
- user configuration is trusted,
|
||||
- package manager configuration (e.g. npm) for downloading additional dependencies (i.e. language servers when using Serena with the LSP backend) is trusted.
|
||||
|
||||
The JetBrains variant of Serena is safe by default.
|
||||
Serena contains tools for executing shell commands and modifying files.
|
||||
As such tools are, however, an essential part of coding agent workflows, they typically need to be made available.
|
||||
Therefore, the only way to *fully* protect against unintended consequences is to use a [sandboxed environment](sandboxing) for running Serena.
|
||||
|
||||
At runtime, Serena does **not** download additional components and does **not** start extra helper processes beyond the Serena MCP server itself. It talks to the already running JetBrains IDE and relies on the IDE's own indexing and language intelligence.
|
||||
## General Recommendations for Risk Reduction
|
||||
|
||||
This means that, from a supply-chain perspective, the JetBrains variant has a much smaller runtime attack surface than the language-server-based variant.
|
||||
To reduce the risk of unintended consequences, we recommend that you:
|
||||
- back up your work regularly (keep the project being worked on under version control),
|
||||
- restrict the set of allowed tools via the [configuration](050_configuration),
|
||||
- do not expose [Serena's network services](network-security) to untrusted networks.
|
||||
|
||||
## Language-Server Variant
|
||||
If you do not fully the trust the client/the LLM, we additionally recommend to monitor tool executions carefully
|
||||
(provided that your MCP client supports this).
|
||||
|
||||
The language-server-based variant supports many languages, including languages whose language servers are not typically preinstalled on a machine. For convenience, Serena can therefore download or install certain language-server dependencies on demand.
|
||||
(sandboxing)=
|
||||
## Sandboxing
|
||||
|
||||
Sandboxing is the most effective way to mitigate risks when using coding agents.
|
||||
[Running Serena inside a docker container](docker) which only exposes the necessary files and tools to the agent is a good way to achieve this.
|
||||
|
||||
While setting up a sandboxed environment may require some initial effort, we highly recommend it for all security-conscious users.
|
||||
|
||||
(network-security)=
|
||||
## Network Security
|
||||
|
||||
Serena includes several network services:
|
||||
- the Serena MCP server itself (when run in [HTTP or SSE mode](streamable-http) instead of stdio mode)
|
||||
- the Serena Dashboard web server
|
||||
- the Serena JetBrains Plugin server, which runs within the JetBrains IDE (when using the JetBrains language backend)
|
||||
- the Serena Project Server (only started explicitly for [project querying](query-projects))
|
||||
|
||||
By default, these services accept connections from localhost only, which is a secure default for most users
|
||||
(given our assumption that the local machine is trusted; see above).
|
||||
|
||||
These services can be reconfigured to listen on other addresses, but doing so may have security implications.
|
||||
If you need to allow connections from other machines, we recommend that you set up a secure networking environment
|
||||
(e.g. using a VPN or SSH tunnels) and ensure that only trusted machines can connect to these services.
|
||||
|
||||
## Supply Chain Security
|
||||
|
||||
Serena has two language backends with different security characteristics:
|
||||
|
||||
- the JetBrains-based variant, which integrates with a running JetBrains IDE, and
|
||||
- the language-server-based variant (the free variant), which can automatically acquire language server dependencies on demand.
|
||||
|
||||
While we can assume that JetBrains IDEs installed by the user do not pose a security risk,
|
||||
language server dependencies (if not handled with care) could.
|
||||
For convenience, Serena downloads or installs certain language server dependencies on demand.
|
||||
We treat this path as security-sensitive and have hardened it accordingly.
|
||||
|
||||
### How Serena Secures Downloaded Language-Server Dependencies
|
||||
The most important supply chain protections are:
|
||||
|
||||
For language servers that download archives, binaries, VSIX packages, NuGet packages, or other release artifacts, Serena uses a hardened shared download path with the following protections:
|
||||
- exact version pinning,
|
||||
- hash verification,
|
||||
- host restriction,
|
||||
- and isolated Serena-managed installation directories.
|
||||
|
||||
### Auto-Downloaded Language Server Dependencies
|
||||
|
||||
For language servers that are auto-installed by downloading archives, binaries, VSIX packages, NuGet packages, or other release artifacts, Serena uses a hardened shared download path with the following protections:
|
||||
|
||||
- **Pinned versions by default**: default downloads use exact versions instead of floating `latest` or nightly channels.
|
||||
- **Integrity verification**: downloaded artifacts are checked against pinned SHA256 hashes stored in Serena's source code.
|
||||
@@ -63,35 +111,6 @@ One important detail is that `uvx` ignores the lockfile when installing directly
|
||||
|
||||
For the `ty` Python language server, Serena also uses an exact pinned version when invoking it through `uvx`.
|
||||
|
||||
### Our Assumptions
|
||||
|
||||
The current security model for Serena's language-server variant assumes:
|
||||
|
||||
- the local machine is trusted,
|
||||
- the checked-out repository is trusted,
|
||||
- user configuration is trusted,
|
||||
- package-manager configuration such as npm config is trusted unless explicitly overridden,
|
||||
- and the main risk to defend against is compromised or unexpectedly changing upstream artifacts.
|
||||
|
||||
Under these assumptions, the most important supply-chain protections are:
|
||||
|
||||
- exact version pinning,
|
||||
- hash verification,
|
||||
- host restriction,
|
||||
- and isolated Serena-managed installation directories.
|
||||
|
||||
## Operational Recommendations
|
||||
|
||||
As fundamental abilities for a coding agent, Serena contains tools for executing shell commands and modifying files. Therefore, if the respective tool calls are not monitored or restricted, and execution takes place in a sensitive environment, there is a risk of unintended consequences.
|
||||
|
||||
To reduce that risk, we recommend that you:
|
||||
|
||||
- back up your work regularly, for example by using Git,
|
||||
- monitor tool executions carefully, if your MCP client supports this,
|
||||
- consider enabling read-only mode for analysis-only sessions by setting `read_only: True` in `project.yml`,
|
||||
- restrict the set of allowed tools via the [configuration](050_configuration),
|
||||
- and use a sandboxed environment for running Serena, for example by [using Docker](docker).
|
||||
|
||||
```{dropdown} What Serena Downloads by Default for Language Servers
|
||||
:open:
|
||||
|
||||
|
||||
+3
-2
@@ -20,7 +20,7 @@ dependencies = [
|
||||
"overrides==7.7.0",
|
||||
"python-dotenv==1.2.1",
|
||||
"mcp==1.26.0",
|
||||
"flask==3.1.3", # bumped from 3.1.1 for CVE fix (also fixes werkzeug alert)
|
||||
"flask==3.1.3", # bumped from 3.1.1 for CVE fix (also fixes werkzeug alert)
|
||||
"sensai-utils==1.5.0",
|
||||
"pydantic==2.12.5",
|
||||
"types-pyyaml==6.0.12.20250516",
|
||||
@@ -43,7 +43,8 @@ dependencies = [
|
||||
"starlette==1.0.0",
|
||||
"python-multipart==0.0.22",
|
||||
"filelock==3.25.2",
|
||||
"cryptography==46.0.5",
|
||||
"cryptography==46.0.6",
|
||||
"regex==2026.2.28",
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
|
||||
+6
-2
@@ -5,6 +5,7 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Iterator, Sequence
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
@@ -187,7 +188,7 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
@click.option(
|
||||
"--host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
default="127.0.0.1",
|
||||
show_default=True,
|
||||
help="Listen address for the MCP server (when using corresponding transport).",
|
||||
)
|
||||
@@ -705,6 +706,7 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
collected_exceptions: list[Exception] = []
|
||||
files_failed = []
|
||||
language_file_counts: dict[Language, int] = collections.defaultdict(lambda: 0)
|
||||
last_save_time = time.monotonic()
|
||||
for i, f in enumerate(tqdm(files, desc="Indexing")):
|
||||
try:
|
||||
ls = ls_mgr.get_language_server(f)
|
||||
@@ -714,8 +716,10 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
log.error(f"Failed to index {f}, continuing.")
|
||||
collected_exceptions.append(e)
|
||||
files_failed.append(f)
|
||||
if (i + 1) % 10 == 0:
|
||||
now = time.monotonic()
|
||||
if now - last_save_time >= 30:
|
||||
ls_mgr.save_all_caches()
|
||||
last_save_time = now
|
||||
reported_language_file_counts = {k.value: v for k, v in language_file_counts.items()}
|
||||
click.echo(f"Indexed files per language: {dict_string(reported_language_file_counts, brackets=None)}")
|
||||
ls_mgr.save_all_caches()
|
||||
|
||||
@@ -565,7 +565,7 @@ class ProjectConfig(SharedConfig):
|
||||
|
||||
# transfer missing comments from the template file
|
||||
template_config, _ = self._load_yaml_dict(PROJECT_TEMPLATE_FILE, self.YAML_COMMENT_NORMALISATION)
|
||||
transfer_missing_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION)
|
||||
transfer_missing_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION, force_update_all=True)
|
||||
|
||||
# save project.yml
|
||||
save_yaml(config_path, config_with_comments)
|
||||
@@ -1047,7 +1047,7 @@ class SerenaConfig(SharedConfig):
|
||||
# For some keys, we force updates, because old comments are problematic/misleading.
|
||||
normalise_yaml_comments(commented_yaml, YamlCommentNormalisation.LEADING_WITH_CONVERSION_FROM_TRAILING)
|
||||
template_yaml = load_yaml(SERENA_CONFIG_TEMPLATE_FILE, comment_normalisation=YamlCommentNormalisation.LEADING)
|
||||
transfer_missing_yaml_comments(template_yaml, commented_yaml, YamlCommentNormalisation.LEADING, forced_update_keys=["projects"])
|
||||
transfer_missing_yaml_comments(template_yaml, commented_yaml, YamlCommentNormalisation.LEADING, force_update_all=True)
|
||||
|
||||
save_yaml(self.config_file_path, commented_yaml)
|
||||
|
||||
|
||||
@@ -368,6 +368,8 @@ class SerenaDashboardAPI:
|
||||
return post_installation_news_ids
|
||||
with open(news_snippet_id_file, encoding="utf-8") as f:
|
||||
last_read_news_id = int(f.read().strip())
|
||||
if last_read_news_id == 20262103:
|
||||
last_read_news_id = 20260321 # fix originally misnamed file
|
||||
return [news_id for news_id in post_installation_news_ids if news_id > last_read_news_id]
|
||||
|
||||
try:
|
||||
@@ -547,7 +549,7 @@ class SerenaDashboardAPI:
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
def run() -> ResponseAvailableLanguages:
|
||||
all_languages = [lang.value for lang in Language.iter_all(include_experimental=False)]
|
||||
all_languages = [lang.value for lang in Language.iter_all(include_experimental=True)]
|
||||
|
||||
# Filter out already added languages for the active project
|
||||
project = self._agent.get_active_project()
|
||||
|
||||
+1
-1
@@ -270,7 +270,7 @@ class SerenaMCPFactory:
|
||||
|
||||
def create_mcp_server(
|
||||
self,
|
||||
host: str = "0.0.0.0",
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 8000,
|
||||
modes: Sequence[str] = (),
|
||||
language_backend: LanguageBackend | None = None,
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,13 @@
|
||||
<div class="news-item">
|
||||
<h3>Querying of External Projects</h3>
|
||||
<p class="date">March 30, 2026</p>
|
||||
<p>
|
||||
We added a new tool for querying external projects that are
|
||||
known to Serena: From the current project, use Serena's tools on another project to retrieve information.
|
||||
This is useful in situations where the current project builds on or is otherwise related
|
||||
to other projects.
|
||||
</p>
|
||||
<p>
|
||||
Find out more about this feature in our <a href="https://oraios.github.io/serena/02-usage/040_workflow.html#reading-from-external-projects">documentation</a>.
|
||||
</p>
|
||||
</div>
|
||||
@@ -29,15 +29,13 @@ line_ending: native
|
||||
# we have to live with this limitation for now.
|
||||
gui_log_window: False
|
||||
|
||||
# whether to open the Serena web dashboard (which will be accessible through your web browser) that
|
||||
# provides access to Serena's configuration and state as well as the current session logs.
|
||||
# whether to start the Serena web dashboard, which provides access to Serena's configuration
|
||||
# and state as well as the current session logs.
|
||||
# The web dashboard is supported on all platforms.
|
||||
# We strongly recommend to always enable this option, since the dashboard provides important information
|
||||
# about the current state of Serena, including the configuration, the logs and tool usage statistics.
|
||||
# If you don't want the browser window to pop up automatically, set the `web_dashboard_open_on_launch` to False
|
||||
# (either here or through the corresponding flag in the `start-mcp-server` CLI command).
|
||||
# You can then open the dashboard by asking your agent to do so (e.g., by saying "open the dashboard"), Serena provides
|
||||
# a tool for this.
|
||||
# about the current state of Serena.
|
||||
# If you don't want the browser window to pop up automatically, set `web_dashboard_open_on_launch` to False
|
||||
# (see option below).
|
||||
# Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html
|
||||
web_dashboard: True
|
||||
|
||||
|
||||
+12
-4
@@ -205,13 +205,14 @@ def yaml_comment_entry_is_empty(comment_entry: Any) -> bool:
|
||||
|
||||
|
||||
def transfer_missing_yaml_comments_by_index(
|
||||
source: CommentedMap, target: CommentedMap, indices: list[int], forced_update_keys: Sequence[str] = ()
|
||||
source: CommentedMap, target: CommentedMap, indices: list[int], forced_update_keys: Sequence[str] = (), force_update_all: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
:param source: the source, from which to transfer missing comments
|
||||
:param target: the target map, whose comments will be updated
|
||||
:param indices: list of comment indices to transfer
|
||||
:param forced_update_keys: keys for which comments are always transferred, even if present in target
|
||||
:param force_update_all: if True, comments are transferred for all keys, even if present in target
|
||||
"""
|
||||
for key in target.keys():
|
||||
if key in source:
|
||||
@@ -224,14 +225,18 @@ def transfer_missing_yaml_comments_by_index(
|
||||
target_comment = [None] * 4
|
||||
target.ca.items[key] = target_comment
|
||||
# transfer comments at specified indices
|
||||
is_forced_update = force_update_all or key in forced_update_keys
|
||||
for index in indices:
|
||||
is_forced_update = key in forced_update_keys
|
||||
if is_forced_update or yaml_comment_entry_is_empty(target_comment[index]):
|
||||
target_comment[index] = source_comment[index]
|
||||
|
||||
|
||||
def transfer_missing_yaml_comments(
|
||||
source: CommentedMap, target: CommentedMap, comment_normalisation: YamlCommentNormalisation, forced_update_keys: Sequence[str] = ()
|
||||
source: CommentedMap,
|
||||
target: CommentedMap,
|
||||
comment_normalisation: YamlCommentNormalisation,
|
||||
forced_update_keys: Sequence[str] = (),
|
||||
force_update_all: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Transfers missing comments from source to target YAML.
|
||||
@@ -240,11 +245,14 @@ def transfer_missing_yaml_comments(
|
||||
:param target: the target map, whose comments will be updated.
|
||||
:param comment_normalisation: the comment normalisation to assume; if NONE, no comments are transferred
|
||||
:param forced_update_keys: keys for which comments are always transferred, even if present in target
|
||||
:param force_update_all: if True, comments are transferred for all keys, even if present in target
|
||||
"""
|
||||
match comment_normalisation:
|
||||
case YamlCommentNormalisation.NONE:
|
||||
pass
|
||||
case YamlCommentNormalisation.LEADING | YamlCommentNormalisation.LEADING_WITH_CONVERSION_FROM_TRAILING:
|
||||
transfer_missing_yaml_comments_by_index(source, target, [ITEM_COMMENT_INDEX_BEFORE], forced_update_keys=forced_update_keys)
|
||||
transfer_missing_yaml_comments_by_index(
|
||||
source, target, [ITEM_COMMENT_INDEX_BEFORE], forced_update_keys=forced_update_keys, force_update_all=force_update_all
|
||||
)
|
||||
case _:
|
||||
raise ValueError(f"Unhandled comment normalisation: {comment_normalisation}")
|
||||
@@ -18,7 +18,7 @@ from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.language_servers.common import quote_windows_path
|
||||
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, RawDocumentSymbol, SolidLanguageServer
|
||||
from solidlsp.ls_config import Language, LanguageServerConfig
|
||||
from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation
|
||||
from solidlsp.ls_utils import FileUtils
|
||||
@@ -1007,35 +1007,25 @@ class ALLanguageServer(SolidLanguageServer):
|
||||
# Get symbols from parent implementation
|
||||
document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
|
||||
|
||||
# Normalize names by stripping AL object metadata, storing originals for hover
|
||||
def normalize_name(symbol: UnifiedSymbolInformation) -> None:
|
||||
original_name = symbol["name"]
|
||||
normalized_name = self._extract_al_display_name(original_name)
|
||||
|
||||
# Store original name if it was normalized (for hover injection)
|
||||
# Only store if we have valid position data to avoid false matches at (0, 0)
|
||||
if original_name != normalized_name:
|
||||
sel_range = symbol.get("selectionRange")
|
||||
if sel_range:
|
||||
start = sel_range.get("start")
|
||||
if start and "line" in start and "character" in start:
|
||||
line = start["line"]
|
||||
char = start["character"]
|
||||
self._al_original_names[(relative_file_path, line, char)] = original_name
|
||||
|
||||
symbol["name"] = normalized_name
|
||||
|
||||
# Process children recursively
|
||||
if symbol.get("children"):
|
||||
for child in symbol["children"]:
|
||||
normalize_name(child)
|
||||
|
||||
# Apply to all root symbols
|
||||
for sym in document_symbols.root_symbols:
|
||||
normalize_name(sym)
|
||||
|
||||
return document_symbols
|
||||
|
||||
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
|
||||
original_name = symbol["name"]
|
||||
normalized_name = self._extract_al_display_name(original_name)
|
||||
|
||||
# Store original name if it was normalized (for hover injection)
|
||||
# Only store if we have valid position data to avoid false matches at (0, 0)
|
||||
if original_name != normalized_name:
|
||||
sel_range = symbol.get("selectionRange")
|
||||
if sel_range:
|
||||
start = sel_range.get("start") # type: ignore
|
||||
if start and "line" in start and "character" in start:
|
||||
line = start["line"]
|
||||
char = start["character"]
|
||||
self._al_original_names[(relative_file_path, line, char)] = original_name
|
||||
|
||||
return normalized_name
|
||||
|
||||
@override
|
||||
def request_hover(
|
||||
self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None
|
||||
|
||||
@@ -117,7 +117,7 @@ class RuntimeDependencyCollection:
|
||||
completed_process = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=True,
|
||||
check=False,
|
||||
cwd=cwd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
@@ -126,10 +126,8 @@ class RuntimeDependencyCollection:
|
||||
if completed_process.returncode != 0:
|
||||
log.warning("Command '%s' failed with return code %d", command, completed_process.returncode)
|
||||
log.warning("Command output:\n%s", completed_process.stdout)
|
||||
else:
|
||||
log.info(
|
||||
"Command completed successfully",
|
||||
)
|
||||
raise subprocess.CalledProcessError(completed_process.returncode, command, completed_process.stdout)
|
||||
log.info("Command completed successfully")
|
||||
|
||||
@staticmethod
|
||||
def _install_from_url(dep: RuntimeDependency, target_dir: str) -> None:
|
||||
|
||||
@@ -7,7 +7,7 @@ import os
|
||||
import platform
|
||||
import shutil
|
||||
import threading
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Hashable, Iterable
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
@@ -15,10 +15,15 @@ from typing import Any, cast
|
||||
from overrides import override
|
||||
|
||||
from serena.util.dotnet import DotNETUtil
|
||||
from solidlsp.ls import DocumentSymbols, LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls import (
|
||||
LanguageServerDependencyProvider,
|
||||
LSPFileBuffer,
|
||||
RawDocumentSymbol,
|
||||
SolidLanguageServer,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.ls_types import Hover, UnifiedSymbolInformation
|
||||
from solidlsp.ls_types import Hover
|
||||
from solidlsp.ls_utils import FileUtils, PathUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
@@ -218,28 +223,6 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]
|
||||
|
||||
@override
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: Any = None) -> DocumentSymbols:
|
||||
"""
|
||||
Override to normalize Roslyn symbol names and cache originals.
|
||||
|
||||
Roslyn 5.5.0+ returns symbol names with type annotations:
|
||||
- Properties: "Name : string"
|
||||
- Methods: "Add(int, int) : int"
|
||||
|
||||
This method:
|
||||
1. Normalizes names to base form ("Name", "Add")
|
||||
2. Caches original names for rich information display
|
||||
3. Populates LSP spec's 'detail' field with type/signature info
|
||||
"""
|
||||
symbols = super().request_document_symbols(relative_file_path, file_buffer)
|
||||
|
||||
# Normalize all symbols recursively
|
||||
for symbol in symbols.iter_symbols():
|
||||
self._normalize_symbol_name(symbol, relative_file_path)
|
||||
|
||||
return symbols
|
||||
|
||||
@override
|
||||
def request_hover(self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None) -> Hover | None:
|
||||
"""
|
||||
@@ -265,12 +248,20 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
|
||||
return hover
|
||||
|
||||
def _normalize_symbol_name(self, symbol: UnifiedSymbolInformation, relative_file_path: str) -> None:
|
||||
"""
|
||||
Normalize a single symbol's name and cache the original.
|
||||
Processes children recursively.
|
||||
"""
|
||||
original_name = symbol.get("name", "")
|
||||
def _document_symbols_cache_fingerprint(self) -> Hashable | None:
|
||||
normalize_symbol_name_version = 1
|
||||
return normalize_symbol_name_version
|
||||
|
||||
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
|
||||
# Roslyn 5.5.0+ returns symbol names with type annotations:
|
||||
# - Properties: "Name : string"
|
||||
# - Methods: "Add(int, int) : int"
|
||||
#
|
||||
# This method:
|
||||
# 1. Normalizes names to base form ("Name", "Add")
|
||||
# 2. Caches original names for rich information display
|
||||
# 3. Populates LSP spec's 'detail' field with type/signature info
|
||||
original_name = symbol["name"]
|
||||
|
||||
# Extract base name and type/signature info
|
||||
normalized_name, type_info = self._extract_base_name_and_type(original_name)
|
||||
@@ -279,24 +270,18 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
if original_name != normalized_name:
|
||||
sel_range = symbol.get("selectionRange")
|
||||
if sel_range:
|
||||
start = sel_range.get("start")
|
||||
start = sel_range.get("start") # type: ignore
|
||||
if start and "line" in start and "character" in start:
|
||||
line = start["line"]
|
||||
char = start["character"]
|
||||
cache_key = (relative_file_path, line, char)
|
||||
self._original_symbol_names[cache_key] = original_name
|
||||
|
||||
# Populate LSP spec's 'detail' field with type/signature information
|
||||
# Populate 'detail' field with type/signature information (for UnifiedSymbolInformation)
|
||||
if type_info and "detail" not in symbol:
|
||||
symbol["detail"] = type_info
|
||||
symbol["detail"] = type_info # type: ignore
|
||||
|
||||
# Update the symbol name
|
||||
symbol["name"] = normalized_name
|
||||
|
||||
# Process children recursively
|
||||
children = symbol.get("children", [])
|
||||
for child in children:
|
||||
self._normalize_symbol_name(child, relative_file_path)
|
||||
return normalized_name
|
||||
|
||||
@staticmethod
|
||||
def _extract_base_name_and_type(roslyn_name: str) -> tuple[str, str]:
|
||||
|
||||
@@ -69,6 +69,10 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
- gradle_wrapper_enabled: Whether to use the project's Gradle wrapper (default: false)
|
||||
- gradle_java_home: Path to JDK for Gradle (default: null, uses bundled JRE)
|
||||
- use_system_java_home: Whether to use the system's JAVA_HOME for JDTLS itself (default: false)
|
||||
- jdtls_xmx: Maximum heap size for the JDTLS server JVM (default: "3G")
|
||||
- jdtls_xms: Initial heap size for the JDTLS server JVM (default: "100m")
|
||||
- intellicode_xmx: Maximum heap size for the IntelliCode embedded JVM (default: "1G")
|
||||
- intellicode_xms: Initial heap size for the IntelliCode embedded JVM (default: "100m")
|
||||
- gradle_version: Override the pinned Gradle distribution version downloaded by Serena
|
||||
- vscode_java_version: Override the pinned vscode-java runtime bundle version downloaded by Serena
|
||||
- intellicode_version: Override the pinned IntelliCode VSIX version downloaded by Serena
|
||||
@@ -84,6 +88,10 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
gradle_wrapper_enabled: true # set to true for projects with custom plugins/repositories
|
||||
gradle_java_home: "/path/to/jdk" # set to override Gradle's JDK
|
||||
use_system_java_home: true # set to true to use system JAVA_HOME for JDTLS
|
||||
jdtls_xmx: "3G" # maximum heap size for the JDTLS server JVM
|
||||
jdtls_xms: "100m" # initial heap size for the JDTLS server JVM
|
||||
intellicode_xmx: "1G" # maximum heap size for the IntelliCode embedded JVM
|
||||
intellicode_xms: "100m" # initial heap size for the IntelliCode embedded JVM
|
||||
gradle_version: "8.14.2"
|
||||
vscode_java_version: "1.42.0-561"
|
||||
intellicode_version: "1.2.30"
|
||||
@@ -361,6 +369,8 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path
|
||||
|
||||
jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path
|
||||
jdtls_xmx = self._custom_settings.get("jdtls_xmx", "3G")
|
||||
jdtls_xms = self._custom_settings.get("jdtls_xms", "100m")
|
||||
|
||||
data_dir = str(PurePath(ws_dir, "data_dir"))
|
||||
jdtls_config_path = str(PurePath(ws_dir, "config_path"))
|
||||
@@ -399,8 +409,8 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"-XX:AdaptiveSizePolicyWeight=90",
|
||||
"-Dsun.zip.disableMemoryMapping=true",
|
||||
"-Djava.lsp.joinOnCompletion=true",
|
||||
"-Xmx3G",
|
||||
"-Xms100m",
|
||||
f"-Xmx{jdtls_xmx}",
|
||||
f"-Xms{jdtls_xms}",
|
||||
"-Xlog:disable",
|
||||
"-Dlog.level=ALL",
|
||||
f"-javaagent:{lombok_jar_path}",
|
||||
@@ -484,6 +494,10 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
gradle_user_home = None
|
||||
log.info(f"Gradle user home not found at default location ({default_gradle_home}), will use JDTLS defaults")
|
||||
|
||||
# IntelliCode JVM settings (used in vmargs for the embedded JVM)
|
||||
intellicode_xmx = self._custom_settings.get("intellicode_xmx", "1G")
|
||||
intellicode_xms = self._custom_settings.get("intellicode_xms", "100m")
|
||||
|
||||
# Gradle wrapper: default to False to preserve existing behaviour
|
||||
gradle_wrapper_enabled = self._custom_settings.get("gradle_wrapper_enabled", False)
|
||||
log.info(
|
||||
@@ -680,7 +694,7 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"jdt": {
|
||||
"ls": {
|
||||
"java": {"home": None},
|
||||
"vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -Xlog:disable",
|
||||
"vmargs": f"-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx{intellicode_xmx} -Xms{intellicode_xms} -Xlog:disable",
|
||||
"lombokSupport": {"enabled": True},
|
||||
"protobufSupport": {"enabled": True},
|
||||
"androidSupport": {"enabled": True},
|
||||
|
||||
@@ -21,7 +21,7 @@ from solidlsp.language_servers.typescript_language_server import (
|
||||
prefer_non_node_modules_definition,
|
||||
)
|
||||
from solidlsp.ls import LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import Language, LanguageServerConfig
|
||||
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.ls_types import Location
|
||||
from solidlsp.ls_utils import PathUtils
|
||||
@@ -47,6 +47,11 @@ class VueTypeScriptServer(TypeScriptLanguageServer):
|
||||
"""
|
||||
return Language.TYPESCRIPT
|
||||
|
||||
def get_source_fn_matcher(self) -> FilenameMatcher:
|
||||
# must override with Vue-specific matcher to ensure .vue files are included (as they can be discovered via references,
|
||||
# for instance; otherwise, we may find references in .vue files but then filter the results out, because .vue files are ignored.)
|
||||
return Language.VUE.get_source_fn_matcher()
|
||||
|
||||
class DependencyProvider(TypeScriptLanguageServer.DependencyProvider):
|
||||
override_ts_ls_executable: str | None = None
|
||||
|
||||
|
||||
+516
-174
@@ -23,7 +23,7 @@ from sensai.util.string import ToStringMixin
|
||||
from serena.util.file_system import match_path
|
||||
from serena.util.text_utils import MatchedConsecutiveLines
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls_config import Language, LanguageServerConfig
|
||||
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.ls_process import LanguageServerProcess
|
||||
from solidlsp.ls_types import UnifiedSymbolInformation
|
||||
@@ -35,6 +35,7 @@ from solidlsp.lsp_protocol_handler.lsp_types import (
|
||||
Definition,
|
||||
DefinitionParams,
|
||||
DocumentSymbol,
|
||||
ImplementationParams,
|
||||
LocationLink,
|
||||
RenameParams,
|
||||
SymbolInformation,
|
||||
@@ -47,7 +48,13 @@ from solidlsp.lsp_protocol_handler.server import (
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
from solidlsp.util.cache import load_cache, save_cache
|
||||
|
||||
GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, ls_types.UnifiedSymbolInformation]
|
||||
RawDocumentSymbol = Union[DocumentSymbol, SymbolInformation]
|
||||
"""
|
||||
Type alias for the raw symbol information returned by a language server in response to a
|
||||
`textDocument/documentSymbol` request.
|
||||
The `DocumentSymbol` is the preferred type, but the legacy type `SymbolInformation` is also still used.
|
||||
"""
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_debug_enabled = log.isEnabledFor(logging.DEBUG)
|
||||
@@ -210,7 +217,7 @@ class SymbolBodyFactory:
|
||||
def __init__(self, file_buffer: LSPFileBuffer):
|
||||
self._lines = file_buffer.split_lines()
|
||||
|
||||
def create_symbol_body(self, symbol: GenericDocumentSymbol) -> SymbolBody:
|
||||
def create_symbol_body(self, symbol: UnifiedSymbolInformation) -> SymbolBody:
|
||||
existing_body = symbol.get("body", None)
|
||||
if existing_body and isinstance(existing_body, SymbolBody):
|
||||
return existing_body
|
||||
@@ -378,6 +385,13 @@ class SolidLanguageServer(ABC):
|
||||
def get_language_enum_instance(cls) -> Language:
|
||||
return Language.from_ls_class(cls)
|
||||
|
||||
@classmethod
|
||||
def supports_implementation_request(cls) -> bool:
|
||||
"""
|
||||
Return whether this language server supports ``textDocument/implementation``.
|
||||
"""
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def ls_resources_dir(cls, solidlsp_settings: SolidLSPSettings, mkdir: bool = True) -> str:
|
||||
"""
|
||||
@@ -575,6 +589,14 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
return self._ignore_spec
|
||||
|
||||
def get_source_fn_matcher(self) -> FilenameMatcher:
|
||||
"""
|
||||
:return: the source filename matcher for this language server, which must positively match all files that
|
||||
are understood by this language server or are discovered as containing sources indirectly, e.g. via references
|
||||
"""
|
||||
# By default, use the matcher of the language
|
||||
return self.language.get_source_fn_matcher()
|
||||
|
||||
def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool:
|
||||
"""
|
||||
Determine if a path should be ignored based on file type
|
||||
@@ -592,7 +614,7 @@ class SolidLanguageServer(ABC):
|
||||
# Check file extension if it's a file
|
||||
is_file = os.path.isfile(abs_path)
|
||||
if is_file and ignore_unsupported_files:
|
||||
fn_matcher = self.language.get_source_fn_matcher()
|
||||
fn_matcher = self.get_source_fn_matcher()
|
||||
if not fn_matcher.is_relevant_filename(abs_path):
|
||||
return True
|
||||
|
||||
@@ -846,6 +868,149 @@ class SolidLanguageServer(ABC):
|
||||
def _send_definition_request(self, definition_params: DefinitionParams) -> Definition | list[LocationLink] | None:
|
||||
return self.server.send.definition(definition_params)
|
||||
|
||||
class SymbolLocationRequest(ABC):
|
||||
def __init__(
|
||||
self,
|
||||
language_server: "SolidLanguageServer",
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
column: int,
|
||||
*,
|
||||
request_name: str,
|
||||
) -> None:
|
||||
self.language_server = language_server
|
||||
self.relative_file_path = relative_file_path
|
||||
self.line = line
|
||||
self.column = column
|
||||
self.request_name = request_name
|
||||
self.skip_ignored_paths = True
|
||||
|
||||
def execute(self) -> list[ls_types.Location]:
|
||||
self._ensure_server_started()
|
||||
|
||||
t0 = perf_counter() if _debug_enabled else None
|
||||
with self.language_server.open_file(self.relative_file_path):
|
||||
self.language_server._wait_for_cross_file_references_if_needed()
|
||||
try:
|
||||
response = self.send_request()
|
||||
except Exception as e:
|
||||
mapped_exception = self.map_exception(e)
|
||||
if mapped_exception is not None:
|
||||
raise mapped_exception from e
|
||||
raise
|
||||
|
||||
result = self.normalize_response(response)
|
||||
if t0 is not None:
|
||||
self.log_perf_result(t0, result)
|
||||
return result
|
||||
|
||||
def _ensure_server_started(self) -> None:
|
||||
if not self.language_server.server_started:
|
||||
log.error("%s called before language server started", self.request_name)
|
||||
raise SolidLSPException("Language Server not started")
|
||||
|
||||
@abstractmethod
|
||||
def send_request(self) -> object | None:
|
||||
pass
|
||||
|
||||
def map_exception(self, error: Exception) -> Exception | None:
|
||||
if isinstance(error, LSPError) and getattr(error, "code", None) == -32603:
|
||||
return RuntimeError(
|
||||
f"LSP internal error (-32603) when requesting {self.request_name} for "
|
||||
f"{self.relative_file_path}:{self.line}:{self.column}. "
|
||||
"This often occurs when requesting a symbol in a way the language server cannot resolve."
|
||||
)
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def normalize_response(self, response: object | None) -> list[ls_types.Location]:
|
||||
pass
|
||||
|
||||
def convert_location_item(self, item: dict[str, object], *, allow_location_links: bool = False) -> ls_types.Location | None:
|
||||
if LSPConstants.URI in item and LSPConstants.RANGE in item:
|
||||
uri = cast(str, item[LSPConstants.URI])
|
||||
range_d = cast(ls_types.Range, item[LSPConstants.RANGE])
|
||||
elif (
|
||||
allow_location_links
|
||||
and LSPConstants.TARGET_URI in item
|
||||
and LSPConstants.TARGET_RANGE in item
|
||||
and LSPConstants.TARGET_SELECTION_RANGE in item
|
||||
):
|
||||
uri = cast(str, item[LSPConstants.TARGET_URI])
|
||||
range_d = cast(ls_types.Range, item[LSPConstants.TARGET_SELECTION_RANGE])
|
||||
else:
|
||||
raise AssertionError(f"Unexpected response from Language Server: {item}")
|
||||
|
||||
abs_path = PathUtils.uri_to_path(uri)
|
||||
rel_path_str = PathUtils.get_relative_path(abs_path, self.language_server.repository_root_path)
|
||||
|
||||
if rel_path_str is None:
|
||||
log.warning(
|
||||
"Found a %s in a path outside the repository, probably the LS is parsing things in installed packages or in the standardlib! "
|
||||
"Path: %s. This is a bug but we currently simply skip these locations.",
|
||||
self.request_name,
|
||||
abs_path,
|
||||
)
|
||||
return None
|
||||
|
||||
if self.skip_ignored_paths and self.language_server.is_ignored_path(rel_path_str):
|
||||
log.info("%s found symbol in ignored path: %s", self.request_name, rel_path_str)
|
||||
return None
|
||||
|
||||
return ls_types.Location(uri=uri, range=range_d, absolutePath=str(abs_path), relativePath=rel_path_str)
|
||||
|
||||
def log_perf_result(self, t0: float, result: list[ls_types.Location]) -> None:
|
||||
return
|
||||
|
||||
class DefinitionLocationRequest(SymbolLocationRequest):
|
||||
def __init__(
|
||||
self,
|
||||
language_server: "SolidLanguageServer",
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
column: int,
|
||||
*,
|
||||
request_name: str = "request_definition",
|
||||
) -> None:
|
||||
super().__init__(
|
||||
language_server,
|
||||
relative_file_path,
|
||||
line,
|
||||
column,
|
||||
request_name=request_name,
|
||||
)
|
||||
|
||||
def send_request(self) -> object | None:
|
||||
return self.language_server._send_definition_request(
|
||||
self.language_server._create_text_document_position_params(self.relative_file_path, self.line, self.column)
|
||||
)
|
||||
|
||||
def normalize_response(self, response: object | None) -> list[ls_types.Location]:
|
||||
if response is None:
|
||||
log.warning(
|
||||
"Language server returned None for %s request at %s:%s:%s",
|
||||
self.request_name,
|
||||
self.relative_file_path,
|
||||
self.line,
|
||||
self.column,
|
||||
)
|
||||
return []
|
||||
|
||||
ret: list[ls_types.Location] = []
|
||||
if isinstance(response, list):
|
||||
for item in response:
|
||||
assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}"
|
||||
if location := self.convert_location_item(cast(dict[str, object], item), allow_location_links=True):
|
||||
ret.append(location)
|
||||
return ret
|
||||
|
||||
if isinstance(response, dict):
|
||||
if location := self.convert_location_item(cast(dict[str, object], response), allow_location_links=True):
|
||||
ret.append(location)
|
||||
return ret
|
||||
|
||||
assert False, f"Unexpected response from Language Server: {response}"
|
||||
|
||||
def request_definition(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
|
||||
"""
|
||||
Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server
|
||||
@@ -857,70 +1022,60 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
:return: the list of locations where the symbol is defined
|
||||
"""
|
||||
if not self.server_started:
|
||||
log.error("request_definition called before language server started")
|
||||
raise SolidLSPException("Language Server not started")
|
||||
request = self.DefinitionLocationRequest(self, relative_file_path, line, column)
|
||||
return request.execute()
|
||||
|
||||
def _send_implementation_request(self, implementation_params: ImplementationParams) -> Definition | list[LocationLink] | None:
|
||||
return self.server.send.implementation(implementation_params)
|
||||
|
||||
def _create_text_document_position_params(self, relative_file_path: str, line: int, column: int) -> DefinitionParams:
|
||||
return cast(
|
||||
DefinitionParams,
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: {
|
||||
LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
|
||||
},
|
||||
LSPConstants.POSITION: {
|
||||
LSPConstants.LINE: line,
|
||||
LSPConstants.CHARACTER: column,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def _wait_for_cross_file_references_if_needed(self) -> None:
|
||||
if not self._has_waited_for_cross_file_references:
|
||||
# Some LS require waiting for a while before they can return cross-file definitions.
|
||||
# This is a workaround for such LS that don't have a reliable "finished initializing" signal.
|
||||
# Some LS require waiting for a while before they can return accurate cross-file results.
|
||||
# The waiting has to happen after at least one file was opened in the LS.
|
||||
sleep(self._get_wait_time_for_cross_file_referencing())
|
||||
self._has_waited_for_cross_file_references = True
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
# sending request to the language server and waiting for response
|
||||
definition_params = cast(
|
||||
DefinitionParams,
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: {
|
||||
LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
|
||||
},
|
||||
LSPConstants.POSITION: {
|
||||
LSPConstants.LINE: line,
|
||||
LSPConstants.CHARACTER: column,
|
||||
},
|
||||
},
|
||||
class ImplementationLocationRequest(DefinitionLocationRequest):
|
||||
def __init__(self, language_server: "SolidLanguageServer", relative_file_path: str, line: int, column: int) -> None:
|
||||
super().__init__(
|
||||
language_server,
|
||||
relative_file_path,
|
||||
line,
|
||||
column,
|
||||
request_name="request_implementation",
|
||||
)
|
||||
response = self._send_definition_request(definition_params)
|
||||
|
||||
ret: list[ls_types.Location] = []
|
||||
if isinstance(response, list):
|
||||
# response is either of type Location[] or LocationLink[]
|
||||
for item in response:
|
||||
assert isinstance(item, dict)
|
||||
if LSPConstants.URI in item and LSPConstants.RANGE in item:
|
||||
new_item: dict = {}
|
||||
new_item.update(item)
|
||||
new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
|
||||
new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path)
|
||||
ret.append(ls_types.Location(**new_item)) # type: ignore
|
||||
elif LSPConstants.TARGET_URI in item and LSPConstants.TARGET_RANGE in item and LSPConstants.TARGET_SELECTION_RANGE in item:
|
||||
new_item: dict = {} # type: ignore
|
||||
new_item["uri"] = item[LSPConstants.TARGET_URI] # type: ignore
|
||||
new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
|
||||
new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path)
|
||||
new_item["range"] = item[LSPConstants.TARGET_SELECTION_RANGE] # type: ignore
|
||||
ret.append(ls_types.Location(**new_item)) # type: ignore
|
||||
else:
|
||||
assert False, f"Unexpected response from Language Server: {item}"
|
||||
elif isinstance(response, dict):
|
||||
# response is of type Location
|
||||
assert LSPConstants.URI in response
|
||||
assert LSPConstants.RANGE in response
|
||||
def send_request(self) -> object | None:
|
||||
return self.language_server._send_implementation_request(
|
||||
self.language_server._create_text_document_position_params(self.relative_file_path, self.line, self.column),
|
||||
)
|
||||
|
||||
new_item: dict = {} # type: ignore
|
||||
new_item.update(response)
|
||||
new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
|
||||
new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path)
|
||||
ret.append(ls_types.Location(**new_item)) # type: ignore
|
||||
elif response is None:
|
||||
# Some language servers return None when they cannot find a definition
|
||||
# This is expected for certain symbol types like generics or types with incomplete information
|
||||
log.warning(f"Language server returned None for definition request at {relative_file_path}:{line}:{column}")
|
||||
else:
|
||||
assert False, f"Unexpected response from Language Server: {response}"
|
||||
def request_implementation(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
|
||||
"""
|
||||
Raise a [textDocument/implementation](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_implementation)
|
||||
request to the Language Server for the symbol at the given line and column in the given file.
|
||||
|
||||
return ret
|
||||
:param relative_file_path: The relative path of the file that has the symbol for which implementations should be looked up
|
||||
:param line: The line number of the symbol
|
||||
:param column: The column number of the symbol
|
||||
:return: the list of locations where the symbol is implemented
|
||||
"""
|
||||
request = self.ImplementationLocationRequest(self, relative_file_path, line, column)
|
||||
return request.execute()
|
||||
|
||||
# Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses
|
||||
def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
|
||||
@@ -932,6 +1087,46 @@ class SolidLanguageServer(ABC):
|
||||
}
|
||||
)
|
||||
|
||||
class ReferencesLocationRequest(SymbolLocationRequest):
|
||||
def __init__(self, language_server: "SolidLanguageServer", relative_file_path: str, line: int, column: int) -> None:
|
||||
super().__init__(
|
||||
language_server,
|
||||
relative_file_path,
|
||||
line,
|
||||
column,
|
||||
request_name="request_references",
|
||||
)
|
||||
|
||||
def send_request(self) -> object | None:
|
||||
return self.language_server._send_references_request(self.relative_file_path, line=self.line, column=self.column)
|
||||
|
||||
def normalize_response(self, response: object | None) -> list[ls_types.Location]:
|
||||
if response is None:
|
||||
return []
|
||||
|
||||
assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
|
||||
ret: list[ls_types.Location] = []
|
||||
for item in response:
|
||||
assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}"
|
||||
if location := self.convert_location_item(cast(dict[str, object], item)):
|
||||
ret.append(location)
|
||||
return ret
|
||||
|
||||
def log_perf_result(self, t0: float, result: list[ls_types.Location]) -> None:
|
||||
elapsed_ms = (perf_counter() - t0) * 1000
|
||||
if not result:
|
||||
log.debug("perf: request_references path=%s elapsed_ms=%.2f count=0", self.relative_file_path, elapsed_ms)
|
||||
return
|
||||
|
||||
unique_files = len({r["relativePath"] for r in result})
|
||||
log.debug(
|
||||
"perf: request_references path=%s elapsed_ms=%.2f count=%d unique_files=%d",
|
||||
self.relative_file_path,
|
||||
elapsed_ms,
|
||||
len(result),
|
||||
unique_files,
|
||||
)
|
||||
|
||||
def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
|
||||
"""
|
||||
Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server
|
||||
@@ -944,111 +1139,8 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
:return: A list of locations where the symbol is referenced (excluding ignored directories)
|
||||
"""
|
||||
if not self.server_started:
|
||||
log.error("request_references called before Language Server started")
|
||||
raise SolidLSPException("Language Server not started")
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
if not self._has_waited_for_cross_file_references:
|
||||
# Some LS require waiting for a while before they can return cross-file references.
|
||||
# This is a workaround for such LS that don't have a reliable "finished initializing" signal.
|
||||
# The waiting has to happen after at least one file was opened in the ls
|
||||
sleep(self._get_wait_time_for_cross_file_referencing())
|
||||
self._has_waited_for_cross_file_references = True
|
||||
t0 = perf_counter() if _debug_enabled else 0.0
|
||||
try:
|
||||
response = self._send_references_request(relative_file_path, line=line, column=column)
|
||||
except Exception as e:
|
||||
# Catch LSP internal error (-32603) and raise a more informative exception
|
||||
if isinstance(e, LSPError) and getattr(e, "code", None) == -32603:
|
||||
raise RuntimeError(
|
||||
f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. "
|
||||
"This often occurs when requesting references for a symbol not referenced in the expected way. "
|
||||
) from e
|
||||
raise
|
||||
if response is None:
|
||||
if _debug_enabled:
|
||||
elapsed_ms = (perf_counter() - t0) * 1000
|
||||
log.debug("perf: request_references path=%s elapsed_ms=%.2f count=0", relative_file_path, elapsed_ms)
|
||||
return []
|
||||
|
||||
ret: list[ls_types.Location] = []
|
||||
assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
|
||||
for item in response:
|
||||
assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}"
|
||||
assert LSPConstants.URI in item
|
||||
assert LSPConstants.RANGE in item
|
||||
|
||||
abs_path = PathUtils.uri_to_path(item[LSPConstants.URI]) # type: ignore
|
||||
if not Path(abs_path).is_relative_to(self.repository_root_path):
|
||||
log.warning(
|
||||
"Found a reference in a path outside the repository, probably the LS is parsing things in installed packages or in the standardlib! "
|
||||
f"Path: {abs_path}. This is a bug but we currently simply skip these references."
|
||||
)
|
||||
continue
|
||||
|
||||
rel_path = Path(abs_path).relative_to(self.repository_root_path)
|
||||
if self.is_ignored_path(str(rel_path)):
|
||||
log.debug("Ignoring reference in %s since it should be ignored", rel_path)
|
||||
continue
|
||||
|
||||
new_item: dict = {}
|
||||
new_item.update(item)
|
||||
new_item["absolutePath"] = str(abs_path)
|
||||
new_item["relativePath"] = str(rel_path)
|
||||
ret.append(ls_types.Location(**new_item)) # type: ignore
|
||||
|
||||
if _debug_enabled:
|
||||
elapsed_ms = (perf_counter() - t0) * 1000
|
||||
unique_files = len({r["relativePath"] for r in ret})
|
||||
log.debug(
|
||||
"perf: request_references path=%s elapsed_ms=%.2f count=%d unique_files=%d",
|
||||
relative_file_path,
|
||||
elapsed_ms,
|
||||
len(ret),
|
||||
unique_files,
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
def request_text_document_diagnostics(self, relative_file_path: str) -> list[ls_types.Diagnostic]:
|
||||
"""
|
||||
Raise a [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_diagnostic) request to the Language Server
|
||||
to find diagnostics for the given file. Wait for the response and return the result.
|
||||
|
||||
:param relative_file_path: The relative path of the file to retrieve diagnostics for
|
||||
|
||||
:return: A list of diagnostics for the file
|
||||
"""
|
||||
if not self.server_started:
|
||||
log.error("request_text_document_diagnostics called before Language Server started")
|
||||
raise SolidLSPException("Language Server not started")
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
response = self.server.send.text_document_diagnostic(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if response is None:
|
||||
return [] # type: ignore
|
||||
|
||||
assert isinstance(response, dict), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
|
||||
ret: list[ls_types.Diagnostic] = []
|
||||
for item in response["items"]: # type: ignore
|
||||
new_item: ls_types.Diagnostic = {
|
||||
"uri": pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri(),
|
||||
"severity": item["severity"],
|
||||
"message": item["message"],
|
||||
"range": item["range"],
|
||||
"code": item["code"], # type: ignore
|
||||
}
|
||||
ret.append(ls_types.Diagnostic(**new_item))
|
||||
|
||||
return ret
|
||||
request = self.ReferencesLocationRequest(self, relative_file_path, line, column)
|
||||
return request.execute()
|
||||
|
||||
def retrieve_full_file_content(self, file_path: str) -> str:
|
||||
"""
|
||||
@@ -1173,6 +1265,14 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
Sends a [documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol)
|
||||
request to the language server to find symbols in the given file - or returns a cached result if available.
|
||||
The returned symbols are considered "raw document symbols" (in contrast to processed symbols returned by `request_document_symbols`).
|
||||
|
||||
NOTE: This method can be overridden in subclasses to post-process the raw results.
|
||||
When doing so after the initial implementation, be sure to update the init parameter `cache_version_raw_document_symbols`
|
||||
to a different version (add 1) to ensure that all caches are invalidated appropriately.
|
||||
IMPORTANT: Since rebuilding the raw document symbol cache from the language server results
|
||||
is potentially expensive, prefer overriding the `request_document_symbols` method
|
||||
if the post-processing can also be done on the processed/high-level symbols.
|
||||
|
||||
:param relative_file_path: the relative path of the file that has the symbols.
|
||||
:param file_data: the file data buffer, if already opened. If None, the file will be opened in this method.
|
||||
@@ -1218,9 +1318,30 @@ class SolidLanguageServer(ABC):
|
||||
with self._open_file_context(relative_file_path, file_buffer=file_data) as fd:
|
||||
return get_raw_document_symbols(fd)
|
||||
|
||||
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
|
||||
"""
|
||||
Normalizes the name of the given symbol, e.g. by removing parameter lists from method symbols.
|
||||
|
||||
Override this method in subclasses to implement language-specific normalization logic.
|
||||
NOTE: When changing the override of this method after the initial LS implementation,
|
||||
be sure to also override `_document_symbols_cache_fingerprint` in order to ensure that
|
||||
the caches are invalidated appropriately.
|
||||
|
||||
:param symbol: the symbol
|
||||
:param relative_file_path: the relative path of the file the symbol is located in
|
||||
:return: the normalized name of the symbol
|
||||
"""
|
||||
# the default implementation does not change the name
|
||||
return symbol["name"]
|
||||
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
|
||||
"""
|
||||
Retrieves the collection of symbols in the given file
|
||||
Retrieves the collection of symbols in the given file.
|
||||
|
||||
NOTE: This method can be overridden in subclasses to post-process the results.
|
||||
When doing so after the initial LS implementation, be sure to also override `_document_symbols_cache_fingerprint`
|
||||
to ensure that the caches are invalidated appropriately.
|
||||
DO NOT override this method to modify symbol names; override `_normalize_symbol_name` instead.
|
||||
|
||||
:param relative_file_path: The relative path of the file that has the symbols
|
||||
:param file_buffer: an optional file buffer if the file is already opened.
|
||||
@@ -1264,7 +1385,7 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
body_factory = SymbolBodyFactory(file_data)
|
||||
|
||||
def convert_to_unified_symbol(original_symbol_dict: GenericDocumentSymbol) -> ls_types.UnifiedSymbolInformation:
|
||||
def convert_to_unified_symbol(original_symbol_dict: RawDocumentSymbol) -> ls_types.UnifiedSymbolInformation:
|
||||
"""
|
||||
Converts the given symbol dictionary to the unified representation, ensuring
|
||||
that all required fields are present (except 'children' which is handled separately).
|
||||
@@ -1305,16 +1426,23 @@ class SolidLanguageServer(ABC):
|
||||
return item
|
||||
|
||||
def convert_symbols_with_common_parent(
|
||||
symbols: list[DocumentSymbol] | list[SymbolInformation] | list[UnifiedSymbolInformation],
|
||||
symbols: list[DocumentSymbol] | list[SymbolInformation],
|
||||
parent: ls_types.UnifiedSymbolInformation | None,
|
||||
) -> list[ls_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Converts the given symbols into UnifiedSymbolInformation with proper parent-child relationships,
|
||||
adding overload indices for symbols with the same name under the same parent.
|
||||
"""
|
||||
# apply name normalization and count occurrences of each symbol name
|
||||
total_name_counts: dict[str, int] = defaultdict(lambda: 0)
|
||||
for symbol in symbols:
|
||||
total_name_counts[symbol["name"]] += 1
|
||||
name = self._normalize_symbol_name(symbol, relative_file_path=relative_file_path)
|
||||
symbol["name"] = name
|
||||
total_name_counts[name] += 1
|
||||
|
||||
# convert symbols to the unified representation and
|
||||
# * add overload indices where necessary
|
||||
# * ensure that the "parent" field is set correctly
|
||||
name_counts: dict[str, int] = defaultdict(lambda: 0)
|
||||
unified_symbols = []
|
||||
for symbol in symbols:
|
||||
@@ -1629,7 +1757,7 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
def create_symbol_body(
|
||||
self,
|
||||
symbol: ls_types.UnifiedSymbolInformation | LSPTypes.SymbolInformation,
|
||||
symbol: ls_types.UnifiedSymbolInformation,
|
||||
factory: SymbolBodyFactory | None = None,
|
||||
) -> SymbolBody:
|
||||
if factory is None:
|
||||
@@ -1945,6 +2073,153 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
return definitions[0]
|
||||
|
||||
def _get_document_symbols_with_locations(self, relative_file_path: str) -> list[ls_types.UnifiedSymbolInformation]:
|
||||
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
|
||||
document_symbols = self.request_document_symbols(relative_file_path)
|
||||
symbols = list(document_symbols.iter_symbols())
|
||||
|
||||
# Make SymbolInformation and DocumentSymbol shapes consistent by ensuring every
|
||||
# symbol exposes a normalized location/range in the current workspace.
|
||||
for symbol in symbols:
|
||||
location = symbol["location"]
|
||||
location["absolutePath"] = absolute_file_path
|
||||
location["relativePath"] = relative_file_path
|
||||
location["uri"] = Path(absolute_file_path).as_uri()
|
||||
return symbols
|
||||
|
||||
@staticmethod
|
||||
def _position_matches_range(range_d: ls_types.Range, line: int, column: int | None = None) -> bool:
|
||||
start = range_d["start"]
|
||||
end = range_d["end"]
|
||||
if not (start["line"] <= line <= end["line"]):
|
||||
return False
|
||||
if column is None:
|
||||
return True
|
||||
if line == start["line"] and column < start["character"]:
|
||||
return False
|
||||
if line == end["line"] and column > end["character"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _symbol_match_sort_key(symbol: ls_types.UnifiedSymbolInformation, match_priority: int) -> tuple[int, int, int, int, int]:
|
||||
location = symbol["location"]
|
||||
symbol_range = location["range"]
|
||||
start = symbol_range["start"]
|
||||
end = symbol_range["end"]
|
||||
line_span = end["line"] - start["line"]
|
||||
character_span = end["character"] - start["character"] if line_span == 0 else end["character"]
|
||||
return match_priority, line_span, character_span, start["line"], start["character"]
|
||||
|
||||
def _request_symbol_at_location(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
column: int,
|
||||
include_body: bool = False,
|
||||
body_factory: SymbolBodyFactory | None = None,
|
||||
) -> ls_types.UnifiedSymbolInformation | None:
|
||||
candidates: list[tuple[tuple[int, int, int, int, int], ls_types.UnifiedSymbolInformation]] = []
|
||||
for symbol in self._get_document_symbols_with_locations(relative_file_path):
|
||||
location = symbol["location"]
|
||||
symbol_range = location["range"]
|
||||
selection_range = symbol.get("selectionRange") or symbol_range
|
||||
|
||||
match_priority: int | None = None
|
||||
if self._position_matches_range(selection_range, line, column):
|
||||
match_priority = 0
|
||||
elif self._position_matches_range(symbol_range, line, column):
|
||||
match_priority = 1
|
||||
else:
|
||||
selection_start = selection_range["start"]
|
||||
symbol_start = symbol_range["start"]
|
||||
if (selection_start["line"], selection_start["character"]) == (line, column):
|
||||
match_priority = 2
|
||||
elif (symbol_start["line"], symbol_start["character"]) == (line, column):
|
||||
match_priority = 3
|
||||
elif selection_start["line"] == line and column <= selection_start["character"]:
|
||||
match_priority = 4
|
||||
elif symbol_start["line"] == line and column <= symbol_start["character"]:
|
||||
match_priority = 5
|
||||
|
||||
if match_priority is None:
|
||||
continue
|
||||
candidates.append((self._symbol_match_sort_key(symbol, match_priority), symbol))
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
candidates.sort(key=lambda item: item[0])
|
||||
best_symbol = candidates[0][1]
|
||||
if include_body:
|
||||
best_symbol["body"] = self.create_symbol_body(best_symbol, factory=body_factory)
|
||||
return best_symbol
|
||||
|
||||
@staticmethod
|
||||
def _iter_symbol_descendants(symbol: ls_types.UnifiedSymbolInformation) -> Iterator[ls_types.UnifiedSymbolInformation]:
|
||||
"""Yield descendant symbols in depth-first order."""
|
||||
for child in symbol.get("children", []):
|
||||
yield child
|
||||
yield from SolidLanguageServer._iter_symbol_descendants(child)
|
||||
|
||||
def _refine_implementing_symbol(
|
||||
self,
|
||||
target_symbol: ls_types.UnifiedSymbolInformation | None,
|
||||
implementing_symbol: ls_types.UnifiedSymbolInformation,
|
||||
include_body: bool = False,
|
||||
) -> ls_types.UnifiedSymbolInformation:
|
||||
"""Resolve member-level implementation symbols when the LS returns a containing type."""
|
||||
if target_symbol is None:
|
||||
return implementing_symbol
|
||||
|
||||
target_kind = target_symbol["kind"]
|
||||
if target_kind not in (ls_types.SymbolKind.Method, ls_types.SymbolKind.Function):
|
||||
return implementing_symbol
|
||||
|
||||
if implementing_symbol["kind"] == target_kind and implementing_symbol.get("name") == target_symbol.get("name"):
|
||||
return implementing_symbol
|
||||
|
||||
candidate_descendants: list[ls_types.UnifiedSymbolInformation] = []
|
||||
for descendant in self._iter_symbol_descendants(implementing_symbol):
|
||||
if descendant.get("name") != target_symbol.get("name"):
|
||||
continue
|
||||
if descendant["kind"] != target_kind:
|
||||
continue
|
||||
candidate_descendants.append(descendant)
|
||||
|
||||
if not candidate_descendants:
|
||||
return implementing_symbol
|
||||
|
||||
refined_symbol = min(
|
||||
candidate_descendants,
|
||||
key=lambda symbol: self._symbol_match_sort_key(symbol, match_priority=0),
|
||||
)
|
||||
if include_body:
|
||||
refined_symbol["body"] = self.create_symbol_body(refined_symbol)
|
||||
return refined_symbol
|
||||
|
||||
def request_symbol_at_location(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
column: int,
|
||||
include_body: bool = False,
|
||||
) -> ls_types.UnifiedSymbolInformation | None:
|
||||
"""
|
||||
Finds the symbol at the given position, preferring exact identifier matches and otherwise
|
||||
falling back to the innermost symbol whose body contains the position.
|
||||
|
||||
:param relative_file_path: The relative path to the file.
|
||||
:param line: The 0-indexed line number.
|
||||
:param column: The 0-indexed column number.
|
||||
:param include_body: whether to include the body of the symbol in the result.
|
||||
:return: The symbol at the given location, or None if no symbol could be resolved.
|
||||
"""
|
||||
if not self.server_started:
|
||||
log.error("request_symbol_at_location called before language server started")
|
||||
raise SolidLSPException("Language Server not started")
|
||||
return self._request_symbol_at_location(relative_file_path, line, column, include_body=include_body)
|
||||
|
||||
def request_defining_symbol(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
@@ -1976,14 +2251,79 @@ class SolidLanguageServer(ABC):
|
||||
# Select the preferred definition (subclasses can override _get_preferred_definition)
|
||||
definition = self._get_preferred_definition(definitions)
|
||||
def_path = definition["relativePath"]
|
||||
assert def_path is not None
|
||||
if def_path is None:
|
||||
return None
|
||||
def_line = definition["range"]["start"]["line"]
|
||||
def_col = definition["range"]["start"]["character"]
|
||||
|
||||
# Find the symbol at or containing this location
|
||||
defining_symbol = self.request_containing_symbol(def_path, def_line, def_col, strict=False, include_body=include_body)
|
||||
return self._request_symbol_at_location(
|
||||
def_path,
|
||||
def_line,
|
||||
def_col,
|
||||
include_body=include_body,
|
||||
)
|
||||
|
||||
return defining_symbol
|
||||
def request_implementing_symbols(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
column: int,
|
||||
include_body: bool = False,
|
||||
) -> list[ls_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Finds the symbols that implement the symbol at the given location.
|
||||
|
||||
This method first finds implementation locations for the symbol at the given position,
|
||||
then retrieves the full symbol information for each implementation and de-duplicates
|
||||
results that map to the same containing symbol.
|
||||
|
||||
:param relative_file_path: The relative path to the file.
|
||||
:param line: The 0-indexed line number.
|
||||
:param column: The 0-indexed column number.
|
||||
:param include_body: whether to include the body of the symbols in the result.
|
||||
:return: The symbol information for each implementation.
|
||||
"""
|
||||
if not self.server_started:
|
||||
log.error("request_implementing_symbols called before language server started")
|
||||
raise SolidLSPException("Language Server not started")
|
||||
|
||||
target_symbol = self._request_symbol_at_location(relative_file_path, line, column, include_body=False)
|
||||
implementation_locations = self.request_implementation(relative_file_path, line, column)
|
||||
if not implementation_locations:
|
||||
return []
|
||||
|
||||
result: list[ls_types.UnifiedSymbolInformation] = []
|
||||
seen_keys: set[tuple[str, int, int, int]] = set()
|
||||
for implementation in implementation_locations:
|
||||
implementation_path = implementation["relativePath"]
|
||||
assert implementation_path is not None
|
||||
implementation_line = implementation["range"]["start"]["line"]
|
||||
implementation_col = implementation["range"]["start"]["character"]
|
||||
implementing_symbol = self._request_symbol_at_location(
|
||||
implementation_path,
|
||||
implementation_line,
|
||||
implementation_col,
|
||||
include_body=include_body,
|
||||
body_factory=None,
|
||||
)
|
||||
if implementing_symbol is None:
|
||||
continue
|
||||
implementing_symbol = self._refine_implementing_symbol(target_symbol, implementing_symbol, include_body=include_body)
|
||||
if "location" not in implementing_symbol:
|
||||
continue
|
||||
symbol_location = implementing_symbol["location"]
|
||||
symbol_key = (
|
||||
cast(str, symbol_location["relativePath"]),
|
||||
symbol_location["range"]["start"]["line"],
|
||||
symbol_location["range"]["start"]["character"],
|
||||
implementing_symbol["kind"],
|
||||
)
|
||||
if symbol_key in seen_keys:
|
||||
continue
|
||||
seen_keys.add(symbol_key)
|
||||
result.append(implementing_symbol)
|
||||
|
||||
return result
|
||||
|
||||
def _document_symbols_cache_fingerprint(self) -> Hashable | None:
|
||||
"""
|
||||
@@ -2001,7 +2341,9 @@ class SolidLanguageServer(ABC):
|
||||
The value must be hashable and safe for inclusion in cache version tuples.
|
||||
E.g. use an integer, a string or a tuple of integers/strings.
|
||||
|
||||
Returns None if no context-specific fingerprint is needed.
|
||||
For example, if there is a single aspect being considered, use an integer to reflect the version
|
||||
of this aspect (incrementing it whenever the implementation changes).
|
||||
If multiple versioned aspects exist, use a tuple of versions, etc.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ class LanguageServerProcess:
|
||||
if not chunk:
|
||||
if process.poll() is not None:
|
||||
raise LanguageServerTerminatedException(
|
||||
f"Process terminated while trying to read response (read {num_bytes} of {len(data)} bytes before termination)",
|
||||
f"Process terminated while trying to read response (read {len(data)} of {num_bytes} bytes before termination)",
|
||||
language=self.language,
|
||||
)
|
||||
# Process still running but no data available yet, retry after a short delay
|
||||
|
||||
@@ -549,7 +549,7 @@ class PlatformUtils:
|
||||
|
||||
# If no supported version found, raise exception with all available versions
|
||||
raise SolidLSPException(
|
||||
f"No supported dotnet version found. Available versions: {', '.join(available_version_cmd_output)}. Supported versions: 4, 6, 7, 8"
|
||||
f"No supported dotnet version found. Available versions: {', '.join(available_version_cmd_output)}. Supported versions: 4, 6, 7, 8, 9"
|
||||
)
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from serena.dashboard import SerenaDashboardAPI
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
|
||||
class _DummyMemoryLogHandler:
|
||||
def get_log_messages(self, from_idx: int = 0): # pragma: no cover - simple stub
|
||||
return SimpleNamespace(messages=[], max_idx=-1)
|
||||
|
||||
def clear_log_messages(self) -> None: # pragma: no cover - simple stub
|
||||
pass
|
||||
|
||||
|
||||
class _DummyAgent:
|
||||
def __init__(self, project: SimpleNamespace | None) -> None:
|
||||
self._project = project
|
||||
|
||||
def execute_task(self, func, *, logged: bool | None = None, name: str | None = None):
|
||||
del logged, name
|
||||
return func()
|
||||
|
||||
def get_active_project(self):
|
||||
return self._project
|
||||
|
||||
|
||||
def _make_dashboard(project_languages: list[Language] | None) -> SerenaDashboardAPI:
|
||||
project = None
|
||||
if project_languages is not None:
|
||||
project = SimpleNamespace(project_config=SimpleNamespace(languages=project_languages))
|
||||
agent = _DummyAgent(project)
|
||||
return SerenaDashboardAPI(
|
||||
memory_log_handler=_DummyMemoryLogHandler(),
|
||||
tool_names=[],
|
||||
agent=agent,
|
||||
shutdown_callback=None,
|
||||
tool_usage_stats=None,
|
||||
)
|
||||
|
||||
|
||||
def test_available_languages_include_experimental_when_no_active_project():
|
||||
dashboard = _make_dashboard(project_languages=None)
|
||||
response = dashboard._get_available_languages()
|
||||
expected = sorted(lang.value for lang in Language.iter_all(include_experimental=True))
|
||||
assert response.languages == expected
|
||||
|
||||
|
||||
def test_available_languages_exclude_project_languages():
|
||||
dashboard = _make_dashboard(project_languages=[Language.PYTHON, Language.MARKDOWN])
|
||||
response = dashboard._get_available_languages()
|
||||
available = set(response.languages)
|
||||
assert Language.PYTHON.value not in available
|
||||
assert Language.MARKDOWN.value not in available
|
||||
# ensure experimental languages remain available for selection
|
||||
assert Language.ANSIBLE.value in available
|
||||
@@ -3,6 +3,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from serena.util.text_utils import find_text_coordinates
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
@@ -243,8 +244,12 @@ class TestDartLanguageServer:
|
||||
def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_defining_symbol for a variable usage."""
|
||||
file_path = os.path.join("lib", "main.dart")
|
||||
# Line 14 contains 'final result = a + b;' - test position on 'result'
|
||||
defining_symbol = language_server.request_defining_symbol(file_path, 13, 10)
|
||||
|
||||
# Find coordinates of 'final result = a + b;' - test position on 'result'
|
||||
with language_server.open_file(file_path, open_in_ls=False) as f:
|
||||
pos = find_text_coordinates(f.contents, r"final (result) = a \+ b;")
|
||||
|
||||
defining_symbol = language_server.request_defining_symbol(file_path, pos.line, pos.col)
|
||||
|
||||
# The defining symbol might be the variable itself or the containing method
|
||||
# This is acceptable behavior - different language servers handle this differently
|
||||
|
||||
@@ -12,6 +12,7 @@ import os
|
||||
|
||||
import pytest
|
||||
|
||||
from serena.util.text_utils import find_text_coordinates
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
@@ -583,15 +584,17 @@ class TestRubyLanguageServerSymbols:
|
||||
"""Test request_defining_symbol for a method call."""
|
||||
# Test finding the definition of a method being called
|
||||
file_path = os.path.join("examples", "user_management.rb")
|
||||
# Position at a method call like create_user
|
||||
defining_symbol = language_server.request_defining_symbol(file_path, 30, 15)
|
||||
|
||||
# Verify that we can find method definitions
|
||||
if defining_symbol is not None:
|
||||
assert "name" in defining_symbol
|
||||
assert "kind" in defining_symbol
|
||||
# Should be a method or constructor
|
||||
assert defining_symbol.get("kind") in [SymbolKind.Method.value, SymbolKind.Constructor.value, SymbolKind.Function.value]
|
||||
# Position at a method call like create_user
|
||||
with language_server.open_file(file_path, open_in_ls=False) as fb:
|
||||
pos = find_text_coordinates(fb.contents, r"user = @service\.(create_user)")
|
||||
|
||||
# Verify that we can find the method definition
|
||||
defining_symbol = language_server.request_defining_symbol(file_path, pos.line, pos.col)
|
||||
assert "name" in defining_symbol
|
||||
assert "kind" in defining_symbol
|
||||
assert defining_symbol.get("name") == "create_user"
|
||||
assert defining_symbol.get("kind") == SymbolKind.Method.value
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
|
||||
def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None:
|
||||
|
||||
Reference in new issue
Block a user