#
tokens: 47924/50000 18/294 files (page 4/14)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 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

--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/models.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Models module that demonstrates various Python class patterns.
  3 | """
  4 | 
  5 | from abc import ABC, abstractmethod
  6 | from typing import Any, Generic, TypeVar
  7 | 
  8 | T = TypeVar("T")
  9 | 
 10 | 
 11 | class BaseModel(ABC):
 12 |     """
 13 |     Abstract base class for all models.
 14 |     """
 15 | 
 16 |     def __init__(self, id: str, name: str | None = None):
 17 |         self.id = id
 18 |         self.name = name or id
 19 | 
 20 |     @abstractmethod
 21 |     def to_dict(self) -> dict[str, Any]:
 22 |         """Convert model to dictionary representation"""
 23 | 
 24 |     @classmethod
 25 |     def from_dict(cls, data: dict[str, Any]) -> "BaseModel":
 26 |         """Create a model instance from dictionary data"""
 27 |         id = data.get("id", "")
 28 |         name = data.get("name")
 29 |         return cls(id=id, name=name)
 30 | 
 31 | 
 32 | class User(BaseModel):
 33 |     """
 34 |     User model representing a system user.
 35 |     """
 36 | 
 37 |     def __init__(self, id: str, name: str | None = None, email: str = "", roles: list[str] | None = None):
 38 |         super().__init__(id, name)
 39 |         self.email = email
 40 |         self.roles = roles or []
 41 | 
 42 |     def to_dict(self) -> dict[str, Any]:
 43 |         return {"id": self.id, "name": self.name, "email": self.email, "roles": self.roles}
 44 | 
 45 |     @classmethod
 46 |     def from_dict(cls, data: dict[str, Any]) -> "User":
 47 |         instance = super().from_dict(data)
 48 |         instance.email = data.get("email", "")
 49 |         instance.roles = data.get("roles", [])
 50 |         return instance
 51 | 
 52 |     def has_role(self, role: str) -> bool:
 53 |         """Check if user has a specific role"""
 54 |         return role in self.roles
 55 | 
 56 | 
 57 | class Item(BaseModel):
 58 |     """
 59 |     Item model representing a product or service.
 60 |     """
 61 | 
 62 |     def __init__(self, id: str, name: str | None = None, price: float = 0.0, category: str = ""):
 63 |         super().__init__(id, name)
 64 |         self.price = price
 65 |         self.category = category
 66 | 
 67 |     def to_dict(self) -> dict[str, Any]:
 68 |         return {"id": self.id, "name": self.name, "price": self.price, "category": self.category}
 69 | 
 70 |     def get_display_price(self) -> str:
 71 |         """Format price for display"""
 72 |         return f"${self.price:.2f}"
 73 | 
 74 | 
 75 | # Generic type example
 76 | class Collection(Generic[T]):
 77 |     def __init__(self, items: list[T] | None = None):
 78 |         self.items = items or []
 79 | 
 80 |     def add(self, item: T) -> None:
 81 |         self.items.append(item)
 82 | 
 83 |     def get_all(self) -> list[T]:
 84 |         return self.items
 85 | 
 86 | 
 87 | # Factory function
 88 | def create_user_object(id: str, name: str, email: str, roles: list[str] | None = None) -> User:
 89 |     """Factory function to create a user"""
 90 |     return User(id=id, name=name, email=email, roles=roles)
 91 | 
 92 | 
 93 | # Multiple inheritance examples
 94 | 
 95 | 
 96 | class Loggable:
 97 |     """
 98 |     Mixin class that provides logging functionality.
 99 |     Example of a common mixin pattern used with multiple inheritance.
