#
tokens: 47801/50000 6/294 files (page 10/14)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 10 of 14. Use http://codebase.md/oraios/serena?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .devcontainer
│   └── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── config.yml
│   │   ├── feature_request.md
│   │   └── issue--bug--performance-problem--question-.md
│   └── workflows
│       ├── codespell.yml
│       ├── docker.yml
│       ├── junie.yml
│       ├── lint_and_docs.yaml
│       ├── publish.yml
│       └── pytest.yml
├── .gitignore
├── .serena
│   ├── memories
│   │   ├── adding_new_language_support_guide.md
│   │   ├── serena_core_concepts_and_architecture.md
│   │   ├── serena_repository_structure.md
│   │   └── suggested_commands.md
│   └── project.yml
├── .vscode
│   └── settings.json
├── CHANGELOG.md
├── CLAUDE.md
├── compose.yaml
├── CONTRIBUTING.md
├── docker_build_and_run.sh
├── DOCKER.md
├── Dockerfile
├── docs
│   ├── custom_agent.md
│   └── serena_on_chatgpt.md
├── flake.lock
├── flake.nix
├── lessons_learned.md
├── LICENSE
├── llms-install.md
├── public
│   └── .gitignore
├── pyproject.toml
├── README.md
├── resources
│   ├── serena-icons.cdr
│   ├── serena-logo-dark-mode.svg
│   ├── serena-logo.cdr
│   ├── serena-logo.svg
│   └── vscode_sponsor_logo.png
├── roadmap.md
├── scripts
│   ├── agno_agent.py
│   ├── demo_run_tools.py
│   ├── gen_prompt_factory.py
│   ├── mcp_server.py
│   ├── print_mode_context_options.py
│   └── print_tool_overview.py
├── src
│   ├── interprompt
│   │   ├── __init__.py
│   │   ├── .syncCommitId.remote
│   │   ├── .syncCommitId.this
│   │   ├── jinja_template.py
│   │   ├── multilang_prompt.py
│   │   ├── prompt_factory.py
│   │   └── util
│   │       ├── __init__.py
│   │       └── class_decorators.py
│   ├── README.md
│   ├── serena
│   │   ├── __init__.py
│   │   ├── agent.py
│   │   ├── agno.py
│   │   ├── analytics.py
│   │   ├── cli.py
│   │   ├── code_editor.py
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   ├── context_mode.py
│   │   │   └── serena_config.py
│   │   ├── constants.py
│   │   ├── dashboard.py
│   │   ├── generated
│   │   │   └── generated_prompt_factory.py
│   │   ├── gui_log_viewer.py
│   │   ├── mcp.py
│   │   ├── project.py
│   │   ├── prompt_factory.py
│   │   ├── resources
│   │   │   ├── config
│   │   │   │   ├── contexts
│   │   │   │   │   ├── agent.yml
│   │   │   │   │   ├── chatgpt.yml
│   │   │   │   │   ├── codex.yml
│   │   │   │   │   ├── context.template.yml
│   │   │   │   │   ├── desktop-app.yml
│   │   │   │   │   ├── ide-assistant.yml
│   │   │   │   │   └── oaicompat-agent.yml
│   │   │   │   ├── internal_modes
│   │   │   │   │   └── jetbrains.yml
│   │   │   │   ├── modes
│   │   │   │   │   ├── editing.yml
│   │   │   │   │   ├── interactive.yml
│   │   │   │   │   ├── mode.template.yml
│   │   │   │   │   ├── no-onboarding.yml
│   │   │   │   │   ├── onboarding.yml
│   │   │   │   │   ├── one-shot.yml
│   │   │   │   │   └── planning.yml
│   │   │   │   └── prompt_templates
│   │   │   │       ├── simple_tool_outputs.yml
│   │   │   │       └── system_prompt.yml
│   │   │   ├── dashboard
│   │   │   │   ├── dashboard.js
│   │   │   │   ├── index.html
│   │   │   │   ├── jquery.min.js
│   │   │   │   ├── serena-icon-16.png
│   │   │   │   ├── serena-icon-32.png
│   │   │   │   ├── serena-icon-48.png
│   │   │   │   ├── serena-logs-dark-mode.png
│   │   │   │   └── serena-logs.png
│   │   │   ├── project.template.yml
│   │   │   └── serena_config.template.yml
│   │   ├── symbol.py
│   │   ├── text_utils.py
│   │   ├── tools
│   │   │   ├── __init__.py
│   │   │   ├── cmd_tools.py
│   │   │   ├── config_tools.py
│   │   │   ├── file_tools.py
│   │   │   ├── jetbrains_plugin_client.py
│   │   │   ├── jetbrains_tools.py
│   │   │   ├── memory_tools.py
│   │   │   ├── symbol_tools.py
│   │   │   ├── tools_base.py
│   │   │   └── workflow_tools.py
│   │   └── util
│   │       ├── class_decorators.py
│   │       ├── exception.py
│   │       ├── file_system.py
│   │       ├── general.py
│   │       ├── git.py
│   │       ├── inspection.py
│   │       ├── logging.py
│   │       ├── shell.py
│   │       └── thread.py
│   └── solidlsp
│       ├── __init__.py
│       ├── .gitignore
│       ├── language_servers
│       │   ├── al_language_server.py
│       │   ├── bash_language_server.py
│       │   ├── clangd_language_server.py
│       │   ├── clojure_lsp.py
│       │   ├── common.py
│       │   ├── csharp_language_server.py
│       │   ├── dart_language_server.py
│       │   ├── eclipse_jdtls.py
│       │   ├── elixir_tools
│       │   │   ├── __init__.py
│       │   │   ├── elixir_tools.py
│       │   │   └── README.md
│       │   ├── elm_language_server.py
│       │   ├── erlang_language_server.py
│       │   ├── gopls.py
│       │   ├── intelephense.py
│       │   ├── jedi_server.py
│       │   ├── kotlin_language_server.py
│       │   ├── lua_ls.py
│       │   ├── marksman.py
│       │   ├── nixd_ls.py
│       │   ├── omnisharp
│       │   │   ├── initialize_params.json
│       │   │   ├── runtime_dependencies.json
│       │   │   └── workspace_did_change_configuration.json
│       │   ├── omnisharp.py
│       │   ├── perl_language_server.py
│       │   ├── pyright_server.py
│       │   ├── r_language_server.py
│       │   ├── ruby_lsp.py
│       │   ├── rust_analyzer.py
│       │   ├── solargraph.py
│       │   ├── sourcekit_lsp.py
│       │   ├── terraform_ls.py
│       │   ├── typescript_language_server.py
│       │   ├── vts_language_server.py
│       │   └── zls.py
│       ├── ls_config.py
│       ├── ls_exceptions.py
│       ├── ls_handler.py
│       ├── ls_logger.py
│       ├── ls_request.py
│       ├── ls_types.py
│       ├── ls_utils.py
│       ├── ls.py
│       ├── lsp_protocol_handler
│       │   ├── lsp_constants.py
│       │   ├── lsp_requests.py
│       │   ├── lsp_types.py
│       │   └── server.py
│       ├── settings.py
│       └── util
│           ├── subprocess_util.py
│           └── zip.py
├── test
│   ├── __init__.py
│   ├── conftest.py
│   ├── resources
│   │   └── repos
│   │       ├── al
│   │       │   └── test_repo
│   │       │       ├── app.json
│   │       │       └── src
│   │       │           ├── Codeunits
│   │       │           │   ├── CustomerMgt.Codeunit.al
│   │       │           │   └── PaymentProcessorImpl.Codeunit.al
│   │       │           ├── Enums
│   │       │           │   └── CustomerType.Enum.al
│   │       │           ├── Interfaces
│   │       │           │   └── IPaymentProcessor.Interface.al
│   │       │           ├── Pages
│   │       │           │   ├── CustomerCard.Page.al
│   │       │           │   └── CustomerList.Page.al
│   │       │           ├── TableExtensions
│   │       │           │   └── Item.TableExt.al
│   │       │           └── Tables
│   │       │               └── Customer.Table.al
│   │       ├── bash
│   │       │   └── test_repo
│   │       │       ├── config.sh
│   │       │       ├── main.sh
│   │       │       └── utils.sh
│   │       ├── clojure
│   │       │   └── test_repo
│   │       │       ├── deps.edn
│   │       │       └── src
│   │       │           └── test_app
│   │       │               ├── core.clj
│   │       │               └── utils.clj
│   │       ├── csharp
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── Models
│   │       │       │   └── Person.cs
│   │       │       ├── Program.cs
│   │       │       ├── serena.sln
│   │       │       └── TestProject.csproj
│   │       ├── dart
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── helper.dart
│   │       │       │   ├── main.dart
│   │       │       │   └── models.dart
│   │       │       └── pubspec.yaml
│   │       ├── elixir
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── examples.ex
│   │       │       │   ├── ignored_dir
│   │       │       │   │   └── ignored_module.ex
│   │       │       │   ├── models.ex
│   │       │       │   ├── services.ex
│   │       │       │   ├── test_repo.ex
│   │       │       │   └── utils.ex
│   │       │       ├── mix.exs
│   │       │       ├── mix.lock
│   │       │       ├── scripts
│   │       │       │   └── build_script.ex
│   │       │       └── test
│   │       │           ├── models_test.exs
│   │       │           └── test_repo_test.exs
│   │       ├── elm
│   │       │   └── test_repo
│   │       │       ├── elm.json
│   │       │       ├── Main.elm
│   │       │       └── Utils.elm
│   │       ├── erlang
│   │       │   └── test_repo
│   │       │       ├── hello.erl
│   │       │       ├── ignored_dir
│   │       │       │   └── ignored_module.erl
│   │       │       ├── include
│   │       │       │   ├── records.hrl
│   │       │       │   └── types.hrl
│   │       │       ├── math_utils.erl
│   │       │       ├── rebar.config
│   │       │       ├── src
│   │       │       │   ├── app.erl
│   │       │       │   ├── models.erl
│   │       │       │   ├── services.erl
│   │       │       │   └── utils.erl
│   │       │       └── test
│   │       │           ├── models_tests.erl
│   │       │           └── utils_tests.erl
│   │       ├── go
│   │       │   └── test_repo
│   │       │       └── main.go
│   │       ├── java
│   │       │   └── test_repo
│   │       │       ├── pom.xml
│   │       │       └── src
│   │       │           └── main
│   │       │               └── java
│   │       │                   └── test_repo
│   │       │                       ├── Main.java
│   │       │                       ├── Model.java
│   │       │                       ├── ModelUser.java
│   │       │                       └── Utils.java
│   │       ├── kotlin
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── build.gradle.kts
│   │       │       └── src
│   │       │           └── main
│   │       │               └── kotlin
│   │       │                   └── test_repo
│   │       │                       ├── Main.kt
│   │       │                       ├── Model.kt
│   │       │                       ├── ModelUser.kt
│   │       │                       └── Utils.kt
│   │       ├── lua
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── main.lua
│   │       │       ├── src
│   │       │       │   ├── calculator.lua
│   │       │       │   └── utils.lua
│   │       │       └── tests
│   │       │           └── test_calculator.lua
│   │       ├── markdown
│   │       │   └── test_repo
│   │       │       ├── api.md
│   │       │       ├── CONTRIBUTING.md
│   │       │       ├── guide.md
│   │       │       └── README.md
│   │       ├── nix
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── default.nix
│   │       │       ├── flake.nix
│   │       │       ├── lib
│   │       │       │   └── utils.nix
│   │       │       ├── modules
│   │       │       │   └── example.nix
│   │       │       └── scripts
│   │       │           └── hello.sh
│   │       ├── perl
│   │       │   └── test_repo
│   │       │       ├── helper.pl
│   │       │       └── main.pl
│   │       ├── php
│   │       │   └── test_repo
│   │       │       ├── helper.php
│   │       │       ├── index.php
│   │       │       └── simple_var.php
│   │       ├── python
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── custom_test
│   │       │       │   ├── __init__.py
│   │       │       │   └── advanced_features.py
│   │       │       ├── examples
│   │       │       │   ├── __init__.py
│   │       │       │   └── user_management.py
│   │       │       ├── ignore_this_dir_with_postfix
│   │       │       │   └── ignored_module.py
│   │       │       ├── scripts
│   │       │       │   ├── __init__.py
│   │       │       │   └── run_app.py
│   │       │       └── test_repo
│   │       │           ├── __init__.py
│   │       │           ├── complex_types.py
│   │       │           ├── models.py
│   │       │           ├── name_collisions.py
│   │       │           ├── nested_base.py
│   │       │           ├── nested.py
│   │       │           ├── overloaded.py
│   │       │           ├── services.py
│   │       │           ├── utils.py
│   │       │           └── variables.py
│   │       ├── r
│   │       │   └── test_repo
│   │       │       ├── .Rbuildignore
│   │       │       ├── DESCRIPTION
│   │       │       ├── examples
│   │       │       │   └── analysis.R
│   │       │       ├── NAMESPACE
│   │       │       └── R
│   │       │           ├── models.R
│   │       │           └── utils.R
│   │       ├── ruby
│   │       │   └── test_repo
│   │       │       ├── .solargraph.yml
│   │       │       ├── examples
│   │       │       │   └── user_management.rb
│   │       │       ├── lib.rb
│   │       │       ├── main.rb
│   │       │       ├── models.rb
│   │       │       ├── nested.rb
│   │       │       ├── services.rb
│   │       │       └── variables.rb
│   │       ├── rust
│   │       │   ├── test_repo
│   │       │   │   ├── Cargo.lock
│   │       │   │   ├── Cargo.toml
│   │       │   │   └── src
│   │       │   │       ├── lib.rs
│   │       │   │       └── main.rs
│   │       │   └── test_repo_2024
│   │       │       ├── Cargo.lock
│   │       │       ├── Cargo.toml
│   │       │       └── src
│   │       │           ├── lib.rs
│   │       │           └── main.rs
│   │       ├── swift
│   │       │   └── test_repo
│   │       │       ├── Package.swift
│   │       │       └── src
│   │       │           ├── main.swift
│   │       │           └── utils.swift
│   │       ├── terraform
│   │       │   └── test_repo
│   │       │       ├── data.tf
│   │       │       ├── main.tf
│   │       │       ├── outputs.tf
│   │       │       └── variables.tf
│   │       ├── typescript
│   │       │   └── test_repo
│   │       │       ├── .serena
│   │       │       │   └── project.yml
│   │       │       ├── index.ts
│   │       │       ├── tsconfig.json
│   │       │       └── use_helper.ts
│   │       └── zig
│   │           └── test_repo
│   │               ├── .gitignore
│   │               ├── build.zig
│   │               ├── src
│   │               │   ├── calculator.zig
│   │               │   ├── main.zig
│   │               │   └── math_utils.zig
│   │               └── zls.json
│   ├── serena
│   │   ├── __init__.py
│   │   ├── __snapshots__
│   │   │   └── test_symbol_editing.ambr
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   └── test_serena_config.py
│   │   ├── test_edit_marker.py
│   │   ├── test_mcp.py
│   │   ├── test_serena_agent.py
│   │   ├── test_symbol_editing.py
│   │   ├── test_symbol.py
│   │   ├── test_text_utils.py
│   │   ├── test_tool_parameter_types.py
│   │   └── util
│   │       ├── test_exception.py
│   │       └── test_file_system.py
│   └── solidlsp
│       ├── al
│       │   └── test_al_basic.py
│       ├── bash
│       │   ├── __init__.py
│       │   └── test_bash_basic.py
│       ├── clojure
│       │   ├── __init__.py
│       │   └── test_clojure_basic.py
│       ├── csharp
│       │   └── test_csharp_basic.py
│       ├── dart
│       │   ├── __init__.py
│       │   └── test_dart_basic.py
│       ├── elixir
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_elixir_basic.py
│       │   ├── test_elixir_ignored_dirs.py
│       │   ├── test_elixir_integration.py
│       │   └── test_elixir_symbol_retrieval.py
│       ├── elm
│       │   └── test_elm_basic.py
│       ├── erlang
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_erlang_basic.py
│       │   ├── test_erlang_ignored_dirs.py
│       │   └── test_erlang_symbol_retrieval.py
│       ├── go
│       │   └── test_go_basic.py
│       ├── java
│       │   └── test_java_basic.py
│       ├── kotlin
│       │   └── test_kotlin_basic.py
│       ├── lua
│       │   └── test_lua_basic.py
│       ├── markdown
│       │   ├── __init__.py
│       │   └── test_markdown_basic.py
│       ├── nix
│       │   └── test_nix_basic.py
│       ├── perl
│       │   └── test_perl_basic.py
│       ├── php
│       │   └── test_php_basic.py
│       ├── python
│       │   ├── test_python_basic.py
│       │   ├── test_retrieval_with_ignored_dirs.py
│       │   └── test_symbol_retrieval.py
│       ├── r
│       │   ├── __init__.py
│       │   └── test_r_basic.py
│       ├── ruby
│       │   ├── test_ruby_basic.py
│       │   └── test_ruby_symbol_retrieval.py
│       ├── rust
│       │   ├── test_rust_2024_edition.py
│       │   └── test_rust_basic.py
│       ├── swift
│       │   └── test_swift_basic.py
│       ├── terraform
│       │   └── test_terraform_basic.py
│       ├── typescript
│       │   └── test_typescript_basic.py
│       ├── util
│       │   └── test_zip.py
│       └── zig
│           └── test_zig_basic.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/src/serena/config/serena_config.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | The Serena Model Context Protocol (MCP) Server
  3 | """
  4 | 
  5 | import os
  6 | import shutil
  7 | from collections.abc import Iterable
  8 | from copy import deepcopy
  9 | from dataclasses import dataclass, field
 10 | from datetime import datetime
 11 | from functools import cached_property
 12 | from pathlib import Path
 13 | from typing import TYPE_CHECKING, Any, Optional, Self, TypeVar
 14 | 
 15 | import yaml
 16 | from ruamel.yaml.comments import CommentedMap
 17 | from sensai.util import logging
 18 | from sensai.util.logging import LogTime, datetime_tag
 19 | from sensai.util.string import ToStringMixin
 20 | 
 21 | from serena.constants import (
 22 |     DEFAULT_ENCODING,
 23 |     PROJECT_TEMPLATE_FILE,
 24 |     REPO_ROOT,
 25 |     SERENA_CONFIG_TEMPLATE_FILE,
 26 |     SERENA_MANAGED_DIR_IN_HOME,
 27 |     SERENA_MANAGED_DIR_NAME,
 28 | )
 29 | from serena.util.general import load_yaml, save_yaml
 30 | from serena.util.inspection import determine_programming_language_composition
 31 | from solidlsp.ls_config import Language
 32 | 
 33 | from ..analytics import RegisteredTokenCountEstimator
 34 | from ..util.class_decorators import singleton
 35 | 
 36 | if TYPE_CHECKING:
 37 |     from ..project import Project
 38 | 
 39 | log = logging.getLogger(__name__)
 40 | T = TypeVar("T")
 41 | DEFAULT_TOOL_TIMEOUT: float = 240
 42 | 
 43 | 
 44 | @singleton
 45 | class SerenaPaths:
 46 |     """
 47 |     Provides paths to various Serena-related directories and files.
 48 |     """
 49 | 
 50 |     def __init__(self) -> None:
 51 |         self.user_config_dir: str = SERENA_MANAGED_DIR_IN_HOME
 52 |         """
 53 |         the path to the user's Serena configuration directory, which is typically ~/.serena
 54 |         """
 55 | 
 56 |     def get_next_log_file_path(self, prefix: str) -> str:
 57 |         """
 58 |         :param prefix: the filename prefix indicating the type of the log file
 59 |         :return: the full path to the log file to use
 60 |         """
 61 |         log_dir = os.path.join(self.user_config_dir, "logs", datetime.now().strftime("%Y-%m-%d"))
 62 |         os.makedirs(log_dir, exist_ok=True)
 63 |         return os.path.join(log_dir, prefix + "_" + datetime_tag() + ".txt")
 64 | 
 65 |     # TODO: Paths from constants.py should be moved here
 66 | 
 67 | 
 68 | class ToolSet:
 69 |     def __init__(self, tool_names: set[str]) -> None:
 70 |         self._tool_names = tool_names
 71 | 
 72 |     @classmethod
 73 |     def default(cls) -> "ToolSet":
 74 |         """
 75 |         :return: the default tool set, which contains all tools that are enabled by default
 76 |         """
 77 |         from serena.tools import ToolRegistry
 78 | 
 79 |         return cls(set(ToolRegistry().get_tool_names_default_enabled()))
 80 | 
 81 |     def apply(self, *tool_inclusion_definitions: "ToolInclusionDefinition") -> "ToolSet":
 82 |         """
 83 |         :param tool_inclusion_definitions: the definitions to apply
 84 |         :return: a new tool set with the definitions applied
 85 |         """
 86 |         from serena.tools import ToolRegistry
 87 | 
 88 |         registry = ToolRegistry()
 89 |         tool_names = set(self._tool_names)
 90 |         for definition in tool_inclusion_definitions:
 91 |             included_tools = []
 92 |             excluded_tools = []
 93 |             for included_tool in definition.included_optional_tools:
 94 |                 if not registry.is_valid_tool_name(included_tool):
 95 |                     raise ValueError(f"Invalid tool name '{included_tool}' provided for inclusion")
 96 |                 if included_tool not in tool_names:
 97 |                     tool_names.add(included_tool)
 98 |                     included_tools.append(included_tool)
 99 |             for excluded_tool in definition.excluded_tools:
100 |                 if not registry.is_valid_tool_name(excluded_tool):
101 |                     raise ValueError(f"Invalid tool name '{excluded_tool}' provided for exclusion")
102 |                 if excluded_tool in tool_names:
103 |                     tool_names.remove(excluded_tool)
104 |                     excluded_tools.append(excluded_tool)
105 |             if included_tools:
106 |                 log.info(f"{definition} included {len(included_tools)} tools: {', '.join(included_tools)}")
107 |             if excluded_tools:
108 |                 log.info(f"{definition} excluded {len(excluded_tools)} tools: {', '.join(excluded_tools)}")
109 |         return ToolSet(tool_names)
110 | 
111 |     def without_editing_tools(self) -> "ToolSet":
112 |         """
113 |         :return: a new tool set that excludes all tools that can edit
114 |         """
115 |         from serena.tools import ToolRegistry
116 | 
117 |         registry = ToolRegistry()
118 |         tool_names = set(self._tool_names)
119 |         for tool_name in self._tool_names:
120 |             if registry.get_tool_class_by_name(tool_name).can_edit():
121 |                 tool_names.remove(tool_name)
122 |         return ToolSet(tool_names)
123 | 
124 |     def get_tool_names(self) -> set[str]:
125 |         """
126 |         Returns the names of the tools that are currently included in the tool set.
127 |         """
128 |         return self._tool_names
129 | 
130 |     def includes_name(self, tool_name: str) -> bool:
131 |         return tool_name in self._tool_names
132 | 
133 | 
134 | @dataclass
135 | class ToolInclusionDefinition:
136 |     excluded_tools: Iterable[str] = ()
137 |     included_optional_tools: Iterable[str] = ()
138 | 
139 | 
140 | class SerenaConfigError(Exception):
141 |     pass
142 | 
143 | 
144 | def get_serena_managed_in_project_dir(project_root: str | Path) -> str:
145 |     return os.path.join(project_root, SERENA_MANAGED_DIR_NAME)
146 | 
147 | 
148 | def is_running_in_docker() -> bool:
149 |     """Check if we're running inside a Docker container."""
150 |     # Check for Docker-specific files
151 |     if os.path.exists("/.dockerenv"):
152 |         return True
153 |     # Check cgroup for docker references
154 |     try:
155 |         with open("/proc/self/cgroup") as f:
156 |             return "docker" in f.read()
157 |     except FileNotFoundError:
158 |         return False
159 | 
160 | 
161 | @dataclass(kw_only=True)
162 | class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
163 |     project_name: str
164 |     language: Language
165 |     ignored_paths: list[str] = field(default_factory=list)
166 |     read_only: bool = False
167 |     ignore_all_files_in_gitignore: bool = True
168 |     initial_prompt: str = ""
169 |     encoding: str = DEFAULT_ENCODING
170 | 
171 |     SERENA_DEFAULT_PROJECT_FILE = "project.yml"
172 | 
173 |     def _tostring_includes(self) -> list[str]:
174 |         return ["project_name"]
175 | 
176 |     @classmethod
177 |     def autogenerate(
178 |         cls, project_root: str | Path, project_name: str | None = None, project_language: Language | None = None, save_to_disk: bool = True
179 |     ) -> Self:
180 |         """
181 |         Autogenerate a project configuration for a given project root.
182 | 
183 |         :param project_root: the path to the project root
184 |         :param project_name: the name of the project; if None, the name of the project will be the name of the directory
185 |             containing the project
186 |         :param project_language: the programming language of the project; if None, it will be determined automatically
187 |         :param save_to_disk: whether to save the project configuration to disk
188 |         :return: the project configuration
189 |         """
190 |         project_root = Path(project_root).resolve()
191 |         if not project_root.exists():
192 |             raise FileNotFoundError(f"Project root not found: {project_root}")
193 |         with LogTime("Project configuration auto-generation", logger=log):
194 |             project_name = project_name or project_root.name
195 |             if project_language is None:
196 |                 language_composition = determine_programming_language_composition(str(project_root))
197 |                 if len(language_composition) == 0:
198 |                     raise ValueError(
199 |                         f"No source files found in {project_root}\n\n"
200 |                         f"To use Serena with this project, you need to either:\n"
201 |                         f"1. Add source files in one of the supported languages (Python, JavaScript/TypeScript, Java, C#, Rust, Go, Ruby, C++, PHP, Swift, Elixir, Terraform, Bash)\n"
202 |                         f"2. Create a project configuration file manually at:\n"
203 |                         f"   {os.path.join(project_root, cls.rel_path_to_project_yml())}\n\n"
204 |                         f"Example project.yml:\n"
205 |                         f"  project_name: {project_name}\n"
206 |                         f"  language: python  # or typescript, java, csharp, rust, go, ruby, cpp, php, swift, elixir, terraform, bash\n"
207 |                     )
208 |                 # find the language with the highest percentage
209 |                 dominant_language = max(language_composition.keys(), key=lambda lang: language_composition[lang])
210 |             else:
211 |                 dominant_language = project_language.value
212 |             config_with_comments = load_yaml(PROJECT_TEMPLATE_FILE, preserve_comments=True)
213 |             config_with_comments["project_name"] = project_name
214 |             config_with_comments["language"] = dominant_language
215 |             if save_to_disk:
216 |                 save_yaml(str(project_root / cls.rel_path_to_project_yml()), config_with_comments, preserve_comments=True)
217 |             return cls._from_dict(config_with_comments)
218 | 
219 |     @classmethod
220 |     def rel_path_to_project_yml(cls) -> str:
221 |         return os.path.join(SERENA_MANAGED_DIR_NAME, cls.SERENA_DEFAULT_PROJECT_FILE)
222 | 
223 |     @classmethod
224 |     def _from_dict(cls, data: dict[str, Any]) -> Self:
225 |         """
226 |         Create a ProjectConfig instance from a configuration dictionary
227 |         """
228 |         language_str = data["language"].lower()
229 |         project_name = data["project_name"]
230 |         # backwards compatibility
231 |         if language_str == "javascript":
232 |             log.warning(f"Found deprecated project language `javascript` in project {project_name}, please change to `typescript`")
233 |             language_str = "typescript"
234 |         try:
235 |             language = Language(language_str)
236 |         except ValueError as e:
237 |             raise ValueError(f"Invalid language: {data['language']}.\nValid languages are: {[l.value for l in Language]}") from e
238 |         return cls(
239 |             project_name=project_name,
240 |             language=language,
241 |             ignored_paths=data.get("ignored_paths", []),
242 |             excluded_tools=data.get("excluded_tools", []),
243 |             included_optional_tools=data.get("included_optional_tools", []),
244 |             read_only=data.get("read_only", False),
245 |             ignore_all_files_in_gitignore=data.get("ignore_all_files_in_gitignore", True),
246 |             initial_prompt=data.get("initial_prompt", ""),
247 |             encoding=data.get("encoding", DEFAULT_ENCODING),
248 |         )
249 | 
250 |     @classmethod
251 |     def load(cls, project_root: Path | str, autogenerate: bool = False) -> Self:
252 |         """
253 |         Load a ProjectConfig instance from the path to the project root.
254 |         """
255 |         project_root = Path(project_root)
256 |         yaml_path = project_root / cls.rel_path_to_project_yml()
257 |         if not yaml_path.exists():
258 |             if autogenerate:
259 |                 return cls.autogenerate(project_root)
260 |             else:
261 |                 raise FileNotFoundError(f"Project configuration file not found: {yaml_path}")
262 |         with open(yaml_path, encoding="utf-8") as f:
263 |             yaml_data = yaml.safe_load(f)
264 |         if "project_name" not in yaml_data:
265 |             yaml_data["project_name"] = project_root.name
266 |         return cls._from_dict(yaml_data)
267 | 
268 | 
269 | class RegisteredProject(ToStringMixin):
270 |     def __init__(self, project_root: str, project_config: "ProjectConfig", project_instance: Optional["Project"] = None) -> None:
271 |         """
272 |         Represents a registered project in the Serena configuration.
273 | 
274 |         :param project_root: the root directory of the project
275 |         :param project_config: the configuration of the project
276 |         """
277 |         self.project_root = Path(project_root).resolve()
278 |         self.project_config = project_config
279 |         self._project_instance = project_instance
280 | 
281 |     def _tostring_exclude_private(self) -> bool:
282 |         return True
283 | 
284 |     @property
285 |     def project_name(self) -> str:
286 |         return self.project_config.project_name
287 | 
288 |     @classmethod
289 |     def from_project_instance(cls, project_instance: "Project") -> "RegisteredProject":
290 |         return RegisteredProject(
291 |             project_root=project_instance.project_root,
292 |             project_config=project_instance.project_config,
293 |             project_instance=project_instance,
294 |         )
295 | 
296 |     def matches_root_path(self, path: str | Path) -> bool:
297 |         """
298 |         Check if the given path matches the project root path.
299 | 
300 |         :param path: the path to check
301 |         :return: True if the path matches the project root, False otherwise
302 |         """
303 |         return self.project_root == Path(path).resolve()
304 | 
305 |     def get_project_instance(self) -> "Project":
306 |         """
307 |         Returns the project instance for this registered project, loading it if necessary.
308 |         """
309 |         if self._project_instance is None:
310 |             from ..project import Project
311 | 
312 |             with LogTime(f"Loading project instance for {self}", logger=log):
313 |                 self._project_instance = Project(project_root=str(self.project_root), project_config=self.project_config)
314 |         return self._project_instance
315 | 
316 | 
317 | @dataclass(kw_only=True)
318 | class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
319 |     """
320 |     Holds the Serena agent configuration, which is typically loaded from a YAML configuration file
321 |     (when instantiated via :method:`from_config_file`), which is updated when projects are added or removed.
322 |     For testing purposes, it can also be instantiated directly with the desired parameters.
323 |     """
324 | 
325 |     projects: list[RegisteredProject] = field(default_factory=list)
326 |     gui_log_window_enabled: bool = False
327 |     log_level: int = logging.INFO
328 |     trace_lsp_communication: bool = False
329 |     web_dashboard: bool = True
330 |     web_dashboard_open_on_launch: bool = True
331 |     tool_timeout: float = DEFAULT_TOOL_TIMEOUT
332 |     loaded_commented_yaml: CommentedMap | None = None
333 |     config_file_path: str | None = None
334 |     """
335 |     the path to the configuration file to which updates of the configuration shall be saved;
336 |     if None, the configuration is not saved to disk
337 |     """
338 |     jetbrains: bool = False
339 |     """
340 |     whether to apply JetBrains mode
341 |     """
342 |     record_tool_usage_stats: bool = False
343 |     """Whether to record tool usage statistics, they will be shown in the web dashboard if recording is active. 
344 |     """
345 |     token_count_estimator: str = RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name
346 |     """Only relevant if `record_tool_usage` is True; the name of the token count estimator to use for tool usage statistics.
347 |     See the `RegisteredTokenCountEstimator` enum for available options.
348 |     
349 |     Note: some token estimators (like tiktoken) may require downloading data files
350 |     on the first run, which can take some time and require internet access. Others, like the Anthropic ones, may require an API key
351 |     and rate limits may apply.
352 |     """
353 |     default_max_tool_answer_chars: int = 150_000
354 |     """Used as default for tools where the apply method has a default maximal answer length.
355 |     Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default 
356 |     through the global configuration.
357 |     """
358 |     ls_specific_settings: dict = field(default_factory=dict)
359 |     """Advanced configuration option allowing to configure language server implementation specific options, see SolidLSPSettings for more info."""
360 | 
361 |     CONFIG_FILE = "serena_config.yml"
362 |     CONFIG_FILE_DOCKER = "serena_config.docker.yml"  # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization
363 | 
364 |     def _tostring_includes(self) -> list[str]:
365 |         return ["config_file_path"]
366 | 
367 |     @classmethod
368 |     def generate_config_file(cls, config_file_path: str) -> None:
369 |         """
370 |         Generates a Serena configuration file at the specified path from the template file.
371 | 
372 |         :param config_file_path: the path where the configuration file should be generated
373 |         """
374 |         log.info(f"Auto-generating Serena configuration file in {config_file_path}")
375 |         loaded_commented_yaml = load_yaml(SERENA_CONFIG_TEMPLATE_FILE, preserve_comments=True)
376 |         save_yaml(config_file_path, loaded_commented_yaml, preserve_comments=True)
377 | 
378 |     @classmethod
379 |     def _determine_config_file_path(cls) -> str:
380 |         """
381 |         :return: the location where the Serena configuration file is stored/should be stored
382 |         """
383 |         if is_running_in_docker():
384 |             return os.path.join(REPO_ROOT, cls.CONFIG_FILE_DOCKER)
385 |         else:
386 |             config_path = os.path.join(SERENA_MANAGED_DIR_IN_HOME, cls.CONFIG_FILE)
387 | 
388 |             # if the config file does not exist, check if we can migrate it from the old location
389 |             if not os.path.exists(config_path):
390 |                 old_config_path = os.path.join(REPO_ROOT, cls.CONFIG_FILE)
391 |                 if os.path.exists(old_config_path):
392 |                     log.info(f"Moving Serena configuration file from {old_config_path} to {config_path}")
393 |                     os.makedirs(os.path.dirname(config_path), exist_ok=True)
394 |                     shutil.move(old_config_path, config_path)
395 | 
396 |             return config_path
397 | 
398 |     @classmethod
399 |     def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig":
400 |         """
401 |         Static constructor to create SerenaConfig from the configuration file
402 |         """
403 |         config_file_path = cls._determine_config_file_path()
404 | 
405 |         # create the configuration file from the template if necessary
406 |         if not os.path.exists(config_file_path):
407 |             if not generate_if_missing:
408 |                 raise FileNotFoundError(f"Serena configuration file not found: {config_file_path}")
409 |             log.info(f"Serena configuration file not found at {config_file_path}, autogenerating...")
410 |             cls.generate_config_file(config_file_path)
411 | 
412 |         # load the configuration
413 |         log.info(f"Loading Serena configuration from {config_file_path}")
414 |         try:
415 |             loaded_commented_yaml = load_yaml(config_file_path, preserve_comments=True)
416 |         except Exception as e:
417 |             raise ValueError(f"Error loading Serena configuration from {config_file_path}: {e}") from e
418 | 
419 |         # create the configuration instance
420 |         instance = cls(loaded_commented_yaml=loaded_commented_yaml, config_file_path=config_file_path)
421 | 
422 |         # read projects
423 |         if "projects" not in loaded_commented_yaml:
424 |             raise SerenaConfigError("`projects` key not found in Serena configuration. Please update your `serena_config.yml` file.")
425 | 
426 |         # load list of known projects
427 |         instance.projects = []
428 |         num_project_migrations = 0
429 |         for path in loaded_commented_yaml["projects"]:
430 |             path = Path(path).resolve()
431 |             if not path.exists() or (path.is_dir() and not (path / ProjectConfig.rel_path_to_project_yml()).exists()):
432 |                 log.warning(f"Project path {path} does not exist or does not contain a project configuration file, skipping.")
433 |                 continue
434 |             if path.is_file():
435 |                 path = cls._migrate_out_of_project_config_file(path)
436 |                 if path is None:
437 |                     continue
438 |                 num_project_migrations += 1
439 |             project_config = ProjectConfig.load(path)
440 |             project = RegisteredProject(
441 |                 project_root=str(path),
442 |                 project_config=project_config,
443 |             )
444 |             instance.projects.append(project)
445 | 
446 |         # set other configuration parameters
447 |         if is_running_in_docker():
448 |             instance.gui_log_window_enabled = False  # not supported in Docker
449 |         else:
450 |             instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False)
451 |         instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO))
452 |         instance.web_dashboard = loaded_commented_yaml.get("web_dashboard", True)
453 |         instance.web_dashboard_open_on_launch = loaded_commented_yaml.get("web_dashboard_open_on_launch", True)
454 |         instance.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT)
455 |         instance.trace_lsp_communication = loaded_commented_yaml.get("trace_lsp_communication", False)
456 |         instance.excluded_tools = loaded_commented_yaml.get("excluded_tools", [])
457 |         instance.included_optional_tools = loaded_commented_yaml.get("included_optional_tools", [])
458 |         instance.jetbrains = loaded_commented_yaml.get("jetbrains", False)
459 |         instance.record_tool_usage_stats = loaded_commented_yaml.get("record_tool_usage_stats", False)
460 |         instance.token_count_estimator = loaded_commented_yaml.get(
461 |             "token_count_estimator", RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name
462 |         )
463 |         instance.default_max_tool_answer_chars = loaded_commented_yaml.get("default_max_tool_answer_chars", 150_000)
464 |         instance.ls_specific_settings = loaded_commented_yaml.get("ls_specific_settings", {})
465 | 
466 |         # re-save the configuration file if any migrations were performed
467 |         if num_project_migrations > 0:
468 |             log.info(
469 |                 f"Migrated {num_project_migrations} project configurations from legacy format to in-project configuration; re-saving configuration"
470 |             )
471 |             instance.save()
472 | 
473 |         return instance
474 | 
475 |     @classmethod
476 |     def _migrate_out_of_project_config_file(cls, path: Path) -> Path | None:
477 |         """
478 |         Migrates a legacy project configuration file (which is a YAML file containing the project root) to the
479 |         in-project configuration file (project.yml) inside the project root directory.
480 | 
481 |         :param path: the path to the legacy project configuration file
482 |         :return: the project root path if the migration was successful, None otherwise.
483 |         """
484 |         log.info(f"Found legacy project configuration file {path}, migrating to in-project configuration.")
485 |         try:
486 |             with open(path, encoding="utf-8") as f:
487 |                 project_config_data = yaml.safe_load(f)
488 |             if "project_name" not in project_config_data:
489 |                 project_name = path.stem
490 |                 with open(path, "a", encoding="utf-8") as f:
491 |                     f.write(f"\nproject_name: {project_name}")
492 |             project_root = project_config_data["project_root"]
493 |             shutil.move(str(path), str(Path(project_root) / ProjectConfig.rel_path_to_project_yml()))
494 |             return Path(project_root).resolve()
495 |         except Exception as e:
496 |             log.error(f"Error migrating configuration file: {e}")
497 |             return None
498 | 
499 |     @cached_property
500 |     def project_paths(self) -> list[str]:
501 |         return sorted(str(project.project_root) for project in self.projects)
502 | 
503 |     @cached_property
504 |     def project_names(self) -> list[str]:
505 |         return sorted(project.project_config.project_name for project in self.projects)
506 | 
507 |     def get_project(self, project_root_or_name: str) -> Optional["Project"]:
508 |         # look for project by name
509 |         project_candidates = []
510 |         for project in self.projects:
511 |             if project.project_config.project_name == project_root_or_name:
512 |                 project_candidates.append(project)
513 |         if len(project_candidates) == 1:
514 |             return project_candidates[0].get_project_instance()
515 |         elif len(project_candidates) > 1:
516 |             raise ValueError(
517 |                 f"Multiple projects found with name '{project_root_or_name}'. Please activate it by location instead. "
518 |                 f"Locations: {[p.project_root for p in project_candidates]}"
519 |             )
520 |         # no project found by name; check if it's a path
521 |         if os.path.isdir(project_root_or_name):
522 |             for project in self.projects:
523 |                 if project.matches_root_path(project_root_or_name):
524 |                     return project.get_project_instance()
525 |         return None
526 | 
527 |     def add_project_from_path(self, project_root: Path | str) -> "Project":
528 |         """
529 |         Add a project to the Serena configuration from a given path. Will raise a FileExistsError if a
530 |         project already exists at the path.
531 | 
532 |         :param project_root: the path to the project to add
533 |         :return: the project that was added
534 |         """
535 |         from ..project import Project
536 | 
537 |         project_root = Path(project_root).resolve()
538 |         if not project_root.exists():
539 |             raise FileNotFoundError(f"Error: Path does not exist: {project_root}")
540 |         if not project_root.is_dir():
541 |             raise FileNotFoundError(f"Error: Path is not a directory: {project_root}")
542 | 
543 |         for already_registered_project in self.projects:
544 |             if str(already_registered_project.project_root) == str(project_root):
545 |                 raise FileExistsError(
546 |                     f"Project with path {project_root} was already added with name '{already_registered_project.project_name}'."
547 |                 )
548 | 
549 |         project_config = ProjectConfig.load(project_root, autogenerate=True)
550 | 
551 |         new_project = Project(project_root=str(project_root), project_config=project_config, is_newly_created=True)
552 |         self.projects.append(RegisteredProject.from_project_instance(new_project))
553 |         self.save()
554 | 
555 |         return new_project
556 | 
557 |     def remove_project(self, project_name: str) -> None:
558 |         # find the index of the project with the desired name and remove it
559 |         for i, project in enumerate(list(self.projects)):
560 |             if project.project_name == project_name:
561 |                 del self.projects[i]
562 |                 break
563 |         else:
564 |             raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}")
565 |         self.save()
566 | 
567 |     def save(self) -> None:
568 |         """
569 |         Saves the configuration to the file from which it was loaded (if any)
570 |         """
571 |         if self.config_file_path is None:
572 |             return
573 |         assert self.loaded_commented_yaml is not None, "Cannot save configuration without loaded YAML"
574 |         loaded_original_yaml = deepcopy(self.loaded_commented_yaml)
575 |         # projects are unique absolute paths
576 |         # we also canonicalize them before saving
577 |         loaded_original_yaml["projects"] = sorted({str(project.project_root) for project in self.projects})
578 |         save_yaml(self.config_file_path, loaded_original_yaml, preserve_comments=True)
579 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/python/test_symbol_retrieval.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the language server symbol-related functionality.
  3 | 
  4 | These tests focus on the following methods:
  5 | - request_containing_symbol
  6 | - request_referencing_symbols
  7 | """
  8 | 
  9 | import os
 10 | 
 11 | import pytest
 12 | 
 13 | from serena.symbol import LanguageServerSymbol
 14 | from solidlsp import SolidLanguageServer
 15 | from solidlsp.ls_config import Language
 16 | from solidlsp.ls_types import SymbolKind
 17 | 
 18 | pytestmark = pytest.mark.python
 19 | 
 20 | 
 21 | class TestLanguageServerSymbols:
 22 |     """Test the language server's symbol-related functionality."""
 23 | 
 24 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
 25 |     def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
 26 |         """Test request_containing_symbol for a function."""
 27 |         # Test for a position inside the create_user method
 28 |         file_path = os.path.join("test_repo", "services.py")
 29 |         # Line 17 is inside the create_user method body
 30 |         containing_symbol = language_server.request_containing_symbol(file_path, 17, 20, include_body=True)
 31 | 
 32 |         # Verify that we found the containing symbol
 33 |         assert containing_symbol is not None
 34 |         assert containing_symbol["name"] == "create_user"
 35 |         assert containing_symbol["kind"] == SymbolKind.Method
 36 |         if "body" in containing_symbol:
 37 |             assert containing_symbol["body"].strip().startswith("def create_user(self")
 38 | 
 39 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
 40 |     def test_references_to_variables(self, language_server: SolidLanguageServer) -> None:
 41 |         """Test request_referencing_symbols for a variable."""
 42 |         file_path = os.path.join("test_repo", "variables.py")
 43 |         # Line 75 contains the field status that is later modified
 44 |         ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 74, 4)]
 45 | 
 46 |         assert len(ref_symbols) > 0
 47 |         ref_lines = [ref["location"]["range"]["start"]["line"] for ref in ref_symbols if "location" in ref and "range" in ref["location"]]
 48 |         ref_names = [ref["name"] for ref in ref_symbols]
 49 |         assert 87 in ref_lines
 50 |         assert 95 in ref_lines
 51 |         assert "dataclass_instance" in ref_names
 52 |         assert "second_dataclass" in ref_names
 53 | 
 54 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
 55 |     def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
 56 |         """Test request_containing_symbol for a class."""
 57 |         # Test for a position inside the UserService class but outside any method
 58 |         file_path = os.path.join("test_repo", "services.py")
 59 |         # Line 9 is the class definition line for UserService
 60 |         containing_symbol = language_server.request_containing_symbol(file_path, 9, 7)
 61 | 
 62 |         # Verify that we found the containing symbol
 63 |         assert containing_symbol is not None
 64 |         assert containing_symbol["name"] == "UserService"
 65 |         assert containing_symbol["kind"] == SymbolKind.Class
 66 | 
 67 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
 68 |     def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
 69 |         """Test request_containing_symbol with nested scopes."""
 70 |         # Test for a position inside a method which is inside a class
 71 |         file_path = os.path.join("test_repo", "services.py")
 72 |         # Line 18 is inside the create_user method inside UserService class
 73 |         containing_symbol = language_server.request_containing_symbol(file_path, 18, 25)
 74 | 
 75 |         # Verify that we found the innermost containing symbol (the method)
 76 |         assert containing_symbol is not None
 77 |         assert containing_symbol["name"] == "create_user"
 78 |         assert containing_symbol["kind"] == SymbolKind.Method
 79 | 
 80 |         # Get the parent containing symbol
 81 |         if "location" in containing_symbol and "range" in containing_symbol["location"]:
 82 |             parent_symbol = language_server.request_containing_symbol(
 83 |                 file_path,
 84 |                 containing_symbol["location"]["range"]["start"]["line"],
 85 |                 containing_symbol["location"]["range"]["start"]["character"] - 1,
 86 |             )
 87 | 
 88 |             # Verify that the parent is the class
 89 |             assert parent_symbol is not None
 90 |             assert parent_symbol["name"] == "UserService"
 91 |             assert parent_symbol["kind"] == SymbolKind.Class
 92 | 
 93 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
 94 |     def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None:
 95 |         """Test request_containing_symbol for a position with no containing symbol."""
 96 |         # Test for a position outside any function/class (e.g., in imports)
 97 |         file_path = os.path.join("test_repo", "services.py")
 98 |         # Line 1 is in imports, not inside any function or class
 99 |         containing_symbol = language_server.request_containing_symbol(file_path, 1, 10)
