mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 12:15:10 +00:00
SolidLSP: Allow workspace folders to be flexibly configured across all language servers
that use the newly introduced InitializeParamsBuilder - 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 that use an InitializeParamsBuilder (previously limited strictly to the TypeScript server). Moved the option from SolidLSPSettings to LanguageServerConfig, where it belongs. Apply InitializeParamsBuilder in TypeScriptLanguageServer and its subclasses.
This commit is contained in:
1 parent
6f1f05407d
commit
c904efed5e
17 files changed
+313
-193
No files matched your search
+42
-28
@@ -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: "serena"
|
||||
|
||||
|
||||
# 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
|
||||
@@ -148,17 +150,6 @@ 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).
|
||||
# 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.
|
||||
# Example:
|
||||
# additional_workspace_folders:
|
||||
# - ../sibling-package
|
||||
# - ../shared-lib
|
||||
additional_workspace_folders: []
|
||||
|
||||
# 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).
|
||||
@@ -171,3 +162,26 @@ 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, 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"
|
||||
ls_additional_workspace_folders: []
|
||||
@@ -63,6 +63,11 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Add **LaTeX** support (experimental) via [texlab](https://github.com/latex-lsp/texlab).
|
||||
- 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).
|
||||
|
||||
* JetBrains:
|
||||
- Add configuration option `jetbrains_launch_command`, allowing Serena to spawn IDE instances automatically
|
||||
|
||||
@@ -51,7 +51,7 @@ 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
|
||||
@@ -70,33 +70,6 @@ You can specify local overrides for the settings in a `project.local.yml` file i
|
||||
(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
|
||||
|
||||
|
||||
@@ -1001,9 +1001,6 @@ Supported settings:
|
||||
| `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.
|
||||
|
||||
#### Svelte
|
||||
|
||||
Serena uses `svelte-language-server` for the `svelte` language key. Use `svelte` for Svelte projects instead of also listing `typescript`, unless you intentionally want multiple language servers active for the same files.
|
||||
|
||||
@@ -303,7 +303,8 @@ 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 = ""
|
||||
@@ -318,6 +319,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
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.
|
||||
@@ -443,9 +445,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
|
||||
@@ -458,13 +473,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.
|
||||
@@ -480,6 +488,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"]:
|
||||
@@ -526,7 +535,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.")
|
||||
@@ -535,7 +544,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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -466,12 +466,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
|
||||
@@ -71,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 **.
|
||||
|
||||
@@ -50,9 +50,20 @@ class DefaultInitializeParamsBuilder(InitializeParamsBuilder):
|
||||
|
||||
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:
|
||||
self._set("workspaceFolders", [self._create_workspace_folder_entry(root_abs_path)])
|
||||
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)
|
||||
@@ -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"] = {
|
||||
|
||||
@@ -120,8 +120,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": [
|
||||
{
|
||||
|
||||
@@ -4,10 +4,9 @@ Provides TypeScript specific instantiation of the LanguageServer class. Contains
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import threading
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from overrides import override
|
||||
from sensai.util.logging import LogTime
|
||||
@@ -16,7 +15,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
|
||||
@@ -224,11 +222,7 @@ 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",
|
||||
"capabilities": {
|
||||
@@ -258,12 +252,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 +356,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",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
+81
-72
@@ -11,6 +11,7 @@ from collections import defaultdict
|
||||
from collections.abc import Callable, Hashable, Iterator, Sequence
|
||||
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
|
||||
@@ -653,7 +654,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.
|
||||
@@ -663,12 +664,13 @@ 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)
|
||||
@@ -746,28 +748,13 @@ 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
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
"""
|
||||
@@ -806,7 +793,7 @@ class SolidLanguageServer(ABC):
|
||||
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 ---
|
||||
@@ -1208,6 +1195,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.
|
||||
|
||||
@@ -1217,36 +1246,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.
|
||||
@@ -1260,7 +1262,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)
|
||||
@@ -2179,24 +2181,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 []
|
||||
|
||||
@@ -2243,7 +2238,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
|
||||
@@ -2293,9 +2288,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:
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
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 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:
|
||||
@@ -826,13 +831,25 @@ class Language(str, Enum):
|
||||
raise ValueError(f"Unhandled language server class: {ls_class}")
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class LanguageServerConfig:
|
||||
"""
|
||||
Configuration parameters
|
||||
"""
|
||||
|
||||
code_language: Language
|
||||
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 +862,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)
|
||||
+12
-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():
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import os
|
||||
from collections.abc import Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from test.conftest import PYTHON_LANGUAGE_BACKENDS
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
from test.conftest import PYTHON_LANGUAGE_BACKENDS, start_ls_context
|
||||
|
||||
|
||||
class TestLanguageServerCommonFunctionality:
|
||||
@@ -41,3 +44,35 @@ class TestLanguageServerCommonFunctionality:
|
||||
|
||||
finally:
|
||||
os.remove(file_path)
|
||||
|
||||
def test_workspace_folders_affect_full_symbol_tree(self):
|
||||
"""
|
||||
Tests that workspace folder configurations are respected when requesting the full symbol tree.
|
||||
"""
|
||||
|
||||
def check(langsrv: SolidLanguageServer, present: Sequence[str] = (), absent: Sequence[str] = ()) -> None:
|
||||
symbols = langsrv.request_full_symbol_tree()
|
||||
for name in present:
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, name)
|
||||
for name in absent:
|
||||
assert not SymbolUtils.symbol_tree_contains_name(symbols, name)
|
||||
|
||||
symbols_in_subfolder_test_repo = ["OuterClass", "UserService"]
|
||||
symbols_in_subfolder_scripts = ["parse_args", "create_sample_users"]
|
||||
all_symbols = symbols_in_subfolder_test_repo + symbols_in_subfolder_scripts
|
||||
|
||||
with start_ls_context(language=Language.PYTHON, workspace_folders=["."]) as ls:
|
||||
ls.request_full_symbol_tree()
|
||||
check(ls, present=all_symbols)
|
||||
|
||||
with start_ls_context(language=Language.PYTHON, workspace_folders=["./test_repo"]) as ls:
|
||||
ls.request_full_symbol_tree()
|
||||
check(ls, present=symbols_in_subfolder_test_repo, absent=symbols_in_subfolder_scripts)
|
||||
|
||||
with start_ls_context(language=Language.PYTHON, workspace_folders=["./scripts"]) as ls:
|
||||
ls.request_full_symbol_tree()
|
||||
check(ls, present=symbols_in_subfolder_scripts, absent=symbols_in_subfolder_test_repo)
|
||||
|
||||
# test that explicit requests for symbols in other workspace folders are rejected
|
||||
with pytest.raises(ValueError, match="outside"):
|
||||
ls.request_full_symbol_tree(within_relative_path="./test_repo")
|
||||
Reference in new issue
Block a user