100 |     """
101 | 
102 |     def __init__(self, **kwargs):
103 |         super().__init__(**kwargs)
104 |         self.log_entries: list[str] = []
105 | 
106 |     def log(self, message: str) -> None:
107 |         """Add a log entry"""
108 |         self.log_entries.append(message)
109 | 
110 |     def get_logs(self) -> list[str]:
111 |         """Get all log entries"""
112 |         return self.log_entries
113 | 
114 | 
115 | class Serializable:
116 |     """
117 |     Mixin class that provides JSON serialization capabilities.
118 |     Another example of a mixin for multiple inheritance.
119 |     """
120 | 
121 |     def __init__(self, **kwargs):
122 |         super().__init__(**kwargs)
123 | 
124 |     def to_json(self) -> dict[str, Any]:
125 |         """Convert to JSON-serializable dictionary"""
126 |         return self.to_dict() if hasattr(self, "to_dict") else {}
127 | 
128 |     @classmethod
129 |     def from_json(cls, data: dict[str, Any]) -> Any:
130 |         """Create instance from JSON data"""
131 |         return cls.from_dict(data) if hasattr(cls, "from_dict") else cls(**data)
132 | 
133 | 
134 | class Auditable:
135 |     """
136 |     Mixin for tracking creation and modification timestamps.
137 |     """
138 | 
139 |     def __init__(self, **kwargs):
140 |         super().__init__(**kwargs)
141 |         self.created_at: str = kwargs.get("created_at", "")
142 |         self.updated_at: str = kwargs.get("updated_at", "")
143 | 
144 |     def update_timestamp(self, timestamp: str) -> None:
145 |         """Update the last modified timestamp"""
146 |         self.updated_at = timestamp
147 | 
148 | 
149 | # Diamond inheritance pattern
150 | class BaseService(ABC):
151 |     """
152 |     Base class for service objects - demonstrates diamond inheritance pattern.
153 |     """
154 | 
155 |     def __init__(self, name: str = "base"):
156 |         self.service_name = name
157 | 
158 |     @abstractmethod
159 |     def get_service_info(self) -> dict[str, str]:
160 |         """Get service information"""
161 | 
162 | 
163 | class DataService(BaseService):
164 |     """
165 |     Data handling service.
166 |     """
167 | 
168 |     def __init__(self, **kwargs):
169 |         name = kwargs.pop("name", "data")
170 |         super().__init__(name=name)
171 |         self.data_source = kwargs.get("data_source", "default")
172 | 
173 |     def get_service_info(self) -> dict[str, str]:
174 |         return {"service_type": "data", "service_name": self.service_name, "data_source": self.data_source}
175 | 
176 | 
177 | class NetworkService(BaseService):
178 |     """
179 |     Network connectivity service.
180 |     """
181 | 
182 |     def __init__(self, **kwargs):
183 |         name = kwargs.pop("name", "network")
184 |         super().__init__(name=name)
185 |         self.endpoint = kwargs.get("endpoint", "localhost")
186 | 
187 |     def get_service_info(self) -> dict[str, str]:
188 |         return {"service_type": "network", "service_name": self.service_name, "endpoint": self.endpoint}
189 | 
190 | 
191 | class DataSyncService(DataService, NetworkService):
192 |     """
193 |     Service that syncs data over network - example of diamond inheritance.
194 |     Inherits from both DataService and NetworkService, which both inherit from BaseService.
195 |     """
196 | 
197 |     def __init__(self, **kwargs):
198 |         super().__init__(**kwargs)
199 |         self.sync_interval = kwargs.get("sync_interval", 60)
200 | 
201 |     def get_service_info(self) -> dict[str, str]:
202 |         info = super().get_service_info()
203 |         info.update({"service_type": "data_sync", "sync_interval": str(self.sync_interval)})
204 |         return info
205 | 
206 | 
207 | # Multiple inheritance with mixins
208 | 
209 | 
210 | class LoggableUser(User, Loggable):
211 |     """
212 |     User class with logging capabilities.
213 |     Example of extending a concrete class with a mixin.
214 |     """
215 | 
216 |     def __init__(self, id: str, name: str | None = None, email: str = "", roles: list[str] | None = None):
217 |         super().__init__(id=id, name=name, email=email, roles=roles)
218 | 
219 |     def add_role(self, role: str) -> None:
220 |         """Add a role to the user and log the action"""
221 |         if role not in self.roles:
222 |             self.roles.append(role)
223 |             self.log(f"Added role '{role}' to user {self.id}")
224 | 
225 | 
226 | class TrackedItem(Item, Serializable, Auditable):
227 |     """
228 |     Item with serialization and auditing capabilities.
229 |     Example of a class inheriting from a concrete class and multiple mixins.
230 |     """
231 | 
232 |     def __init__(
233 |         self, id: str, name: str | None = None, price: float = 0.0, category: str = "", created_at: str = "", updated_at: str = ""
234 |     ):
235 |         super().__init__(id=id, name=name, price=price, category=category, created_at=created_at, updated_at=updated_at)
236 |         self.stock_level = 0
237 | 
238 |     def update_stock(self, quantity: int) -> None:
239 |         """Update stock level and timestamp"""
240 |         self.stock_level = quantity
241 |         self.update_timestamp(f"stock_update_{quantity}")
242 | 
243 |     def to_dict(self) -> dict[str, Any]:
244 |         result = super().to_dict()
245 |         result.update({"stock_level": self.stock_level, "created_at": self.created_at, "updated_at": self.updated_at})
246 |         return result
247 | 
```

--------------------------------------------------------------------------------
/.serena/memories/adding_new_language_support_guide.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Adding New Language Support to Serena
  2 | 
  3 | This guide explains how to add support for a new programming language to Serena.
  4 | 
  5 | ## Overview
  6 | 
  7 | Adding a new language involves:
  8 | 
  9 | 1. **Language Server Implementation** - Creating a language-specific server class
 10 | 2. **Language Registration** - Adding the language to enums and configurations  
 11 | 3. **Test Repository** - Creating a minimal test project
 12 | 4. **Test Suite** - Writing comprehensive tests
 13 | 5. **Runtime Dependencies** - Configuring automatic language server downloads
 14 | 
 15 | ## Step 1: Language Server Implementation
 16 | 
 17 | ### 1.1 Create Language Server Class
 18 | 
 19 | Create a new file in `src/solidlsp/language_servers/` (e.g., `new_language_server.py`).
 20 | Have a look at `intelephense.py` for a reference implementation of a language server which downloads all its dependencies, at `gopls.py` for an LS that needs some preinstalled
 21 | dependencies, and on `pyright_server.py` that does not need any additional dependencies
 22 | because the language server can be installed directly as python package.
 23 | 
 24 | 
 25 | ```python
 26 | from solidlsp.ls import SolidLanguageServer
 27 | from solidlsp.ls_config import LanguageServerConfig
 28 | from solidlsp.ls_logger import LanguageServerLogger
 29 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 30 | 
 31 | class NewLanguageServer(SolidLanguageServer):
 32 |     """
 33 |     Language server implementation for NewLanguage.
 34 |     """
 35 |     
 36 |     def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
 37 |         # Determine language server command
 38 |         cmd = self._get_language_server_command()
 39 |         
 40 |         super().__init__(
 41 |             config,
 42 |             logger,
 43 |             repository_root_path,
 44 |             ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
 45 |             "new_language",  # Language ID for LSP
 46 |         )
 47 |     
 48 |     def _get_language_server_command(self) -> list[str]:
 49 |         """Get the command to start the language server."""
 50 |         # Example: return ["new-language-server", "--stdio"]
 51 |         pass
 52 |     
 53 |     @override
 54 |     def is_ignored_dirname(self, dirname: str) -> bool:
 55 |         """Define language-specific directories to ignore."""
 56 |         return super().is_ignored_dirname(dirname) or dirname in ["build", "dist", "target"]
 57 | ```
 58 | 
 59 | ### 1.2 Language Server Discovery and Installation
 60 | 
 61 | For languages requiring automatic installation, implement download logic similar to C#:
 62 | 
 63 | ```python
 64 | @classmethod
 65 | def _ensure_server_installed(cls, logger: LanguageServerLogger) -> str:
 66 |     """Ensure language server is installed and return path."""
 67 |     # Check system installation first
 68 |     system_server = shutil.which("new-language-server")
 69 |     if system_server:
 70 |         return system_server
 71 |     
 72 |     # Download and install if needed
 73 |     server_path = cls._download_and_install_server(logger)
 74 |     return server_path
 75 | 
 76 | def _download_and_install_server(cls, logger: LanguageServerLogger) -> str:
 77 |     """Download and install the language server."""
 78 |     # Implementation specific to your language server
 79 |     pass
 80 | ```
 81 | 
 82 | ### 1.3 LSP Initialization
 83 | 
 84 | Override initialization methods if needed:
 85 | 
 86 | ```python
 87 | def _get_initialize_params(self) -> InitializeParams:
 88 |     """Return language-specific initialization parameters."""
 89 |     return {
 90 |         "processId": os.getpid(),
 91 |         "rootUri": PathUtils.path_to_uri(self.repository_root_path),
 92 |         "capabilities": {
 93 |             # Language-specific capabilities
 94 |         }
 95 |     }
 96 | 
 97 | def _start_server(self):
 98 |     """Start the language server with custom handlers."""
 99 |     # Set up notification handlers
100 |     self.server.on_notification("window/logMessage", self._handle_log_message)
101 |     
102 |     # Start server and initialize
103 |     self.server.start()
104 |     init_response = self.server.send.initialize(self._get_initialize_params())
105 |     self.server.notify.initialized({})
106 | ```
107 | 
108 | ## Step 2: Language Registration
109 | 
110 | ### 2.1 Add to Language Enum
111 | 
112 | In `src/solidlsp/ls_config.py`, add your language to the `Language` enum:
113 | 
114 | ```python
115 | class Language(str, Enum):
116 |     # Existing languages...
117 |     NEW_LANGUAGE = "new_language"
118 |     
119 |     def get_source_fn_matcher(self) -> FilenameMatcher:
120 |         match self:
121 |             # Existing cases...
122 |             case self.NEW_LANGUAGE:
123 |                 return FilenameMatcher("*.newlang", "*.nl")  # File extensions
124 | ```
125 | 
126 | ### 2.2 Update Language Server Factory
127 | 
128 | In `src/solidlsp/ls.py`, add your language to the `create` method:
129 | 
130 | ```python
131 | @classmethod
132 | def create(cls, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str) -> "SolidLanguageServer":
133 |     match config.code_language:
134 |         # Existing cases...
135 |         case Language.NEW_LANGUAGE:
136 |             from solidlsp.language_servers.new_language_server import NewLanguageServer
137 |             return NewLanguageServer(config, logger, repository_root_path)
138 | ```
139 | 
140 | ## Step 3: Test Repository
141 | 
142 | ### 3.1 Create Test Project
143 | 
144 | Create a minimal project in `test/resources/repos/new_language/test_repo/`:
145 | 
146 | ```
147 | test/resources/repos/new_language/test_repo/
148 | ├── main.newlang              # Main source file
149 | ├── lib/
150 | │   └── helper.newlang       # Additional source for testing
151 | ├── project.toml             # Project configuration (if applicable)
152 | └── .gitignore              # Ignore build artifacts
153 | ```
154 | 
155 | ### 3.2 Example Source Files
156 | 
157 | Create meaningful source files that demonstrate:
158 | 
159 | - **Classes/Types** - For symbol testing
160 | - **Functions/Methods** - For reference finding
161 | - **Imports/Dependencies** - For cross-file operations
162 | - **Nested Structures** - For hierarchical symbol testing
163 | 
164 | Example `main.newlang`:
165 | ```
166 | import lib.helper
167 | 
168 | class Calculator {
169 |     func add(a: Int, b: Int) -> Int {
170 |         return a + b
171 |     }
172 |     
173 |     func subtract(a: Int, b: Int) -> Int {
174 |         return helper.subtract(a, b)  // Reference to imported function
175 |     }
176 | }
177 | 
178 | class Program {
179 |     func main() {
180 |         let calc = Calculator()
181 |         let result = calc.add(5, 3)  // Reference to add method
182 |         print(result)
183 |     }
184 | }
185 | ```
186 | 
187 | ## Step 4: Test Suite
188 | 
189 | Testing the language server implementation is of crucial importance, and the tests will
190 | form the main part of the review process. Make sure that the tests are up to the standard
191 | of Serena to make the review go smoother.
192 | 
193 | General rules for tests:
194 | 
195 | 1. Tests for symbols and references should always check that the expected symbol names and references were actually found.
196 |    Just testing that a list came back or that the result is not None is insufficient.
197 | 2. Tests should never be skipped, the only exception is skipping based on some package being available or on an unsupported OS.
198 | 3. Tests should run in CI, check if there is a suitable GitHub action for installing the dependencies.
199 | 
200 | ### 4.1 Basic Tests
201 | 
202 | Create `test/solidlsp/new_language/test_new_language_basic.py`.
203 | Have a look at the structure of existing tests, for example, in `test/solidlsp/php/test_php_basic.py`
204 | You should at least test:
205 | 
206 | 1. Finding symbols
207 | 2. Finding within-file references
208 | 3. Finding cross-file references
209 | 
210 | Have a look at `test/solidlsp/php/test_php_basic.py` as an example for what should be tested.
211 | Don't forget to add a new language marker to `pytest.ini`.
212 | 
213 | ### 4.2 Integration Tests
214 | 
215 | Consider adding new cases to the parametrized tests in `test_serena_agent.py` for the new language.
216 | 
217 | 
218 | ### 5 Documentation
219 | 
220 | Update:
221 | 
222 | - **README.md** - Add language to supported languages list
223 | - **CHANGELOG.md** - Document the new language support
224 | - **Language-specific docs** - Installation requirements, known issues
225 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/php/test_php_basic.py:
--------------------------------------------------------------------------------

```python
  1 | from pathlib import Path
  2 | 
  3 | import pytest
  4 | 
  5 | from solidlsp import SolidLanguageServer
  6 | from solidlsp.ls_config import Language
  7 | 
  8 | 
  9 | @pytest.mark.php
 10 | class TestPhpLanguageServer:
 11 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
 12 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
 13 |     def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 14 |         """Test that the language server starts and stops successfully."""
 15 |         # The fixture already handles start and stop
 16 |         assert language_server.is_running()
 17 |         assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
 18 | 
 19 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
 20 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
 21 |     def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 22 | 
 23 |         # In index.php:
 24 |         # Line 9 (1-indexed): $greeting = greet($userName);
 25 |         # Line 11 (1-indexed): echo $greeting;
 26 |         # We want to find the definition of $greeting (defined on line 9)
 27 |         # from its usage in echo $greeting; on line 11.
 28 |         # LSP is 0-indexed: definition on line 8, usage on line 10.
 29 |         # $greeting in echo $greeting; is at char 5 on line 11 (0-indexed: line 10, char 5)
 30 |         # e c h o   $ g r e e t i n g
 31 |         #           ^ char 5
 32 |         definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 10, 6)  # cursor on 'g' in $greeting
 33 | 
 34 |         assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
 35 |         assert len(definition_location_list) == 1
 36 |         definition_location = definition_location_list[0]
 37 |         assert definition_location["uri"].endswith("index.php")
 38 |         # Definition of $greeting is on line 10 (1-indexed) / line 9 (0-indexed), char 0
 39 |         assert definition_location["range"]["start"]["line"] == 9
 40 |         assert definition_location["range"]["start"]["character"] == 0
 41 | 
 42 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
 43 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
 44 |     def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 45 |         definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 12, 5)  # helperFunction
 46 | 
 47 |         assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
 48 |         assert len(definition_location_list) == 1
 49 |         definition_location = definition_location_list[0]
 50 |         assert definition_location["uri"].endswith("helper.php")
 51 |         assert definition_location["range"]["start"]["line"] == 2
 52 |         assert definition_location["range"]["start"]["character"] == 0
 53 | 
 54 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
 55 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
 56 |     def test_find_definition_simple_variable(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 57 |         file_path = str(repo_path / "simple_var.php")
 58 | 
 59 |         # In simple_var.php:
 60 |         # Line 2 (1-indexed): $localVar = "test";
 61 |         # Line 3 (1-indexed): echo $localVar;
 62 |         # LSP is 0-indexed: definition on line 1, usage on line 2
 63 |         # Find definition of $localVar (char 5 on line 3 / 0-indexed: line 2, char 5)
 64 |         # $localVar in echo $localVar;  (e c h o   $ l o c a l V a r)
 65 |         #                           ^ char 5
 66 |         definition_location_list = language_server.request_definition(file_path, 2, 6)  # cursor on 'l' in $localVar
 67 | 
 68 |         assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
 69 |         assert len(definition_location_list) == 1
 70 |         definition_location = definition_location_list[0]
 71 |         assert definition_location["uri"].endswith("simple_var.php")
 72 |         assert definition_location["range"]["start"]["line"] == 1  # Definition of $localVar (0-indexed)
 73 |         assert definition_location["range"]["start"]["character"] == 0  # $localVar (0-indexed)
 74 | 
 75 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
 76 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
 77 |     def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 78 |         index_php_path = str(repo_path / "index.php")
 79 | 
 80 |         # In index.php (0-indexed lines):
 81 |         # Line 9: $greeting = greet($userName); // Definition of $greeting
 82 |         # Line 11: echo $greeting;            // Usage of $greeting
 83 |         # Find references for $greeting from its usage in "echo $greeting;" (line 11, char 6 for 'g')
 84 |         references = language_server.request_references(index_php_path, 11, 6)
 85 | 
 86 |         assert references
 87 |         # Intelephense, when asked for references from usage, seems to only return the usage itself.
 88 |         assert len(references) == 1, "Expected to find 1 reference for $greeting (the usage itself)"
 89 | 
 90 |         expected_locations = [{"uri_suffix": "index.php", "line": 11, "character": 5}]  # Usage: echo $greeting (points to $)
 91 | 
 92 |         # Convert actual references to a comparable format and sort
 93 |         actual_locations = sorted(
 94 |             [
 95 |                 {
 96 |                     "uri_suffix": loc["uri"].split("/")[-1],
 97 |                     "line": loc["range"]["start"]["line"],
 98 |                     "character": loc["range"]["start"]["character"],
 99 |                 }
100 |                 for loc in references
101 |             ],
102 |             key=lambda x: (x["uri_suffix"], x["line"], x["character"]),
103 |         )
104 | 
105 |         expected_locations = sorted(expected_locations, key=lambda x: (x["uri_suffix"], x["line"], x["character"]))
106 | 
107 |         assert actual_locations == expected_locations
108 | 
109 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
110 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
111 |     def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
112 |         helper_php_path = str(repo_path / "helper.php")
113 |         # In index.php (0-indexed lines):
114 |         # Line 13: helperFunction(); // Usage of helperFunction
115 |         # Find references for helperFunction from its definition
116 |         references = language_server.request_references(helper_php_path, 2, len("function "))
117 | 
118 |         assert references, f"Expected non-empty references for helperFunction but got {references=}"
119 |         # Intelephense might return 1 (usage) or 2 (usage + definition) references.
120 |         # Let's check for at least the usage in index.php
121 |         # Definition is in helper.php, line 2, char 0 (based on previous findings)
122 |         # Usage is in index.php, line 13, char 0
123 | 
124 |         actual_locations_comparable = []
125 |         for loc in references:
126 |             actual_locations_comparable.append(
127 |                 {
128 |                     "uri_suffix": loc["uri"].split("/")[-1],
129 |                     "line": loc["range"]["start"]["line"],
130 |                     "character": loc["range"]["start"]["character"],
131 |                 }
132 |             )
133 | 
134 |         usage_in_index_php = {"uri_suffix": "index.php", "line": 13, "character": 0}
135 |         assert usage_in_index_php in actual_locations_comparable, "Usage of helperFunction in index.php not found"
136 | 
```

--------------------------------------------------------------------------------
/src/serena/resources/config/modes/editing.yml:
--------------------------------------------------------------------------------

```yaml
  1 | description: All tools, with detailed instructions for code editing
  2 | prompt: |
  3 |   You are operating in editing mode. You can edit files with the provided tools
  4 |   to implement the requested changes to the code base while adhering to the project's code style and patterns.
  5 |   Use symbolic editing tools whenever possible for precise code modifications.
  6 |   If no editing task has yet been provided, wait for the user to provide one.
  7 | 
  8 |   When writing new code, think about where it belongs best. Don't generate new files if you don't plan on actually
  9 |   integrating them into the codebase, instead use the editing tools to insert the code directly into the existing files in that case.
 10 | 
 11 |   You have two main approaches for editing code - editing by regex and editing by symbol.
 12 |   The symbol-based approach is appropriate if you need to adjust an entire symbol, e.g. a method, a class, a function, etc.
 13 |   But it is not appropriate if you need to adjust just a few lines of code within a symbol, for that you should
 14 |   use the regex-based approach that is described below.
 15 | 
 16 |   Let us first discuss the symbol-based approach.
 17 |   Symbols are identified by their name path and relative file path, see the description of the `find_symbol` tool for more details
 18 |   on how the `name_path` matches symbols.
 19 |   You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file,
 20 |   or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible
 21 |   while still solving your task, meaning you only read the bodies when you need to, and after you have found the symbol you want to edit.
 22 |   Before calling symbolic reading tools, you should have a basic understanding of the repository structure that you can get from memories
 23 |   or by using the `list_dir` and `find_file` tools (or similar).
 24 |   For example, if you are working with python code and already know that you need to read the body of the constructor of the class Foo, you can directly
 25 |   use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit,
 26 |   you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding
 27 |   to read the desired methods with `include_body=True`.
 28 |   In particular, keep in mind the description of the `replace_symbol_body` tool. If you want to add some new code at the end of the file, you should
 29 |   use the `insert_after_symbol` tool with the last top-level symbol in the file. If you want to add an import, often a good strategy is to use
 30 |   `insert_before_symbol` with the first top-level symbol in the file.
 31 |   You can understand relationships between symbols by using the `find_referencing_symbols` tool. If not explicitly requested otherwise by a user,
 32 |   you make sure that when you edit a symbol, it is either done in a backward-compatible way, or you find and adjust the references as needed.
 33 |   The `find_referencing_symbols` tool will give you code snippets around the references, as well as symbolic information.
 34 |   You will generally be able to use the info from the snippets and the regex-based approach to adjust the references as well.
 35 |   You can assume that all symbol editing tools are reliable, so you don't need to verify the results if the tool returns without error.
 36 | 
 37 |   {% if 'replace_regex' in available_tools %}
 38 |   Let us discuss the regex-based approach.
 39 |   The regex-based approach is your primary tool for editing code whenever replacing or deleting a whole symbol would be a more expensive operation.
 40 |   This is the case if you need to adjust just a few lines of code within a method, or a chunk that is much smaller than a whole symbol.
 41 |   You use other tools to find the relevant content and
 42 |   then use your knowledge of the codebase to write the regex, if you haven't collected enough information of this content yet.
 43 |   You are extremely good at regex, so you never need to check whether the replacement produced the correct result.
 44 |   In particular, you know what to escape and what not to escape, and you know how to use wildcards.
 45 |   Also, the regex tool never adds any indentation (contrary to the symbolic editing tools), so you have to take care to add the correct indentation
 46 |   when using it to insert code.
 47 |   Moreover, the replacement tool will fail if it can't perform the desired replacement, and this is all the feedback you need.
 48 |   Your overall goal for replacement operations is to use relatively short regexes, since I want you to minimize the number
 49 |   of output tokens. For replacements of larger chunks of code, this means you intelligently make use of wildcards for the middle part 
 50 |   and of characteristic snippets for the before/after parts that uniquely identify the chunk.
 51 |   
 52 |   For small replacements, up to a single line, you follow the following rules:
 53 | 
 54 |     1. If the snippet to be replaced is likely to be unique within the file, you perform the replacement by directly using the escaped version of the 
 55 |        original.
 56 |     2. If the snippet is probably not unique, and you want to replace all occurrences, you use the `allow_multiple_occurrences` flag.
 57 |     3. If the snippet is not unique, and you want to replace a specific occurrence, you make use of the code surrounding the snippet
 58 |        to extend the regex with content before/after such that the regex will have exactly one match.
 59 |     4. You generally assume that a snippet is unique, knowing that the tool will return an error on multiple matches. You only read more file content
 60 |        (for crafvarting a more specific regex) if such a failure unexpectedly occurs. 
 61 | 
 62 |   Examples:
 63 | 
 64 |   1 Small replacement
 65 |   You have read code like
 66 |     
 67 |     ```python
 68 |     ...
 69 |     x = linear(x)
 70 |     x = relu(x)
 71 |     return x
 72 |     ...
 73 |     ```
 74 | 
 75 |   and you want to replace `x = relu(x)` with `x = gelu(x)`.
 76 |   You first try `replace_regex()` with the regex `x = relu\(x\)` and the replacement `x = gelu(x)`.
 77 |   If this fails due to multiple matches, you will try `(linear\(x\)\s*)x = relu\(x\)(\s*return)` with the replacement `\1x = gelu(x)\2`.
 78 | 
 79 |   2 Larger replacement
 80 | 
 81 |   You have read code like
 82 | 
 83 |   ```python
 84 |   def my_func():
 85 |     ...
 86 |     # a comment before the snippet
 87 |     x = add_fifteen(x)
 88 |     # beginning of long section within my_func
 89 |     ....
 90 |     # end of long section
 91 |     call_subroutine(z)
 92 |     call_second_subroutine(z)
 93 |   ```
 94 |   and you want to replace the code starting with `x = add_fifteen(x)` until (including) `call_subroutine(z)`, but not `call_second_subroutine(z)`.
 95 |   Initially, you assume that the the beginning and end of the chunk uniquely determine it within the file.
 96 |   Therefore, you perform the replacement by using the regex `x = add_fifteen\(x\)\s*.*?call_subroutine\(z\)`
 97 |   and the replacement being the new code you want to insert.
 98 | 
 99 |   If this fails due to multiple matches, you will try to extend the regex with the content before/after the snippet and match groups. 
100 |   The matching regex becomes:
101 |   `(before the snippet\s*)x = add_fifteen\(x\)\s*.*?call_subroutine\(z\)` 
102 |   and the replacement includes the group as (schematically):
103 |   `\1<new_code>`
104 | 
105 |   Generally, I remind you that you rely on the regex tool with providing you the correct feedback, no need for more verification!
106 | 
107 |   IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD!
108 |   {% endif %}
109 | excluded_tools:
110 |  - replace_lines
111 |  - insert_at_line
112 |  - delete_lines
113 | 
```

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

```python
  1 | """
  2 | Provides Markdown specific instantiation of the LanguageServer class using marksman.
  3 | Contains various configurations and settings specific to Markdown.
  4 | """
  5 | 
  6 | import logging
  7 | import os
  8 | import pathlib
  9 | import threading
 10 | 
 11 | from overrides import override
 12 | 
 13 | from solidlsp.ls import SolidLanguageServer
 14 | from solidlsp.ls_config import LanguageServerConfig
 15 | from solidlsp.ls_logger import LanguageServerLogger
 16 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 17 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 18 | from solidlsp.settings import SolidLSPSettings
 19 | 
 20 | from .common import RuntimeDependency, RuntimeDependencyCollection
 21 | 
 22 | 
 23 | class Marksman(SolidLanguageServer):
 24 |     """
 25 |     Provides Markdown specific instantiation of the LanguageServer class using marksman.
 26 |     """
 27 | 
 28 |     marksman_releases = "https://github.com/artempyanykh/marksman/releases/download/2024-12-18"
 29 |     runtime_dependencies = RuntimeDependencyCollection(
 30 |         [
 31 |             RuntimeDependency(
 32 |                 id="marksman",
 33 |                 url=f"{marksman_releases}/marksman-linux-x64",
 34 |                 platform_id="linux-x64",
 35 |                 archive_type="binary",
 36 |                 binary_name="marksman",
 37 |             ),
 38 |             RuntimeDependency(
 39 |                 id="marksman",
 40 |                 url=f"{marksman_releases}/marksman-linux-arm64",
 41 |                 platform_id="linux-arm64",
 42 |                 archive_type="binary",
 43 |                 binary_name="marksman",
 44 |             ),
 45 |             RuntimeDependency(
 46 |                 id="marksman",
 47 |                 url=f"{marksman_releases}/marksman-macos",
 48 |                 platform_id="osx-x64",
 49 |                 archive_type="binary",
 50 |                 binary_name="marksman",
 51 |             ),
 52 |             RuntimeDependency(
 53 |                 id="marksman",
 54 |                 url=f"{marksman_releases}/marksman-macos",
 55 |                 platform_id="osx-arm64",
 56 |                 archive_type="binary",
 57 |                 binary_name="marksman",
 58 |             ),
 59 |             RuntimeDependency(
 60 |                 id="marksman",
 61 |                 url=f"{marksman_releases}/marksman.exe",
 62 |                 platform_id="win-x64",
 63 |                 archive_type="binary",
 64 |                 binary_name="marksman.exe",
 65 |             ),
 66 |         ]
 67 |     )
 68 | 
 69 |     @classmethod
 70 |     def _setup_runtime_dependencies(
 71 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
 72 |     ) -> str:
 73 |         """Setup runtime dependencies for marksman and return the command to start the server."""
 74 |         deps = cls.runtime_dependencies
 75 |         dependency = deps.get_single_dep_for_current_platform()
 76 | 
 77 |         marksman_ls_dir = cls.ls_resources_dir(solidlsp_settings)
 78 |         marksman_executable_path = deps.binary_path(marksman_ls_dir)
 79 |         if not os.path.exists(marksman_executable_path):
 80 |             logger.log(
 81 |                 f"Downloading marksman from {dependency.url} to {marksman_ls_dir}",
 82 |                 logging.INFO,
 83 |             )
 84 |             deps.install(logger, marksman_ls_dir)
 85 |         if not os.path.exists(marksman_executable_path):
 86 |             raise FileNotFoundError(f"Download failed? Could not find marksman executable at {marksman_executable_path}")
 87 |         os.chmod(marksman_executable_path, 0o755)
 88 |         return marksman_executable_path
 89 | 
 90 |     def __init__(
 91 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 92 |     ):
 93 |         """
 94 |         Creates a Marksman instance. This class is not meant to be instantiated directly.
 95 |         Use LanguageServer.create() instead.
 96 |         """
 97 |         marksman_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 98 | 
 99 |         super().__init__(
100 |             config,
101 |             logger,
102 |             repository_root_path,
103 |             ProcessLaunchInfo(cmd=f"{marksman_executable_path} server", cwd=repository_root_path),
104 |             "markdown",
105 |             solidlsp_settings,
106 |         )
107 |         self.server_ready = threading.Event()
108 | 
109 |     @override
110 |     def is_ignored_dirname(self, dirname: str) -> bool:
111 |         return super().is_ignored_dirname(dirname) or dirname in ["node_modules", ".obsidian", ".vitepress", ".vuepress"]
112 | 
113 |     @staticmethod
114 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
115 |         """
116 |         Returns the initialize params for the Marksman Language Server.
117 |         """
118 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
119 |         initialize_params: InitializeParams = {  # type: ignore
120 |             "processId": os.getpid(),
121 |             "locale": "en",
122 |             "rootPath": repository_absolute_path,
123 |             "rootUri": root_uri,
124 |             "capabilities": {
125 |                 "textDocument": {
126 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
127 |                     "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
128 |                     "definition": {"dynamicRegistration": True},
129 |                     "references": {"dynamicRegistration": True},
130 |                     "documentSymbol": {
131 |                         "dynamicRegistration": True,
132 |                         "hierarchicalDocumentSymbolSupport": True,
133 |                         "symbolKind": {"valueSet": list(range(1, 27))},
134 |                     },
135 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
136 |                     "codeAction": {"dynamicRegistration": True},
137 |                 },
138 |                 "workspace": {
139 |                     "workspaceFolders": True,
140 |                     "didChangeConfiguration": {"dynamicRegistration": True},
141 |                     "symbol": {"dynamicRegistration": True},
142 |                 },
143 |             },
144 |             "workspaceFolders": [
145 |                 {
146 |                     "uri": root_uri,
147 |                     "name": os.path.basename(repository_absolute_path),
148 |                 }
149 |             ],
150 |         }
151 |         return initialize_params
152 | 
153 |     def _start_server(self):
154 |         """
155 |         Starts the Marksman Language Server and waits for it to be ready.
156 |         """
157 | 
158 |         def register_capability_handler(_params):
159 |             return
160 | 
161 |         def window_log_message(msg):
162 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
163 | 
164 |         def do_nothing(_params):
165 |             return
166 | 
167 |         self.server.on_request("client/registerCapability", register_capability_handler)
168 |         self.server.on_notification("window/logMessage", window_log_message)
169 |         self.server.on_notification("$/progress", do_nothing)
170 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
171 | 
172 |         self.logger.log("Starting marksman server process", logging.INFO)
173 |         self.server.start()
174 |         initialize_params = self._get_initialize_params(self.repository_root_path)
175 | 
176 |         self.logger.log(
177 |             "Sending initialize request from LSP client to marksman server and awaiting response",
178 |             logging.INFO,
179 |         )
180 |         init_response = self.server.send.initialize(initialize_params)
181 |         self.logger.log(f"Received initialize response from marksman server: {init_response}", logging.DEBUG)
182 | 
183 |         # Verify server capabilities
184 |         assert "textDocumentSync" in init_response["capabilities"]
185 |         assert "completionProvider" in init_response["capabilities"]
186 |         assert "definitionProvider" in init_response["capabilities"]
187 | 
188 |         self.server.notify.initialized({})
189 | 
190 |         # marksman is typically ready immediately after initialization
191 |         self.logger.log("Marksman server initialization complete", logging.INFO)
192 |         self.server_ready.set()
193 |         self.completions_available.set()
194 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/elixir/test_elixir_integration.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Integration tests for Elixir language server with test repository.
  3 | 
  4 | These tests verify that the language server works correctly with a real Elixir project
  5 | and can perform advanced operations like cross-file symbol resolution.
  6 | """
  7 | 
  8 | import os
  9 | from pathlib import Path
 10 | 
 11 | import pytest
 12 | 
 13 | from serena.project import Project
 14 | from solidlsp import SolidLanguageServer
 15 | from solidlsp.ls_config import Language
 16 | 
 17 | from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON
 18 | 
 19 | # These marks will be applied to all tests in this module
 20 | pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")]
 21 | 
 22 | 
 23 | class TestElixirIntegration:
 24 |     """Integration tests for Elixir language server with test repository."""
 25 | 
 26 |     @pytest.fixture
 27 |     def elixir_test_repo_path(self):
 28 |         """Get the path to the Elixir test repository."""
 29 |         test_dir = Path(__file__).parent.parent.parent
 30 |         return str(test_dir / "resources" / "repos" / "elixir" / "test_repo")
 31 | 
 32 |     def test_elixir_repo_structure(self, elixir_test_repo_path):
 33 |         """Test that the Elixir test repository has the expected structure."""
 34 |         repo_path = Path(elixir_test_repo_path)
 35 | 
 36 |         # Check that key files exist
 37 |         assert (repo_path / "mix.exs").exists(), "mix.exs should exist"
 38 |         assert (repo_path / "lib" / "test_repo.ex").exists(), "main module should exist"
 39 |         assert (repo_path / "lib" / "utils.ex").exists(), "utils module should exist"
 40 |         assert (repo_path / "lib" / "models.ex").exists(), "models module should exist"
 41 |         assert (repo_path / "lib" / "services.ex").exists(), "services module should exist"
 42 |         assert (repo_path / "lib" / "examples.ex").exists(), "examples module should exist"
 43 |         assert (repo_path / "test" / "test_repo_test.exs").exists(), "test file should exist"
 44 |         assert (repo_path / "test" / "models_test.exs").exists(), "models test should exist"
 45 | 
 46 |     @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
 47 |     def test_cross_file_symbol_resolution(self, language_server: SolidLanguageServer):
 48 |         """Test that symbols can be resolved across different files."""
 49 |         # Test that User struct from models.ex can be found when referenced in services.ex
 50 |         services_file = os.path.join("lib", "services.ex")
 51 | 
 52 |         # Find where User is referenced in services.ex
 53 |         content = language_server.retrieve_full_file_content(services_file)
 54 |         lines = content.split("\n")
 55 |         user_reference_line = None
 56 |         for i, line in enumerate(lines):
 57 |             if "alias TestRepo.Models.{User" in line:
 58 |                 user_reference_line = i
 59 |                 break
 60 | 
 61 |         if user_reference_line is None:
 62 |             pytest.skip("Could not find User reference in services.ex")
 63 | 
 64 |         # Try to find the definition
 65 |         defining_symbol = language_server.request_defining_symbol(services_file, user_reference_line, 30)
 66 | 
 67 |         if defining_symbol and "location" in defining_symbol:
 68 |             # Should point to models.ex
 69 |             assert "models.ex" in defining_symbol["location"]["uri"]
 70 | 
 71 |     @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
 72 |     def test_module_hierarchy_understanding(self, language_server: SolidLanguageServer):
 73 |         """Test that the language server understands Elixir module hierarchy."""
 74 |         models_file = os.path.join("lib", "models.ex")
 75 |         symbols = language_server.request_document_symbols(models_file)
 76 | 
 77 |         if symbols:
 78 |             # Flatten symbol structure
 79 |             all_symbols = []
 80 |             for symbol_group in symbols:
 81 |                 if isinstance(symbol_group, list):
 82 |                     all_symbols.extend(symbol_group)
 83 |                 else:
 84 |                     all_symbols.append(symbol_group)
 85 | 
 86 |             symbol_names = [s.get("name", "") for s in all_symbols]
 87 | 
 88 |             # Should understand nested module structure
 89 |             expected_modules = ["TestRepo.Models", "User", "Item", "Order"]
 90 |             found_modules = [name for name in expected_modules if any(name in symbol_name for symbol_name in symbol_names)]
 91 |             assert len(found_modules) > 0, f"Expected modules {expected_modules}, found symbols {symbol_names}"
 92 | 
 93 |     def test_file_extension_matching(self):
 94 |         """Test that the Elixir language recognizes the correct file extensions."""
 95 |         language = Language.ELIXIR
 96 |         matcher = language.get_source_fn_matcher()
 97 | 
 98 |         # Test Elixir file extensions
 99 |         assert matcher.is_relevant_filename("lib/test_repo.ex")