100 | 
101 |         # Should return None or an empty dictionary
102 |         assert containing_symbol is None or containing_symbol == {}
103 | 
104 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
105 |     def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer) -> None:
106 |         """Test request_referencing_symbols for a function."""
107 |         # Test referencing symbols for create_user function
108 |         file_path = os.path.join("test_repo", "services.py")
109 |         # Line 15 contains the create_user function definition
110 |         symbols = language_server.request_document_symbols(file_path)
111 |         create_user_symbol = next((s for s in symbols[0] if s.get("name") == "create_user"), None)
112 |         if not create_user_symbol or "selectionRange" not in create_user_symbol:
113 |             raise AssertionError("create_user symbol or its selectionRange not found")
114 |         sel_start = create_user_symbol["selectionRange"]["start"]
115 |         ref_symbols = [
116 |             ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
117 |         ]
118 |         assert len(ref_symbols) > 0, "No referencing symbols found for create_user (selectionRange)"
119 | 
120 |         # Verify the structure of referencing symbols
121 |         for symbol in ref_symbols:
122 |             assert "name" in symbol
123 |             assert "kind" in symbol
124 |             if "location" in symbol and "range" in symbol["location"]:
125 |                 assert "start" in symbol["location"]["range"]
126 |                 assert "end" in symbol["location"]["range"]
127 | 
128 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
129 |     def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None:
130 |         """Test request_referencing_symbols for a class."""
131 |         # Test referencing symbols for User class
132 |         file_path = os.path.join("test_repo", "models.py")
133 |         # Line 31 contains the User class definition
134 |         symbols = language_server.request_document_symbols(file_path)
135 |         user_symbol = next((s for s in symbols[0] if s.get("name") == "User"), None)
136 |         if not user_symbol or "selectionRange" not in user_symbol:
137 |             raise AssertionError("User symbol or its selectionRange not found")
138 |         sel_start = user_symbol["selectionRange"]["start"]
139 |         ref_symbols = [
140 |             ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
141 |         ]
142 |         services_references = [
143 |             symbol
144 |             for symbol in ref_symbols
145 |             if "location" in symbol and "uri" in symbol["location"] and "services.py" in symbol["location"]["uri"]
146 |         ]
147 |         assert len(services_references) > 0, "No referencing symbols from services.py for User (selectionRange)"
148 | 
149 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
150 |     def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None:
151 |         """Test request_referencing_symbols for a function parameter."""
152 |         # Test referencing symbols for id parameter in get_user
153 |         file_path = os.path.join("test_repo", "services.py")
154 |         # Line 24 contains the get_user method with id parameter
155 |         symbols = language_server.request_document_symbols(file_path)
156 |         get_user_symbol = next((s for s in symbols[0] if s.get("name") == "get_user"), None)
157 |         if not get_user_symbol or "selectionRange" not in get_user_symbol:
158 |             raise AssertionError("get_user symbol or its selectionRange not found")
159 |         sel_start = get_user_symbol["selectionRange"]["start"]
160 |         ref_symbols = [
161 |             ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
162 |         ]
163 |         method_refs = [
164 |             symbol
165 |             for symbol in ref_symbols
166 |             if "location" in symbol and "range" in symbol["location"] and symbol["location"]["range"]["start"]["line"] > sel_start["line"]
167 |         ]
168 |         assert len(method_refs) > 0, "No referencing symbols within method body for get_user (selectionRange)"
169 | 
170 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
171 |     def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None:
172 |         """Test request_referencing_symbols for a position with no symbol."""
173 |         # For positions with no symbol, the method might throw an error or return None/empty list
174 |         # We'll modify our test to handle this by using a try-except block
175 | 
176 |         file_path = os.path.join("test_repo", "services.py")
177 |         # Line 3 is a blank line or comment
178 |         try:
179 |             ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 3, 0)]
180 |             # If we get here, make sure we got an empty result
181 |             assert ref_symbols == [] or ref_symbols is None
182 |         except Exception:
183 |             # The method might raise an exception for invalid positions
184 |             # which is acceptable behavior
185 |             pass
186 | 
187 |     # Tests for request_defining_symbol
188 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
189 |     def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
190 |         """Test request_defining_symbol for a variable usage."""
191 |         # Test finding the definition of a symbol in the create_user method
192 |         file_path = os.path.join("test_repo", "services.py")
193 |         # Line 21 contains self.users[id] = user
194 |         defining_symbol = language_server.request_defining_symbol(file_path, 21, 10)
195 | 
196 |         # Verify that we found the defining symbol
197 |         # The defining symbol method returns a dictionary with information about the defining symbol
198 |         assert defining_symbol is not None
199 |         assert defining_symbol.get("name") == "create_user"
200 | 
201 |         # Verify the location and kind of the symbol
202 |         # SymbolKind.Method = 6 for a method
203 |         assert defining_symbol.get("kind") == SymbolKind.Method.value
204 |         if "location" in defining_symbol and "uri" in defining_symbol["location"]:
205 |             assert "services.py" in defining_symbol["location"]["uri"]
206 | 
207 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
208 |     def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
209 |         """Test request_defining_symbol for an imported class."""
210 |         # Test finding the definition of the 'User' class used in the UserService.create_user method
211 |         file_path = os.path.join("test_repo", "services.py")
212 |         # Line 20 references 'User' which was imported from models
213 |         defining_symbol = language_server.request_defining_symbol(file_path, 20, 15)
214 | 
215 |         # Verify that we found the defining symbol - this should be the User class from models
216 |         assert defining_symbol is not None
217 |         assert defining_symbol.get("name") == "User"
218 | 
219 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
220 |     def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None:
221 |         """Test request_defining_symbol for a method call."""
222 |         # Create an example file path for a file that calls UserService.create_user
223 |         examples_file_path = os.path.join("examples", "user_management.py")
224 | 
225 |         # Find the line number where create_user is called
226 |         # This could vary, so we'll use a relative position that makes sense
227 |         defining_symbol = language_server.request_defining_symbol(examples_file_path, 10, 30)
228 | 
229 |         # Verify that we found the defining symbol - should be the create_user method
230 |         # Because this might fail if the structure isn't exactly as expected, we'll use try-except
231 |         try:
232 |             assert defining_symbol is not None
233 |             assert defining_symbol.get("name") == "create_user"
234 |             # The defining symbol should be in the services.py file
235 |             if "location" in defining_symbol and "uri" in defining_symbol["location"]:
236 |                 assert "services.py" in defining_symbol["location"]["uri"]
237 |         except AssertionError:
238 |             # If the file structure doesn't match what we expect, we can't guarantee this test
239 |             # will pass, so we'll consider it a warning rather than a failure
240 |             import warnings
241 | 
242 |             warnings.warn("Could not verify method call definition - file structure may differ from expected")
243 | 
244 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
245 |     def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
246 |         """Test request_defining_symbol for a position with no symbol."""
247 |         # Test for a position with no symbol (e.g., whitespace or comment)
248 |         file_path = os.path.join("test_repo", "services.py")
249 |         # Line 3 is a blank line
250 |         defining_symbol = language_server.request_defining_symbol(file_path, 3, 0)
251 | 
252 |         # Should return None for positions with no symbol
253 |         assert defining_symbol is None
254 | 
255 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
256 |     def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None:
257 |         """Test request_containing_symbol where the symbol is a variable."""
258 |         # Test for a position inside a variable definition
259 |         file_path = os.path.join("test_repo", "services.py")
260 |         # Line 74 defines the 'user' variable
261 |         containing_symbol = language_server.request_containing_symbol(file_path, 73, 1)
262 | 
263 |         # Verify that we found the containing symbol
264 |         assert containing_symbol is not None
265 |         assert containing_symbol["name"] == "user_var_str"
266 |         assert containing_symbol["kind"] == SymbolKind.Variable
267 | 
268 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
269 |     def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None:
270 |         """Test request_defining_symbol for a nested function or closure."""
271 |         # Use the existing nested.py file which contains nested classes and methods
272 |         file_path = os.path.join("test_repo", "nested.py")
273 | 
274 |         # Test 1: Find definition of nested method - line with 'b = OuterClass().NestedClass().find_me()'
275 |         defining_symbol = language_server.request_defining_symbol(file_path, 15, 35)  # Position of find_me() call
276 | 
277 |         # This should resolve to the find_me method in the NestedClass
278 |         assert defining_symbol is not None
279 |         assert defining_symbol.get("name") == "find_me"
280 |         assert defining_symbol.get("kind") == SymbolKind.Method.value
281 | 
282 |         # Test 2: Find definition of the nested class
283 |         defining_symbol = language_server.request_defining_symbol(file_path, 15, 18)  # Position of NestedClass
284 | 
285 |         # This should resolve to the NestedClass
286 |         assert defining_symbol is not None
287 |         assert defining_symbol.get("name") == "NestedClass"
288 |         assert defining_symbol.get("kind") == SymbolKind.Class.value
289 | 
290 |         # Test 3: Find definition of a method-local function
291 |         defining_symbol = language_server.request_defining_symbol(file_path, 9, 15)  # Position inside func_within_func
292 | 
293 |         # This is challenging for many language servers and may fail
294 |         try:
295 |             assert defining_symbol is not None
296 |             assert defining_symbol.get("name") == "func_within_func"
297 |         except (AssertionError, TypeError, KeyError):
298 |             # This is expected to potentially fail in many implementations
299 |             import warnings
300 | 
301 |             warnings.warn("Could not resolve nested class method definition - implementation limitation")
302 | 
303 |         # Test 2: Find definition of the nested class
304 |         defining_symbol = language_server.request_defining_symbol(file_path, 15, 18)  # Position of NestedClass
305 | 
306 |         # This should resolve to the NestedClass
307 |         assert defining_symbol is not None
308 |         assert defining_symbol.get("name") == "NestedClass"
309 |         assert defining_symbol.get("kind") == SymbolKind.Class.value
310 | 
311 |         # Test 3: Find definition of a method-local function
312 |         defining_symbol = language_server.request_defining_symbol(file_path, 9, 15)  # Position inside func_within_func
313 | 
314 |         # This is challenging for many language servers and may fail
315 |         assert defining_symbol is not None
316 |         assert defining_symbol.get("name") == "func_within_func"
317 |         assert defining_symbol.get("kind") == SymbolKind.Function.value
318 | 
319 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
320 |     def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
321 |         """Test the integration between different symbol-related methods."""
322 |         # This test demonstrates using the various symbol methods together
323 |         # by finding a symbol and then checking its definition
324 | 
325 |         file_path = os.path.join("test_repo", "services.py")
326 | 
327 |         # First approach: Use a method from the UserService class
328 |         # Step 1: Find a method we know exists
329 |         containing_symbol = language_server.request_containing_symbol(file_path, 15, 8)  # create_user method
330 |         assert containing_symbol is not None
331 |         assert containing_symbol["name"] == "create_user"
332 | 
333 |         # Step 2: Get the defining symbol for the same position
334 |         # This should be the same method
335 |         defining_symbol = language_server.request_defining_symbol(file_path, 15, 8)
336 |         assert defining_symbol is not None
337 |         assert defining_symbol["name"] == "create_user"
338 | 
339 |         # Step 3: Verify that they refer to the same symbol
340 |         assert defining_symbol["kind"] == containing_symbol["kind"]
341 |         if "location" in defining_symbol and "location" in containing_symbol:
342 |             assert defining_symbol["location"]["uri"] == containing_symbol["location"]["uri"]
343 | 
344 |         # The integration test is successful if we've gotten this far,
345 |         # as it demonstrates the integration between request_containing_symbol and request_defining_symbol
346 | 
347 |         # Try to get the container information for our method, but be flexible
348 |         # since implementations may vary
349 |         container_name = defining_symbol.get("containerName", None)
350 |         if container_name and "UserService" in container_name:
351 |             # If containerName contains UserService, that's a valid implementation
352 |             pass
353 |         else:
354 |             # Try an alternative approach - looking for the containing class
355 |             try:
356 |                 # Look for the class symbol in the file
357 |                 for line in range(5, 12):  # Approximate range where UserService class should be defined
358 |                     symbol = language_server.request_containing_symbol(file_path, line, 5)  # column 5 should be within class definition
359 |                     if symbol and symbol.get("name") == "UserService" and symbol.get("kind") == SymbolKind.Class.value:
360 |                         # Found the class - this is also a valid implementation
361 |                         break
362 |             except Exception:
363 |                 # Just log a warning - this is an alternative verification and not essential
364 |                 import warnings
365 | 
366 |                 warnings.warn("Could not verify container hierarchy - implementation detail")
367 | 
368 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
369 |     def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None:
370 |         """Test that the symbol tree structure is correctly built."""
371 |         # Get all symbols in the test file
372 |         repo_structure = language_server.request_full_symbol_tree()
373 |         assert len(repo_structure) == 1
374 |         # Assert that the root symbol is the test_repo directory
375 |         assert repo_structure[0]["name"] == "test_repo"
376 |         assert repo_structure[0]["kind"] == SymbolKind.Package
377 |         assert "children" in repo_structure[0]
378 |         # Assert that the children are the top-level packages
379 |         child_names = {child["name"] for child in repo_structure[0]["children"]}
380 |         child_kinds = {child["kind"] for child in repo_structure[0]["children"]}
381 |         assert child_names == {"test_repo", "custom_test", "examples", "scripts"}
382 |         assert child_kinds == {SymbolKind.Package}
383 |         examples_package = next(child for child in repo_structure[0]["children"] if child["name"] == "examples")
384 |         # assert that children are __init__ and user_management
385 |         assert {child["name"] for child in examples_package["children"]} == {"__init__", "user_management"}
386 |         assert {child["kind"] for child in examples_package["children"]} == {SymbolKind.File}
387 | 
388 |         # assert that tree of user_management node is same as retrieved directly
389 |         user_management_node = next(child for child in examples_package["children"] if child["name"] == "user_management")
390 |         if "location" in user_management_node and "relativePath" in user_management_node["location"]:
391 |             user_management_rel_path = user_management_node["location"]["relativePath"]
392 |             assert user_management_rel_path == os.path.join("examples", "user_management.py")
393 |             _, user_management_roots = language_server.request_document_symbols(os.path.join("examples", "user_management.py"))
394 |             assert user_management_roots == user_management_node["children"]
395 | 
396 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
397 |     def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None:
398 |         """Test that the symbol tree structure is correctly built."""
399 |         # Get all symbols in the test file
400 |         examples_package_roots = language_server.request_full_symbol_tree(within_relative_path="examples")
401 |         assert len(examples_package_roots) == 1
402 |         examples_package = examples_package_roots[0]
403 |         assert examples_package["name"] == "examples"
404 |         assert examples_package["kind"] == SymbolKind.Package
405 |         # assert that children are __init__ and user_management
406 |         assert {child["name"] for child in examples_package["children"]} == {"__init__", "user_management"}
407 |         assert {child["kind"] for child in examples_package["children"]} == {SymbolKind.File}
408 | 
409 |         # assert that tree of user_management node is same as retrieved directly
410 |         user_management_node = next(child for child in examples_package["children"] if child["name"] == "user_management")
411 |         if "location" in user_management_node and "relativePath" in user_management_node["location"]:
412 |             user_management_rel_path = user_management_node["location"]["relativePath"]
413 |             assert user_management_rel_path == os.path.join("examples", "user_management.py")
414 |             _, user_management_roots = language_server.request_document_symbols(os.path.join("examples", "user_management.py"))
415 |             assert user_management_roots == user_management_node["children"]
416 | 
417 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
418 |     def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
419 |         """Test that request_dir_overview returns correct symbol information for files in a directory."""
420 |         # Get overview of the examples directory
421 |         overview = language_server.request_dir_overview("test_repo")
422 | 
423 |         # Verify that we have entries for both files
424 |         assert os.path.join("test_repo", "nested.py") in overview
425 | 
426 |         # Get the symbols for user_management.py
427 |         services_symbols = overview[os.path.join("test_repo", "services.py")]
428 |         assert len(services_symbols) > 0
429 | 
430 |         # Check for specific symbols from services.py
431 |         expected_symbols = {
432 |             "UserService",
433 |             "ItemService",
434 |             "create_service_container",
435 |             "user_var_str",
436 |             "user_service",
437 |         }
438 |         retrieved_symbols = {symbol["name"] for symbol in services_symbols if "name" in symbol}
439 |         assert expected_symbols.issubset(retrieved_symbols)
440 | 
441 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
442 |     def test_request_document_overview(self, language_server: SolidLanguageServer) -> None:
443 |         """Test that request_document_overview returns correct symbol information for a file."""
444 |         # Get overview of the user_management.py file
445 |         overview = language_server.request_document_overview(os.path.join("examples", "user_management.py"))
446 | 
447 |         # Verify that we have entries for both files
448 |         symbol_names = {LanguageServerSymbol(s_info).name for s_info in overview}
449 |         assert {"UserStats", "UserManager", "process_user_data", "main"}.issubset(symbol_names)
450 | 
451 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
452 |     def test_containing_symbol_of_var_is_file(self, language_server: SolidLanguageServer) -> None:
453 |         """Test that the containing symbol of a variable is the file itself."""
454 |         # Get the containing symbol of a variable in a file
455 |         file_path = os.path.join("test_repo", "services.py")
456 |         # import of typing
457 |         references_to_typing = [
458 |             ref.symbol
459 |             for ref in language_server.request_referencing_symbols(file_path, 4, 6, include_imports=False, include_file_symbols=True)
460 |         ]
461 |         assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File}
462 |         assert {ref["body"] for ref in references_to_typing} == {""}
463 | 
464 |         # now include bodies
465 |         references_to_typing = [
466 |             ref.symbol
467 |             for ref in language_server.request_referencing_symbols(
468 |                 file_path, 4, 6, include_imports=False, include_file_symbols=True, include_body=True
469 |             )
470 |         ]
471 |         assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File}
472 |         assert references_to_typing[0]["body"]
473 | 
```

--------------------------------------------------------------------------------
/src/serena/symbol.py:
--------------------------------------------------------------------------------

```python
  1 | import json
  2 | import logging
  3 | import os
  4 | from abc import ABC, abstractmethod
  5 | from collections.abc import Iterator, Sequence
  6 | from dataclasses import asdict, dataclass
  7 | from typing import TYPE_CHECKING, Any, Self, Union
  8 | 
  9 | from sensai.util.string import ToStringMixin
 10 | 
 11 | from solidlsp import SolidLanguageServer
 12 | from solidlsp.ls import ReferenceInSymbol as LSPReferenceInSymbol
 13 | from solidlsp.ls_types import Position, SymbolKind, UnifiedSymbolInformation
 14 | 
 15 | from .project import Project
 16 | 
 17 | if TYPE_CHECKING:
 18 |     from .agent import SerenaAgent
 19 | 
 20 | log = logging.getLogger(__name__)
 21 | 
 22 | 
 23 | @dataclass
 24 | class LanguageServerSymbolLocation:
 25 |     """
 26 |     Represents the (start) location of a symbol identifier, which, within Serena, uniquely identifies the symbol.
 27 |     """
 28 | 
 29 |     relative_path: str | None
 30 |     """
 31 |     the relative path of the file containing the symbol; if None, the symbol is defined outside of the project's scope
 32 |     """
 33 |     line: int | None
 34 |     """
 35 |     the line number in which the symbol identifier is defined (if the symbol is a function, class, etc.);
 36 |     may be None for some types of symbols (e.g. SymbolKind.File)
 37 |     """
 38 |     column: int | None
 39 |     """
 40 |     the column number in which the symbol identifier is defined (if the symbol is a function, class, etc.);
 41 |     may be None for some types of symbols (e.g. SymbolKind.File)
 42 |     """
 43 | 
 44 |     def __post_init__(self) -> None:
 45 |         if self.relative_path is not None:
 46 |             self.relative_path = self.relative_path.replace("/", os.path.sep)
 47 | 
 48 |     def to_dict(self, include_relative_path: bool = True) -> dict[str, Any]:
 49 |         result = asdict(self)
 50 |         if not include_relative_path:
 51 |             result.pop("relative_path", None)
 52 |         return result
 53 | 
 54 |     def has_position_in_file(self) -> bool:
 55 |         return self.relative_path is not None and self.line is not None and self.column is not None
 56 | 
 57 | 
 58 | @dataclass
 59 | class PositionInFile:
 60 |     """
 61 |     Represents a character position within a file
 62 |     """
 63 | 
 64 |     line: int
 65 |     """
 66 |     the 0-based line number in the file
 67 |     """
 68 |     col: int
 69 |     """
 70 |     the 0-based column
 71 |     """
 72 | 
 73 |     def to_lsp_position(self) -> Position:
 74 |         """
 75 |         Convert to LSP Position.
 76 |         """
 77 |         return Position(line=self.line, character=self.col)
 78 | 
 79 | 
 80 | class Symbol(ABC):
 81 |     @abstractmethod
 82 |     def get_body_start_position(self) -> PositionInFile | None:
 83 |         pass
 84 | 
 85 |     @abstractmethod
 86 |     def get_body_end_position(self) -> PositionInFile | None:
 87 |         pass
 88 | 
 89 |     def get_body_start_position_or_raise(self) -> PositionInFile:
 90 |         """
 91 |         Get the start position of the symbol body, raising an error if it is not defined.
 92 |         """
 93 |         pos = self.get_body_start_position()
 94 |         if pos is None:
 95 |             raise ValueError(f"Body start position is not defined for {self}")
 96 |         return pos
 97 | 
 98 |     def get_body_end_position_or_raise(self) -> PositionInFile:
 99 |         """
100 |         Get the end position of the symbol body, raising an error if it is not defined.
101 |         """
102 |         pos = self.get_body_end_position()
103 |         if pos is None:
104 |             raise ValueError(f"Body end position is not defined for {self}")
105 |         return pos
106 | 
107 |     @abstractmethod
108 |     def is_neighbouring_definition_separated_by_empty_line(self) -> bool:
109 |         """
110 |         :return: whether a symbol definition of this symbol's kind is usually separated from the
111 |             previous/next definition by at least one empty line.
112 |         """
113 | 
114 | 
115 | class LanguageServerSymbol(Symbol, ToStringMixin):
116 |     _NAME_PATH_SEP = "/"
117 | 
118 |     @staticmethod
119 |     def match_name_path(
120 |         name_path: str,
121 |         symbol_name_path_parts: list[str],
122 |         substring_matching: bool,
123 |     ) -> bool:
124 |         """
125 |         Checks if a given `name_path` matches a symbol's qualified name parts.
126 |         See docstring of `Symbol.find` for more details.
127 |         """
128 |         assert name_path, "name_path must not be empty"
129 |         assert symbol_name_path_parts, "symbol_name_path_parts must not be empty"
130 |         name_path_sep = LanguageServerSymbol._NAME_PATH_SEP
131 | 
132 |         is_absolute_pattern = name_path.startswith(name_path_sep)
133 |         pattern_parts = name_path.lstrip(name_path_sep).rstrip(name_path_sep).split(name_path_sep)
134 | 
135 |         # filtering based on ancestors
136 |         if len(pattern_parts) > len(symbol_name_path_parts):
137 |             # can't possibly match if pattern has more parts than symbol
138 |             return False
139 |         if is_absolute_pattern and len(pattern_parts) != len(symbol_name_path_parts):
140 |             # for absolute patterns, the number of parts must match exactly
141 |             return False
142 |         if symbol_name_path_parts[-len(pattern_parts) : -1] != pattern_parts[:-1]:
143 |             # ancestors must match
144 |             return False
145 | 
146 |         # matching the last part of the symbol name
147 |         name_to_match = pattern_parts[-1]
148 |         symbol_name = symbol_name_path_parts[-1]
149 |         if substring_matching:
150 |             return name_to_match in symbol_name
151 |         else:
152 |             return name_to_match == symbol_name
153 | 
154 |     def __init__(self, symbol_root_from_ls: UnifiedSymbolInformation) -> None:
155 |         self.symbol_root = symbol_root_from_ls
156 | 
157 |     def _tostring_includes(self) -> list[str]:
158 |         return []
159 | 
160 |     def _tostring_additional_entries(self) -> dict[str, Any]:
161 |         return dict(name=self.name, kind=self.kind, num_children=len(self.symbol_root["children"]))
162 | 
163 |     @property
164 |     def name(self) -> str:
165 |         return self.symbol_root["name"]
166 | 
167 |     @property
168 |     def kind(self) -> str:
169 |         return SymbolKind(self.symbol_kind).name
170 | 
171 |     @property
172 |     def symbol_kind(self) -> SymbolKind:
173 |         return self.symbol_root["kind"]
174 | 
175 |     def is_neighbouring_definition_separated_by_empty_line(self) -> bool:
176 |         return self.symbol_kind in (SymbolKind.Function, SymbolKind.Method, SymbolKind.Class, SymbolKind.Interface, SymbolKind.Struct)
177 | 
178 |     @property
179 |     def relative_path(self) -> str | None:
180 |         location = self.symbol_root.get("location")
181 |         if location:
182 |             return location.get("relativePath")
183 |         return None
184 | 
185 |     @property
186 |     def location(self) -> LanguageServerSymbolLocation:
187 |         """
188 |         :return: the start location of the actual symbol identifier
189 |         """
190 |         return LanguageServerSymbolLocation(relative_path=self.relative_path, line=self.line, column=self.column)
191 | 
192 |     @property
193 |     def body_start_position(self) -> Position | None:
194 |         location = self.symbol_root.get("location")
195 |         if location:
196 |             range_info = location.get("range")
197 |             if range_info:
198 |                 start_pos = range_info.get("start")
199 |                 if start_pos:
200 |                     return start_pos
201 |         return None
202 | 
203 |     @property
204 |     def body_end_position(self) -> Position | None:
205 |         location = self.symbol_root.get("location")
206 |         if location:
207 |             range_info = location.get("range")
208 |             if range_info:
209 |                 end_pos = range_info.get("end")
210 |                 if end_pos:
211 |                     return end_pos
212 |         return None
213 | 
214 |     def get_body_start_position(self) -> PositionInFile | None:
215 |         start_pos = self.body_start_position
216 |         if start_pos is None:
217 |             return None
218 |         return PositionInFile(line=start_pos["line"], col=start_pos["character"])
219 | 
220 |     def get_body_end_position(self) -> PositionInFile | None:
221 |         end_pos = self.body_end_position
222 |         if end_pos is None:
223 |             return None
224 |         return PositionInFile(line=end_pos["line"], col=end_pos["character"])
225 | 
226 |     def get_body_line_numbers(self) -> tuple[int | None, int | None]:
227 |         start_pos = self.body_start_position
228 |         end_pos = self.body_end_position
229 |         start_line = start_pos["line"] if start_pos else None
230 |         end_line = end_pos["line"] if end_pos else None
231 |         return start_line, end_line
232 | 
233 |     @property
234 |     def line(self) -> int | None:
235 |         """
236 |         :return: the line in which the symbol identifier is defined.
237 |         """
238 |         if "selectionRange" in self.symbol_root:
239 |             return self.symbol_root["selectionRange"]["start"]["line"]
240 |         else:
241 |             # line is expected to be undefined for some types of symbols (e.g. SymbolKind.File)
242 |             return None
243 | 
244 |     @property
245 |     def column(self) -> int | None:
246 |         if "selectionRange" in self.symbol_root:
247 |             return self.symbol_root["selectionRange"]["start"]["character"]
248 |         else:
249 |             # precise location is expected to be undefined for some types of symbols (e.g. SymbolKind.File)
250 |             return None
251 | 
252 |     @property
253 |     def body(self) -> str | None:
254 |         return self.symbol_root.get("body")
255 | 
256 |     def get_name_path(self) -> str:
257 |         """
258 |         Get the name path of the symbol (e.g. "class/method/inner_function").
259 |         """
260 |         return self._NAME_PATH_SEP.join(self.get_name_path_parts())
261 | 
262 |     def get_name_path_parts(self) -> list[str]:
263 |         """
264 |         Get the parts of the name path of the symbol (e.g. ["class", "method", "inner_function"]).
265 |         """
266 |         ancestors_within_file = list(self.iter_ancestors(up_to_symbol_kind=SymbolKind.File))
267 |         ancestors_within_file.reverse()
268 |         return [a.name for a in ancestors_within_file] + [self.name]
269 | 
270 |     def iter_children(self) -> Iterator[Self]:
271 |         for c in self.symbol_root["children"]:
272 |             yield self.__class__(c)
273 | 
274 |     def iter_ancestors(self, up_to_symbol_kind: SymbolKind | None = None) -> Iterator[Self]:
275 |         """
276 |         Iterate over all ancestors of the symbol, starting with the parent and going up to the root or
277 |         the given symbol kind.
278 | 
279 |         :param up_to_symbol_kind: if provided, iteration will stop *before* the first ancestor of the given kind.
280 |             A typical use case is to pass `SymbolKind.File` or `SymbolKind.Package`.
281 |         """
282 |         parent = self.get_parent()
283 |         if parent is not None:
284 |             if up_to_symbol_kind is None or parent.symbol_kind != up_to_symbol_kind:
285 |                 yield parent
286 |                 yield from parent.iter_ancestors(up_to_symbol_kind=up_to_symbol_kind)
287 | 
288 |     def get_parent(self) -> Self | None:
289 |         parent_root = self.symbol_root.get("parent")
290 |         if parent_root is None:
291 |             return None
292 |         return self.__class__(parent_root)
293 | 
294 |     def find(
295 |         self,
296 |         name_path: str,
297 |         substring_matching: bool = False,
298 |         include_kinds: Sequence[SymbolKind] | None = None,
299 |         exclude_kinds: Sequence[SymbolKind] | None = None,
300 |     ) -> list[Self]:
301 |         """
302 |         Find all symbols within the symbol's subtree that match the given `name_path`.
303 |         The matching behavior is determined by the structure of `name_path`, which can
304 |         either be a simple name (e.g. "method") or a name path like "class/method" (relative name path)
305 |         or "/class/method" (absolute name path).
306 | 
307 |         Key aspects of the name path matching behavior:
308 |         - Trailing slashes in `name_path` play no role and are ignored.
309 |         - The name of the retrieved symbols will match (either exactly or as a substring)
310 |           the last segment of `name_path`, while other segments will restrict the search to symbols that
311 |           have a desired sequence of ancestors.
312 |         - If there is no starting or intermediate slash in `name_path`, there is no
313 |           restriction on the ancestor symbols. For example, passing `method` will match
314 |           against symbols with name paths like `method`, `class/method`, `class/nested_class/method`, etc.
315 |         - If `name_path` contains a `/` but doesn't start with a `/`, the matching is restricted to symbols
316 |           with the same ancestors as the last segment of `name_path`. For example, passing `class/method` will match against
317 |           `class/method` as well as `nested_class/class/method` but not `method`.
318 |         - If `name_path` starts with a `/`, it will be treated as an absolute name path pattern, meaning
319 |           that the first segment of it must match the first segment of the symbol's name path.
320 |           For example, passing `/class` will match only against top-level symbols like `class` but not against `nested_class/class`.
321 |           Passing `/class/method` will match against `class/method` but not `nested_class/class/method` or `method`.
322 | 
323 |         :param name_path: the name path to match against
324 |         :param substring_matching: whether to use substring matching (as opposed to exact matching)
325 |             of the last segment of `name_path` against the symbol name.
326 |         :param include_kinds: an optional sequence of ints representing the LSP symbol kind.
327 |             If provided, only symbols of the given kinds will be included in the result.
328 |         :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result.
329 |         """
330 |         result = []
331 | 
332 |         def should_include(s: "LanguageServerSymbol") -> bool:
333 |             if include_kinds is not None and s.symbol_kind not in include_kinds:
334 |                 return False
335 |             if exclude_kinds is not None and s.symbol_kind in exclude_kinds:
336 |                 return False
337 |             return LanguageServerSymbol.match_name_path(
338 |                 name_path=name_path,
339 |                 symbol_name_path_parts=s.get_name_path_parts(),
340 |                 substring_matching=substring_matching,
341 |             )
342 | 
343 |         def traverse(s: "LanguageServerSymbol") -> None:
344 |             if should_include(s):
345 |                 result.append(s)
346 |             for c in s.iter_children():
347 |                 traverse(c)
348 | 
349 |         traverse(self)
350 |         return result
351 | 
352 |     def to_dict(
353 |         self,
354 |         kind: bool = False,
355 |         location: bool = False,
356 |         depth: int = 0,
357 |         include_body: bool = False,
358 |         include_children_body: bool = False,
359 |         include_relative_path: bool = True,
360 |     ) -> dict[str, Any]:
361 |         """
362 |         Converts the symbol to a dictionary.
363 | 
364 |         :param kind: whether to include the kind of the symbol
365 |         :param location: whether to include the location of the symbol
366 |         :param depth: the depth of the symbol
367 |         :param include_body: whether to include the body of the top-level symbol.
368 |         :param include_children_body: whether to also include the body of the children.
369 |             Note that the body of the children is part of the body of the parent symbol,
370 |             so there is usually no need to set this to True unless you want process the output
371 |             and pass the children without passing the parent body to the LM.
372 |         :param include_relative_path: whether to include the relative path of the symbol in the location
373 |             entry. Relative paths of the symbol's children are always excluded.
374 |         :return: a dictionary representation of the symbol
375 |         """
376 |         result: dict[str, Any] = {"name": self.name, "name_path": self.get_name_path()}
377 | 
378 |         if kind:
379 |             result["kind"] = self.kind
380 | 
381 |         if location:
382 |             result["location"] = self.location.to_dict(include_relative_path=include_relative_path)
383 |             body_start_line, body_end_line = self.get_body_line_numbers()
384 |             result["body_location"] = {"start_line": body_start_line, "end_line": body_end_line}
385 | 
386 |         if include_body:
387 |             if self.body is None:
388 |                 log.warning("Requested body for symbol, but it is not present. The symbol might have been loaded with include_body=False.")
389 |             result["body"] = self.body
390 | 
391 |         def add_children(s: Self) -> list[dict[str, Any]]:
392 |             children = []
393 |             for c in s.iter_children():
394 |                 children.append(
395 |                     c.to_dict(
396 |                         kind=kind,
397 |                         location=location,
398 |                         depth=depth - 1,
399 |                         include_body=include_children_body,
400 |                         include_children_body=include_children_body,
401 |                         # all children have the same relative path as the parent
402 |                         include_relative_path=False,
403 |                     )
404 |                 )
405 |             return children
406 | 
407 |         if depth > 0:
408 |             result["children"] = add_children(self)
409 | 
410 |         return result
411 | 
412 | 
413 | @dataclass
414 | class ReferenceInLanguageServerSymbol(ToStringMixin):
415 |     """
416 |     Represents the location of a reference to another symbol within a symbol/file.
417 | 
418 |     The contained symbol is the symbol within which the reference is located,
419 |     not the symbol that is referenced.
420 |     """
421 | 
422 |     symbol: LanguageServerSymbol
423 |     """
424 |     the symbol within which the reference is located
425 |     """
426 |     line: int
427 |     """
428 |     the line number in which the reference is located (0-based)
429 |     """
430 |     character: int
431 |     """
432 |     the column number in which the reference is located (0-based)
433 |     """
434 | 
435 |     @classmethod
436 |     def from_lsp_reference(cls, reference: LSPReferenceInSymbol) -> Self:
437 |         return cls(symbol=LanguageServerSymbol(reference.symbol), line=reference.line, character=reference.character)
438 | 
439 |     def get_relative_path(self) -> str | None:
440 |         return self.symbol.location.relative_path
441 | 
442 | 
443 | class LanguageServerSymbolRetriever:
444 |     def __init__(self, lang_server: SolidLanguageServer, agent: Union["SerenaAgent", None] = None) -> None:
445 |         """
446 |         :param lang_server: the language server to use for symbol retrieval as well as editing operations.
447 |         :param agent: the agent to use (only needed for marking files as modified). You can pass None if you don't
448 |             need an agent to be aware of file modifications performed by the symbol manager.
449 |         """
450 |         self._lang_server = lang_server
451 |         self.agent = agent
452 | 
453 |     def set_language_server(self, lang_server: SolidLanguageServer) -> None:
454 |         """
455 |         Set the language server to use for symbol retrieval and editing operations.
456 |         This is useful if you want to change the language server after initializing the SymbolManager.
457 |         """
458 |         self._lang_server = lang_server
459 | 
460 |     def get_language_server(self) -> SolidLanguageServer:
461 |         return self._lang_server
462 | 
463 |     def find_by_name(
464 |         self,
465 |         name_path: str,
466 |         include_body: bool = False,
467 |         include_kinds: Sequence[SymbolKind] | None = None,
468 |         exclude_kinds: Sequence[SymbolKind] | None = None,
469 |         substring_matching: bool = False,
470 |         within_relative_path: str | None = None,
471 |     ) -> list[LanguageServerSymbol]:
472 |         """
473 |         Find all symbols that match the given name. See docstring of `Symbol.find` for more details.
474 |         The only parameter not mentioned there is `within_relative_path`, which can be used to restrict the search
475 |         to symbols within a specific file or directory.
476 |         """
477 |         symbols: list[LanguageServerSymbol] = []
478 |         symbol_roots = self._lang_server.request_full_symbol_tree(within_relative_path=within_relative_path, include_body=include_body)
479 |         for root in symbol_roots:
480 |             symbols.extend(
481 |                 LanguageServerSymbol(root).find(
482 |                     name_path, include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching
483 |                 )
484 |             )
485 |         return symbols
486 | 
487 |     def get_document_symbols(self, relative_path: str) -> list[LanguageServerSymbol]:
488 |         symbol_dicts, _roots = self._lang_server.request_document_symbols(relative_path, include_body=False)
489 |         symbols = [LanguageServerSymbol(s) for s in symbol_dicts]
490 |         return symbols
491 | 
492 |     def find_by_location(self, location: LanguageServerSymbolLocation) -> LanguageServerSymbol | None:
493 |         if location.relative_path is None:
494 |             return None
495 |         symbol_dicts, _roots = self._lang_server.request_document_symbols(location.relative_path, include_body=False)
496 |         for symbol_dict in symbol_dicts:
497 |             symbol = LanguageServerSymbol(symbol_dict)
498 |             if symbol.location == location:
499 |                 return symbol
500 |         return None
501 | 
502 |     def find_referencing_symbols(
503 |         self,
504 |         name_path: str,
505 |         relative_file_path: str,
506 |         include_body: bool = False,
507 |         include_kinds: Sequence[SymbolKind] | None = None,
508 |         exclude_kinds: Sequence[SymbolKind] | None = None,
509 |     ) -> list[ReferenceInLanguageServerSymbol]:
510 |         """
511 |         Find all symbols that reference the symbol with the given name.
512 |         If multiple symbols fit the name (e.g. for variables that are overwritten), will use the first one.
513 | 
514 |         :param name_path: the name path of the symbol to find
515 |         :param relative_file_path: the relative path of the file in which the referenced symbol is defined.
516 |         :param include_body: whether to include the body of all symbols in the result.
517 |             Not recommended, as the referencing symbols will often be files, and thus the bodies will be very long.
518 |         :param include_kinds: which kinds of symbols to include in the result.
519 |         :param exclude_kinds: which kinds of symbols to exclude from the result.
520 |         """
521 |         symbol_candidates = self.find_by_name(name_path, substring_matching=False, within_relative_path=relative_file_path)
522 |         if len(symbol_candidates) == 0:
523 |             log.warning(f"No symbol with name {name_path} found in file {relative_file_path}")
524 |             return []
525 |         if len(symbol_candidates) > 1:
526 |             log.error(
527 |                 f"Found {len(symbol_candidates)} symbols with name {name_path} in file {relative_file_path}."
528 |                 f"May be an overwritten variable, in which case you can ignore this error. Proceeding with the first one. "
529 |                 f"Found symbols for {name_path=} in {relative_file_path=}: \n"
530 |                 f"{json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2)}"
531 |             )
532 |         symbol = symbol_candidates[0]
533 |         return self.find_referencing_symbols_by_location(
534 |             symbol.location, include_body=include_body, include_kinds=include_kinds, exclude_kinds=exclude_kinds
535 |         )
536 | 
537 |     def find_referencing_symbols_by_location(
538 |         self,
539 |         symbol_location: LanguageServerSymbolLocation,
540 |         include_body: bool = False,
541 |         include_kinds: Sequence[SymbolKind] | None = None,
542 |         exclude_kinds: Sequence[SymbolKind] | None = None,
543 |     ) -> list[ReferenceInLanguageServerSymbol]:
544 |         """
545 |         Find all symbols that reference the symbol at the given location.
546 | 
547 |         :param symbol_location: the location of the symbol for which to find references.
548 |             Does not need to include an end_line, as it is unused in the search.
549 |         :param include_body: whether to include the body of all symbols in the result.
550 |             Not recommended, as the referencing symbols will often be files, and thus the bodies will be very long.
551 |             Note: you can filter out the bodies of the children if you set include_children_body=False
552 |             in the to_dict method.
553 |         :param include_kinds: an optional sequence of ints representing the LSP symbol kind.
554 |             If provided, only symbols of the given kinds will be included in the result.
555 |         :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result.
556 |             Takes precedence over include_kinds.
557 |         :return: a list of symbols that reference the given symbol
558 |         """
559 |         if not symbol_location.has_position_in_file():
560 |             raise ValueError("Symbol location does not contain a valid position in a file")
561 |         assert symbol_location.relative_path is not None
562 |         assert symbol_location.line is not None
563 |         assert symbol_location.column is not None
564 |         references = self._lang_server.request_referencing_symbols(
565 |             relative_file_path=symbol_location.relative_path,
566 |             line=symbol_location.line,
567 |             column=symbol_location.column,
568 |             include_imports=False,
569 |             include_self=False,
570 |             include_body=include_body,
571 |             include_file_symbols=True,
572 |         )
573 | 
574 |         if include_kinds is not None:
575 |             references = [s for s in references if s.symbol["kind"] in include_kinds]
576 | 
577 |         if exclude_kinds is not None:
578 |             references = [s for s in references if s.symbol["kind"] not in exclude_kinds]
579 | 
580 |         return [ReferenceInLanguageServerSymbol.from_lsp_reference(r) for r in references]
581 | 
582 |     @dataclass
583 |     class SymbolOverviewElement:
584 |         name_path: str
585 |         kind: int
586 | 
587 |         @classmethod
588 |         def from_symbol(cls, symbol: LanguageServerSymbol) -> Self:
589 |             return cls(name_path=symbol.get_name_path(), kind=int(symbol.symbol_kind))
590 | 
591 |     def get_symbol_overview(self, relative_path: str) -> dict[str, list[SymbolOverviewElement]]:
592 |         path_to_unified_symbols = self._lang_server.request_overview(relative_path)
593 |         result = {}
594 |         for file_path, unified_symbols in path_to_unified_symbols.items():
595 |             # TODO: maybe include not just top-level symbols? We could filter by kind to exclude variables
596 |             #  The language server methods would need to be adjusted for this.
597 |             result[file_path] = [self.SymbolOverviewElement.from_symbol(LanguageServerSymbol(s)) for s in unified_symbols]
598 |         return result
599 | 
600 | 
601 | class JetBrainsSymbol(Symbol):
602 |     def __init__(self, symbol_dict: dict, project: Project) -> None:
603 |         """
604 |         :param symbol_dict: dictionary as returned by the JetBrains plugin client.
605 |         """
606 |         self._project = project
607 |         self._dict = symbol_dict
608 |         self._cached_file_content: str | None = None
609 |         self._cached_body_start_position: PositionInFile | None = None
610 |         self._cached_body_end_position: PositionInFile | None = None
611 | 
612 |     def get_relative_path(self) -> str:
613 |         return self._dict["relative_path"]
614 | 
615 |     def get_file_content(self) -> str:
616 |         if self._cached_file_content is None:
617 |             path = os.path.join(self._project.project_root, self.get_relative_path())
618 |             with open(path, encoding=self._project.project_config.encoding) as f:
619 |                 self._cached_file_content = f.read()
620 |         return self._cached_file_content
621 | 
622 |     def is_position_in_file_available(self) -> bool:
623 |         return "text_range" in self._dict
624 | 
625 |     def get_body_start_position(self) -> PositionInFile | None:
626 |         if not self.is_position_in_file_available():
627 |             return None
628 |         if self._cached_body_start_position is None:
629 |             pos = self._dict["text_range"]["start_pos"]
630 |             line, col = pos["line"], pos["col"]
631 |             self._cached_body_start_position = PositionInFile(line=line, col=col)
632 |         return self._cached_body_start_position
633 | 
634 |     def get_body_end_position(self) -> PositionInFile | None:
635 |         if not self.is_position_in_file_available():
636 |             return None
637 |         if self._cached_body_end_position is None:
638 |             pos = self._dict["text_range"]["end_pos"]
639 |             line, col = pos["line"], pos["col"]
640 |             self._cached_body_end_position = PositionInFile(line=line, col=col)
641 |         return self._cached_body_end_position
642 | 
643 |     def is_neighbouring_definition_separated_by_empty_line(self) -> bool:
644 |         # NOTE: Symbol types cannot really be differentiated, because types are not handled in a language-agnostic way.
645 |         return False
646 | 
```

--------------------------------------------------------------------------------
/src/serena/agent.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | The Serena Model Context Protocol (MCP) Server
  3 | """
  4 | 
  5 | import multiprocessing
  6 | import os
  7 | import platform
  8 | import sys
  9 | import threading
 10 | import webbrowser
 11 | from collections.abc import Callable
 12 | from concurrent.futures import Future, ThreadPoolExecutor
 13 | from logging import Logger
 14 | from pathlib import Path
 15 | from typing import TYPE_CHECKING, Any, Optional, TypeVar
 16 | 
 17 | from sensai.util import logging
 18 | from sensai.util.logging import LogTime
 19 | 
 20 | from interprompt.jinja_template import JinjaTemplate
 21 | from serena import serena_version
 22 | from serena.analytics import RegisteredTokenCountEstimator, ToolUsageStats
 23 | from serena.config.context_mode import RegisteredContext, SerenaAgentContext, SerenaAgentMode
 24 | from serena.config.serena_config import SerenaConfig, ToolInclusionDefinition, ToolSet, get_serena_managed_in_project_dir
 25 | from serena.dashboard import SerenaDashboardAPI
 26 | from serena.project import Project
 27 | from serena.prompt_factory import SerenaPromptFactory
 28 | from serena.tools import ActivateProjectTool, GetCurrentConfigTool, Tool, ToolMarker, ToolRegistry
 29 | from serena.util.inspection import iter_subclasses
 30 | from serena.util.logging import MemoryLogHandler
 31 | from solidlsp import SolidLanguageServer
 32 | 
 33 | if TYPE_CHECKING:
 34 |     from serena.gui_log_viewer import GuiLogViewer
 35 | 
 36 | log = logging.getLogger(__name__)
 37 | TTool = TypeVar("TTool", bound="Tool")
 38 | T = TypeVar("T")
 39 | SUCCESS_RESULT = "OK"
 40 | 
 41 | 
 42 | class ProjectNotFoundError(Exception):
 43 |     pass
 44 | 
 45 | 
 46 | class MemoriesManager:
 47 |     def __init__(self, project_root: str):
 48 |         self._memory_dir = Path(get_serena_managed_in_project_dir(project_root)) / "memories"
 49 |         self._memory_dir.mkdir(parents=True, exist_ok=True)
 50 | 
 51 |     def _get_memory_file_path(self, name: str) -> Path:
 52 |         # strip all .md from the name. Models tend to get confused, sometimes passing the .md extension and sometimes not.
 53 |         name = name.replace(".md", "")
 54 |         filename = f"{name}.md"
 55 |         return self._memory_dir / filename
 56 | 
 57 |     def load_memory(self, name: str) -> str:
 58 |         memory_file_path = self._get_memory_file_path(name)
 59 |         if not memory_file_path.exists():
 60 |             return f"Memory file {name} not found, consider creating it with the `write_memory` tool if you need it."
 61 |         with open(memory_file_path, encoding="utf-8") as f:
 62 |             return f.read()
 63 | 
 64 |     def save_memory(self, name: str, content: str) -> str:
 65 |         memory_file_path = self._get_memory_file_path(name)
 66 |         with open(memory_file_path, "w", encoding="utf-8") as f:
 67 |             f.write(content)
 68 |         return f"Memory {name} written."
 69 | 
 70 |     def list_memories(self) -> list[str]:
 71 |         return [f.name.replace(".md", "") for f in self._memory_dir.iterdir() if f.is_file()]
 72 | 
 73 |     def delete_memory(self, name: str) -> str:
 74 |         memory_file_path = self._get_memory_file_path(name)
 75 |         memory_file_path.unlink()
 76 |         return f"Memory {name} deleted."
 77 | 
 78 | 
 79 | class AvailableTools:
 80 |     def __init__(self, tools: list[Tool]):
 81 |         """
 82 |         :param tools: the list of available tools
 83 |         """
 84 |         self.tools = tools
 85 |         self.tool_names = [tool.get_name_from_cls() for tool in tools]
 86 |         self.tool_marker_names = set()
 87 |         for marker_class in iter_subclasses(ToolMarker):
 88 |             for tool in tools:
 89 |                 if isinstance(tool, marker_class):
 90 |                     self.tool_marker_names.add(marker_class.__name__)
 91 | 
 92 |     def __len__(self) -> int:
 93 |         return len(self.tools)
 94 | 
 95 | 
 96 | class SerenaAgent:
 97 |     def __init__(
 98 |         self,
 99 |         project: str | None = None,
100 |         project_activation_callback: Callable[[], None] | None = None,
101 |         serena_config: SerenaConfig | None = None,
102 |         context: SerenaAgentContext | None = None,
103 |         modes: list[SerenaAgentMode] | None = None,
104 |         memory_log_handler: MemoryLogHandler | None = None,
105 |     ):
106 |         """
107 |         :param project: the project to load immediately or None to not load any project; may be a path to the project or a name of
108 |             an already registered project;
109 |         :param project_activation_callback: a callback function to be called when a project is activated.
110 |         :param serena_config: the Serena configuration or None to read the configuration from the default location.
111 |         :param context: the context in which the agent is operating, None for default context.
112 |             The context may adjust prompts, tool availability, and tool descriptions.
113 |         :param modes: list of modes in which the agent is operating (they will be combined), None for default modes.
114 |             The modes may adjust prompts, tool availability, and tool descriptions.
115 |         :param memory_log_handler: a MemoryLogHandler instance from which to read log messages; if None, a new one will be created
116 |             if necessary.
117 |         """
118 |         # obtain serena configuration using the decoupled factory function
119 |         self.serena_config = serena_config or SerenaConfig.from_config_file()
120 | 
121 |         # project-specific instances, which will be initialized upon project activation
122 |         self._active_project: Project | None = None
123 |         self.language_server: SolidLanguageServer | None = None
124 |         self.memories_manager: MemoriesManager | None = None
125 | 
126 |         # adjust log level
127 |         serena_log_level = self.serena_config.log_level
128 |         if Logger.root.level > serena_log_level:
129 |             log.info(f"Changing the root logger level to {serena_log_level}")
130 |             Logger.root.setLevel(serena_log_level)
131 | 
132 |         def get_memory_log_handler() -> MemoryLogHandler:
133 |             nonlocal memory_log_handler
134 |             if memory_log_handler is None:
135 |                 memory_log_handler = MemoryLogHandler(level=serena_log_level)
136 |                 Logger.root.addHandler(memory_log_handler)
137 |             return memory_log_handler
138 | 
139 |         # open GUI log window if enabled
140 |         self._gui_log_viewer: Optional["GuiLogViewer"] = None
141 |         if self.serena_config.gui_log_window_enabled:
142 |             if platform.system() == "Darwin":
143 |                 log.warning("GUI log window is not supported on macOS")
144 |             else:
145 |                 # even importing on macOS may fail if tkinter dependencies are unavailable (depends on Python interpreter installation
146 |                 # which uv used as a base, unfortunately)
147 |                 from serena.gui_log_viewer import GuiLogViewer
148 | 
149 |                 self._gui_log_viewer = GuiLogViewer("dashboard", title="Serena Logs", memory_log_handler=get_memory_log_handler())
150 |                 self._gui_log_viewer.start()
151 | 
152 |         # set the agent context
153 |         if context is None:
154 |             context = SerenaAgentContext.load_default()
155 |         self._context = context
156 | 
157 |         # instantiate all tool classes
158 |         self._all_tools: dict[type[Tool], Tool] = {tool_class: tool_class(self) for tool_class in ToolRegistry().get_all_tool_classes()}
159 |         tool_names = [tool.get_name_from_cls() for tool in self._all_tools.values()]
160 | 
161 |         # If GUI log window is enabled, set the tool names for highlighting
162 |         if self._gui_log_viewer is not None:
163 |             self._gui_log_viewer.set_tool_names(tool_names)
164 | 
165 |         self._tool_usage_stats: ToolUsageStats | None = None
166 |         if self.serena_config.record_tool_usage_stats:
167 |             token_count_estimator = RegisteredTokenCountEstimator[self.serena_config.token_count_estimator]
168 |             log.info(f"Tool usage statistics recording is enabled with token count estimator: {token_count_estimator.name}.")
169 |             self._tool_usage_stats = ToolUsageStats(token_count_estimator)
170 | 
171 |         # start the dashboard (web frontend), registering its log handler
172 |         if self.serena_config.web_dashboard:
173 |             self._dashboard_thread, port = SerenaDashboardAPI(
174 |                 get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats
175 |             ).run_in_thread()
176 |             dashboard_url = f"http://127.0.0.1:{port}/dashboard/index.html"
177 |             log.info("Serena web dashboard started at %s", dashboard_url)
178 |             if self.serena_config.web_dashboard_open_on_launch:
179 |                 # open the dashboard URL in the default web browser (using a separate process to control
180 |                 # output redirection)
181 |                 process = multiprocessing.Process(target=self._open_dashboard, args=(dashboard_url,))
182 |                 process.start()
183 |                 process.join(timeout=1)
184 | 
185 |         # log fundamental information
186 |         log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})")
187 |         log.info("Configuration file: %s", self.serena_config.config_file_path)
188 |         log.info("Available projects: {}".format(", ".join(self.serena_config.project_names)))
189 |         log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name_from_cls() for tool in self._all_tools.values()])}")
190 | 
191 |         self._check_shell_settings()
192 | 
193 |         # determine the base toolset defining the set of exposed tools (which e.g. the MCP shall see),
194 |         # limited by the Serena config, the context (which is fixed for the session) and JetBrains mode
195 |         tool_inclusion_definitions: list[ToolInclusionDefinition] = [self.serena_config, self._context]
196 |         if self._context.name == RegisteredContext.IDE_ASSISTANT.value:
197 |             tool_inclusion_definitions.extend(self._ide_assistant_context_tool_inclusion_definitions(project))
198 |         if self.serena_config.jetbrains:
199 |             tool_inclusion_definitions.append(SerenaAgentMode.from_name_internal("jetbrains"))
200 | 
201 |         self._base_tool_set = ToolSet.default().apply(*tool_inclusion_definitions)
202 |         self._exposed_tools = AvailableTools([t for t in self._all_tools.values() if self._base_tool_set.includes_name(t.get_name())])
203 |         log.info(f"Number of exposed tools: {len(self._exposed_tools)}")
204 | 
205 |         # create executor for starting the language server and running tools in another thread
206 |         # This executor is used to achieve linear task execution, so it is important to use a single-threaded executor.
207 |         self._task_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="SerenaAgentExecutor")
208 |         self._task_executor_lock = threading.Lock()
209 |         self._task_executor_task_index = 1
210 | 
211 |         # Initialize the prompt factory
212 |         self.prompt_factory = SerenaPromptFactory()
213 |         self._project_activation_callback = project_activation_callback
214 | 
215 |         # set the active modes
216 |         if modes is None:
217 |             modes = SerenaAgentMode.load_default_modes()
218 |         self._modes = modes
219 | 
220 |         self._active_tools: dict[type[Tool], Tool] = {}
221 |         self._update_active_tools()
222 | 
223 |         # activate a project configuration (if provided or if there is only a single project available)
224 |         if project is not None:
225 |             try:
226 |                 self.activate_project_from_path_or_name(project)
227 |             except Exception as e:
228 |                 log.error(f"Error activating project '{project}' at startup: {e}", exc_info=e)
229 | 
230 |     def get_context(self) -> SerenaAgentContext:
231 |         return self._context
232 | 
233 |     def get_tool_description_override(self, tool_name: str) -> str | None:
234 |         return self._context.tool_description_overrides.get(tool_name, None)
235 | 
236 |     def _check_shell_settings(self) -> None:
237 |         # On Windows, Claude Code sets COMSPEC to Git-Bash (often even with a path containing spaces),
238 |         # which causes all sorts of trouble, preventing language servers from being launched correctly.
239 |         # So we make sure that COMSPEC is unset if it has been set to bash specifically.
240 |         if platform.system() == "Windows":
241 |             comspec = os.environ.get("COMSPEC", "")
242 |             if "bash" in comspec:
243 |                 os.environ["COMSPEC"] = ""  # force use of default shell
244 |                 log.info("Adjusting COMSPEC environment variable to use the default shell instead of '%s'", comspec)
245 | 
246 |     def _ide_assistant_context_tool_inclusion_definitions(self, project_root_or_name: str | None) -> list[ToolInclusionDefinition]:
247 |         """
248 |         In the IDE assistant context, the agent is assumed to work on a single project, and we thus
249 |         want to apply that project's tool exclusions/inclusions from the get-go, limiting the set
250 |         of tools that will be exposed to the client.
251 |         Furthermore, we disable tools that are only relevant for project activation.
252 |         So if the project exists, we apply all the aforementioned exclusions.
253 | 
254 |         :param project_root_or_name: the project root path or project name
255 |         :return:
256 |         """
257 |         tool_inclusion_definitions = []
258 |         if project_root_or_name is not None:
259 |             # Note: Auto-generation is disabled, because the result must be returned instantaneously
260 |             #   (project generation could take too much time), so as not to delay MCP server startup
261 |             #   and provide responses to the client immediately.
262 |             project = self.load_project_from_path_or_name(project_root_or_name, autogenerate=False)
263 |             if project is not None:
264 |                 tool_inclusion_definitions.append(
265 |                     ToolInclusionDefinition(
266 |                         excluded_tools=[ActivateProjectTool.get_name_from_cls(), GetCurrentConfigTool.get_name_from_cls()]
267 |                     )
268 |                 )
269 |                 tool_inclusion_definitions.append(project.project_config)
270 |         return tool_inclusion_definitions
271 | 
272 |     def record_tool_usage_if_enabled(self, input_kwargs: dict, tool_result: str | dict, tool: Tool) -> None:
273 |         """
274 |         Record the usage of a tool with the given input and output strings if tool usage statistics recording is enabled.
275 |         """
276 |         tool_name = tool.get_name()
277 |         if self._tool_usage_stats is not None:
278 |             input_str = str(input_kwargs)
279 |             output_str = str(tool_result)
280 |             log.debug(f"Recording tool usage for tool '{tool_name}'")
281 |             self._tool_usage_stats.record_tool_usage(tool_name, input_str, output_str)
282 |         else:
283 |             log.debug(f"Tool usage statistics recording is disabled, not recording usage of '{tool_name}'.")
284 | 
285 |     @staticmethod
286 |     def _open_dashboard(url: str) -> None:
287 |         # Redirect stdout and stderr file descriptors to /dev/null,
288 |         # making sure that nothing can be written to stdout/stderr, even by subprocesses
289 |         null_fd = os.open(os.devnull, os.O_WRONLY)
290 |         os.dup2(null_fd, sys.stdout.fileno())
291 |         os.dup2(null_fd, sys.stderr.fileno())
292 |         os.close(null_fd)
293 | 
294 |         # open the dashboard URL in the default web browser
295 |         webbrowser.open(url)
296 | 
297 |     def get_project_root(self) -> str:
298 |         """
299 |         :return: the root directory of the active project (if any); raises a ValueError if there is no active project
300 |         """
301 |         project = self.get_active_project()
302 |         if project is None:
303 |             raise ValueError("Cannot get project root if no project is active.")
304 |         return project.project_root
305 | 
306 |     def get_exposed_tool_instances(self) -> list["Tool"]:
307 |         """
308 |         :return: the tool instances which are exposed (e.g. to the MCP client).
309 |             Note that the set of exposed tools is fixed for the session, as
310 |             clients don't react to changes in the set of tools, so this is the superset
311 |             of tools that can be offered during the session.
312 |             If a client should attempt to use a tool that is dynamically disabled
313 |             (e.g. because a project is activated that disables it), it will receive an error.
314 |         """
315 |         return list(self._exposed_tools.tools)
316 | 
317 |     def get_active_project(self) -> Project | None:
318 |         """
319 |         :return: the active project or None if no project is active
320 |         """
321 |         return self._active_project
322 | 
323 |     def get_active_project_or_raise(self) -> Project:
324 |         """
325 |         :return: the active project or raises an exception if no project is active
326 |         """
327 |         project = self.get_active_project()
328 |         if project is None:
329 |             raise ValueError("No active project. Please activate a project first.")
330 |         return project
331 | 
332 |     def set_modes(self, modes: list[SerenaAgentMode]) -> None:
333 |         """
334 |         Set the current mode configurations.
335 | 
336 |         :param modes: List of mode names or paths to use
337 |         """
338 |         self._modes = modes
339 |         self._update_active_tools()
340 | 
341 |         log.info(f"Set modes to {[mode.name for mode in modes]}")
342 | 
343 |     def get_active_modes(self) -> list[SerenaAgentMode]:
344 |         """
345 |         :return: the list of active modes
346 |         """
347 |         return list(self._modes)
348 | 
349 |     def _format_prompt(self, prompt_template: str) -> str:
350 |         template = JinjaTemplate(prompt_template)
351 |         return template.render(available_tools=self._exposed_tools.tool_names, available_markers=self._exposed_tools.tool_marker_names)
352 | 
353 |     def create_system_prompt(self) -> str:
354 |         available_markers = self._exposed_tools.tool_marker_names
355 |         log.info("Generating system prompt with available_tools=(see exposed tools), available_markers=%s", available_markers)
356 |         system_prompt = self.prompt_factory.create_system_prompt(
357 |             context_system_prompt=self._format_prompt(self._context.prompt),
358 |             mode_system_prompts=[self._format_prompt(mode.prompt) for mode in self._modes],
359 |             available_tools=self._exposed_tools.tool_names,
360 |             available_markers=available_markers,
361 |         )
362 |         log.info("System prompt:\n%s", system_prompt)
363 |         return system_prompt
364 | 
365 |     def _update_active_tools(self) -> None:
366 |         """
367 |         Update the active tools based on enabled modes and the active project.
368 |         The base tool set already takes the Serena configuration and the context into account
369 |         (as well as any internal modes that are not handled dynamically, such as JetBrains mode).
370 |         """
371 |         tool_set = self._base_tool_set.apply(*self._modes)
372 |         if self._active_project is not None:
373 |             tool_set = tool_set.apply(self._active_project.project_config)
374 |             if self._active_project.project_config.read_only:
375 |                 tool_set = tool_set.without_editing_tools()
376 | 
377 |         self._active_tools = {
378 |             tool_class: tool_instance
379 |             for tool_class, tool_instance in self._all_tools.items()
380 |             if tool_set.includes_name(tool_instance.get_name())
381 |         }
382 | 
383 |         log.info(f"Active tools ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}")
384 | 
385 |     def issue_task(self, task: Callable[[], Any], name: str | None = None) -> Future:
386 |         """
387 |         Issue a task to the executor for asynchronous execution.
388 |         It is ensured that tasks are executed in the order they are issued, one after another.
389 | 
390 |         :param task: the task to execute
391 |         :param name: the name of the task for logging purposes; if None, use the task function's name
392 |         :return: a Future object representing the execution of the task
393 |         """
394 |         with self._task_executor_lock:
395 |             task_name = f"Task-{self._task_executor_task_index}[{name or task.__name__}]"
396 |             self._task_executor_task_index += 1
397 | 
398 |             def task_execution_wrapper() -> Any:
399 |                 with LogTime(task_name, logger=log):
400 |                     return task()
401 | 
402 |             log.info(f"Scheduling {task_name}")
403 |             return self._task_executor.submit(task_execution_wrapper)
404 | 
405 |     def execute_task(self, task: Callable[[], T]) -> T:
406 |         """
407 |         Executes the given task synchronously via the agent's task executor.
408 |         This is useful for tasks that need to be executed immediately and whose results are needed right away.
409 | 
410 |         :param task: the task to execute
411 |         :return: the result of the task execution
412 |         """
413 |         future = self.issue_task(task)
414 |         return future.result()
415 | 
416 |     def is_using_language_server(self) -> bool:
417 |         """
418 |         :return: whether this agent uses language server-based code analysis
419 |         """
420 |         return not self.serena_config.jetbrains
421 | 
422 |     def _activate_project(self, project: Project) -> None:
423 |         log.info(f"Activating {project.project_name} at {project.project_root}")
424 |         self._active_project = project
425 |         self._update_active_tools()
426 | 
427 |         # initialize project-specific instances which do not depend on the language server
428 |         self.memories_manager = MemoriesManager(project.project_root)
429 | 
430 |         def init_language_server() -> None:
431 |             # start the language server
432 |             with LogTime("Language server initialization", logger=log):
433 |                 self.reset_language_server()
434 |                 assert self.language_server is not None
435 | 
436 |         # initialize the language server in the background (if in language server mode)
437 |         if self.is_using_language_server():
438 |             self.issue_task(init_language_server)
439 | 
440 |         if self._project_activation_callback is not None:
441 |             self._project_activation_callback()
442 | 
443 |     def load_project_from_path_or_name(self, project_root_or_name: str, autogenerate: bool) -> Project | None:
444 |         """
445 |         Get a project instance from a path or a name.
446 | 
447 |         :param project_root_or_name: the path to the project root or the name of the project
448 |         :param autogenerate: whether to autogenerate the project for the case where first argument is a directory
449 |             which does not yet contain a Serena project configuration file
450 |         :return: the project instance if it was found/could be created, None otherwise
451 |         """
452 |         project_instance: Project | None = self.serena_config.get_project(project_root_or_name)
453 |         if project_instance is not None:
454 |             log.info(f"Found registered project '{project_instance.project_name}' at path {project_instance.project_root}")
455 |         elif autogenerate and os.path.isdir(project_root_or_name):
456 |             project_instance = self.serena_config.add_project_from_path(project_root_or_name)
457 |             log.info(f"Added new project {project_instance.project_name} for path {project_instance.project_root}")
458 |         return project_instance
459 | 
460 |     def activate_project_from_path_or_name(self, project_root_or_name: str) -> Project:
461 |         """
462 |         Activate a project from a path or a name.
463 |         If the project was already registered, it will just be activated.
464 |         If the argument is a path at which no Serena project previously existed, the project will be created beforehand.
465 |         Raises ProjectNotFoundError if the project could neither be found nor created.
466 | 
467 |         :return: a tuple of the project instance and a Boolean indicating whether the project was newly
468 |             created
469 |         """
470 |         project_instance: Project | None = self.load_project_from_path_or_name(project_root_or_name, autogenerate=True)
471 |         if project_instance is None:
472 |             raise ProjectNotFoundError(
473 |                 f"Project '{project_root_or_name}' not found: Not a valid project name or directory. "
474 |                 f"Existing project names: {self.serena_config.project_names}"
475 |             )
476 |         self._activate_project(project_instance)
477 |         return project_instance
478 | 
479 |     def get_active_tool_classes(self) -> list[type["Tool"]]:
480 |         """
481 |         :return: the list of active tool classes for the current project
482 |         """
483 |         return list(self._active_tools.keys())
484 | 
485 |     def get_active_tool_names(self) -> list[str]:
486 |         """
487 |         :return: the list of names of the active tools for the current project
488 |         """
489 |         return sorted([tool.get_name_from_cls() for tool in self.get_active_tool_classes()])
490 | 
491 |     def tool_is_active(self, tool_class: type["Tool"] | str) -> bool:
492 |         """
493 |         :param tool_class: the class or name of the tool to check
494 |         :return: True if the tool is active, False otherwise
495 |         """
496 |         if isinstance(tool_class, str):
497 |             return tool_class in self.get_active_tool_names()
498 |         else:
499 |             return tool_class in self.get_active_tool_classes()
500 | 
501 |     def get_current_config_overview(self) -> str:
502 |         """
503 |         :return: a string overview of the current configuration, including the active and available configuration options
504 |         """
505 |         result_str = "Current configuration:\n"
506 |         result_str += f"Serena version: {serena_version()}\n"
507 |         result_str += f"Loglevel: {self.serena_config.log_level}, trace_lsp_communication={self.serena_config.trace_lsp_communication}\n"
508 |         if self._active_project is not None:
509 |             result_str += f"Active project: {self._active_project.project_name}\n"
510 |         else:
511 |             result_str += "No active project\n"
512 |         result_str += "Available projects:\n" + "\n".join(list(self.serena_config.project_names)) + "\n"
513 |         result_str += f"Active context: {self._context.name}\n"
514 | 
515 |         # Active modes
516 |         active_mode_names = [mode.name for mode in self.get_active_modes()]
517 |         result_str += "Active modes: {}\n".format(", ".join(active_mode_names)) + "\n"
518 | 
519 |         # Available but not active modes
520 |         all_available_modes = SerenaAgentMode.list_registered_mode_names()
521 |         inactive_modes = [mode for mode in all_available_modes if mode not in active_mode_names]
522 |         if inactive_modes:
523 |             result_str += "Available but not active modes: {}\n".format(", ".join(inactive_modes)) + "\n"
524 | 
525 |         # Active tools
526 |         result_str += "Active tools (after all exclusions from the project, context, and modes):\n"
527 |         active_tool_names = self.get_active_tool_names()
528 |         # print the tool names in chunks
529 |         chunk_size = 4
530 |         for i in range(0, len(active_tool_names), chunk_size):
531 |             chunk = active_tool_names[i : i + chunk_size]
532 |             result_str += "  " + ", ".join(chunk) + "\n"
533 | 
534 |         # Available but not active tools
535 |         all_tool_names = sorted([tool.get_name_from_cls() for tool in self._all_tools.values()])
536 |         inactive_tool_names = [tool for tool in all_tool_names if tool not in active_tool_names]
537 |         if inactive_tool_names:
538 |             result_str += "Available but not active tools:\n"
539 |             for i in range(0, len(inactive_tool_names), chunk_size):
540 |                 chunk = inactive_tool_names[i : i + chunk_size]
541 |                 result_str += "  " + ", ".join(chunk) + "\n"
542 | 
543 |         return result_str
544 | 
545 |     def is_language_server_running(self) -> bool:
546 |         return self.language_server is not None and self.language_server.is_running()
547 | 
548 |     def reset_language_server(self) -> None:
549 |         """
550 |         Starts/resets the language server for the current project
551 |         """
552 |         tool_timeout = self.serena_config.tool_timeout
553 |         if tool_timeout is None or tool_timeout < 0:
554 |             ls_timeout = None
555 |         else:
556 |             if tool_timeout < 10:
557 |                 raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds")
558 |             ls_timeout = tool_timeout - 5  # the LS timeout is for a single call, it should be smaller than the tool timeout
559 | 
560 |         # stop the language server if it is running
561 |         if self.is_language_server_running():
562 |             assert self.language_server is not None
563 |             log.info(f"Stopping the current language server at {self.language_server.repository_root_path} ...")
564 |             self.language_server.stop()
565 |             self.language_server = None
566 | 
567 |         # instantiate and start the language server
568 |         assert self._active_project is not None
569 |         self.language_server = self._active_project.create_language_server(
570 |             log_level=self.serena_config.log_level,
571 |             ls_timeout=ls_timeout,
572 |             trace_lsp_communication=self.serena_config.trace_lsp_communication,
573 |             ls_specific_settings=self.serena_config.ls_specific_settings,
574 |         )
575 |         log.info(f"Starting the language server for {self._active_project.project_name}")
576 |         self.language_server.start()
577 |         if not self.language_server.is_running():
578 |             raise RuntimeError(
579 |                 f"Failed to start the language server for {self._active_project.project_name} at {self._active_project.project_root}"
580 |             )
581 | 
582 |     def get_tool(self, tool_class: type[TTool]) -> TTool:
583 |         return self._all_tools[tool_class]  # type: ignore
584 | 
585 |     def print_tool_overview(self) -> None:
586 |         ToolRegistry().print_tool_overview(self._active_tools.values())
587 | 
588 |     def __del__(self) -> None:
589 |         """
590 |         Destructor to clean up the language server instance and GUI logger
591 |         """
592 |         if not hasattr(self, "_is_initialized"):
593 |             return
594 |         log.info("SerenaAgent is shutting down ...")
595 |         if self.is_language_server_running():
596 |             log.info("Stopping the language server ...")
597 |             assert self.language_server is not None
598 |             self.language_server.save_cache()
599 |             self.language_server.stop()
600 |         if self._gui_log_viewer:
601 |             log.info("Stopping the GUI log window ...")
602 |             self._gui_log_viewer.stop()
603 | 
604 |     def get_tool_by_name(self, tool_name: str) -> Tool:
605 |         tool_class = ToolRegistry().get_tool_class_by_name(tool_name)
606 |         return self.get_tool(tool_class)
607 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/lsp_protocol_handler/lsp_requests.py:
--------------------------------------------------------------------------------

```python
  1 | # Code generated. DO NOT EDIT.
  2 | # LSP v3.17.0
  3 | # TODO: Look into use of https://pypi.org/project/ts2python/ to generate the types for https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
  4 | 
  5 | """
  6 | This file provides the python interface corresponding to the requests and notifications defined in Typescript in the language server protocol.
  7 | This file is obtained from https://github.com/predragnikolic/OLSP under the MIT License with the following terms:
  8 | 
  9 | MIT License
 10 | 
 11 | Copyright (c) 2023 Предраг Николић
 12 | 
 13 | Permission is hereby granted, free of charge, to any person obtaining a copy
 14 | of this software and associated documentation files (the "Software"), to deal
 15 | in the Software without restriction, including without limitation the rights
 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 17 | copies of the Software, and to permit persons to whom the Software is
 18 | furnished to do so, subject to the following conditions:
 19 | 
 20 | The above copyright notice and this permission notice shall be included in all
 21 | copies or substantial portions of the Software.
 22 | 
 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 29 | SOFTWARE.
 30 | """
 31 | 
 32 | from typing import Union
 33 | 
 34 | from solidlsp.lsp_protocol_handler import lsp_types
 35 | 
 36 | 
 37 | class LspRequest:
 38 |     def __init__(self, send_request):
 39 |         self.send_request = send_request
 40 | 
 41 |     async def implementation(
 42 |         self, params: lsp_types.ImplementationParams
 43 |     ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
 44 |         """A request to resolve the implementation locations of a symbol at a given text
 45 |         document position. The request's parameter is of type [TextDocumentPositionParams]
 46 |         (#TextDocumentPositionParams) the response is of type {@link Definition} or a
 47 |         Thenable that resolves to such.
 48 |         """
 49 |         return await self.send_request("textDocument/implementation", params)
 50 | 
 51 |     async def type_definition(
 52 |         self, params: lsp_types.TypeDefinitionParams
 53 |     ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
 54 |         """A request to resolve the type definition locations of a symbol at a given text
 55 |         document position. The request's parameter is of type [TextDocumentPositionParams]
 56 |         (#TextDocumentPositionParams) the response is of type {@link Definition} or a
 57 |         Thenable that resolves to such.
 58 |         """
 59 |         return await self.send_request("textDocument/typeDefinition", params)
 60 | 
 61 |     async def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]:
 62 |         """A request to list all color symbols found in a given text document. The request's
 63 |         parameter is of type {@link DocumentColorParams} the
 64 |         response is of type {@link ColorInformation ColorInformation[]} or a Thenable
 65 |         that resolves to such.
 66 |         """
 67 |         return await self.send_request("textDocument/documentColor", params)
 68 | 
 69 |     async def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]:
 70 |         """A request to list all presentation for a color. The request's
 71 |         parameter is of type {@link ColorPresentationParams} the
 72 |         response is of type {@link ColorInformation ColorInformation[]} or a Thenable
 73 |         that resolves to such.
 74 |         """
 75 |         return await self.send_request("textDocument/colorPresentation", params)
 76 | 
 77 |     async def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None:
 78 |         """A request to provide folding ranges in a document. The request's
 79 |         parameter is of type {@link FoldingRangeParams}, the
 80 |         response is of type {@link FoldingRangeList} or a Thenable
 81 |         that resolves to such.
 82 |         """
 83 |         return await self.send_request("textDocument/foldingRange", params)
 84 | 
 85 |     async def declaration(
 86 |         self, params: lsp_types.DeclarationParams
 87 |     ) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]:
 88 |         """A request to resolve the type definition locations of a symbol at a given text
 89 |         document position. The request's parameter is of type [TextDocumentPositionParams]
 90 |         (#TextDocumentPositionParams) the response is of type {@link Declaration}
 91 |         or a typed array of {@link DeclarationLink} or a Thenable that resolves
 92 |         to such.
 93 |         """
 94 |         return await self.send_request("textDocument/declaration", params)
 95 | 
 96 |     async def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None:
 97 |         """A request to provide selection ranges in a document. The request's
 98 |         parameter is of type {@link SelectionRangeParams}, the
 99 |         response is of type {@link SelectionRange SelectionRange[]} or a Thenable
100 |         that resolves to such.
101 |         """
102 |         return await self.send_request("textDocument/selectionRange", params)
103 | 
104 |     async def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None:
105 |         """A request to result a `CallHierarchyItem` in a document at a given position.
106 |         Can be used as an input to an incoming or outgoing call hierarchy.
107 | 
108 |         @since 3.16.0
109 |         """
110 |         return await self.send_request("textDocument/prepareCallHierarchy", params)
111 | 
112 |     async def incoming_calls(
113 |         self, params: lsp_types.CallHierarchyIncomingCallsParams
114 |     ) -> list["lsp_types.CallHierarchyIncomingCall"] | None:
115 |         """A request to resolve the incoming calls for a given `CallHierarchyItem`.
116 | 
117 |         @since 3.16.0
118 |         """
119 |         return await self.send_request("callHierarchy/incomingCalls", params)
120 | 
121 |     async def outgoing_calls(
122 |         self, params: lsp_types.CallHierarchyOutgoingCallsParams
123 |     ) -> list["lsp_types.CallHierarchyOutgoingCall"] | None:
124 |         """A request to resolve the outgoing calls for a given `CallHierarchyItem`.
125 | 
126 |         @since 3.16.0
127 |         """
128 |         return await self.send_request("callHierarchy/outgoingCalls", params)
129 | 
130 |     async def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]:
131 |         """@since 3.16.0"""
132 |         return await self.send_request("textDocument/semanticTokens/full", params)
133 | 
134 |     async def semantic_tokens_delta(
135 |         self, params: lsp_types.SemanticTokensDeltaParams
136 |     ) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]:
137 |         """@since 3.16.0"""
138 |         return await self.send_request("textDocument/semanticTokens/full/delta", params)
139 | 
140 |     async def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]:
141 |         """@since 3.16.0"""
142 |         return await self.send_request("textDocument/semanticTokens/range", params)
143 | 
144 |     async def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]:
145 |         """A request to provide ranges that can be edited together.
146 | 
147 |         @since 3.16.0
148 |         """
149 |         return await self.send_request("textDocument/linkedEditingRange", params)
150 | 
151 |     async def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
152 |         """The will create files request is sent from the client to the server before files are actually
153 |         created as long as the creation is triggered from within the client.
154 | 
155 |         @since 3.16.0
156 |         """
157 |         return await self.send_request("workspace/willCreateFiles", params)
158 | 
159 |     async def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
160 |         """The will rename files request is sent from the client to the server before files are actually
161 |         renamed as long as the rename is triggered from within the client.
162 | 
163 |         @since 3.16.0
164 |         """
165 |         return await self.send_request("workspace/willRenameFiles", params)
166 | 
167 |     async def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
168 |         """The did delete files notification is sent from the client to the server when
169 |         files were deleted from within the client.
170 | 
171 |         @since 3.16.0
172 |         """
173 |         return await self.send_request("workspace/willDeleteFiles", params)
174 | 
175 |     async def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None:
176 |         """A request to get the moniker of a symbol at a given text document position.
177 |         The request parameter is of type {@link TextDocumentPositionParams}.
178 |         The response is of type {@link Moniker Moniker[]} or `null`.
179 |         """
180 |         return await self.send_request("textDocument/moniker", params)
181 | 
182 |     async def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None:
183 |         """A request to result a `TypeHierarchyItem` in a document at a given position.
184 |         Can be used as an input to a subtypes or supertypes type hierarchy.
185 | 
186 |         @since 3.17.0
187 |         """
188 |         return await self.send_request("textDocument/prepareTypeHierarchy", params)
189 | 
190 |     async def type_hierarchy_supertypes(
191 |         self, params: lsp_types.TypeHierarchySupertypesParams
192 |     ) -> list["lsp_types.TypeHierarchyItem"] | None:
193 |         """A request to resolve the supertypes for a given `TypeHierarchyItem`.
194 | 
195 |         @since 3.17.0
196 |         """
197 |         return await self.send_request("typeHierarchy/supertypes", params)
198 | 
199 |     async def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None:
200 |         """A request to resolve the subtypes for a given `TypeHierarchyItem`.
201 | 
202 |         @since 3.17.0
203 |         """
204 |         return await self.send_request("typeHierarchy/subtypes", params)
205 | 
206 |     async def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None:
207 |         """A request to provide inline values in a document. The request's parameter is of
208 |         type {@link InlineValueParams}, the response is of type
209 |         {@link InlineValue InlineValue[]} or a Thenable that resolves to such.
210 | 
211 |         @since 3.17.0
212 |         """
213 |         return await self.send_request("textDocument/inlineValue", params)
214 | 
215 |     async def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None:
216 |         """A request to provide inlay hints in a document. The request's parameter is of
217 |         type {@link InlayHintsParams}, the response is of type
218 |         {@link InlayHint InlayHint[]} or a Thenable that resolves to such.
219 | 
220 |         @since 3.17.0
221 |         """
222 |         return await self.send_request("textDocument/inlayHint", params)
223 | 
224 |     async def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint":
225 |         """A request to resolve additional properties for an inlay hint.
226 |         The request's parameter is of type {@link InlayHint}, the response is
227 |         of type {@link InlayHint} or a Thenable that resolves to such.
228 | 
229 |         @since 3.17.0
230 |         """
231 |         return await self.send_request("inlayHint/resolve", params)
232 | 
233 |     async def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport":
234 |         """The document diagnostic request definition.
235 | 
236 |         @since 3.17.0
237 |         """
238 |         return await self.send_request("textDocument/diagnostic", params)
239 | 
240 |     async def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport":
241 |         """The workspace diagnostic request definition.
242 | 
243 |         @since 3.17.0
244 |         """
245 |         return await self.send_request("workspace/diagnostic", params)
246 | 
247 |     async def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult":
248 |         """The initialize request is sent from the client to the server.
249 |         It is sent once as the request after starting up the server.
250 |         The requests parameter is of type {@link InitializeParams}
251 |         the response if of type {@link InitializeResult} of a Thenable that
252 |         resolves to such.
253 |         """
254 |         return await self.send_request("initialize", params)
255 | 
256 |     async def shutdown(self) -> None:
257 |         """A shutdown request is sent from the client to the server.
258 |         It is sent once when the client decides to shutdown the
259 |         server. The only notification that is sent after a shutdown request
260 |         is the exit event.
261 |         """
262 |         return await self.send_request("shutdown")
263 | 
264 |     async def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None:
265 |         """A document will save request is sent from the client to the server before
266 |         the document is actually saved. The request can return an array of TextEdits
267 |         which will be applied to the text document before it is saved. Please note that
268 |         clients might drop results if computing the text edits took too long or if a
269 |         server constantly fails on this request. This is done to keep the save fast and
270 |         reliable.
271 |         """
272 |         return await self.send_request("textDocument/willSaveWaitUntil", params)
273 | 
274 |     async def completion(
275 |         self, params: lsp_types.CompletionParams
276 |     ) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]:
277 |         """Request to request completion at a given text document position. The request's
278 |         parameter is of type {@link TextDocumentPosition} the response
279 |         is of type {@link CompletionItem CompletionItem[]} or {@link CompletionList}
280 |         or a Thenable that resolves to such.
281 | 
282 |         The request can delay the computation of the {@link CompletionItem.detail `detail`}
283 |         and {@link CompletionItem.documentation `documentation`} properties to the `completionItem/resolve`
284 |         request. However, properties that are needed for the initial sorting and filtering, like `sortText`,
285 |         `filterText`, `insertText`, and `textEdit`, must not be changed during resolve.
286 |         """
287 |         return await self.send_request("textDocument/completion", params)
288 | 
289 |     async def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem":
290 |         """Request to resolve additional information for a given completion item.The request's
291 |         parameter is of type {@link CompletionItem} the response
292 |         is of type {@link CompletionItem} or a Thenable that resolves to such.
293 |         """
294 |         return await self.send_request("completionItem/resolve", params)
295 | 
296 |     async def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]:
297 |         """Request to request hover information at a given text document position. The request's
298 |         parameter is of type {@link TextDocumentPosition} the response is of
299 |         type {@link Hover} or a Thenable that resolves to such.
300 |         """
301 |         return await self.send_request("textDocument/hover", params)
302 | 
303 |     async def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]:
304 |         return await self.send_request("textDocument/signatureHelp", params)
305 | 
306 |     async def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
307 |         """A request to resolve the definition location of a symbol at a given text
308 |         document position. The request's parameter is of type [TextDocumentPosition]
309 |         (#TextDocumentPosition) the response is of either type {@link Definition}
310 |         or a typed array of {@link DefinitionLink} or a Thenable that resolves
311 |         to such.
312 |         """
313 |         return await self.send_request("textDocument/definition", params)
314 | 
315 |     async def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None:
316 |         """A request to resolve project-wide references for the symbol denoted
317 |         by the given text document position. The request's parameter is of
318 |         type {@link ReferenceParams} the response is of type
319 |         {@link Location Location[]} or a Thenable that resolves to such.
320 |         """
321 |         return await self.send_request("textDocument/references", params)
322 | 
323 |     async def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None:
324 |         """Request to resolve a {@link DocumentHighlight} for a given
325 |         text document position. The request's parameter is of type [TextDocumentPosition]
326 |         (#TextDocumentPosition) the request response is of type [DocumentHighlight[]]
327 |         (#DocumentHighlight) or a Thenable that resolves to such.
328 |         """
329 |         return await self.send_request("textDocument/documentHighlight", params)
330 | 
331 |     async def document_symbol(
332 |         self, params: lsp_types.DocumentSymbolParams
333 |     ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.DocumentSymbol"] | None:
334 |         """A request to list all symbols found in a given text document. The request's
335 |         parameter is of type {@link TextDocumentIdentifier} the
336 |         response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable
337 |         that resolves to such.
338 |         """
339 |         return await self.send_request("textDocument/documentSymbol", params)
340 | 
341 |     async def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None:
342 |         """A request to provide commands for the given text document and range."""
343 |         return await self.send_request("textDocument/codeAction", params)
344 | 
345 |     async def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction":
346 |         """Request to resolve additional information for a given code action.The request's
347 |         parameter is of type {@link CodeAction} the response
348 |         is of type {@link CodeAction} or a Thenable that resolves to such.
349 |         """
350 |         return await self.send_request("codeAction/resolve", params)
351 | 
352 |     async def workspace_symbol(
353 |         self, params: lsp_types.WorkspaceSymbolParams
354 |     ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.WorkspaceSymbol"] | None:
355 |         """A request to list project-wide symbols matching the query string given
356 |         by the {@link WorkspaceSymbolParams}. The response is
357 |         of type {@link SymbolInformation SymbolInformation[]} or a Thenable that
358 |         resolves to such.
359 | 
360 |         @since 3.17.0 - support for WorkspaceSymbol in the returned data. Clients
361 |          need to advertise support for WorkspaceSymbols via the client capability
362 |          `workspace.symbol.resolveSupport`.
363 |         """
364 |         return await self.send_request("workspace/symbol", params)
365 | 
366 |     async def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol":
367 |         """A request to resolve the range inside the workspace
368 |         symbol's location.
369 | 
370 |         @since 3.17.0
371 |         """
372 |         return await self.send_request("workspaceSymbol/resolve", params)
373 | 
374 |     async def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None:
375 |         """A request to provide code lens for the given text document."""
376 |         return await self.send_request("textDocument/codeLens", params)
377 | 
378 |     async def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens":
379 |         """A request to resolve a command for a given code lens."""
380 |         return await self.send_request("codeLens/resolve", params)
381 | 
382 |     async def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None:
383 |         """A request to provide document links"""
384 |         return await self.send_request("textDocument/documentLink", params)
385 | 
386 |     async def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink":
387 |         """Request to resolve additional information for a given document link. The request's
388 |         parameter is of type {@link DocumentLink} the response
389 |         is of type {@link DocumentLink} or a Thenable that resolves to such.
390 |         """
391 |         return await self.send_request("documentLink/resolve", params)
392 | 
393 |     async def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None:
394 |         """A request to to format a whole document."""
395 |         return await self.send_request("textDocument/formatting", params)
396 | 
397 |     async def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None:
398 |         """A request to to format a range in a document."""
399 |         return await self.send_request("textDocument/rangeFormatting", params)
400 | 
401 |     async def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None:
402 |         """A request to format a document on type."""
403 |         return await self.send_request("textDocument/onTypeFormatting", params)
404 | 
405 |     async def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]:
406 |         """A request to rename a symbol."""
407 |         return await self.send_request("textDocument/rename", params)
408 | 
409 |     async def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]:
410 |         """A request to test and perform the setup necessary for a rename.
411 | 
412 |         @since 3.16 - support for default behavior
413 |         """
414 |         return await self.send_request("textDocument/prepareRename", params)
415 | 
416 |     async def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]:
417 |         """A request send from the client to the server to execute a command. The request might return
418 |         a workspace edit which the client will apply to the workspace.
419 |         """
420 |         return await self.send_request("workspace/executeCommand", params)
421 | 
422 | 
423 | class LspNotification:
424 |     def __init__(self, send_notification):
425 |         self.send_notification = send_notification
426 | 
427 |     def did_change_workspace_folders(self, params: lsp_types.DidChangeWorkspaceFoldersParams) -> None:
428 |         """The `workspace/didChangeWorkspaceFolders` notification is sent from the client to the server when the workspace
429 |         folder configuration changes.
430 |         """
431 |         return self.send_notification("workspace/didChangeWorkspaceFolders", params)
432 | 
433 |     def cancel_work_done_progress(self, params: lsp_types.WorkDoneProgressCancelParams) -> None:
434 |         """The `window/workDoneProgress/cancel` notification is sent from  the client to the server to cancel a progress
435 |         initiated on the server side.
436 |         """
437 |         return self.send_notification("window/workDoneProgress/cancel", params)
438 | 
439 |     def did_create_files(self, params: lsp_types.CreateFilesParams) -> None:
440 |         """The did create files notification is sent from the client to the server when
441 |         files were created from within the client.
442 | 
443 |         @since 3.16.0
444 |         """
445 |         return self.send_notification("workspace/didCreateFiles", params)
446 | 
447 |     def did_rename_files(self, params: lsp_types.RenameFilesParams) -> None:
448 |         """The did rename files notification is sent from the client to the server when
449 |         files were renamed from within the client.
450 | 
451 |         @since 3.16.0
452 |         """
453 |         return self.send_notification("workspace/didRenameFiles", params)
454 | 
455 |     def did_delete_files(self, params: lsp_types.DeleteFilesParams) -> None:
456 |         """The will delete files request is sent from the client to the server before files are actually
457 |         deleted as long as the deletion is triggered from within the client.
458 | 
459 |         @since 3.16.0
460 |         """
461 |         return self.send_notification("workspace/didDeleteFiles", params)
462 | 
463 |     def did_open_notebook_document(self, params: lsp_types.DidOpenNotebookDocumentParams) -> None:
464 |         """A notification sent when a notebook opens.
465 | 
466 |         @since 3.17.0
467 |         """
468 |         return self.send_notification("notebookDocument/didOpen", params)
469 | 
470 |     def did_change_notebook_document(self, params: lsp_types.DidChangeNotebookDocumentParams) -> None:
471 |         return self.send_notification("notebookDocument/didChange", params)
472 | 
473 |     def did_save_notebook_document(self, params: lsp_types.DidSaveNotebookDocumentParams) -> None:
474 |         """A notification sent when a notebook document is saved.
475 | 
476 |         @since 3.17.0
477 |         """
478 |         return self.send_notification("notebookDocument/didSave", params)
479 | 
480 |     def did_close_notebook_document(self, params: lsp_types.DidCloseNotebookDocumentParams) -> None:
481 |         """A notification sent when a notebook closes.
482 | 
483 |         @since 3.17.0
484 |         """
485 |         return self.send_notification("notebookDocument/didClose", params)
486 | 
487 |     def initialized(self, params: lsp_types.InitializedParams) -> None:
488 |         """The initialized notification is sent from the client to the
489 |         server after the client is fully initialized and the server
490 |         is allowed to send requests from the server to the client.
491 |         """
492 |         return self.send_notification("initialized", params)
493 | 
494 |     def exit(self) -> None:
495 |         """The exit event is sent from the client to the server to
496 |         ask the server to exit its process.
497 |         """
498 |         return self.send_notification("exit")
499 | 
500 |     def workspace_did_change_configuration(self, params: lsp_types.DidChangeConfigurationParams) -> None:
501 |         """The configuration change notification is sent from the client to the server
502 |         when the client's configuration has changed. The notification contains
503 |         the changed configuration as defined by the language client.
504 |         """
505 |         return self.send_notification("workspace/didChangeConfiguration", params)
506 | 
507 |     def did_open_text_document(self, params: lsp_types.DidOpenTextDocumentParams) -> None:
508 |         """The document open notification is sent from the client to the server to signal
509 |         newly opened text documents. The document's truth is now managed by the client
510 |         and the server must not try to read the document's truth using the document's
511 |         uri. Open in this sense means it is managed by the client. It doesn't necessarily
512 |         mean that its content is presented in an editor. An open notification must not
513 |         be sent more than once without a corresponding close notification send before.
514 |         This means open and close notification must be balanced and the max open count
515 |         is one.
516 |         """
517 |         return self.send_notification("textDocument/didOpen", params)
518 | 
519 |     def did_change_text_document(self, params: lsp_types.DidChangeTextDocumentParams) -> None:
520 |         """The document change notification is sent from the client to the server to signal
521 |         changes to a text document.
522 |         """
523 |         return self.send_notification("textDocument/didChange", params)
524 | 
525 |     def did_close_text_document(self, params: lsp_types.DidCloseTextDocumentParams) -> None:
526 |         """The document close notification is sent from the client to the server when
527 |         the document got closed in the client. The document's truth now exists where
528 |         the document's uri points to (e.g. if the document's uri is a file uri the
529 |         truth now exists on disk). As with the open notification the close notification
530 |         is about managing the document's content. Receiving a close notification
531 |         doesn't mean that the document was open in an editor before. A close
532 |         notification requires a previous open notification to be sent.
533 |         """
534 |         return self.send_notification("textDocument/didClose", params)
535 | 
536 |     def did_save_text_document(self, params: lsp_types.DidSaveTextDocumentParams) -> None:
537 |         """The document save notification is sent from the client to the server when
538 |         the document got saved in the client.
539 |         """
540 |         return self.send_notification("textDocument/didSave", params)
541 | 
542 |     def will_save_text_document(self, params: lsp_types.WillSaveTextDocumentParams) -> None:
543 |         """A document will save notification is sent from the client to the server before
544 |         the document is actually saved.
545 |         """
546 |         return self.send_notification("textDocument/willSave", params)
547 | 
548 |     def did_change_watched_files(self, params: lsp_types.DidChangeWatchedFilesParams) -> None:
549 |         """The watched files notification is sent from the client to the server when
550 |         the client detects changes to file watched by the language client.
551 |         """
552 |         return self.send_notification("workspace/didChangeWatchedFiles", params)
553 | 
554 |     def set_trace(self, params: lsp_types.SetTraceParams) -> None:
555 |         return self.send_notification("$/setTrace", params)
556 | 
557 |     def cancel_request(self, params: lsp_types.CancelParams) -> None:
558 |         return self.send_notification("$/cancelRequest", params)
559 | 
560 |     def progress(self, params: lsp_types.ProgressParams) -> None:
561 |         return self.send_notification("$/progress", params)
562 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/language_servers/rust_analyzer.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import shutil
  9 | import subprocess
 10 | import threading
 11 | 
 12 | from overrides import override
 13 | 
 14 | from solidlsp.ls import SolidLanguageServer
 15 | from solidlsp.ls_config import LanguageServerConfig
 16 | from solidlsp.ls_logger import LanguageServerLogger
 17 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 18 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 19 | from solidlsp.settings import SolidLSPSettings
 20 | 
 21 | 
 22 | class RustAnalyzer(SolidLanguageServer):
 23 |     """
 24 |     Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust.
 25 |     """
 26 | 
 27 |     @staticmethod
 28 |     def _get_rustup_version():
 29 |         """Get installed rustup version or None if not found."""
 30 |         try:
 31 |             result = subprocess.run(["rustup", "--version"], capture_output=True, text=True, check=False)
 32 |             if result.returncode == 0:
 33 |                 return result.stdout.strip()
 34 |         except FileNotFoundError:
 35 |             return None
 36 |         return None
 37 | 
 38 |     @staticmethod
 39 |     def _get_rust_analyzer_path():
 40 |         """Get rust-analyzer path via rustup or system PATH."""
 41 |         # First try rustup
 42 |         try:
 43 |             result = subprocess.run(["rustup", "which", "rust-analyzer"], capture_output=True, text=True, check=False)
 44 |             if result.returncode == 0:
 45 |                 return result.stdout.strip()
 46 |         except FileNotFoundError:
 47 |             pass
 48 | 
 49 |         # Fallback to system PATH
 50 |         return shutil.which("rust-analyzer")
 51 | 
 52 |     @staticmethod
 53 |     def _ensure_rust_analyzer_installed():
 54 |         """Ensure rust-analyzer is available, install via rustup if needed."""
 55 |         path = RustAnalyzer._get_rust_analyzer_path()
 56 |         if path:
 57 |             return path
 58 | 
 59 |         # Check if rustup is available
 60 |         if not RustAnalyzer._get_rustup_version():
 61 |             raise RuntimeError(
 62 |                 "Neither rust-analyzer nor rustup is installed.\n"
 63 |                 "Please install Rust via https://rustup.rs/ or install rust-analyzer separately."
 64 |             )
 65 | 
 66 |         # Try to install rust-analyzer component
 67 |         result = subprocess.run(["rustup", "component", "add", "rust-analyzer"], check=False, capture_output=True, text=True)
 68 |         if result.returncode != 0:
 69 |             raise RuntimeError(f"Failed to install rust-analyzer via rustup: {result.stderr}")
 70 | 
 71 |         # Try again after installation
 72 |         path = RustAnalyzer._get_rust_analyzer_path()
 73 |         if not path:
 74 |             raise RuntimeError("rust-analyzer installation succeeded but binary not found in PATH")
 75 | 
 76 |         return path
 77 | 
 78 |     def __init__(
 79 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 80 |     ):
 81 |         """
 82 |         Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 83 |         """
 84 |         rustanalyzer_executable_path = self._ensure_rust_analyzer_installed()
 85 |         logger.log(f"Using rust-analyzer at: {rustanalyzer_executable_path}", logging.INFO)
 86 | 
 87 |         super().__init__(
 88 |             config,
 89 |             logger,
 90 |             repository_root_path,
 91 |             ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path),
 92 |             "rust",
 93 |             solidlsp_settings,
 94 |         )
 95 |         self.server_ready = threading.Event()
 96 |         self.service_ready_event = threading.Event()
 97 |         self.initialize_searcher_command_available = threading.Event()
 98 |         self.resolve_main_method_available = threading.Event()
 99 | 