100 |         assert matcher.is_relevant_filename("test/test_repo_test.exs")
101 |         assert matcher.is_relevant_filename("config/config.exs")
102 |         assert matcher.is_relevant_filename("mix.exs")
103 |         assert matcher.is_relevant_filename("lib/models.ex")
104 |         assert matcher.is_relevant_filename("lib/services.ex")
105 | 
106 |         # Test non-Elixir files
107 |         assert not matcher.is_relevant_filename("README.md")
108 |         assert not matcher.is_relevant_filename("lib/test_repo.py")
109 |         assert not matcher.is_relevant_filename("package.json")
110 |         assert not matcher.is_relevant_filename("Cargo.toml")
111 | 
112 | 
113 | class TestElixirProject:
114 |     @pytest.mark.parametrize("project", [Language.ELIXIR], indirect=True)
115 |     def test_comprehensive_symbol_search(self, project: Project):
116 |         """Test comprehensive symbol search across the entire project."""
117 |         # Search for all function definitions
118 |         function_pattern = r"def\s+\w+\s*[\(\s]"
119 |         function_matches = project.search_source_files_for_pattern(function_pattern)
120 | 
121 |         # Should find functions across multiple files
122 |         if function_matches:
123 |             files_with_functions = set()
124 |             for match in function_matches:
125 |                 if match.source_file_path:
126 |                     files_with_functions.add(os.path.basename(match.source_file_path))
127 | 
128 |             # Should find functions in multiple files
129 |             expected_files = {"models.ex", "services.ex", "examples.ex", "utils.ex", "test_repo.ex"}
130 |             found_files = expected_files.intersection(files_with_functions)
131 |             assert len(found_files) > 0, f"Expected functions in {expected_files}, found in {files_with_functions}"
132 | 
133 |         # Search for struct definitions
134 |         struct_pattern = r"defstruct\s+\["
135 |         struct_matches = project.search_source_files_for_pattern(struct_pattern)
136 | 
137 |         if struct_matches:
138 |             # Should find structs primarily in models.ex
139 |             models_structs = [m for m in struct_matches if m.source_file_path and "models.ex" in m.source_file_path]
140 |             assert len(models_structs) > 0, "Should find struct definitions in models.ex"
141 | 
142 |     @pytest.mark.parametrize("project", [Language.ELIXIR], indirect=True)
143 |     def test_protocol_and_implementation_understanding(self, project: Project):
144 |         """Test that the language server understands Elixir protocols and implementations."""
145 |         # Search for protocol definitions
146 |         protocol_pattern = r"defprotocol\s+\w+"
147 |         protocol_matches = project.search_source_files_for_pattern(protocol_pattern, paths_include_glob="**/models.ex")
148 | 
149 |         if protocol_matches:
150 |             # Should find the Serializable protocol
151 |             serializable_matches = [m for m in protocol_matches if "Serializable" in str(m)]
152 |             assert len(serializable_matches) > 0, "Should find Serializable protocol definition"
153 | 
154 |         # Search for protocol implementations
155 |         impl_pattern = r"defimpl\s+\w+"
156 |         impl_matches = project.search_source_files_for_pattern(impl_pattern, paths_include_glob="**/models.ex")
157 | 
158 |         if impl_matches:
159 |             # Should find multiple implementations
160 |             assert len(impl_matches) >= 3, f"Should find at least 3 protocol implementations, found {len(impl_matches)}"
161 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/al/test_al_basic.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | 
  3 | import pytest
  4 | 
  5 | from solidlsp import SolidLanguageServer
  6 | from solidlsp.ls_config import Language
  7 | from solidlsp.ls_utils import SymbolUtils
  8 | 
  9 | 
 10 | @pytest.mark.al
 11 | class TestALLanguageServer:
 12 |     @pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
 13 |     def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
 14 |         """Test that AL Language Server can find symbols in the test repository."""
 15 |         symbols = language_server.request_full_symbol_tree()
 16 | 
 17 |         # Check for table symbols - AL returns full object names like 'Table 50000 "TEST Customer"'
 18 |         assert SymbolUtils.symbol_tree_contains_name(symbols, 'Table 50000 "TEST Customer"'), "TEST Customer table not found in symbol tree"
 19 | 
 20 |         # Check for page symbols
 21 |         assert SymbolUtils.symbol_tree_contains_name(
 22 |             symbols, 'Page 50001 "TEST Customer Card"'
 23 |         ), "TEST Customer Card page not found in symbol tree"
 24 |         assert SymbolUtils.symbol_tree_contains_name(
 25 |             symbols, 'Page 50002 "TEST Customer List"'
 26 |         ), "TEST Customer List page not found in symbol tree"
 27 | 
 28 |         # Check for codeunit symbols
 29 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Codeunit 50000 CustomerMgt"), "CustomerMgt codeunit not found in symbol tree"
 30 |         assert SymbolUtils.symbol_tree_contains_name(
 31 |             symbols, "Codeunit 50001 PaymentProcessorImpl"
 32 |         ), "PaymentProcessorImpl codeunit not found in symbol tree"
 33 | 
 34 |         # Check for enum symbol
 35 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Enum 50000 CustomerType"), "CustomerType enum not found in symbol tree"
 36 | 
 37 |         # Check for interface symbol
 38 |         assert SymbolUtils.symbol_tree_contains_name(
 39 |             symbols, "Interface IPaymentProcessor"
 40 |         ), "IPaymentProcessor interface not found in symbol tree"
 41 | 
 42 |     @pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
 43 |     def test_find_table_fields(self, language_server: SolidLanguageServer) -> None:
 44 |         """Test that AL Language Server can find fields within a table."""
 45 |         file_path = os.path.join("src", "Tables", "Customer.Table.al")
 46 |         symbols = language_server.request_document_symbols(file_path)
 47 | 
 48 |         # AL tables should have their fields as child symbols
 49 |         customer_table = None
 50 |         _all_symbols, root_symbols = symbols
 51 |         for sym in root_symbols:
 52 |             if "TEST Customer" in sym.get("name", ""):
 53 |                 customer_table = sym
 54 |                 break
 55 | 
 56 |         assert customer_table is not None, "Could not find TEST Customer table symbol"
 57 | 
 58 |         # Check for field symbols (AL nests fields under a "fields" group)
 59 |         if "children" in customer_table:
 60 |             # Find the fields group
 61 |             fields_group = None
 62 |             for child in customer_table.get("children", []):
 63 |                 if child.get("name") == "fields":
 64 |                     fields_group = child
 65 |                     break
 66 | 
 67 |             assert fields_group is not None, "Fields group not found in Customer table"
 68 | 
 69 |             # Check actual field names
 70 |             if "children" in fields_group:
 71 |                 field_names = [child.get("name", "") for child in fields_group.get("children", [])]
 72 |                 assert any("Name" in name for name in field_names), f"Name field not found. Fields: {field_names}"
 73 |                 assert any("Balance" in name for name in field_names), f"Balance field not found. Fields: {field_names}"
 74 | 
 75 |     @pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
 76 |     def test_find_procedures(self, language_server: SolidLanguageServer) -> None:
 77 |         """Test that AL Language Server can find procedures in codeunits."""
 78 |         file_path = os.path.join("src", "Codeunits", "CustomerMgt.Codeunit.al")
 79 |         symbols = language_server.request_document_symbols(file_path)
 80 | 
 81 |         # Find the codeunit symbol - AL returns 'Codeunit 50000 CustomerMgt'
 82 |         codeunit_symbol = None
 83 |         _all_symbols, root_symbols = symbols
 84 |         for sym in root_symbols:
 85 |             if "CustomerMgt" in sym.get("name", ""):
 86 |                 codeunit_symbol = sym
 87 |                 break
 88 | 
 89 |         assert codeunit_symbol is not None, "Could not find CustomerMgt codeunit symbol"
 90 | 
 91 |         # Check for procedure symbols (if hierarchical)
 92 |         if "children" in codeunit_symbol:
 93 |             procedure_names = [child.get("name", "") for child in codeunit_symbol.get("children", [])]
 94 |             assert any("CreateCustomer" in name for name in procedure_names), "CreateCustomer procedure not found"
 95 |             # Note: UpdateCustomerBalance doesn't exist in our test repo, check for actual procedures
 96 |             assert any("TestNoSeries" in name for name in procedure_names), "TestNoSeries procedure not found"
 97 | 
 98 |     @pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
 99 |     def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
100 |         """Test that AL Language Server can find references to symbols."""
101 |         # Find references to the Customer table from the CustomerMgt codeunit
102 |         table_file = os.path.join("src", "Tables", "Customer.Table.al")
103 |         symbols = language_server.request_document_symbols(table_file)
104 | 
105 |         # Find the Customer table symbol
106 |         customer_symbol = None
107 |         _all_symbols, root_symbols = symbols
108 |         for sym in root_symbols:
109 |             if "TEST Customer" in sym.get("name", ""):
110 |                 customer_symbol = sym
111 |                 break
112 | 
113 |         if customer_symbol and "selectionRange" in customer_symbol:
114 |             sel_start = customer_symbol["selectionRange"]["start"]
115 |             refs = language_server.request_references(table_file, sel_start["line"], sel_start["character"])
116 | 
117 |             # The Customer table should be referenced in CustomerMgt.Codeunit.al
118 |             assert any(
119 |                 "CustomerMgt.Codeunit.al" in ref.get("relativePath", "") for ref in refs
120 |             ), "Customer table should be referenced in CustomerMgt.Codeunit.al"
121 | 
122 |             # It should also be referenced in CustomerCard.Page.al
123 |             assert any(
124 |                 "CustomerCard.Page.al" in ref.get("relativePath", "") for ref in refs
125 |             ), "Customer table should be referenced in CustomerCard.Page.al"
126 | 
127 |     @pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
128 |     def test_cross_file_symbols(self, language_server: SolidLanguageServer) -> None:
129 |         """Test that AL Language Server can handle cross-file symbol relationships."""
130 |         # Get all symbols to verify cross-file visibility
131 |         symbols = language_server.request_full_symbol_tree()
132 | 
133 |         # Count how many AL-specific symbols we found
134 |         al_symbols = []
135 | 
136 |         def collect_symbols(syms):
137 |             for sym in syms:
138 |                 if isinstance(sym, dict):
139 |                     name = sym.get("name", "")
140 |                     # Look for AL object names (Table, Page, Codeunit, etc.)
141 |                     if any(keyword in name for keyword in ["Table", "Page", "Codeunit", "Enum", "Interface"]):
142 |                         al_symbols.append(name)
143 |                     if "children" in sym:
144 |                         collect_symbols(sym["children"])
145 | 
146 |         collect_symbols(symbols)
147 | 
148 |         # We should find symbols from multiple files
149 |         assert len(al_symbols) >= 5, f"Expected at least 5 AL object symbols, found {len(al_symbols)}: {al_symbols}"
150 | 
151 |         # Verify we have symbols from different AL object types
152 |         has_table = any("Table" in s for s in al_symbols)
153 |         has_page = any("Page" in s for s in al_symbols)
154 |         has_codeunit = any("Codeunit" in s for s in al_symbols)
155 | 
156 |         assert has_table, f"No Table symbols found in: {al_symbols}"
157 |         assert has_page, f"No Page symbols found in: {al_symbols}"
158 |         assert has_codeunit, f"No Codeunit symbols found in: {al_symbols}"
159 | 
```

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

```python
  1 | """
  2 | Provides Elm specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Elm.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import shutil
  9 | import threading
 10 | 
 11 | from overrides import override
 12 | from sensai.util.logging import LogTime
 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 | from .common import RuntimeDependency, RuntimeDependencyCollection
 22 | 
 23 | 
 24 | class ElmLanguageServer(SolidLanguageServer):
 25 |     """
 26 |     Provides Elm specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Elm.
 27 |     """
 28 | 
 29 |     def __init__(
 30 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 31 |     ):
 32 |         """
 33 |         Creates an ElmLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 34 |         """
 35 |         elm_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 36 |         super().__init__(
 37 |             config,
 38 |             logger,
 39 |             repository_root_path,
 40 |             ProcessLaunchInfo(cmd=elm_lsp_executable_path, cwd=repository_root_path),
 41 |             "elm",
 42 |             solidlsp_settings,
 43 |         )
 44 |         self.server_ready = threading.Event()
 45 | 
 46 |     @override
 47 |     def is_ignored_dirname(self, dirname: str) -> bool:
 48 |         return super().is_ignored_dirname(dirname) or dirname in [
 49 |             "elm-stuff",
 50 |             "node_modules",
 51 |             "dist",
 52 |             "build",
 53 |         ]
 54 | 
 55 |     @classmethod
 56 |     def _setup_runtime_dependencies(
 57 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
 58 |     ) -> list[str]:
 59 |         """
 60 |         Setup runtime dependencies for Elm Language Server and return the command to start the server.
 61 |         """
 62 |         # Check if elm-language-server is already installed globally
 63 |         system_elm_ls = shutil.which("elm-language-server")
 64 |         if system_elm_ls:
 65 |             logger.log(f"Found system-installed elm-language-server at {system_elm_ls}", logging.INFO)
 66 |             return [system_elm_ls, "--stdio"]
 67 | 
 68 |         # Verify node and npm are installed
 69 |         is_node_installed = shutil.which("node") is not None
 70 |         assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
 71 |         is_npm_installed = shutil.which("npm") is not None
 72 |         assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
 73 | 
 74 |         deps = RuntimeDependencyCollection(
 75 |             [
 76 |                 RuntimeDependency(
 77 |                     id="elm-language-server",
 78 |                     description="@elm-tooling/elm-language-server package",
 79 |                     command=["npm", "install", "--prefix", "./", "@elm-tooling/[email protected]"],
 80 |                     platform_id="any",
 81 |                 ),
 82 |             ]
 83 |         )
 84 | 
 85 |         # Install elm-language-server if not already installed
 86 |         elm_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "elm-lsp")
 87 |         elm_ls_executable_path = os.path.join(elm_ls_dir, "node_modules", ".bin", "elm-language-server")
 88 |         if not os.path.exists(elm_ls_executable_path):
 89 |             logger.log(f"Elm Language Server executable not found at {elm_ls_executable_path}. Installing...", logging.INFO)
 90 |             with LogTime("Installation of Elm language server dependencies", logger=logger.logger):
 91 |                 deps.install(logger, elm_ls_dir)
 92 | 
 93 |         if not os.path.exists(elm_ls_executable_path):
 94 |             raise FileNotFoundError(
 95 |                 f"elm-language-server executable not found at {elm_ls_executable_path}, something went wrong with the installation."
 96 |             )
 97 |         return [elm_ls_executable_path, "--stdio"]
 98 | 
 99 |     @staticmethod
100 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
101 |         """
102 |         Returns the initialize params for the Elm Language Server.
103 |         """
104 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
105 | 
106 |         initialize_params = {
107 |             "locale": "en",
108 |             "capabilities": {
109 |                 "textDocument": {
110 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
111 |                     "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
112 |                     "definition": {"dynamicRegistration": True},
113 |                     "references": {"dynamicRegistration": True},
114 |                     "documentSymbol": {
115 |                         "dynamicRegistration": True,
116 |                         "hierarchicalDocumentSymbolSupport": True,
117 |                         "symbolKind": {"valueSet": list(range(1, 27))},
118 |                     },
119 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
120 |                     "codeAction": {"dynamicRegistration": True},
121 |                     "rename": {"dynamicRegistration": True},
122 |                 },
123 |                 "workspace": {
124 |                     "workspaceFolders": True,
125 |                     "didChangeConfiguration": {"dynamicRegistration": True},
126 |                     "symbol": {"dynamicRegistration": True},
127 |                 },
128 |             },
129 |             "initializationOptions": {
130 |                 "elmPath": "elm",
131 |                 "elmFormatPath": "elm-format",
132 |                 "elmTestPath": "elm-test",
133 |                 "skipInstallPackageConfirmation": True,
134 |                 "onlyUpdateDiagnosticsOnSave": False,
135 |             },
136 |             "processId": os.getpid(),
137 |             "rootPath": repository_absolute_path,
138 |             "rootUri": root_uri,
139 |             "workspaceFolders": [
140 |                 {
141 |                     "uri": root_uri,
142 |                     "name": os.path.basename(repository_absolute_path),
143 |                 }
144 |             ],
145 |         }
146 |         return initialize_params
147 | 
148 |     def _start_server(self):
149 |         """
150 |         Starts the Elm Language Server, waits for the server to be ready and yields the LanguageServer instance.
151 |         """
152 | 
153 |         def do_nothing(params):
154 |             return
155 | 
156 |         def window_log_message(msg):
157 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
158 | 
159 |         self.server.on_notification("window/logMessage", window_log_message)
160 |         self.server.on_notification("$/progress", do_nothing)
161 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
162 | 
163 |         self.logger.log("Starting Elm server process", logging.INFO)
164 |         self.server.start()
165 |         initialize_params = self._get_initialize_params(self.repository_root_path)
166 | 
167 |         self.logger.log(
168 |             "Sending initialize request from LSP client to LSP server and awaiting response",
169 |             logging.INFO,
170 |         )
171 |         init_response = self.server.send.initialize(initialize_params)
172 | 
173 |         # Elm-specific capability checks
174 |         assert "textDocumentSync" in init_response["capabilities"]
175 |         assert "completionProvider" in init_response["capabilities"]
176 |         assert "definitionProvider" in init_response["capabilities"]
177 |         assert "referencesProvider" in init_response["capabilities"]
178 |         assert "documentSymbolProvider" in init_response["capabilities"]
179 | 
180 |         self.server.notify.initialized({})
181 |         self.logger.log("Elm server initialized successfully, waiting for workspace scan...", logging.INFO)
182 | 
183 |         self.server_ready.set()
184 |         self.completions_available.set()
185 |         self.logger.log("Elm server ready", logging.INFO)
186 | 
187 |     @override
188 |     def _get_wait_time_for_cross_file_referencing(self) -> float:
189 |         return 1.0
190 | 
```

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

```python
  1 | """
  2 | Provides PHP specific instantiation of the LanguageServer class using Intelephense.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import shutil
  9 | from time import sleep
 10 | 
 11 | from overrides import override
 12 | 
 13 | from solidlsp.ls import SolidLanguageServer
 14 | from solidlsp.ls_config import LanguageServerConfig
 15 | from solidlsp.ls_logger import LanguageServerLogger
 16 | from solidlsp.ls_utils import PlatformId, PlatformUtils
 17 | from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams
 18 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 19 | from solidlsp.settings import SolidLSPSettings
 20 | 
 21 | from .common import RuntimeDependency, RuntimeDependencyCollection
 22 | 
 23 | 
 24 | class Intelephense(SolidLanguageServer):
 25 |     """
 26 |     Provides PHP specific instantiation of the LanguageServer class using Intelephense.
 27 | 
 28 |     You can pass the following entries in ls_specific_settings["php"]:
 29 |         - maxMemory
 30 |         - maxFileSize
 31 |     """
 32 | 
 33 |     @override
 34 |     def is_ignored_dirname(self, dirname: str) -> bool:
 35 |         # For PHP projects, we should ignore:
 36 |         # - vendor: third-party dependencies managed by Composer
 37 |         # - node_modules: if the project has JavaScript components
 38 |         # - cache: commonly used for caching
 39 |         return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"]
 40 | 
 41 |     @classmethod
 42 |     def _setup_runtime_dependencies(
 43 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
 44 |     ) -> str:
 45 |         """
 46 |         Setup runtime dependencies for Intelephense and return the command to start the server.
 47 |         """
 48 |         platform_id = PlatformUtils.get_platform_id()
 49 | 
 50 |         valid_platforms = [
 51 |             PlatformId.LINUX_x64,
 52 |             PlatformId.LINUX_arm64,
 53 |             PlatformId.OSX,
 54 |             PlatformId.OSX_x64,
 55 |             PlatformId.OSX_arm64,
 56 |             PlatformId.WIN_x64,
 57 |             PlatformId.WIN_arm64,
 58 |         ]
 59 |         assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy PHP at the moment"
 60 | 
 61 |         # Verify both node and npm are installed
 62 |         is_node_installed = shutil.which("node") is not None
 63 |         assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
 64 |         is_npm_installed = shutil.which("npm") is not None
 65 |         assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
 66 | 
 67 |         # Install intelephense if not already installed
 68 |         intelephense_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "php-lsp")
 69 |         os.makedirs(intelephense_ls_dir, exist_ok=True)
 70 |         intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense")
 71 |         if not os.path.exists(intelephense_executable_path):
 72 |             deps = RuntimeDependencyCollection(
 73 |                 [
 74 |                     RuntimeDependency(
 75 |                         id="intelephense",
 76 |                         command="npm install --prefix ./ [email protected]",
 77 |                         platform_id="any",
 78 |                     )
 79 |                 ]
 80 |             )
 81 |             deps.install(logger, intelephense_ls_dir)
 82 | 
 83 |         assert os.path.exists(
 84 |             intelephense_executable_path
 85 |         ), f"intelephense executable not found at {intelephense_executable_path}, something went wrong."
 86 | 
 87 |         return f"{intelephense_executable_path} --stdio"
 88 | 
 89 |     def __init__(
 90 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 91 |     ):
 92 |         # Setup runtime dependencies before initializing
 93 |         intelephense_cmd = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 94 | 
 95 |         super().__init__(
 96 |             config,
 97 |             logger,
 98 |             repository_root_path,
 99 |             ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path),
100 |             "php",
101 |             solidlsp_settings,
102 |         )
103 |         self.request_id = 0
104 | 
105 |     def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
106 |         """
107 |         Returns the initialization params for the Intelephense Language Server.
108 |         """
109 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
110 |         initialize_params = {
111 |             "locale": "en",
112 |             "capabilities": {
113 |                 "textDocument": {
114 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
115 |                     "definition": {"dynamicRegistration": True},
116 |                 },
117 |                 "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
118 |             },
119 |             "processId": os.getpid(),
120 |             "rootPath": repository_absolute_path,
121 |             "rootUri": root_uri,
122 |             "workspaceFolders": [
123 |                 {
124 |                     "uri": root_uri,
125 |                     "name": os.path.basename(repository_absolute_path),
126 |                 }
127 |             ],
128 |         }
129 |         initialization_options = {}
130 |         # Add license key if provided via environment variable
131 |         license_key = os.environ.get("INTELEPHENSE_LICENSE_KEY")
132 |         if license_key:
133 |             initialization_options["licenceKey"] = license_key
134 | 
135 |         custom_intelephense_settings = self._solidlsp_settings.ls_specific_settings.get(self.get_language_enum_instance(), {})
136 |         max_memory = custom_intelephense_settings.get("maxMemory")
137 |         max_file_size = custom_intelephense_settings.get("maxFileSize")
138 |         if max_memory is not None:
139 |             initialization_options["intelephense.maxMemory"] = max_memory
140 |         if max_file_size is not None:
141 |             initialization_options["intelephense.files.maxSize"] = max_file_size
142 | 
143 |         initialize_params["initializationOptions"] = initialization_options
144 |         return initialize_params
145 | 
146 |     def _start_server(self):
147 |         """Start Intelephense server process"""
148 | 
149 |         def register_capability_handler(params):
150 |             return
151 | 
152 |         def window_log_message(msg):
153 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
154 | 
155 |         def do_nothing(params):
156 |             return
157 | 
158 |         self.server.on_request("client/registerCapability", register_capability_handler)
159 |         self.server.on_notification("window/logMessage", window_log_message)
160 |         self.server.on_notification("$/progress", do_nothing)
161 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
162 | 
163 |         self.logger.log("Starting Intelephense server process", logging.INFO)
164 |         self.server.start()
165 |         initialize_params = self._get_initialize_params(self.repository_root_path)
166 | 
167 |         self.logger.log(
168 |             "Sending initialize request from LSP client to LSP server and awaiting response",
169 |             logging.INFO,
170 |         )
171 |         init_response = self.server.send.initialize(initialize_params)
172 |         self.logger.log(
173 |             "After sent initialize params",
174 |             logging.INFO,
175 |         )
176 | 
177 |         # Verify server capabilities
178 |         assert "textDocumentSync" in init_response["capabilities"]
179 |         assert "completionProvider" in init_response["capabilities"]
180 |         assert "definitionProvider" in init_response["capabilities"]
181 | 
182 |         self.server.notify.initialized({})
183 |         self.completions_available.set()
184 | 
185 |         # Intelephense server is typically ready immediately after initialization
186 |         # TODO: This is probably incorrect; the server does send an initialized notification, which we could wait for!
187 | 
188 |     @override
189 |     # For some reason, the LS may need longer to process this, so we just retry
190 |     def _send_references_request(self, relative_file_path: str, line: int, column: int):
191 |         # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is
192 |         #   despite the LS having processed requests already. I don't know what causes this, but sleeping
193 |         #   one second helps. It may be that sleeping only once is enough but that's hard to reliably test.
194 |         # May be related to the time it takes to read the files or something like that.
195 |         # The sleeping doesn't seem to be needed on all systems
196 |         sleep(1)
197 |         return super()._send_references_request(relative_file_path, line, column)
198 | 
199 |     @override
200 |     def _send_definition_request(self, definition_params: DefinitionParams):
201 |         # TODO: same as above, also only a problem if the definition is in another file
202 |         sleep(1)
203 |         return super()._send_definition_request(definition_params)
204 | 
```

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

```python
  1 | """
  2 | Context and Mode configuration loader
  3 | """
  4 | 
  5 | import os
  6 | from dataclasses import dataclass, field
  7 | from enum import Enum
  8 | from pathlib import Path
  9 | from typing import TYPE_CHECKING, Self
 10 | 
 11 | import yaml
 12 | from sensai.util import logging
 13 | from sensai.util.string import ToStringMixin
 14 | 
 15 | from serena.config.serena_config import ToolInclusionDefinition
 16 | from serena.constants import (
 17 |     DEFAULT_CONTEXT,
 18 |     DEFAULT_MODES,
 19 |     INTERNAL_MODE_YAMLS_DIR,
 20 |     SERENAS_OWN_CONTEXT_YAMLS_DIR,
 21 |     SERENAS_OWN_MODE_YAMLS_DIR,
 22 |     USER_CONTEXT_YAMLS_DIR,
 23 |     USER_MODE_YAMLS_DIR,
 24 | )
 25 | 
 26 | if TYPE_CHECKING:
 27 |     pass
 28 | 
 29 | log = logging.getLogger(__name__)
 30 | 
 31 | 
 32 | @dataclass(kw_only=True)
 33 | class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin):
 34 |     """Represents a mode of operation for the agent, typically read off a YAML file.
 35 |     An agent can be in multiple modes simultaneously as long as they are not mutually exclusive.
 36 |     The modes can be adjusted after the agent is running, for example for switching from planning to editing.
 37 |     """
 38 | 
 39 |     name: str
 40 |     prompt: str
 41 |     """
 42 |     a Jinja2 template for the generation of the system prompt.
 43 |     It is formatted by the agent (see SerenaAgent._format_prompt()).
 44 |     """
 45 |     description: str = ""
 46 | 
 47 |     def _tostring_includes(self) -> list[str]:
 48 |         return ["name"]
 49 | 
 50 |     def print_overview(self) -> None:
 51 |         """Print an overview of the mode."""
 52 |         print(f"{self.name}:\n {self.description}")
 53 |         if self.excluded_tools:
 54 |             print(" excluded tools:\n  " + ", ".join(sorted(self.excluded_tools)))
 55 | 
 56 |     @classmethod
 57 |     def from_yaml(cls, yaml_path: str | Path) -> Self:
 58 |         """Load a mode from a YAML file."""
 59 |         with open(yaml_path, encoding="utf-8") as f:
 60 |             data = yaml.safe_load(f)
 61 |         name = data.pop("name", Path(yaml_path).stem)
 62 |         return cls(name=name, **data)
 63 | 
 64 |     @classmethod
 65 |     def get_path(cls, name: str) -> str:
 66 |         """Get the path to the YAML file for a mode."""
 67 |         fname = f"{name}.yml"
 68 |         custom_mode_path = os.path.join(USER_MODE_YAMLS_DIR, fname)
 69 |         if os.path.exists(custom_mode_path):
 70 |             return custom_mode_path
 71 | 
 72 |         own_yaml_path = os.path.join(SERENAS_OWN_MODE_YAMLS_DIR, fname)
 73 |         if not os.path.exists(own_yaml_path):
 74 |             raise FileNotFoundError(
 75 |                 f"Mode {name} not found in {USER_MODE_YAMLS_DIR} or in {SERENAS_OWN_MODE_YAMLS_DIR}."
 76 |                 f"Available modes:\n{cls.list_registered_mode_names()}"
 77 |             )
 78 |         return own_yaml_path
 79 | 
 80 |     @classmethod
 81 |     def from_name(cls, name: str) -> Self:
 82 |         """Load a registered Serena mode."""
 83 |         mode_path = cls.get_path(name)
 84 |         return cls.from_yaml(mode_path)
 85 | 
 86 |     @classmethod
 87 |     def from_name_internal(cls, name: str) -> Self:
 88 |         """Loads an internal Serena mode"""
 89 |         yaml_path = os.path.join(INTERNAL_MODE_YAMLS_DIR, f"{name}.yml")
 90 |         if not os.path.exists(yaml_path):
 91 |             raise FileNotFoundError(f"Internal mode '{name}' not found in {INTERNAL_MODE_YAMLS_DIR}")
 92 |         return cls.from_yaml(yaml_path)
 93 | 
 94 |     @classmethod
 95 |     def list_registered_mode_names(cls, include_user_modes: bool = True) -> list[str]:
 96 |         """Names of all registered modes (from the corresponding YAML files in the serena repo)."""
 97 |         modes = [f.stem for f in Path(SERENAS_OWN_MODE_YAMLS_DIR).glob("*.yml") if f.name != "mode.template.yml"]
 98 |         if include_user_modes:
 99 |             modes += cls.list_custom_mode_names()