100 |     @override
101 |     def is_ignored_dirname(self, dirname: str) -> bool:
102 |         return super().is_ignored_dirname(dirname) or dirname in ["target"]
103 | 
104 |     @staticmethod
105 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
106 |         """
107 |         Returns the initialize params for the Rust Analyzer Language Server.
108 |         """
109 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
110 |         initialize_params = {
111 |             "clientInfo": {"name": "Visual Studio Code - Insiders", "version": "1.82.0-insider"},
112 |             "locale": "en",
113 |             "capabilities": {
114 |                 "workspace": {
115 |                     "applyEdit": True,
116 |                     "workspaceEdit": {
117 |                         "documentChanges": True,
118 |                         "resourceOperations": ["create", "rename", "delete"],
119 |                         "failureHandling": "textOnlyTransactional",
120 |                         "normalizesLineEndings": True,
121 |                         "changeAnnotationSupport": {"groupsOnLabel": True},
122 |                     },
123 |                     "configuration": True,
124 |                     "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True},
125 |                     "symbol": {
126 |                         "dynamicRegistration": True,
127 |                         "symbolKind": {"valueSet": list(range(1, 27))},
128 |                         "tagSupport": {"valueSet": [1]},
129 |                         "resolveSupport": {"properties": ["location.range"]},
130 |                     },
131 |                     "codeLens": {"refreshSupport": True},
132 |                     "executeCommand": {"dynamicRegistration": True},
133 |                     "didChangeConfiguration": {"dynamicRegistration": True},
134 |                     "workspaceFolders": True,
135 |                     "semanticTokens": {"refreshSupport": True},
136 |                     "fileOperations": {
137 |                         "dynamicRegistration": True,
138 |                         "didCreate": True,
139 |                         "didRename": True,
140 |                         "didDelete": True,
141 |                         "willCreate": True,
142 |                         "willRename": True,
143 |                         "willDelete": True,
144 |                     },
145 |                     "inlineValue": {"refreshSupport": True},
146 |                     "inlayHint": {"refreshSupport": True},
147 |                     "diagnostics": {"refreshSupport": True},
148 |                 },
149 |                 "textDocument": {
150 |                     "publishDiagnostics": {
151 |                         "relatedInformation": True,
152 |                         "versionSupport": False,
153 |                         "tagSupport": {"valueSet": [1, 2]},
154 |                         "codeDescriptionSupport": True,
155 |                         "dataSupport": True,
156 |                     },
157 |                     "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
158 |                     "completion": {
159 |                         "dynamicRegistration": True,
160 |                         "contextSupport": True,
161 |                         "completionItem": {
162 |                             "snippetSupport": True,
163 |                             "commitCharactersSupport": True,
164 |                             "documentationFormat": ["markdown", "plaintext"],
165 |                             "deprecatedSupport": True,
166 |                             "preselectSupport": True,
167 |                             "tagSupport": {"valueSet": [1]},
168 |                             "insertReplaceSupport": True,
169 |                             "resolveSupport": {"properties": ["documentation", "detail", "additionalTextEdits"]},
170 |                             "insertTextModeSupport": {"valueSet": [1, 2]},
171 |                             "labelDetailsSupport": True,
172 |                         },
173 |                         "insertTextMode": 2,
174 |                         "completionItemKind": {
175 |                             "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
176 |                         },
177 |                         "completionList": {"itemDefaults": ["commitCharacters", "editRange", "insertTextFormat", "insertTextMode"]},
178 |                     },
179 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
180 |                     "signatureHelp": {
181 |                         "dynamicRegistration": True,
182 |                         "signatureInformation": {
183 |                             "documentationFormat": ["markdown", "plaintext"],
184 |                             "parameterInformation": {"labelOffsetSupport": True},
185 |                             "activeParameterSupport": True,
186 |                         },
187 |                         "contextSupport": True,
188 |                     },
189 |                     "definition": {"dynamicRegistration": True, "linkSupport": True},
190 |                     "references": {"dynamicRegistration": True},
191 |                     "documentHighlight": {"dynamicRegistration": True},
192 |                     "documentSymbol": {
193 |                         "dynamicRegistration": True,
194 |                         "symbolKind": {"valueSet": list(range(1, 27))},
195 |                         "hierarchicalDocumentSymbolSupport": True,
196 |                         "tagSupport": {"valueSet": [1]},
197 |                         "labelSupport": True,
198 |                     },
199 |                     "codeAction": {
200 |                         "dynamicRegistration": True,
201 |                         "isPreferredSupport": True,
202 |                         "disabledSupport": True,
203 |                         "dataSupport": True,
204 |                         "resolveSupport": {"properties": ["edit"]},
205 |                         "codeActionLiteralSupport": {
206 |                             "codeActionKind": {
207 |                                 "valueSet": [
208 |                                     "",
209 |                                     "quickfix",
210 |                                     "refactor",
211 |                                     "refactor.extract",
212 |                                     "refactor.inline",
213 |                                     "refactor.rewrite",
214 |                                     "source",
215 |                                     "source.organizeImports",
216 |                                 ]
217 |                             }
218 |                         },
219 |                         "honorsChangeAnnotations": False,
220 |                     },
221 |                     "codeLens": {"dynamicRegistration": True},
222 |                     "formatting": {"dynamicRegistration": True},
223 |                     "rangeFormatting": {"dynamicRegistration": True},
224 |                     "onTypeFormatting": {"dynamicRegistration": True},
225 |                     "rename": {
226 |                         "dynamicRegistration": True,
227 |                         "prepareSupport": True,
228 |                         "prepareSupportDefaultBehavior": 1,
229 |                         "honorsChangeAnnotations": True,
230 |                     },
231 |                     "documentLink": {"dynamicRegistration": True, "tooltipSupport": True},
232 |                     "typeDefinition": {"dynamicRegistration": True, "linkSupport": True},
233 |                     "implementation": {"dynamicRegistration": True, "linkSupport": True},
234 |                     "colorProvider": {"dynamicRegistration": True},
235 |                     "foldingRange": {
236 |                         "dynamicRegistration": True,
237 |                         "rangeLimit": 5000,
238 |                         "lineFoldingOnly": True,
239 |                         "foldingRangeKind": {"valueSet": ["comment", "imports", "region"]},
240 |                         "foldingRange": {"collapsedText": False},
241 |                     },
242 |                     "declaration": {"dynamicRegistration": True, "linkSupport": True},
243 |                     "selectionRange": {"dynamicRegistration": True},
244 |                     "callHierarchy": {"dynamicRegistration": True},
245 |                     "semanticTokens": {
246 |                         "dynamicRegistration": True,
247 |                         "tokenTypes": [
248 |                             "namespace",
249 |                             "type",
250 |                             "class",
251 |                             "enum",
252 |                             "interface",
253 |                             "struct",
254 |                             "typeParameter",
255 |                             "parameter",
256 |                             "variable",
257 |                             "property",
258 |                             "enumMember",
259 |                             "event",
260 |                             "function",
261 |                             "method",
262 |                             "macro",
263 |                             "keyword",
264 |                             "modifier",
265 |                             "comment",
266 |                             "string",
267 |                             "number",
268 |                             "regexp",
269 |                             "operator",
270 |                             "decorator",
271 |                         ],
272 |                         "tokenModifiers": [
273 |                             "declaration",
274 |                             "definition",
275 |                             "readonly",
276 |                             "static",
277 |                             "deprecated",
278 |                             "abstract",
279 |                             "async",
280 |                             "modification",
281 |                             "documentation",
282 |                             "defaultLibrary",
283 |                         ],
284 |                         "formats": ["relative"],
285 |                         "requests": {"range": True, "full": {"delta": True}},
286 |                         "multilineTokenSupport": False,
287 |                         "overlappingTokenSupport": False,
288 |                         "serverCancelSupport": True,
289 |                         "augmentsSyntaxTokens": False,
290 |                     },
291 |                     "linkedEditingRange": {"dynamicRegistration": True},
292 |                     "typeHierarchy": {"dynamicRegistration": True},
293 |                     "inlineValue": {"dynamicRegistration": True},
294 |                     "inlayHint": {
295 |                         "dynamicRegistration": True,
296 |                         "resolveSupport": {"properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"]},
297 |                     },
298 |                     "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False},
299 |                 },
300 |                 "window": {
301 |                     "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}},
302 |                     "showDocument": {"support": True},
303 |                     "workDoneProgress": True,
304 |                 },
305 |                 "general": {
306 |                     "staleRequestSupport": {
307 |                         "cancel": True,
308 |                         "retryOnContentModified": [
309 |                             "textDocument/semanticTokens/full",
310 |                             "textDocument/semanticTokens/range",
311 |                             "textDocument/semanticTokens/full/delta",
312 |                         ],
313 |                     },
314 |                     "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"},
315 |                     "markdown": {
316 |                         "parser": "marked",
317 |                         "version": "1.1.0",
318 |                         "allowedTags": [
319 |                             "ul",
320 |                             "li",
321 |                             "p",
322 |                             "code",
323 |                             "blockquote",
324 |                             "ol",
325 |                             "h1",
326 |                             "h2",
327 |                             "h3",
328 |                             "h4",
329 |                             "h5",
330 |                             "h6",
331 |                             "hr",
332 |                             "em",
333 |                             "pre",
334 |                             "table",
335 |                             "thead",
336 |                             "tbody",
337 |                             "tr",
338 |                             "th",
339 |                             "td",
340 |                             "div",
341 |                             "del",
342 |                             "a",
343 |                             "strong",
344 |                             "br",
345 |                             "img",
346 |                             "span",
347 |                         ],
348 |                     },
349 |                     "positionEncodings": ["utf-16"],
350 |                 },
351 |                 "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}},
352 |                 "experimental": {
353 |                     "snippetTextEdit": True,
354 |                     "codeActionGroup": True,
355 |                     "hoverActions": True,
356 |                     "serverStatusNotification": True,
357 |                     "colorDiagnosticOutput": True,
358 |                     "openServerLogs": True,
359 |                     "localDocs": True,
360 |                     "commands": {
361 |                         "commands": [
362 |                             "rust-analyzer.runSingle",
363 |                             "rust-analyzer.debugSingle",
364 |                             "rust-analyzer.showReferences",
365 |                             "rust-analyzer.gotoLocation",
366 |                             "editor.action.triggerParameterHints",
367 |                         ]
368 |                     },
369 |                 },
370 |             },
371 |             "initializationOptions": {
372 |                 "cargoRunner": None,
373 |                 "runnables": {"extraEnv": None, "problemMatcher": ["$rustc"], "command": None, "extraArgs": []},
374 |                 "statusBar": {"clickAction": "openLogs"},
375 |                 "server": {"path": None, "extraEnv": None},
376 |                 "trace": {"server": "verbose", "extension": False},
377 |                 "debug": {
378 |                     "engine": "auto",
379 |                     "sourceFileMap": {"/rustc/<id>": "${env:USERPROFILE}/.rustup/toolchains/<toolchain-id>/lib/rustlib/src/rust"},
380 |                     "openDebugPane": False,
381 |                     "engineSettings": {},
382 |                 },
383 |                 "restartServerOnConfigChange": False,
384 |                 "typing": {"continueCommentsOnNewline": True, "autoClosingAngleBrackets": {"enable": False}},
385 |                 "diagnostics": {
386 |                     "previewRustcOutput": False,
387 |                     "useRustcErrorCode": False,
388 |                     "disabled": [],
389 |                     "enable": True,
390 |                     "experimental": {"enable": False},
391 |                     "remapPrefix": {},
392 |                     "warningsAsHint": [],
393 |                     "warningsAsInfo": [],
394 |                 },
395 |                 "discoverProjectRunner": None,
396 |                 "showUnlinkedFileNotification": True,
397 |                 "showDependenciesExplorer": True,
398 |                 "assist": {"emitMustUse": False, "expressionFillDefault": "todo"},
399 |                 "cachePriming": {"enable": True, "numThreads": 0},
400 |                 "cargo": {
401 |                     "autoreload": True,
402 |                     "buildScripts": {
403 |                         "enable": True,
404 |                         "invocationLocation": "workspace",
405 |                         "invocationStrategy": "per_workspace",
406 |                         "overrideCommand": None,
407 |                         "useRustcWrapper": True,
408 |                     },
409 |                     "cfgs": {},
410 |                     "extraArgs": [],
411 |                     "extraEnv": {},
412 |                     "features": [],
413 |                     "noDefaultFeatures": False,
414 |                     "sysroot": "discover",
415 |                     "sysrootSrc": None,
416 |                     "target": None,
417 |                     "unsetTest": ["core"],
418 |                 },
419 |                 "checkOnSave": True,
420 |                 "check": {
421 |                     "allTargets": True,
422 |                     "command": "check",
423 |                     "extraArgs": [],
424 |                     "extraEnv": {},
425 |                     "features": None,
426 |                     "ignore": [],
427 |                     "invocationLocation": "workspace",
428 |                     "invocationStrategy": "per_workspace",
429 |                     "noDefaultFeatures": None,
430 |                     "overrideCommand": None,
431 |                     "targets": None,
432 |                 },
433 |                 "completion": {
434 |                     "autoimport": {"enable": True},
435 |                     "autoself": {"enable": True},
436 |                     "callable": {"snippets": "fill_arguments"},
437 |                     "fullFunctionSignatures": {"enable": False},
438 |                     "limit": None,
439 |                     "postfix": {"enable": True},
440 |                     "privateEditable": {"enable": False},
441 |                     "snippets": {
442 |                         "custom": {
443 |                             "Arc::new": {
444 |                                 "postfix": "arc",
445 |                                 "body": "Arc::new(${receiver})",
446 |                                 "requires": "std::sync::Arc",
447 |                                 "description": "Put the expression into an `Arc`",
448 |                                 "scope": "expr",
449 |                             },
450 |                             "Rc::new": {
451 |                                 "postfix": "rc",
452 |                                 "body": "Rc::new(${receiver})",
453 |                                 "requires": "std::rc::Rc",
454 |                                 "description": "Put the expression into an `Rc`",
455 |                                 "scope": "expr",
456 |                             },
457 |                             "Box::pin": {
458 |                                 "postfix": "pinbox",
459 |                                 "body": "Box::pin(${receiver})",
460 |                                 "requires": "std::boxed::Box",
461 |                                 "description": "Put the expression into a pinned `Box`",
462 |                                 "scope": "expr",
463 |                             },
464 |                             "Ok": {
465 |                                 "postfix": "ok",
466 |                                 "body": "Ok(${receiver})",
467 |                                 "description": "Wrap the expression in a `Result::Ok`",
468 |                                 "scope": "expr",
469 |                             },
470 |                             "Err": {
471 |                                 "postfix": "err",
472 |                                 "body": "Err(${receiver})",
473 |                                 "description": "Wrap the expression in a `Result::Err`",
474 |                                 "scope": "expr",
475 |                             },
476 |                             "Some": {
477 |                                 "postfix": "some",
478 |                                 "body": "Some(${receiver})",
479 |                                 "description": "Wrap the expression in an `Option::Some`",
480 |                                 "scope": "expr",
481 |                             },
482 |                         }
483 |                     },
484 |                 },
485 |                 "files": {"excludeDirs": [], "watcher": "client"},
486 |                 "highlightRelated": {
487 |                     "breakPoints": {"enable": True},
488 |                     "closureCaptures": {"enable": True},
489 |                     "exitPoints": {"enable": True},
490 |                     "references": {"enable": True},
491 |                     "yieldPoints": {"enable": True},
492 |                 },
493 |                 "hover": {
494 |                     "actions": {
495 |                         "debug": {"enable": True},
496 |                         "enable": True,
497 |                         "gotoTypeDef": {"enable": True},
498 |                         "implementations": {"enable": True},
499 |                         "references": {"enable": False},
500 |                         "run": {"enable": True},
501 |                     },
502 |                     "documentation": {"enable": True, "keywords": {"enable": True}},
503 |                     "links": {"enable": True},
504 |                     "memoryLayout": {"alignment": "hexadecimal", "enable": True, "niches": False, "offset": "hexadecimal", "size": "both"},
505 |                 },
506 |                 "imports": {
507 |                     "granularity": {"enforce": False, "group": "crate"},
508 |                     "group": {"enable": True},
509 |                     "merge": {"glob": True},
510 |                     "preferNoStd": False,
511 |                     "preferPrelude": False,
512 |                     "prefix": "plain",
513 |                 },
514 |                 "inlayHints": {
515 |                     "bindingModeHints": {"enable": False},
516 |                     "chainingHints": {"enable": True},
517 |                     "closingBraceHints": {"enable": True, "minLines": 25},
518 |                     "closureCaptureHints": {"enable": False},
519 |                     "closureReturnTypeHints": {"enable": "never"},
520 |                     "closureStyle": "impl_fn",
521 |                     "discriminantHints": {"enable": "never"},
522 |                     "expressionAdjustmentHints": {"enable": "never", "hideOutsideUnsafe": False, "mode": "prefix"},
523 |                     "lifetimeElisionHints": {"enable": "never", "useParameterNames": False},
524 |                     "maxLength": 25,
525 |                     "parameterHints": {"enable": True},
526 |                     "reborrowHints": {"enable": "never"},
527 |                     "renderColons": True,
528 |                     "typeHints": {"enable": True, "hideClosureInitialization": False, "hideNamedConstructor": False},
529 |                 },
530 |                 "interpret": {"tests": False},
531 |                 "joinLines": {"joinAssignments": True, "joinElseIf": True, "removeTrailingComma": True, "unwrapTrivialBlock": True},
532 |                 "lens": {
533 |                     "debug": {"enable": True},
534 |                     "enable": True,
535 |                     "forceCustomCommands": True,
536 |                     "implementations": {"enable": True},
537 |                     "location": "above_name",
538 |                     "references": {
539 |                         "adt": {"enable": False},
540 |                         "enumVariant": {"enable": False},
541 |                         "method": {"enable": False},
542 |                         "trait": {"enable": False},
543 |                     },
544 |                     "run": {"enable": True},
545 |                 },
546 |                 "linkedProjects": [],
547 |                 "lru": {"capacity": None, "query": {"capacities": {}}},
548 |                 "notifications": {"cargoTomlNotFound": True},
549 |                 "numThreads": None,
550 |                 "procMacro": {"attributes": {"enable": True}, "enable": True, "ignored": {}, "server": None},
551 |                 "references": {"excludeImports": False},
552 |                 "rust": {"analyzerTargetDir": None},
553 |                 "rustc": {"source": None},
554 |                 "rustfmt": {"extraArgs": [], "overrideCommand": None, "rangeFormatting": {"enable": False}},
555 |                 "semanticHighlighting": {
556 |                     "doc": {"comment": {"inject": {"enable": True}}},
557 |                     "nonStandardTokens": True,
558 |                     "operator": {"enable": True, "specialization": {"enable": False}},
559 |                     "punctuation": {"enable": False, "separate": {"macro": {"bang": False}}, "specialization": {"enable": False}},
560 |                     "strings": {"enable": True},
561 |                 },
562 |                 "signatureInfo": {"detail": "full", "documentation": {"enable": True}},
563 |                 "workspace": {"symbol": {"search": {"kind": "only_types", "limit": 128, "scope": "workspace"}}},
564 |             },
565 |             "trace": "verbose",
566 |             "processId": os.getpid(),
567 |             "rootPath": repository_absolute_path,
568 |             "rootUri": root_uri,
569 |             "workspaceFolders": [
570 |                 {
571 |                     "uri": root_uri,
572 |                     "name": os.path.basename(repository_absolute_path),
573 |                 }
574 |             ],
575 |         }
576 |         return initialize_params
577 | 
578 |     def _start_server(self):
579 |         """
580 |         Starts the Rust Analyzer Language Server
581 |         """
582 | 
583 |         def register_capability_handler(params):
584 |             assert "registrations" in params
585 |             for registration in params["registrations"]:
586 |                 if registration["method"] == "workspace/executeCommand":
587 |                     self.initialize_searcher_command_available.set()
588 |                     self.resolve_main_method_available.set()
589 |             return
590 | 
591 |         def lang_status_handler(params):
592 |             # TODO: Should we wait for
593 |             # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
594 |             # Before proceeding?
595 |             if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
596 |                 self.service_ready_event.set()
597 | 
598 |         def execute_client_command_handler(params):
599 |             return []
600 | 
601 |         def do_nothing(params):
602 |             return
603 | 
604 |         def check_experimental_status(params):
605 |             if params["quiescent"] == True:
606 |                 self.server_ready.set()
607 | 
608 |         def window_log_message(msg):
609 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
610 | 
611 |         self.server.on_request("client/registerCapability", register_capability_handler)
612 |         self.server.on_notification("language/status", lang_status_handler)
613 |         self.server.on_notification("window/logMessage", window_log_message)
614 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
615 |         self.server.on_notification("$/progress", do_nothing)
616 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
617 |         self.server.on_notification("language/actionableNotification", do_nothing)
618 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
619 | 
620 |         self.logger.log("Starting RustAnalyzer server process", logging.INFO)
621 |         self.server.start()
622 |         initialize_params = self._get_initialize_params(self.repository_root_path)
623 | 
624 |         self.logger.log(
625 |             "Sending initialize request from LSP client to LSP server and awaiting response",
626 |             logging.INFO,
627 |         )
628 |         init_response = self.server.send.initialize(initialize_params)
629 |         assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
630 |         assert "completionProvider" in init_response["capabilities"]
631 |         assert init_response["capabilities"]["completionProvider"] == {
632 |             "resolveProvider": True,
633 |             "triggerCharacters": [":", ".", "'", "("],
634 |             "completionItem": {"labelDetailsSupport": True},
635 |         }
636 |         self.server.notify.initialized({})
637 |         self.completions_available.set()
638 | 
639 |         self.server_ready.wait()
640 | 
```
Page 10/14FirstPrevNextLast