100 |         return sorted(set(modes))
101 | 
102 |     @classmethod
103 |     def list_custom_mode_names(cls) -> list[str]:
104 |         """Names of all custom modes defined by the user."""
105 |         return [f.stem for f in Path(USER_MODE_YAMLS_DIR).glob("*.yml")]
106 | 
107 |     @classmethod
108 |     def load_default_modes(cls) -> list[Self]:
109 |         """Load the default modes (interactive and editing)."""
110 |         return [cls.from_name(mode) for mode in DEFAULT_MODES]
111 | 
112 |     @classmethod
113 |     def load(cls, name_or_path: str | Path) -> Self:
114 |         if str(name_or_path).endswith(".yml"):
115 |             return cls.from_yaml(name_or_path)
116 |         return cls.from_name(str(name_or_path))
117 | 
118 | 
119 | @dataclass(kw_only=True)
120 | class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin):
121 |     """Represents a context where the agent is operating (an IDE, a chat, etc.), typically read off a YAML file.
122 |     An agent can only be in a single context at a time.
123 |     The contexts cannot be changed after the agent is running.
124 |     """
125 | 
126 |     name: str
127 |     prompt: str
128 |     """
129 |     a Jinja2 template for the generation of the system prompt.
130 |     It is formatted by the agent (see SerenaAgent._format_prompt()).
131 |     """
132 |     description: str = ""
133 |     tool_description_overrides: dict[str, str] = field(default_factory=dict)
134 |     """Maps tool names to custom descriptions, default descriptions are extracted from the tool docstrings."""
135 | 
136 |     def _tostring_includes(self) -> list[str]:
137 |         return ["name"]
138 | 
139 |     @classmethod
140 |     def from_yaml(cls, yaml_path: str | Path) -> Self:
141 |         """Load a context from a YAML file."""
142 |         with open(yaml_path, encoding="utf-8") as f:
143 |             data = yaml.safe_load(f)
144 |         name = data.pop("name", Path(yaml_path).stem)
145 |         # Ensure backwards compatibility for tool_description_overrides
146 |         if "tool_description_overrides" not in data:
147 |             data["tool_description_overrides"] = {}
148 |         return cls(name=name, **data)
149 | 
150 |     @classmethod
151 |     def get_path(cls, name: str) -> str:
152 |         """Get the path to the YAML file for a context."""
153 |         fname = f"{name}.yml"
154 |         custom_context_path = os.path.join(USER_CONTEXT_YAMLS_DIR, fname)
155 |         if os.path.exists(custom_context_path):
156 |             return custom_context_path
157 | 
158 |         own_yaml_path = os.path.join(SERENAS_OWN_CONTEXT_YAMLS_DIR, fname)
159 |         if not os.path.exists(own_yaml_path):
160 |             raise FileNotFoundError(
161 |                 f"Context {name} not found in {USER_CONTEXT_YAMLS_DIR} or in {SERENAS_OWN_CONTEXT_YAMLS_DIR}."
162 |                 f"Available contexts:\n{cls.list_registered_context_names()}"
163 |             )
164 |         return own_yaml_path
165 | 
166 |     @classmethod
167 |     def from_name(cls, name: str) -> Self:
168 |         """Load a registered Serena context."""
169 |         context_path = cls.get_path(name)
170 |         return cls.from_yaml(context_path)
171 | 
172 |     @classmethod
173 |     def load(cls, name_or_path: str | Path) -> Self:
174 |         if str(name_or_path).endswith(".yml"):
175 |             return cls.from_yaml(name_or_path)
176 |         return cls.from_name(str(name_or_path))
177 | 
178 |     @classmethod
179 |     def list_registered_context_names(cls, include_user_contexts: bool = True) -> list[str]:
180 |         """Names of all registered contexts (from the corresponding YAML files in the serena repo)."""
181 |         contexts = [f.stem for f in Path(SERENAS_OWN_CONTEXT_YAMLS_DIR).glob("*.yml")]
182 |         if include_user_contexts:
183 |             contexts += cls.list_custom_context_names()
184 |         return sorted(set(contexts))
185 | 
186 |     @classmethod
187 |     def list_custom_context_names(cls) -> list[str]:
188 |         """Names of all custom contexts defined by the user."""
189 |         return [f.stem for f in Path(USER_CONTEXT_YAMLS_DIR).glob("*.yml")]
190 | 
191 |     @classmethod
192 |     def load_default(cls) -> Self:
193 |         """Load the default context."""
194 |         return cls.from_name(DEFAULT_CONTEXT)
195 | 
196 |     def print_overview(self) -> None:
197 |         """Print an overview of the mode."""
198 |         print(f"{self.name}:\n {self.description}")
199 |         if self.excluded_tools:
200 |             print(" excluded tools:\n  " + ", ".join(sorted(self.excluded_tools)))
201 | 
202 | 
203 | class RegisteredContext(Enum):
204 |     """A registered context."""
205 | 
206 |     IDE_ASSISTANT = "ide-assistant"
207 |     """For Serena running within an assistant that already has basic tools, like Claude Code, Cline, Cursor, etc."""
208 |     DESKTOP_APP = "desktop-app"
209 |     """For Serena running within Claude Desktop or a similar app which does not have built-in tools for code editing."""
210 |     AGENT = "agent"
211 |     """For Serena running as a standalone agent, e.g. through agno."""
212 | 
213 |     def load(self) -> SerenaAgentContext:
214 |         """Load the context."""
215 |         return SerenaAgentContext.from_name(self.value)
216 | 
217 | 
218 | class RegisteredMode(Enum):
219 |     """A registered mode."""
220 | 
221 |     INTERACTIVE = "interactive"
222 |     """Interactive mode, for multi-turn interactions."""
223 |     EDITING = "editing"
224 |     """Editing tools are activated."""
225 |     PLANNING = "planning"
226 |     """Editing tools are deactivated."""
227 |     ONE_SHOT = "one-shot"
228 |     """Non-interactive mode, where the goal is to finish a task autonomously."""
229 | 
230 |     def load(self) -> SerenaAgentMode:
231 |         """Load the mode."""
232 |         return SerenaAgentMode.from_name(self.value)
233 | 
```

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

```python
  1 | """
  2 | Provides Perl specific instantiation of the LanguageServer class using Perl::LanguageServer.
  3 | 
  4 | Note: Windows is not supported as Nix itself doesn't support Windows natively.
  5 | """
  6 | 
  7 | import logging
  8 | import os
  9 | import pathlib
 10 | import subprocess
 11 | import time
 12 | 
 13 | from overrides import override
 14 | 
 15 | from solidlsp.ls import SolidLanguageServer
 16 | from solidlsp.ls_config import LanguageServerConfig
 17 | from solidlsp.ls_logger import LanguageServerLogger
 18 | from solidlsp.ls_utils import PlatformId, PlatformUtils
 19 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 20 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 21 | from solidlsp.settings import SolidLSPSettings
 22 | 
 23 | 
 24 | class PerlLanguageServer(SolidLanguageServer):
 25 |     """
 26 |     Provides Perl specific instantiation of the LanguageServer class using Perl::LanguageServer.
 27 |     """
 28 | 
 29 |     @staticmethod
 30 |     def _get_perl_version():
 31 |         """Get the installed Perl version or None if not found."""
 32 |         try:
 33 |             result = subprocess.run(["perl", "-v"], capture_output=True, text=True, check=False)
 34 |             if result.returncode == 0:
 35 |                 return result.stdout.strip()
 36 |         except FileNotFoundError:
 37 |             return None
 38 |         return None
 39 | 
 40 |     @staticmethod
 41 |     def _get_perl_language_server_version():
 42 |         """Get the installed Perl::LanguageServer version or None if not found."""
 43 |         try:
 44 |             result = subprocess.run(
 45 |                 ["perl", "-MPerl::LanguageServer", "-e", "print $Perl::LanguageServer::VERSION"],
 46 |                 capture_output=True,
 47 |                 text=True,
 48 |                 check=False,
 49 |             )
 50 |             if result.returncode == 0:
 51 |                 return result.stdout.strip()
 52 |         except FileNotFoundError:
 53 |             return None
 54 |         return None
 55 | 
 56 |     @override
 57 |     def is_ignored_dirname(self, dirname: str) -> bool:
 58 |         # For Perl projects, we should ignore:
 59 |         # - blib: build library directory
 60 |         # - local: local Perl module installation
 61 |         # - .carton: Carton dependency manager cache
 62 |         # - vendor: vendored dependencies
 63 |         # - _build: Module::Build output
 64 |         return super().is_ignored_dirname(dirname) or dirname in ["blib", "local", ".carton", "vendor", "_build", "cover_db"]
 65 | 
 66 |     @classmethod
 67 |     def _setup_runtime_dependencies(cls) -> str:
 68 |         """
 69 |         Check if required Perl runtime dependencies are available.
 70 |         Raises RuntimeError with helpful message if dependencies are missing.
 71 |         """
 72 |         platform_id = PlatformUtils.get_platform_id()
 73 | 
 74 |         valid_platforms = [
 75 |             PlatformId.LINUX_x64,
 76 |             PlatformId.LINUX_arm64,
 77 |             PlatformId.OSX,
 78 |             PlatformId.OSX_x64,
 79 |             PlatformId.OSX_arm64,
 80 |         ]
 81 |         if platform_id not in valid_platforms:
 82 |             raise RuntimeError(f"Platform {platform_id} is not supported for Perl at the moment")
 83 | 
 84 |         perl_version = cls._get_perl_version()
 85 |         if not perl_version:
 86 |             raise RuntimeError(
 87 |                 "Perl is not installed. Please install Perl from https://www.perl.org/get.html and make sure it is added to your PATH."
 88 |             )
 89 | 
 90 |         perl_ls_version = cls._get_perl_language_server_version()
 91 |         if not perl_ls_version:
 92 |             raise RuntimeError(
 93 |                 "Found a Perl version but Perl::LanguageServer is not installed.\n"
 94 |                 "Please install Perl::LanguageServer: cpanm Perl::LanguageServer\n"
 95 |                 "See: https://metacpan.org/pod/Perl::LanguageServer"
 96 |             )
 97 | 
 98 |         return "perl -MPerl::LanguageServer -e 'Perl::LanguageServer::run'"
 99 | 
100 |     def __init__(
101 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
102 |     ):
103 |         # Setup runtime dependencies before initializing
104 |         perl_ls_cmd = self._setup_runtime_dependencies()
105 | 
106 |         super().__init__(
107 |             config,
108 |             logger,
109 |             repository_root_path,
110 |             ProcessLaunchInfo(cmd=perl_ls_cmd, cwd=repository_root_path),
111 |             "perl",
112 |             solidlsp_settings,
113 |         )
114 |         self.request_id = 0
115 | 
116 |     @staticmethod
117 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
118 |         """
119 |         Returns the initialize params for Perl::LanguageServer.
120 |         Based on the expected structure from Perl::LanguageServer::Methods::_rpcreq_initialize.
121 |         """
122 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
123 |         initialize_params = {
124 |             "processId": os.getpid(),
125 |             "rootPath": repository_absolute_path,
126 |             "rootUri": root_uri,
127 |             "capabilities": {
128 |                 "textDocument": {
129 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
130 |                     "definition": {"dynamicRegistration": True},
131 |                     "references": {"dynamicRegistration": True},
132 |                     "documentSymbol": {"dynamicRegistration": True},
133 |                     "hover": {"dynamicRegistration": True},
134 |                 },
135 |                 "workspace": {
136 |                     "workspaceFolders": True,
137 |                     "didChangeConfiguration": {"dynamicRegistration": True},
138 |                     "symbol": {"dynamicRegistration": True},
139 |                 },
140 |             },
141 |             "initializationOptions": {},
142 |             "workspaceFolders": [
143 |                 {
144 |                     "uri": root_uri,
145 |                     "name": os.path.basename(repository_absolute_path),
146 |                 }
147 |             ],
148 |         }
149 | 
150 |         return initialize_params
151 | 
152 |     def _start_server(self):
153 |         """Start Perl::LanguageServer process"""
154 | 
155 |         def register_capability_handler(params):
156 |             return
157 | 
158 |         def window_log_message(msg):
159 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
160 | 
161 |         def do_nothing(params):
162 |             return
163 | 
164 |         def workspace_configuration_handler(params):
165 |             """Handle workspace/configuration request from Perl::LanguageServer."""
166 |             self.logger.log(f"Received workspace/configuration request: {params}", logging.INFO)
167 | 
168 |             perl_config = {
169 |                 "perlInc": [self.repository_root_path, "."],
170 |                 "fileFilter": [".pm", ".pl"],
171 |                 "ignoreDirs": [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"],
172 |             }
173 | 
174 |             return [perl_config]
175 | 
176 |         self.server.on_request("client/registerCapability", register_capability_handler)
177 |         self.server.on_request("workspace/configuration", workspace_configuration_handler)
178 |         self.server.on_notification("window/logMessage", window_log_message)
179 |         self.server.on_notification("$/progress", do_nothing)
180 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
181 | 
182 |         self.logger.log("Starting Perl::LanguageServer process", logging.INFO)
183 |         self.server.start()
184 |         initialize_params = self._get_initialize_params(self.repository_root_path)
185 | 
186 |         self.logger.log(
187 |             "Sending initialize request from LSP client to LSP server and awaiting response",
188 |             logging.INFO,
189 |         )
190 |         init_response = self.server.send.initialize(initialize_params)
191 |         self.logger.log(
192 |             "After sent initialize params",
193 |             logging.INFO,
194 |         )
195 | 
196 |         # Verify server capabilities
197 |         assert "textDocumentSync" in init_response["capabilities"]
198 |         assert "definitionProvider" in init_response["capabilities"]
199 |         assert "referencesProvider" in init_response["capabilities"]
200 | 
201 |         self.server.notify.initialized({})
202 | 
203 |         # Send workspace configuration to Perl::LanguageServer
204 |         # Perl::LanguageServer requires didChangeConfiguration to set perlInc, fileFilter, and ignoreDirs
205 |         # See: Perl::LanguageServer::Methods::workspace::_rpcnot_didChangeConfiguration
206 |         perl_config = {
207 |             "settings": {
208 |                 "perl": {
209 |                     "perlInc": [self.repository_root_path, "."],
210 |                     "fileFilter": [".pm", ".pl"],
211 |                     "ignoreDirs": [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"],
212 |                 }
213 |             }
214 |         }
215 |         self.logger.log(f"Sending workspace/didChangeConfiguration notification with config: {perl_config}", logging.INFO)
216 |         self.server.notify.workspace_did_change_configuration(perl_config)
217 | 
218 |         self.completions_available.set()
219 | 
220 |         # Perl::LanguageServer needs time to index files and resolve cross-file references
221 |         # Without this delay, requests for definitions/references may return empty results
222 |         settling_time = 0.5
223 |         self.logger.log(f"Allowing {settling_time} seconds for Perl::LanguageServer to index files...", logging.INFO)
224 |         time.sleep(settling_time)
225 |         self.logger.log("Perl::LanguageServer settling period complete", logging.INFO)
226 | 
```

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

```python
  1 | """
  2 | Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import re
  9 | import threading
 10 | 
 11 | from overrides import override
 12 | 
 13 | from solidlsp.ls import SolidLanguageServer
 14 | from solidlsp.ls_config import LanguageServerConfig
 15 | from solidlsp.ls_logger import LanguageServerLogger
 16 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 17 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 18 | from solidlsp.settings import SolidLSPSettings
 19 | 
 20 | 
 21 | class PyrightServer(SolidLanguageServer):
 22 |     """
 23 |     Provides Python specific instantiation of the LanguageServer class using Pyright.
 24 |     Contains various configurations and settings specific to Python.
 25 |     """
 26 | 
 27 |     def __init__(
 28 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 29 |     ):
 30 |         """
 31 |         Creates a PyrightServer instance. This class is not meant to be instantiated directly.
 32 |         Use LanguageServer.create() instead.
 33 |         """
 34 |         super().__init__(
 35 |             config,
 36 |             logger,
 37 |             repository_root_path,
 38 |             # Note 1: we can also use `pyright-langserver --stdio` but it requires pyright to be installed with npm
 39 |             # Note 2: we can also use `bpyright-langserver --stdio` if we ever are unhappy with pyright
 40 |             ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path),
 41 |             "python",
 42 |             solidlsp_settings,
 43 |         )
 44 | 
 45 |         # Event to signal when initial workspace analysis is complete
 46 |         self.analysis_complete = threading.Event()
 47 |         self.found_source_files = False
 48 | 
 49 |     @override
 50 |     def is_ignored_dirname(self, dirname: str) -> bool:
 51 |         return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
 52 | 
 53 |     @staticmethod
 54 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
 55 |         """
 56 |         Returns the initialize params for the Pyright Language Server.
 57 |         """
 58 |         # Create basic initialization parameters
 59 |         initialize_params: InitializeParams = {  # type: ignore
 60 |             "processId": os.getpid(),
 61 |             "rootPath": repository_absolute_path,
 62 |             "rootUri": pathlib.Path(repository_absolute_path).as_uri(),
 63 |             "initializationOptions": {
 64 |                 "exclude": [
 65 |                     "**/__pycache__",
 66 |                     "**/.venv",
 67 |                     "**/.env",
 68 |                     "**/build",
 69 |                     "**/dist",
 70 |                     "**/.pixi",
 71 |                 ],
 72 |                 "reportMissingImports": "error",
 73 |             },
 74 |             "capabilities": {
 75 |                 "workspace": {
 76 |                     "workspaceEdit": {"documentChanges": True},
 77 |                     "didChangeConfiguration": {"dynamicRegistration": True},
 78 |                     "didChangeWatchedFiles": {"dynamicRegistration": True},
 79 |                     "symbol": {
 80 |                         "dynamicRegistration": True,
 81 |                         "symbolKind": {"valueSet": list(range(1, 27))},
 82 |                     },
 83 |                     "executeCommand": {"dynamicRegistration": True},
 84 |                 },
 85 |                 "textDocument": {
 86 |                     "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
 87 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
 88 |                     "signatureHelp": {
 89 |                         "dynamicRegistration": True,
 90 |                         "signatureInformation": {
 91 |                             "documentationFormat": ["markdown", "plaintext"],
 92 |                             "parameterInformation": {"labelOffsetSupport": True},
 93 |                         },
 94 |                     },
 95 |                     "definition": {"dynamicRegistration": True},
 96 |                     "references": {"dynamicRegistration": True},
 97 |                     "documentSymbol": {
 98 |                         "dynamicRegistration": True,
 99 |                         "symbolKind": {"valueSet": list(range(1, 27))},
100 |                         "hierarchicalDocumentSymbolSupport": True,
101 |                     },
102 |                     "publishDiagnostics": {"relatedInformation": True},
103 |                 },
104 |             },
105 |             "workspaceFolders": [
106 |                 {"uri": pathlib.Path(repository_absolute_path).as_uri(), "name": os.path.basename(repository_absolute_path)}
107 |             ],
108 |         }
109 | 
110 |         return initialize_params
111 | 
112 |     def _start_server(self):
113 |         """
114 |         Starts the Pyright Language Server and waits for initial workspace analysis to complete.
115 | 
116 |         This prevents zombie processes by ensuring Pyright has finished its initial background
117 |         tasks before we consider the server ready.
118 | 
119 |         Usage:
120 |         ```
121 |         async with lsp.start_server():
122 |             # LanguageServer has been initialized and workspace analysis is complete
123 |             await lsp.request_definition(...)
124 |             await lsp.request_references(...)
125 |             # Shutdown the LanguageServer on exit from scope
126 |         # LanguageServer has been shutdown cleanly
127 |         ```
128 |         """
129 | 
130 |         def execute_client_command_handler(params):
131 |             return []
132 | 
133 |         def do_nothing(params):
134 |             return
135 | 
136 |         def window_log_message(msg):
137 |             """
138 |             Monitor Pyright's log messages to detect when initial analysis is complete.
139 |             Pyright logs "Found X source files" when it finishes scanning the workspace.
140 |             """
141 |             message_text = msg.get("message", "")
142 |             self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO)
143 | 
144 |             # Look for "Found X source files" which indicates workspace scanning is complete
145 |             # Unfortunately, pyright is unreliable and there seems to be no better way
146 |             if re.search(r"Found \d+ source files?", message_text):
147 |                 self.logger.log("Pyright workspace scanning complete", logging.INFO)
148 |                 self.found_source_files = True
149 |                 self.analysis_complete.set()
150 |                 self.completions_available.set()
151 | 
152 |         def check_experimental_status(params):
153 |             """
154 |             Also listen for experimental/serverStatus as a backup signal
155 |             """
156 |             if params.get("quiescent") == True:
157 |                 self.logger.log("Received experimental/serverStatus with quiescent=true", logging.INFO)
158 |                 if not self.found_source_files:
159 |                     self.analysis_complete.set()
160 |                     self.completions_available.set()
161 | 
162 |         # Set up notification handlers
163 |         self.server.on_request("client/registerCapability", do_nothing)
164 |         self.server.on_notification("language/status", do_nothing)
165 |         self.server.on_notification("window/logMessage", window_log_message)
166 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
167 |         self.server.on_notification("$/progress", do_nothing)
168 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
169 |         self.server.on_notification("language/actionableNotification", do_nothing)
170 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
171 | 
172 |         self.logger.log("Starting pyright-langserver server process", logging.INFO)
173 |         self.server.start()
174 | 
175 |         # Send proper initialization parameters
176 |         initialize_params = self._get_initialize_params(self.repository_root_path)
177 | 
178 |         self.logger.log(
179 |             "Sending initialize request from LSP client to pyright server and awaiting response",
180 |             logging.INFO,
181 |         )
182 |         init_response = self.server.send.initialize(initialize_params)
183 |         self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO)
184 | 
185 |         # Verify that the server supports our required features
186 |         assert "textDocumentSync" in init_response["capabilities"]
187 |         assert "completionProvider" in init_response["capabilities"]
188 |         assert "definitionProvider" in init_response["capabilities"]
189 | 
190 |         # Complete the initialization handshake
191 |         self.server.notify.initialized({})
192 | 
193 |         # Wait for Pyright to complete its initial workspace analysis
194 |         # This prevents zombie processes by ensuring background tasks finish
195 |         self.logger.log("Waiting for Pyright to complete initial workspace analysis...", logging.INFO)
196 |         if self.analysis_complete.wait(timeout=5.0):
197 |             self.logger.log("Pyright initial analysis complete, server ready", logging.INFO)
198 |         else:
199 |             self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING)
200 |             # Fallback: assume analysis is complete after timeout
201 |             self.analysis_complete.set()
202 |             self.completions_available.set()
203 | 
```

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

```python
  1 | """
  2 | Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import threading
  9 | 
 10 | from solidlsp.ls import SolidLanguageServer
 11 | from solidlsp.ls_config import LanguageServerConfig
 12 | from solidlsp.ls_logger import LanguageServerLogger
 13 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 14 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 15 | from solidlsp.settings import SolidLSPSettings
 16 | 
 17 | from .common import RuntimeDependency, RuntimeDependencyCollection
 18 | 
 19 | 
 20 | class ClangdLanguageServer(SolidLanguageServer):
 21 |     """
 22 |     Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++.
 23 |     As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly.
 24 |     Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example.
 25 |     """
 26 | 
 27 |     def __init__(
 28 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 29 |     ):
 30 |         """
 31 |         Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 32 |         """
 33 |         clangd_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 34 |         super().__init__(
 35 |             config,
 36 |             logger,
 37 |             repository_root_path,
 38 |             ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path),
 39 |             "cpp",
 40 |             solidlsp_settings,
 41 |         )
 42 |         self.server_ready = threading.Event()
 43 |         self.service_ready_event = threading.Event()
 44 |         self.initialize_searcher_command_available = threading.Event()
 45 |         self.resolve_main_method_available = threading.Event()
 46 | 
 47 |     @classmethod
 48 |     def _setup_runtime_dependencies(
 49 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
 50 |     ) -> str:
 51 |         """
 52 |         Setup runtime dependencies for ClangdLanguageServer and return the command to start the server.
 53 |         """
 54 |         deps = RuntimeDependencyCollection(
 55 |             [
 56 |                 RuntimeDependency(
 57 |                     id="Clangd",
 58 |                     description="Clangd for Linux (x64)",
 59 |                     url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip",
 60 |                     platform_id="linux-x64",
 61 |                     archive_type="zip",
 62 |                     binary_name="clangd_19.1.2/bin/clangd",
 63 |                 ),
 64 |                 RuntimeDependency(
 65 |                     id="Clangd",
 66 |                     description="Clangd for Windows (x64)",
 67 |                     url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip",
 68 |                     platform_id="win-x64",
 69 |                     archive_type="zip",
 70 |                     binary_name="clangd_19.1.2/bin/clangd.exe",
 71 |                 ),
 72 |                 RuntimeDependency(
 73 |                     id="Clangd",
 74 |                     description="Clangd for macOS (x64)",
 75 |                     url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
 76 |                     platform_id="osx-x64",
 77 |                     archive_type="zip",
 78 |                     binary_name="clangd_19.1.2/bin/clangd",
 79 |                 ),
 80 |                 RuntimeDependency(
 81 |                     id="Clangd",
 82 |                     description="Clangd for macOS (Arm64)",
 83 |                     url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
 84 |                     platform_id="osx-arm64",
 85 |                     archive_type="zip",
 86 |                     binary_name="clangd_19.1.2/bin/clangd",
 87 |                 ),
 88 |             ]
 89 |         )
 90 | 
 91 |         clangd_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "clangd")
 92 |         dep = deps.get_single_dep_for_current_platform()
 93 |         clangd_executable_path = deps.binary_path(clangd_ls_dir)
 94 |         if not os.path.exists(clangd_executable_path):
 95 |             logger.log(
 96 |                 f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}",
 97 |                 logging.INFO,
 98 |             )
 99 |             deps.install(logger, clangd_ls_dir)
100 |         if not os.path.exists(clangd_executable_path):
101 |             raise FileNotFoundError(
102 |                 f"Clangd executable not found at {clangd_executable_path}.\n"
103 |                 "Make sure you have installed clangd. See https://clangd.llvm.org/installation"
104 |             )
105 |         os.chmod(clangd_executable_path, 0o755)
106 | 
107 |         return clangd_executable_path
108 | 
109 |     @staticmethod
110 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
111 |         """
112 |         Returns the initialize params for the clangd Language Server.
113 |         """
114 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
115 |         initialize_params = {
116 |             "locale": "en",
117 |             "capabilities": {
118 |                 "textDocument": {
119 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
120 |                     "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
121 |                     "definition": {"dynamicRegistration": True},
122 |                 },
123 |                 "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
124 |             },
125 |             "processId": os.getpid(),
126 |             "rootPath": repository_absolute_path,
127 |             "rootUri": root_uri,
128 |             "workspaceFolders": [
129 |                 {
130 |                     "uri": root_uri,
131 |                     "name": "$name",
132 |                 }
133 |             ],
134 |         }
135 | 
136 |         return initialize_params
137 | 
138 |     def _start_server(self):
139 |         """
140 |         Starts the Clangd Language Server, waits for the server to be ready and yields the LanguageServer instance.
141 | 
142 |         Usage:
143 |         ```
144 |         async with lsp.start_server():
145 |             # LanguageServer has been initialized and ready to serve requests
146 |             await lsp.request_definition(...)
147 |             await lsp.request_references(...)
148 |             # Shutdown the LanguageServer on exit from scope
149 |         # LanguageServer has been shutdown
150 |         """
151 | 
152 |         def register_capability_handler(params):
153 |             assert "registrations" in params
154 |             for registration in params["registrations"]:
155 |                 if registration["method"] == "workspace/executeCommand":
156 |                     self.initialize_searcher_command_available.set()
157 |                     self.resolve_main_method_available.set()
158 |             return
159 | 
160 |         def lang_status_handler(params):
161 |             # TODO: Should we wait for
162 |             # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
163 |             # Before proceeding?
164 |             if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
165 |                 self.service_ready_event.set()
166 | 
167 |         def execute_client_command_handler(params):
168 |             return []
169 | 
170 |         def do_nothing(params):
171 |             return
172 | 
173 |         def check_experimental_status(params):
174 |             if params["quiescent"] == True:
175 |                 self.server_ready.set()
176 | 
177 |         def window_log_message(msg):
178 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
179 | 
180 |         self.server.on_request("client/registerCapability", register_capability_handler)
181 |         self.server.on_notification("language/status", lang_status_handler)
182 |         self.server.on_notification("window/logMessage", window_log_message)
183 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
184 |         self.server.on_notification("$/progress", do_nothing)
185 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
186 |         self.server.on_notification("language/actionableNotification", do_nothing)
187 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
188 | 
189 |         self.logger.log("Starting Clangd server process", logging.INFO)
190 |         self.server.start()
191 |         initialize_params = self._get_initialize_params(self.repository_root_path)
192 | 
193 |         self.logger.log(
194 |             "Sending initialize request from LSP client to LSP server and awaiting response",
195 |             logging.INFO,
196 |         )
197 |         init_response = self.server.send.initialize(initialize_params)
198 |         assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
199 |         assert "completionProvider" in init_response["capabilities"]
200 |         assert init_response["capabilities"]["completionProvider"] == {
201 |             "triggerCharacters": [".", "<", ">", ":", '"', "/", "*"],
202 |             "resolveProvider": False,
203 |         }
204 | 
205 |         self.server.notify.initialized({})
206 | 
207 |         self.completions_available.set()
208 |         # set ready flag
209 |         self.server_ready.set()
210 |         self.server_ready.wait()
211 | 
```

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

```python
  1 | """
  2 | Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | 
  9 | from overrides import override
 10 | 
 11 | from solidlsp.ls import SolidLanguageServer
 12 | from solidlsp.ls_config import LanguageServerConfig
 13 | from solidlsp.ls_logger import LanguageServerLogger
 14 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 15 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 16 | from solidlsp.settings import SolidLSPSettings
 17 | 
 18 | 
 19 | class JediServer(SolidLanguageServer):
 20 |     """
 21 |     Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
 22 |     """
 23 | 
 24 |     def __init__(
 25 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 26 |     ):
 27 |         """
 28 |         Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 29 |         """
 30 |         super().__init__(
 31 |             config,
 32 |             logger,
 33 |             repository_root_path,
 34 |             ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path),
 35 |             "python",
 36 |             solidlsp_settings,
 37 |         )
 38 | 
 39 |     @override
 40 |     def is_ignored_dirname(self, dirname: str) -> bool:
 41 |         return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
 42 | 
 43 |     @staticmethod
 44 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
 45 |         """
 46 |         Returns the initialize params for the Jedi Language Server.
 47 |         """
 48 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
 49 |         initialize_params = {
 50 |             "processId": os.getpid(),
 51 |             "clientInfo": {"name": "Serena", "version": "0.1.0"},
 52 |             "locale": "en",
 53 |             "rootPath": repository_absolute_path,
 54 |             "rootUri": root_uri,
 55 |             # Note: this is not necessarily the minimal set of capabilities...
 56 |             "capabilities": {
 57 |                 "workspace": {
 58 |                     "applyEdit": True,
 59 |                     "workspaceEdit": {
 60 |                         "documentChanges": True,
 61 |                         "resourceOperations": ["create", "rename", "delete"],
 62 |                         "failureHandling": "textOnlyTransactional",
 63 |                         "normalizesLineEndings": True,
 64 |                         "changeAnnotationSupport": {"groupsOnLabel": True},
 65 |                     },
 66 |                     "configuration": True,
 67 |                     "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True},
 68 |                     "symbol": {
 69 |                         "dynamicRegistration": True,
 70 |                         "symbolKind": {"valueSet": list(range(1, 27))},
 71 |                         "tagSupport": {"valueSet": [1]},
 72 |                         "resolveSupport": {"properties": ["location.range"]},
 73 |                     },
 74 |                     "workspaceFolders": True,
 75 |                     "fileOperations": {
 76 |                         "dynamicRegistration": True,
 77 |                         "didCreate": True,
 78 |                         "didRename": True,
 79 |                         "didDelete": True,
 80 |                         "willCreate": True,
 81 |                         "willRename": True,
 82 |                         "willDelete": True,
 83 |                     },
 84 |                     "inlineValue": {"refreshSupport": True},
 85 |                     "inlayHint": {"refreshSupport": True},
 86 |                     "diagnostics": {"refreshSupport": True},
 87 |                 },
 88 |                 "textDocument": {
 89 |                     "publishDiagnostics": {
 90 |                         "relatedInformation": True,
 91 |                         "versionSupport": False,
 92 |                         "tagSupport": {"valueSet": [1, 2]},
 93 |                         "codeDescriptionSupport": True,
 94 |                         "dataSupport": True,
 95 |                     },
 96 |                     "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
 97 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
 98 |                     "signatureHelp": {
 99 |                         "dynamicRegistration": True,
100 |                         "signatureInformation": {
101 |                             "documentationFormat": ["markdown", "plaintext"],
102 |                             "parameterInformation": {"labelOffsetSupport": True},
103 |                             "activeParameterSupport": True,
104 |                         },
105 |                         "contextSupport": True,
106 |                     },
107 |                     "definition": {"dynamicRegistration": True, "linkSupport": True},
108 |                     "references": {"dynamicRegistration": True},
109 |                     "documentHighlight": {"dynamicRegistration": True},
110 |                     "documentSymbol": {
111 |                         "dynamicRegistration": True,
112 |                         "symbolKind": {"valueSet": list(range(1, 27))},
113 |                         "hierarchicalDocumentSymbolSupport": True,
114 |                         "tagSupport": {"valueSet": [1]},
115 |                         "labelSupport": True,
116 |                     },
117 |                     "documentLink": {"dynamicRegistration": True, "tooltipSupport": True},
118 |                     "typeDefinition": {"dynamicRegistration": True, "linkSupport": True},
119 |                     "implementation": {"dynamicRegistration": True, "linkSupport": True},
120 |                     "declaration": {"dynamicRegistration": True, "linkSupport": True},
121 |                     "selectionRange": {"dynamicRegistration": True},
122 |                     "callHierarchy": {"dynamicRegistration": True},
123 |                     "linkedEditingRange": {"dynamicRegistration": True},
124 |                     "typeHierarchy": {"dynamicRegistration": True},
125 |                     "inlineValue": {"dynamicRegistration": True},
126 |                     "inlayHint": {
127 |                         "dynamicRegistration": True,
128 |                         "resolveSupport": {"properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"]},
129 |                     },
130 |                     "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False},
131 |                 },
132 |                 "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}},
133 |                 "experimental": {
134 |                     "serverStatusNotification": True,
135 |                     "openServerLogs": True,
136 |                 },
137 |             },
138 |             # See https://github.com/pappasam/jedi-language-server?tab=readme-ov-file
139 |             # We use the default options except for maxSymbols, where 0 means no limit
140 |             "initializationOptions": {
141 |                 "workspace": {
142 |                     "symbols": {"ignoreFolders": [".nox", ".tox", ".venv", "__pycache__", "venv"], "maxSymbols": 0},
143 |                 },
144 |             },
145 |             "trace": "verbose",
146 |             "workspaceFolders": [
147 |                 {
148 |                     "uri": root_uri,
149 |                     "name": os.path.basename(repository_absolute_path),
150 |                 }
151 |             ],
152 |         }
153 |         return initialize_params
154 | 
155 |     def _start_server(self):
156 |         """
157 |         Starts the JEDI Language Server
158 |         """
159 | 
160 |         def execute_client_command_handler(params):
161 |             return []
162 | 
163 |         def do_nothing(params):
164 |             return
165 | 
166 |         def check_experimental_status(params):
167 |             if params["quiescent"] == True:
168 |                 self.completions_available.set()
169 | 
170 |         def window_log_message(msg):
171 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
172 | 
173 |         self.server.on_request("client/registerCapability", do_nothing)
174 |         self.server.on_notification("language/status", do_nothing)
175 |         self.server.on_notification("window/logMessage", window_log_message)
176 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
177 |         self.server.on_notification("$/progress", do_nothing)
178 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
179 |         self.server.on_notification("language/actionableNotification", do_nothing)
180 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
181 | 
182 |         self.logger.log("Starting jedi-language-server server process", logging.INFO)
183 |         self.server.start()
184 |         initialize_params = self._get_initialize_params(self.repository_root_path)
185 | 
186 |         self.logger.log(
187 |             "Sending initialize request from LSP client to LSP server and awaiting response",
188 |             logging.INFO,
189 |         )
190 |         init_response = self.server.send.initialize(initialize_params)
191 |         assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
192 |         assert "completionProvider" in init_response["capabilities"]
193 |         assert init_response["capabilities"]["completionProvider"] == {
194 |             "triggerCharacters": [".", "'", '"'],
195 |             "resolveProvider": True,
196 |         }
197 | 
198 |         self.server.notify.initialized({})
199 | 
```

--------------------------------------------------------------------------------
/src/serena/resources/dashboard/index.html:
--------------------------------------------------------------------------------

```html
  1 | <!DOCTYPE html>
  2 | <html lang="en">
  3 | 
  4 | <head>
  5 |     <meta charset="UTF-8">
  6 |     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7 |     <title>Serena Dashboard</title>
  8 |     <link rel="icon" type="image/png" sizes="16x16" href="serena-icon-16.png">
  9 |     <link rel="icon" type="image/png" sizes="32x32" href="serena-icon-32.png">
 10 |     <link rel="icon" type="image/png" sizes="48x48" href="serena-icon-48.png">
 11 |     <script src="jquery.min.js"></script>
 12 |     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 13 |     <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
 14 |     <script src="dashboard.js"></script>
 15 |     <style>
 16 |         :root {
 17 |             /* Light theme variables */
 18 |             --bg-primary: #f5f5f5;
 19 |             --bg-secondary: #ffffff;
 20 |             --text-primary: #000000;
 21 |             --text-secondary: #333333;
 22 |             --text-muted: #666666;
 23 |             --border-color: #ddd;
 24 |             --btn-primary: #eaa45d;
 25 |             --btn-hover: #dca662;
 26 |             --btn-disabled: #6c757d;
 27 |             --shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 28 |             --tool-highlight: #ffff00;
 29 |             --tool-highlight-text: #000000;
 30 |             --log-debug: #808080;
 31 |             --log-info: #000000;
 32 |             --log-warning: #FF8C00;
 33 |             --log-error: #FF0000;
 34 |             --stats-header: #f8f9fa;
 35 |         }
 36 | 
 37 |         [data-theme="dark"] {
 38 |             /* Dark theme variables */
 39 |             --bg-primary: #1a1a1a;
 40 |             --bg-secondary: #2d2d2d;
 41 |             --text-primary: #ffffff;
 42 |             --text-secondary: #e0e0e0;
 43 |             --text-muted: #b0b0b0;
 44 |             --border-color: #444;
 45 |             --btn-primary: #eaa45d;
 46 |             --btn-hover: #dca662;
 47 |             --btn-disabled: #6c757d;
 48 |             --shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
 49 |             --tool-highlight: #ffd700;
 50 |             --tool-highlight-text: #000000;
 51 |             --log-debug: #808080;
 52 |             --log-info: #ffffff;
 53 |             --log-warning: #FF8C00;
 54 |             --log-error: #FF0000;
 55 |             --stats-header: #3a3a3a;
 56 |         }
 57 | 
 58 |         body {
 59 |             font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
 60 |             margin: 0;
 61 |             padding: 20px;
 62 |             background-color: var(--bg-primary);
 63 |             color: var(--text-primary);
 64 |             transition: background-color 0.3s ease, color 0.3s ease;
 65 |         }
 66 | 
 67 |         .header {
 68 |             text-align: center;
 69 |             margin-bottom: 20px;
 70 |         }
 71 | 
 72 |         .log-container {
 73 |             background-color: var(--bg-secondary);
 74 |             border: 1px solid var(--border-color);
 75 |             border-radius: 5px;
 76 |             height: 600px;
 77 |             overflow-y: auto;
 78 |             overflow-x: auto;
 79 |             padding: 10px;
 80 |             white-space: pre-wrap;
 81 |             font-size: 12px;
 82 |             line-height: 1.4;
 83 |             color: var(--text-primary);
 84 |             transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
 85 |         }
 86 | 
 87 |         .controls {
 88 |             margin-bottom: 10px;
 89 |             text-align: center;
 90 |             display: flex;
 91 |             justify-content: center;
 92 |             align-items: center;
 93 |             gap: 10px;
 94 |             flex-wrap: wrap;
 95 |         }
 96 | 
 97 |         .logo {
 98 |             margin-bottom: 10px;
 99 |             text-align: center;
100 |         }
101 | 
102 |         .btn {
103 |             background-color: var(--btn-primary);
104 |             color: white;
105 |             border: none;
106 |             padding: 8px 16px;
107 |             border-radius: 4px;
108 |             cursor: pointer;
109 |             font-size: 14px;
110 |             transition: background-color 0.3s ease;
111 |         }
112 | 
113 |         .btn:hover {
114 |             background-color: var(--btn-hover);
115 |         }
116 | 
117 |         .btn:disabled {
118 |             background-color: var(--btn-disabled);
119 |             cursor: not-allowed;
120 |         }
121 | 
122 |         .theme-toggle {
123 |             display: flex;
124 |             align-items: center;
125 |             gap: 5px;
126 |             background-color: var(--bg-secondary);
127 |             border: 1px solid var(--border-color);
128 |             border-radius: 4px;
129 |             padding: 6px 12px;
130 |             cursor: pointer;
131 |             transition: background-color 0.3s ease, border-color 0.3s ease;
132 |         }
133 | 
134 |         .theme-toggle:hover {
135 |             background-color: var(--border-color);
136 |         }
137 | 
138 |         .theme-toggle .icon {
139 |             font-size: 16px;
140 |         }
141 | 
142 |         .log-debug {
143 |             color: var(--log-debug);
144 |         }
145 | 
146 |         .log-info {
147 |             color: var(--log-info);
148 |         }
149 | 
150 |         .log-warning {
151 |             color: var(--log-warning);
152 |         }
153 | 
154 |         .log-error {
155 |             color: var(--log-error);
156 |         }
157 | 
158 |         .log-default {
159 |             color: var(--log-info);
160 |         }
161 | 
162 |         /* Tool name highlighting */
163 |         .tool-name {
164 |             background-color: var(--tool-highlight);
165 |             color: var(--tool-highlight-text);
166 |             font-weight: bold;
167 |         }
168 | 
169 |         .loading {
170 |             text-align: center;
171 |             color: var(--text-muted);
172 |             font-style: italic;
173 |         }
174 | 
175 |         .error-message {
176 |             color: var(--log-error);
177 |             text-align: center;
178 |             margin: 10px 0;
179 |         }
180 | 
181 |         .charts-container {
182 |             display: flex;
183 |             flex-wrap: wrap;
184 |             gap: 15px;
185 |             justify-content: space-between;
186 |             max-width: 1400px;
187 |             margin: 0 auto;
188 |         }
189 | 
190 |         .chart-group {
191 |             flex: 1;
192 |             min-width: 280px;
193 |             max-width: 320px;
194 |             text-align: center;
195 |         }
196 | 
197 |         .chart-wide {
198 |             flex: 0 0 100%;
199 |             min-width: 100%;
200 |             margin-top: 10px;
201 |         }
202 | 
203 |         .chart-group h3 {
204 |             margin: 0 0 10px 0;
205 |             color: var(--text-secondary);
206 |         }
207 | 
208 |         .stats-summary {
209 |             margin: 0 auto;
210 |             border-collapse: collapse;
211 |             background: var(--bg-secondary);
212 |             border-radius: 5px;
213 |             overflow: hidden;
214 |             box-shadow: var(--shadow);
215 |             transition: background-color 0.3s ease, box-shadow 0.3s ease;
216 |         }
217 | 
218 |         .stats-summary th,
219 |         .stats-summary td {
220 |             padding: 10px 20px;
221 |             text-align: left;
222 |             border-bottom: 1px solid var(--border-color);
223 |             color: var(--text-primary);
224 |             transition: border-color 0.3s ease, color 0.3s ease;
225 |         }
226 | 
227 |         .stats-summary th {
228 |             background-color: var(--stats-header);
229 |             font-weight: bold;
230 |             transition: background-color 0.3s ease;
231 |         }
232 | 
233 |         .stats-summary tr:last-child td {
234 |             border-bottom: none;
235 |         }
236 | 
237 |         @media (max-width: 768px) {
238 |             .charts-container {
239 |                 flex-direction: column;
240 |             }
241 | 
242 |             .chart-group,
243 |             .chart-wide {
244 |                 min-width: auto;
245 |                 max-width: none;
246 |             }
247 | 
248 |             .controls {
249 |                 flex-direction: column;
250 |                 gap: 5px;
251 |             }
252 |         }
253 |     </style>
254 | </head>
255 | 
256 | <body>
257 |     <div class="header">
258 |         <img id="serena-logo" src="serena-logs.png" alt="Serena" style="max-width: 400px; height: auto;">
259 |     </div>
260 | 
261 |     <div class="controls">
262 |         <button id="load-logs" class="btn">Reload Log</button>
263 |         <button id="shutdown" class="btn">Shutdown Server</button>
264 |         <button id="toggle-stats" class="btn">Show Stats</button>
265 |         <div id="theme-toggle" class="theme-toggle" title="Toggle theme">
266 |             <span class="icon" id="theme-icon">🌙</span>
267 |             <span id="theme-text">Dark</span>
268 |         </div>
269 |     </div>
270 | 
271 |     <div id="error-container"></div>
272 |     <div id="log-container" class="log-container"></div>
273 | 
274 |     <div id="stats-section" style="display:none; margin-top:20px;">
275 |         <div style="text-align:center; margin-bottom:20px;">
276 |             <button id="refresh-stats" class="btn">Refresh Stats</button>
277 |             <button id="clear-stats" class="btn">Clear Stats</button>
278 |         </div>
279 | 
280 |         <div id="stats-summary" style="margin-bottom:20px; text-align:center;"></div>
281 |         <div id="estimator-name" style="text-align:center; margin-bottom:10px;"></div>
282 |         <div id="no-stats-message" style="text-align:center; color:var(--text-muted); font-style:italic; display:none;">
283 |             No tool stats collected. Have you enabled tool stats collection in the configuration?
284 |         </div>
285 | 
286 | 
287 |         <div class="charts-container">
288 |             <div class="chart-group">
289 |                 <h3>Tool Calls</h3>
290 |                 <canvas id="count-chart" height="200"></canvas>
291 |             </div>
292 |             <div class="chart-group">
293 |                 <h3>Input Tokens</h3>
294 |                 <canvas id="input-chart" height="200"></canvas>
295 |             </div>
296 |             <div class="chart-group">
297 |                 <h3>Output Tokens</h3>
298 |                 <canvas id="output-chart" height="200"></canvas>
299 |             </div>
300 |             <div class="chart-group chart-wide">
301 |                 <h3>Input vs Output Tokens</h3>
302 |                 <canvas id="tokens-chart" height="120"></canvas>
303 |             </div>
304 |         </div>
305 |     </div>
306 | 
307 |     <script>
308 |         $(document).ready(function () {
309 |             const dashboard = new Dashboard();
310 |         });
311 |     </script>
312 | </body>
313 | 
314 | </html>
```

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

```python
  1 | import logging
  2 | import os
  3 | import shutil
  4 | import threading
  5 | 
  6 | from overrides import override
  7 | 
  8 | from solidlsp.ls import SolidLanguageServer
  9 | from solidlsp.ls_config import LanguageServerConfig
 10 | from solidlsp.ls_logger import LanguageServerLogger
 11 | from solidlsp.ls_utils import PathUtils, PlatformUtils
 12 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 13 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 14 | from solidlsp.settings import SolidLSPSettings
 15 | 
 16 | from .common import RuntimeDependency, RuntimeDependencyCollection
 17 | 
 18 | 
 19 | class TerraformLS(SolidLanguageServer):
 20 |     """
 21 |     Provides Terraform specific instantiation of the LanguageServer class using terraform-ls.
 22 |     """
 23 | 
 24 |     @override
 25 |     def is_ignored_dirname(self, dirname: str) -> bool:
 26 |         return super().is_ignored_dirname(dirname) or dirname in [".terraform", "terraform.tfstate.d"]
 27 | 
 28 |     @staticmethod
 29 |     def _ensure_tf_command_available(logger: LanguageServerLogger):
 30 |         logger.log("Starting terraform version detection...", logging.DEBUG)
 31 | 
 32 |         # 1. Try to find terraform using shutil.which
 33 |         terraform_cmd = shutil.which("terraform")
 34 |         if terraform_cmd is not None:
 35 |             logger.log(f"Found terraform via shutil.which: {terraform_cmd}", logging.DEBUG)
 36 |             return
 37 | 
 38 |         # TODO: is this needed?
 39 |         # 2. Fallback to TERRAFORM_CLI_PATH (set by hashicorp/setup-terraform action)
 40 |         if not terraform_cmd:
 41 |             terraform_cli_path = os.environ.get("TERRAFORM_CLI_PATH")
 42 |             if terraform_cli_path:
 43 |                 logger.log(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}", logging.DEBUG)
 44 |                 # TODO: use binary name from runtime dependencies if we keep this code
 45 |                 if os.name == "nt":
 46 |                     terraform_binary = os.path.join(terraform_cli_path, "terraform.exe")
 47 |                 else:
 48 |                     terraform_binary = os.path.join(terraform_cli_path, "terraform")
 49 |                 if os.path.exists(terraform_binary):
 50 |                     terraform_cmd = terraform_binary
 51 |                     logger.log(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}", logging.DEBUG)
 52 |                     return
 53 | 
 54 |         raise RuntimeError(
 55 |             "Terraform executable not found, please ensure Terraform is installed."
 56 |             "See https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli for instructions."
 57 |         )
 58 | 
 59 |     @classmethod
 60 |     def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, solidlsp_settings: SolidLSPSettings) -> str:
 61 |         """
 62 |         Setup runtime dependencies for terraform-ls.
 63 |         Downloads and installs terraform-ls if not already present.
 64 |         """
 65 |         cls._ensure_tf_command_available(logger)
 66 |         platform_id = PlatformUtils.get_platform_id()
 67 |         deps = RuntimeDependencyCollection(
 68 |             [
 69 |                 RuntimeDependency(
 70 |                     id="TerraformLS",
 71 |                     description="terraform-ls for macOS (ARM64)",
 72 |                     url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip",
 73 |                     platform_id="osx-arm64",
 74 |                     archive_type="zip",
 75 |                     binary_name="terraform-ls",
 76 |                 ),
 77 |                 RuntimeDependency(
 78 |                     id="TerraformLS",
 79 |                     description="terraform-ls for macOS (x64)",
 80 |                     url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip",
 81 |                     platform_id="osx-x64",
 82 |                     archive_type="zip",
 83 |                     binary_name="terraform-ls",
 84 |                 ),
 85 |                 RuntimeDependency(
 86 |                     id="TerraformLS",
 87 |                     description="terraform-ls for Linux (ARM64)",
 88 |                     url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_arm64.zip",
 89 |                     platform_id="linux-arm64",
 90 |                     archive_type="zip",
 91 |                     binary_name="terraform-ls",
 92 |                 ),
 93 |                 RuntimeDependency(
 94 |                     id="TerraformLS",
 95 |                     description="terraform-ls for Linux (x64)",
 96 |                     url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip",
 97 |                     platform_id="linux-x64",
 98 |                     archive_type="zip",
 99 |                     binary_name="terraform-ls",
100 |                 ),
101 |                 RuntimeDependency(
102 |                     id="TerraformLS",
103 |                     description="terraform-ls for Windows (x64)",
104 |                     url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip",
105 |                     platform_id="win-x64",
106 |                     archive_type="zip",
107 |                     binary_name="terraform-ls.exe",
108 |                 ),
109 |             ]
110 |         )
111 |         dependency = deps.get_single_dep_for_current_platform()
112 | 
113 |         terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir(solidlsp_settings))
114 |         if not os.path.exists(terraform_ls_executable_path):
115 |             logger.log(f"Downloading terraform-ls from {dependency.url}", logging.INFO)
116 |             deps.install(logger, cls.ls_resources_dir(solidlsp_settings))
117 | 
118 |         assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}"
119 | 
120 |         # Make the executable file executable on Unix-like systems
121 |         if platform_id.value != "win-x64":
122 |             os.chmod(terraform_ls_executable_path, 0o755)
123 | 
124 |         return terraform_ls_executable_path
125 | 
126 |     def __init__(
127 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
128 |     ):
129 |         """
130 |         Creates a TerraformLS instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
131 |         """
132 |         terraform_ls_executable_path = self._setup_runtime_dependencies(logger, solidlsp_settings)
133 | 
134 |         super().__init__(
135 |             config,
136 |             logger,
137 |             repository_root_path,
138 |             ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path),
139 |             "terraform",
140 |             solidlsp_settings,
141 |         )
142 |         self.server_ready = threading.Event()
143 |         self.request_id = 0
144 | 
145 |     @staticmethod
146 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
147 |         """
148 |         Returns the initialize params for the Terraform Language Server.
149 |         """
150 |         root_uri = PathUtils.path_to_uri(repository_absolute_path)
151 |         return {
152 |             "processId": os.getpid(),
153 |             "locale": "en",
154 |             "rootPath": repository_absolute_path,
155 |             "rootUri": root_uri,
156 |             "capabilities": {
157 |                 "textDocument": {
158 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
159 |                     "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
160 |                     "definition": {"dynamicRegistration": True},
161 |                     "documentSymbol": {
162 |                         "dynamicRegistration": True,
163 |                         "hierarchicalDocumentSymbolSupport": True,
164 |                         "symbolKind": {"valueSet": list(range(1, 27))},
165 |                     },
166 |                 },
167 |                 "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
168 |             },
169 |             "workspaceFolders": [
170 |                 {
171 |                     "name": os.path.basename(repository_absolute_path),
172 |                     "uri": root_uri,
173 |                 }
174 |             ],
175 |         }
176 | 
177 |     def _start_server(self):
178 |         """Start terraform-ls server process"""
179 | 
180 |         def register_capability_handler(params):
181 |             return
182 | 
183 |         def window_log_message(msg):
184 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
185 | 
186 |         def do_nothing(params):
187 |             return
188 | 
189 |         self.server.on_request("client/registerCapability", register_capability_handler)
190 |         self.server.on_notification("window/logMessage", window_log_message)
191 |         self.server.on_notification("$/progress", do_nothing)
192 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
193 | 
194 |         self.logger.log("Starting terraform-ls server process", logging.INFO)
195 |         self.server.start()
196 |         initialize_params = self._get_initialize_params(self.repository_root_path)
197 | 
198 |         self.logger.log(
199 |             "Sending initialize request from LSP client to LSP server and awaiting response",
200 |             logging.INFO,
201 |         )
202 |         init_response = self.server.send.initialize(initialize_params)
203 | 
204 |         # Verify server capabilities
205 |         assert "textDocumentSync" in init_response["capabilities"]
206 |         assert "completionProvider" in init_response["capabilities"]
207 |         assert "definitionProvider" in init_response["capabilities"]
208 | 
209 |         self.server.notify.initialized({})
210 |         self.completions_available.set()
211 | 
212 |         # terraform-ls server is typically ready immediately after initialization
213 |         self.server_ready.set()
214 |         self.server_ready.wait()
215 | 
```

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

```python
  1 | """Erlang Language Server implementation using Erlang LS."""
  2 | 
  3 | import logging
  4 | import os
  5 | import shutil
  6 | import subprocess
  7 | import threading
  8 | import time
  9 | 
 10 | from overrides import override
 11 | 
 12 | from solidlsp.ls import SolidLanguageServer
 13 | from solidlsp.ls_config import LanguageServerConfig
 14 | from solidlsp.ls_logger import LanguageServerLogger
 15 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 16 | from solidlsp.settings import SolidLSPSettings
 17 | 
 18 | 
 19 | class ErlangLanguageServer(SolidLanguageServer):
 20 |     """Language server for Erlang using Erlang LS."""
 21 | 
 22 |     def __init__(
 23 |         self,
 24 |         config: LanguageServerConfig,
 25 |         logger: LanguageServerLogger,
 26 |         repository_root_path: str,
 27 |         solidlsp_settings: SolidLSPSettings,
 28 |     ):
 29 |         """
 30 |         Creates an ErlangLanguageServer instance. This class is not meant to be instantiated directly.
 31 |         Use LanguageServer.create() instead.
 32 |         """
 33 |         self.erlang_ls_path = shutil.which("erlang_ls")
 34 |         if not self.erlang_ls_path:
 35 |             raise RuntimeError("Erlang LS not found. Install from: https://github.com/erlang-ls/erlang_ls")
 36 | 
 37 |         if not self._check_erlang_installation():
 38 |             raise RuntimeError("Erlang/OTP not found. Install from: https://www.erlang.org/downloads")
 39 | 
 40 |         super().__init__(
 41 |             config,
 42 |             logger,
 43 |             repository_root_path,
 44 |             ProcessLaunchInfo(cmd=[self.erlang_ls_path, "--transport", "stdio"], cwd=repository_root_path),
 45 |             "erlang",
 46 |             solidlsp_settings,
 47 |         )
 48 | 
 49 |         # Add server readiness tracking like Elixir
 50 |         self.server_ready = threading.Event()
 51 | 
 52 |         # Set generous timeout for Erlang LS initialization
 53 |         self.set_request_timeout(120.0)
 54 | 
 55 |     def _check_erlang_installation(self) -> bool:
 56 |         """Check if Erlang/OTP is available."""
 57 |         try:
 58 |             result = subprocess.run(["erl", "-version"], check=False, capture_output=True, text=True, timeout=10)
 59 |             return result.returncode == 0
 60 |         except (subprocess.SubprocessError, FileNotFoundError):
 61 |             return False
 62 | 
 63 |     @classmethod
 64 |     def _get_erlang_version(cls):
 65 |         """Get the installed Erlang/OTP version or None if not found."""
 66 |         try:
 67 |             result = subprocess.run(["erl", "-version"], check=False, capture_output=True, text=True, timeout=10)
 68 |             if result.returncode == 0:
 69 |                 return result.stderr.strip()  # erl -version outputs to stderr
 70 |         except (subprocess.SubprocessError, FileNotFoundError):
 71 |             return None
 72 |         return None
 73 | 
 74 |     @classmethod
 75 |     def _check_rebar3_available(cls) -> bool:
 76 |         """Check if rebar3 build tool is available."""
 77 |         try:
 78 |             result = subprocess.run(["rebar3", "version"], check=False, capture_output=True, text=True, timeout=10)
 79 |             return result.returncode == 0
 80 |         except (subprocess.SubprocessError, FileNotFoundError):
 81 |             return False
 82 | 
 83 |     def _start_server(self):
 84 |         """Start Erlang LS server process with proper initialization waiting."""
 85 | 
 86 |         def register_capability_handler(params):
 87 |             return
 88 | 
 89 |         def window_log_message(msg):
 90 |             """Handle window/logMessage notifications from Erlang LS"""
 91 |             message_text = msg.get("message", "")
 92 |             self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO)
 93 | 
 94 |             # Look for Erlang LS readiness signals
 95 |             # Common patterns: "Started Erlang LS", "initialized", "ready"
 96 |             readiness_signals = [
 97 |                 "Started Erlang LS",
 98 |                 "server started",
 99 |                 "initialized",
100 |                 "ready to serve requests",
101 |                 "compilation finished",
102 |                 "indexing complete",
103 |             ]
104 | 
105 |             message_lower = message_text.lower()
106 |             for signal in readiness_signals:
107 |                 if signal.lower() in message_lower:
108 |                     self.logger.log(f"Erlang LS readiness signal detected: {message_text}", logging.INFO)
109 |                     self.server_ready.set()
110 |                     break
111 | 
112 |         def do_nothing(params):
113 |             return
114 | 
115 |         def check_server_ready(params):
116 |             """Handle $/progress notifications from Erlang LS as fallback."""
117 |             value = params.get("value", {})
118 | 
119 |             # Check for initialization completion progress
120 |             if value.get("kind") == "end":
121 |                 message = value.get("message", "")
122 |                 if any(word in message.lower() for word in ["initialized", "ready", "complete"]):
123 |                     self.logger.log("Erlang LS initialization progress completed", logging.INFO)
124 |                     # Set as fallback if no window/logMessage was received
125 |                     if not self.server_ready.is_set():
126 |                         self.server_ready.set()
127 | 
128 |         # Set up notification handlers
129 |         self.server.on_request("client/registerCapability", register_capability_handler)
130 |         self.server.on_notification("window/logMessage", window_log_message)
131 |         self.server.on_notification("$/progress", check_server_ready)
132 |         self.server.on_notification("window/workDoneProgress/create", do_nothing)
133 |         self.server.on_notification("$/workDoneProgress", do_nothing)
134 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
135 | 
136 |         self.logger.log("Starting Erlang LS server process", logging.INFO)
137 |         self.server.start()
138 | 
139 |         # Send initialize request
140 |         initialize_params = {
141 |             "processId": None,
142 |             "rootPath": self.repository_root_path,
143 |             "rootUri": f"file://{self.repository_root_path}",
144 |             "capabilities": {
145 |                 "textDocument": {
146 |                     "synchronization": {"didSave": True},
147 |                     "completion": {"dynamicRegistration": True},
148 |                     "definition": {"dynamicRegistration": True},
149 |                     "references": {"dynamicRegistration": True},
150 |                     "documentSymbol": {"dynamicRegistration": True},
151 |                     "hover": {"dynamicRegistration": True},
152 |                 }
153 |             },
154 |         }
155 | 
156 |         self.logger.log("Sending initialize request to Erlang LS", logging.INFO)
157 |         init_response = self.server.send.initialize(initialize_params)
158 | 
159 |         # Verify server capabilities
160 |         if "capabilities" in init_response:
161 |             self.logger.log(f"Erlang LS capabilities: {list(init_response['capabilities'].keys())}", logging.INFO)
162 | 
163 |         self.server.notify.initialized({})
164 |         self.completions_available.set()
165 | 
166 |         # Wait for Erlang LS to be ready - adjust timeout based on environment
167 |         is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true"
168 |         is_macos = os.uname().sysname == "Darwin" if hasattr(os, "uname") else False
169 | 
170 |         # macOS in CI can be particularly slow for language server startup
171 |         if is_ci and is_macos:
172 |             ready_timeout = 240.0  # 4 minutes for macOS CI
173 |             env_desc = "macOS CI"
174 |         elif is_ci:
175 |             ready_timeout = 180.0  # 3 minutes for other CI
176 |             env_desc = "CI"
177 |         else:
178 |             ready_timeout = 60.0  # 1 minute for local
179 |             env_desc = "local"
180 | 
181 |         self.logger.log(f"Waiting up to {ready_timeout} seconds for Erlang LS readiness ({env_desc} environment)...", logging.INFO)
182 | 
183 |         if self.server_ready.wait(timeout=ready_timeout):
184 |             self.logger.log("Erlang LS is ready and available for requests", logging.INFO)
185 | 
186 |             # Add settling period for indexing - adjust based on environment
187 |             settling_time = 15.0 if is_ci else 5.0
188 |             self.logger.log(f"Allowing {settling_time} seconds for Erlang LS indexing to complete...", logging.INFO)
189 |             time.sleep(settling_time)
190 |             self.logger.log("Erlang LS settling period complete", logging.INFO)
191 |         else:
192 |             # Set ready anyway and continue - Erlang LS might not send explicit ready messages
193 |             self.logger.log(
194 |                 f"Erlang LS readiness timeout reached after {ready_timeout}s, proceeding anyway (common in CI)", logging.WARNING
195 |             )
196 |             self.server_ready.set()
197 | 
198 |             # Still give some time for basic initialization even without explicit readiness signal
199 |             basic_settling_time = 20.0 if is_ci else 10.0
200 |             self.logger.log(f"Allowing {basic_settling_time} seconds for basic Erlang LS initialization...", logging.INFO)
201 |             time.sleep(basic_settling_time)
202 |             self.logger.log("Basic Erlang LS initialization period complete", logging.INFO)
203 | 
204 |     @override
205 |     def is_ignored_dirname(self, dirname: str) -> bool:
206 |         # For Erlang projects, we should ignore:
207 |         # - _build: rebar3 build artifacts
208 |         # - deps: dependencies
209 |         # - ebin: compiled beam files
210 |         # - .rebar3: rebar3 cache
211 |         # - logs: log files
212 |         # - node_modules: if the project has JavaScript components
213 |         return super().is_ignored_dirname(dirname) or dirname in [
214 |             "_build",
215 |             "deps",
216 |             "ebin",
217 |             ".rebar3",
218 |             "logs",
219 |             "node_modules",
220 |             "_checkouts",
221 |             "cover",
222 |         ]
223 | 
224 |     def is_ignored_filename(self, filename: str) -> bool:
225 |         """Check if a filename should be ignored."""
226 |         # Ignore compiled BEAM files
227 |         if filename.endswith(".beam"):
228 |             return True
229 |         # Don't ignore Erlang source files, header files, or configuration files
230 |         return False
231 | 
```

--------------------------------------------------------------------------------
/test/serena/test_mcp.py:
--------------------------------------------------------------------------------

```python
  1 | """Tests for the mcp.py module in serena."""
  2 | 
  3 | import pytest
  4 | from mcp.server.fastmcp.tools.base import Tool as MCPTool
  5 | 
  6 | from serena.agent import Tool, ToolRegistry
  7 | from serena.config.context_mode import SerenaAgentContext
  8 | from serena.mcp import SerenaMCPFactory
  9 | 
 10 | make_tool = SerenaMCPFactory.make_mcp_tool
 11 | 
 12 | 
 13 | # Create a mock agent for tool initialization
 14 | class MockAgent:
 15 |     def __init__(self):
 16 |         self.project_config = None
 17 |         self.serena_config = None
 18 | 
 19 |     @staticmethod
 20 |     def get_context() -> SerenaAgentContext:
 21 |         return SerenaAgentContext.load_default()
 22 | 
 23 | 
 24 | class BaseMockTool(Tool):
 25 |     """A mock Tool class for testing."""
 26 | 
 27 |     def __init__(self):
 28 |         super().__init__(MockAgent())  # type: ignore
 29 | 
 30 | 
 31 | class BasicTool(BaseMockTool):
 32 |     """A mock Tool class for testing."""
 33 | 
 34 |     def apply(self, name: str, age: int = 0) -> str:
 35 |         """This is a test function.
 36 | 
 37 |         :param name: The person's name
 38 |         :param age: The person's age
 39 |         :return: A greeting message
 40 |         """
 41 |         return f"Hello {name}, you are {age} years old!"
 42 | 
 43 |     def apply_ex(
 44 |         self,
 45 |         log_call: bool = True,
 46 |         catch_exceptions: bool = True,
 47 |         **kwargs,
 48 |     ) -> str:
 49 |         """Mock implementation of apply_ex."""
 50 |         return self.apply(**kwargs)
 51 | 
 52 | 
 53 | def test_make_tool_basic() -> None:
 54 |     """Test that make_tool correctly creates an MCP tool from a Tool object."""
 55 |     mock_tool = BasicTool()
 56 | 
 57 |     mcp_tool = make_tool(mock_tool)
 58 | 
 59 |     # Test that the MCP tool has the correct properties
 60 |     assert isinstance(mcp_tool, MCPTool)
 61 |     assert mcp_tool.name == "basic"
 62 |     assert "This is a test function. Returns A greeting message." in mcp_tool.description
 63 | 
 64 |     # Test that the parameters were correctly processed
 65 |     parameters = mcp_tool.parameters
 66 |     assert "properties" in parameters
 67 |     assert "name" in parameters["properties"]
 68 |     assert "age" in parameters["properties"]
 69 |     assert parameters["properties"]["name"]["description"] == "The person's name."
 70 |     assert parameters["properties"]["age"]["description"] == "The person's age."
 71 | 
 72 | 
 73 | def test_make_tool_execution() -> None:
 74 |     """Test that the execution function created by make_tool works correctly."""
 75 |     mock_tool = BasicTool()
 76 |     mcp_tool = make_tool(mock_tool)
 77 | 
 78 |     # Execute the MCP tool function
 79 |     result = mcp_tool.fn(name="Alice", age=30)
 80 | 
 81 |     assert result == "Hello Alice, you are 30 years old!"
 82 | 
 83 | 
 84 | def test_make_tool_no_params() -> None:
 85 |     """Test make_tool with a function that has no parameters."""
 86 | 
 87 |     class NoParamsTool(BaseMockTool):
 88 |         def apply(self) -> str:
 89 |             """This is a test function with no parameters.
 90 | 
 91 |             :return: A simple result
 92 |             """
 93 |             return "Simple result"
 94 | 
 95 |         def apply_ex(self, *args, **kwargs) -> str:
 96 |             return self.apply()
 97 | 
 98 |     tool = NoParamsTool()
 99 |     mcp_tool = make_tool(tool)
100 | 
101 |     assert mcp_tool.name == "no_params"
102 |     assert "This is a test function with no parameters. Returns A simple result." in mcp_tool.description
103 |     assert mcp_tool.parameters["properties"] == {}
104 | 
105 | 
106 | def test_make_tool_no_return_description() -> None:
107 |     """Test make_tool with a function that has no return description."""
108 | 
109 |     class NoReturnTool(BaseMockTool):
110 |         def apply(self, param: str) -> str:
111 |             """This is a test function.
112 | 
113 |             :param param: The parameter
114 |             """
115 |             return f"Processed: {param}"
116 | 
117 |         def apply_ex(self, *args, **kwargs) -> str:
118 |             return self.apply(**kwargs)
119 | 
120 |     tool = NoReturnTool()
121 |     mcp_tool = make_tool(tool)
122 | 
123 |     assert mcp_tool.name == "no_return"
124 |     assert mcp_tool.description == "This is a test function."
125 |     assert mcp_tool.parameters["properties"]["param"]["description"] == "The parameter."
126 | 
127 | 
128 | def test_make_tool_parameter_not_in_docstring() -> None:
129 |     """Test make_tool when a parameter in properties is not in the docstring."""
130 | 
131 |     class MissingParamTool(BaseMockTool):
132 |         def apply(self, name: str, missing_param: str = "") -> str:
133 |             """This is a test function.
134 | 
135 |             :param name: The person's name
136 |             """
137 |             return f"Hello {name}! Missing param: {missing_param}"
138 | 
139 |         def apply_ex(self, *args, **kwargs) -> str:
140 |             return self.apply(**kwargs)
141 | 
142 |     tool = MissingParamTool()
143 |     mcp_tool = make_tool(tool)
144 | 
145 |     assert "name" in mcp_tool.parameters["properties"]
146 |     assert "missing_param" in mcp_tool.parameters["properties"]
147 |     assert mcp_tool.parameters["properties"]["name"]["description"] == "The person's name."
148 |     assert "description" not in mcp_tool.parameters["properties"]["missing_param"]
149 | 
150 | 
151 | def test_make_tool_multiline_docstring() -> None:
152 |     """Test make_tool with a complex multi-line docstring."""
153 | 
154 |     class ComplexDocTool(BaseMockTool):
155 |         def apply(self, project_file_path: str, host: str, port: int) -> str:
156 |             """Create an MCP server.
157 | 
158 |             This function creates and configures a Model Context Protocol server
159 |             with the specified settings.
160 | 
161 |             :param project_file_path: The path to the project file, or None
162 |             :param host: The host to bind to
163 |             :param port: The port to bind to
164 |             :return: A configured FastMCP server instance
165 |             """
166 |             return f"Server config: {project_file_path}, {host}:{port}"
167 | 
168 |         def apply_ex(self, *args, **kwargs) -> str:
169 |             return self.apply(**kwargs)
170 | 
171 |     tool = ComplexDocTool()
172 |     mcp_tool = make_tool(tool)
173 | 
174 |     assert "Create an MCP server" in mcp_tool.description
175 |     assert "Returns A configured FastMCP server instance" in mcp_tool.description
176 |     assert mcp_tool.parameters["properties"]["project_file_path"]["description"] == "The path to the project file, or None."
177 |     assert mcp_tool.parameters["properties"]["host"]["description"] == "The host to bind to."
178 |     assert mcp_tool.parameters["properties"]["port"]["description"] == "The port to bind to."
179 | 
180 | 
181 | def test_make_tool_capitalization_and_periods() -> None:
182 |     """Test that make_tool properly handles capitalization and periods in descriptions."""
183 | 
184 |     class FormatTool(BaseMockTool):
185 |         def apply(self, param1: str, param2: str, param3: str) -> str:
186 |             """Test function.
187 | 
188 |             :param param1: lowercase description
189 |             :param param2: description with period.
190 |             :param param3: description with Capitalized word.
191 |             """
192 |             return f"Formatted: {param1}, {param2}, {param3}"
193 | 
194 |         def apply_ex(self, *args, **kwargs) -> str:
195 |             return self.apply(**kwargs)
196 | 
197 |     tool = FormatTool()
198 |     mcp_tool = make_tool(tool)
199 | 
200 |     assert mcp_tool.parameters["properties"]["param1"]["description"] == "Lowercase description."
201 |     assert mcp_tool.parameters["properties"]["param2"]["description"] == "Description with period."
202 |     assert mcp_tool.parameters["properties"]["param3"]["description"] == "Description with Capitalized word."
203 | 
204 | 
205 | def test_make_tool_missing_apply() -> None:
206 |     """Test make_tool with a tool that doesn't have an apply method."""
207 | 
208 |     class BadTool(BaseMockTool):
209 |         pass
210 | 
211 |     tool = BadTool()
212 | 
213 |     with pytest.raises(AttributeError):
214 |         make_tool(tool)
215 | 
216 | 
217 | @pytest.mark.parametrize(
218 |     "docstring, expected_description",
219 |     [
220 |         (
221 |             """This is a test function.
222 | 
223 |             :param param: The parameter
224 |             :return: A result
225 |             """,
226 |             "This is a test function. Returns A result.",
227 |         ),
228 |         (
229 |             """
230 |             :param param: The parameter
231 |             :return: A result
232 |             """,
233 |             "Returns A result.",
234 |         ),
235 |         (
236 |             """
237 |             :param param: The parameter
238 |             """,
239 |             "",
240 |         ),
241 |         ("Description without params.", "Description without params."),
242 |     ],
243 | )
244 | def test_make_tool_descriptions(docstring, expected_description) -> None:
245 |     """Test make_tool with various docstring formats."""
246 | 
247 |     class TestTool(BaseMockTool):
248 |         def apply(self, param: str) -> str:
249 |             return f"Result: {param}"
250 | 
251 |         def apply_ex(self, *args, **kwargs) -> str:
252 |             return self.apply(**kwargs)
253 | 
254 |     # Dynamically set the docstring
255 |     TestTool.apply.__doc__ = docstring
256 | 
257 |     tool = TestTool()
258 |     mcp_tool = make_tool(tool)
259 | 
260 |     assert mcp_tool.name == "test"
261 |     assert mcp_tool.description == expected_description
262 | 
263 | 
264 | def is_test_mock_class(tool_class: type) -> bool:
265 |     """Check if a class is a test mock class."""
266 |     # Check if the class is defined in a test module
267 |     module_name = tool_class.__module__
268 |     return (
269 |         module_name.startswith(("test.", "tests."))
270 |         or "test_" in module_name
271 |         or tool_class.__name__
272 |         in [
273 |             "BaseMockTool",
274 |             "BasicTool",
275 |             "BadTool",
276 |             "NoParamsTool",
277 |             "NoReturnTool",
278 |             "MissingParamTool",
279 |             "ComplexDocTool",
280 |             "FormatTool",
281 |             "NoDescriptionTool",
282 |         ]
283 |     )
284 | 
285 | 
286 | @pytest.mark.parametrize("tool_class", ToolRegistry().get_all_tool_classes())
287 | def test_make_tool_all_tools(tool_class) -> None:
288 |     """Test that make_tool works for all tools in the codebase."""
289 |     # Create an instance of the tool
290 |     tool_instance = tool_class(MockAgent())
291 | 
292 |     # Try to create an MCP tool from it
293 |     mcp_tool = make_tool(tool_instance)
294 | 
295 |     # Basic validation
296 |     assert isinstance(mcp_tool, MCPTool)
297 |     assert mcp_tool.name == tool_class.get_name_from_cls()
298 | 
299 |     # The description should be a string (either from docstring or default)
300 |     assert isinstance(mcp_tool.description, str)
301 | 
```
Page 4/14FirstPrevNextLast