#
tokens: 45955/50000 6/413 files (page 12/17)
lines: off (toggle) GitHub
raw markdown copy
This is page 12 of 17. Use http://codebase.md/oraios/serena?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
│       ├── docs.yaml
│       ├── junie.yml
│       ├── publish.yml
│       └── pytest.yml
├── .gitignore
├── .serena
│   ├── .gitignore
│   ├── 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
│   ├── _config.yml
│   ├── _static
│   │   └── images
│   │       └── jetbrains-marketplace-button.png
│   ├── .gitignore
│   ├── 01-about
│   │   ├── 000_intro.md
│   │   ├── 010_llm-integration.md
│   │   ├── 020_programming-languages.md
│   │   ├── 030_serena-in-action.md
│   │   ├── 035_tools.md
│   │   ├── 040_comparison-to-other-agents.md
│   │   └── 050_acknowledgements.md
│   ├── 02-usage
│   │   ├── 000_intro.md
│   │   ├── 010_prerequisites.md
│   │   ├── 020_running.md
│   │   ├── 025_jetbrains_plugin.md
│   │   ├── 030_clients.md
│   │   ├── 040_workflow.md
│   │   ├── 050_configuration.md
│   │   ├── 060_dashboard.md
│   │   ├── 070_security.md
│   │   └── 999_additional-usage.md
│   ├── 03-special-guides
│   │   ├── 000_intro.md
│   │   ├── custom_agent.md
│   │   ├── groovy_setup_guide_for_serena.md
│   │   ├── scala_setup_guide_for_serena.md
│   │   └── serena_on_chatgpt.md
│   ├── autogen_rst.py
│   ├── create_toc.py
│   └── index.md
├── flake.lock
├── flake.nix
├── lessons_learned.md
├── LICENSE
├── llms-install.md
├── pyproject.toml
├── README.md
├── repo_dir_sync.py
├── resources
│   ├── jetbrains-marketplace-button.cdr
│   ├── 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
│   └── profile_tool_call.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
│   │   ├── ls_manager.py
│   │   ├── mcp.py
│   │   ├── project.py
│   │   ├── prompt_factory.py
│   │   ├── resources
│   │   │   ├── config
│   │   │   │   ├── contexts
│   │   │   │   │   ├── agent.yml
│   │   │   │   │   ├── chatgpt.yml
│   │   │   │   │   ├── claude-code.yml
│   │   │   │   │   ├── codex.yml
│   │   │   │   │   ├── context.template.yml
│   │   │   │   │   ├── desktop-app.yml
│   │   │   │   │   ├── ide.yml
│   │   │   │   │   └── oaicompat-agent.yml
│   │   │   │   ├── internal_modes
│   │   │   │   │   └── jetbrains.yml
│   │   │   │   ├── modes
│   │   │   │   │   ├── editing.yml
│   │   │   │   │   ├── interactive.yml
│   │   │   │   │   ├── mode.template.yml
│   │   │   │   │   ├── no-memories.yml
│   │   │   │   │   ├── no-onboarding.yml
│   │   │   │   │   ├── onboarding.yml
│   │   │   │   │   ├── one-shot.yml
│   │   │   │   │   └── planning.yml
│   │   │   │   └── prompt_templates
│   │   │   │       ├── simple_tool_outputs.yml
│   │   │   │       └── system_prompt.yml
│   │   │   ├── dashboard
│   │   │   │   ├── dashboard.css
│   │   │   │   ├── dashboard.js
│   │   │   │   ├── index.html
│   │   │   │   ├── jquery.min.js
│   │   │   │   ├── news
│   │   │   │   │   └── 20260111.html
│   │   │   │   ├── serena-icon-16.png
│   │   │   │   ├── serena-icon-32.png
│   │   │   │   ├── serena-icon-48.png
│   │   │   │   ├── serena-logo-dark-mode.svg
│   │   │   │   ├── serena-logo.svg
│   │   │   │   ├── serena-logs-dark-mode.png
│   │   │   │   └── serena-logs.png
│   │   │   ├── project.template.yml
│   │   │   └── serena_config.template.yml
│   │   ├── symbol.py
│   │   ├── task_executor.py
│   │   ├── text_utils.py
│   │   ├── tools
│   │   │   ├── __init__.py
│   │   │   ├── cmd_tools.py
│   │   │   ├── config_tools.py
│   │   │   ├── file_tools.py
│   │   │   ├── jetbrains_plugin_client.py
│   │   │   ├── jetbrains_tools.py
│   │   │   ├── jetbrains_types.py
│   │   │   ├── memory_tools.py
│   │   │   ├── symbol_tools.py
│   │   │   ├── tools_base.py
│   │   │   └── workflow_tools.py
│   │   └── util
│   │       ├── class_decorators.py
│   │       ├── cli_util.py
│   │       ├── exception.py
│   │       ├── file_system.py
│   │       ├── general.py
│   │       ├── git.py
│   │       ├── gui.py
│   │       ├── inspection.py
│   │       ├── logging.py
│   │       ├── shell.py
│   │       ├── thread.py
│   │       └── version.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
│       │   ├── fortran_language_server.py
│       │   ├── fsharp_language_server.py
│       │   ├── gopls.py
│       │   ├── groovy_language_server.py
│       │   ├── haskell_language_server.py
│       │   ├── intelephense.py
│       │   ├── jedi_server.py
│       │   ├── julia_server.py
│       │   ├── kotlin_language_server.py
│       │   ├── lua_ls.py
│       │   ├── marksman.py
│       │   ├── matlab_language_server.py
│       │   ├── nixd_ls.py
│       │   ├── omnisharp
│       │   │   ├── initialize_params.json
│       │   │   ├── runtime_dependencies.json
│       │   │   └── workspace_did_change_configuration.json
│       │   ├── omnisharp.py
│       │   ├── pascal_server.py
│       │   ├── perl_language_server.py
│       │   ├── powershell_language_server.py
│       │   ├── pyright_server.py
│       │   ├── r_language_server.py
│       │   ├── regal_server.py
│       │   ├── ruby_lsp.py
│       │   ├── rust_analyzer.py
│       │   ├── scala_language_server.py
│       │   ├── solargraph.py
│       │   ├── sourcekit_lsp.py
│       │   ├── taplo_server.py
│       │   ├── terraform_ls.py
│       │   ├── typescript_language_server.py
│       │   ├── vts_language_server.py
│       │   ├── vue_language_server.py
│       │   ├── yaml_language_server.py
│       │   └── zls.py
│       ├── ls_config.py
│       ├── ls_exceptions.py
│       ├── ls_handler.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
│           ├── cache.py
│           ├── subprocess_util.py
│           └── zip.py
├── sync.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
│   │       ├── fortran
│   │       │   └── test_repo
│   │       │       ├── main.f90
│   │       │       └── modules
│   │       │           ├── geometry.f90
│   │       │           └── math_utils.f90
│   │       ├── fsharp
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── Calculator.fs
│   │       │       ├── Models
│   │       │       │   └── Person.fs
│   │       │       ├── Program.fs
│   │       │       ├── README.md
│   │       │       └── TestProject.fsproj
│   │       ├── go
│   │       │   └── test_repo
│   │       │       └── main.go
│   │       ├── groovy
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── build.gradle
│   │       │       └── src
│   │       │           └── main
│   │       │               └── groovy
│   │       │                   └── com
│   │       │                       └── example
│   │       │                           ├── Main.groovy
│   │       │                           ├── Model.groovy
│   │       │                           ├── ModelUser.groovy
│   │       │                           └── Utils.groovy
│   │       ├── haskell
│   │       │   └── test_repo
│   │       │       ├── app
│   │       │       │   └── Main.hs
│   │       │       ├── haskell-test-repo.cabal
│   │       │       ├── package.yaml
│   │       │       ├── src
│   │       │       │   ├── Calculator.hs
│   │       │       │   └── Helper.hs
│   │       │       └── stack.yaml
│   │       ├── java
│   │       │   └── test_repo
│   │       │       ├── pom.xml
│   │       │       └── src
│   │       │           └── main
│   │       │               └── java
│   │       │                   └── test_repo
│   │       │                       ├── Main.java
│   │       │                       ├── Model.java
│   │       │                       ├── ModelUser.java
│   │       │                       └── Utils.java
│   │       ├── julia
│   │       │   └── test_repo
│   │       │       ├── lib
│   │       │       │   └── helper.jl
│   │       │       └── main.jl
│   │       ├── 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
│   │       ├── matlab
│   │       │   └── test_repo
│   │       │       ├── Calculator.m
│   │       │       └── main.m
│   │       ├── nix
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── default.nix
│   │       │       ├── flake.nix
│   │       │       ├── lib
│   │       │       │   └── utils.nix
│   │       │       ├── modules
│   │       │       │   └── example.nix
│   │       │       └── scripts
│   │       │           └── hello.sh
│   │       ├── pascal
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   └── helper.pas
│   │       │       └── main.pas
│   │       ├── perl
│   │       │   └── test_repo
│   │       │       ├── helper.pl
│   │       │       └── main.pl
│   │       ├── php
│   │       │   └── test_repo
│   │       │       ├── helper.php
│   │       │       ├── index.php
│   │       │       └── simple_var.php
│   │       ├── powershell
│   │       │   └── test_repo
│   │       │       ├── main.ps1
│   │       │       ├── PowerShellEditorServices.json
│   │       │       └── utils.ps1
│   │       ├── 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
│   │       ├── rego
│   │       │   └── test_repo
│   │       │       ├── policies
│   │       │       │   ├── authz.rego
│   │       │       │   └── validation.rego
│   │       │       └── utils
│   │       │           └── helpers.rego
│   │       ├── 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
│   │       ├── scala
│   │       │   ├── build.sbt
│   │       │   ├── project
│   │       │   │   ├── build.properties
│   │       │   │   ├── metals.sbt
│   │       │   │   └── plugins.sbt
│   │       │   └── src
│   │       │       └── main
│   │       │           └── scala
│   │       │               └── com
│   │       │                   └── example
│   │       │                       ├── Main.scala
│   │       │                       └── Utils.scala
│   │       ├── swift
│   │       │   └── test_repo
│   │       │       ├── Package.swift
│   │       │       └── src
│   │       │           ├── main.swift
│   │       │           └── utils.swift
│   │       ├── terraform
│   │       │   └── test_repo
│   │       │       ├── data.tf
│   │       │       ├── main.tf
│   │       │       ├── outputs.tf
│   │       │       └── variables.tf
│   │       ├── toml
│   │       │   └── test_repo
│   │       │       ├── Cargo.toml
│   │       │       ├── config.toml
│   │       │       └── pyproject.toml
│   │       ├── typescript
│   │       │   └── test_repo
│   │       │       ├── .serena
│   │       │       │   └── project.yml
│   │       │       ├── index.ts
│   │       │       ├── tsconfig.json
│   │       │       ├── use_helper.ts
│   │       │       └── ws_manager.js
│   │       ├── vue
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── index.html
│   │       │       ├── package.json
│   │       │       ├── src
│   │       │       │   ├── App.vue
│   │       │       │   ├── components
│   │       │       │   │   ├── CalculatorButton.vue
│   │       │       │   │   ├── CalculatorDisplay.vue
│   │       │       │   │   └── CalculatorInput.vue
│   │       │       │   ├── composables
│   │       │       │   │   ├── useFormatter.ts
│   │       │       │   │   └── useTheme.ts
│   │       │       │   ├── main.ts
│   │       │       │   ├── stores
│   │       │       │   │   └── calculator.ts
│   │       │       │   └── types
│   │       │       │       └── index.ts
│   │       │       ├── tsconfig.json
│   │       │       ├── tsconfig.node.json
│   │       │       └── vite.config.ts
│   │       ├── yaml
│   │       │   └── test_repo
│   │       │       ├── config.yaml
│   │       │       ├── data.yaml
│   │       │       └── services.yml
│   │       └── 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_cli_project_commands.py
│   │   ├── test_edit_marker.py
│   │   ├── test_mcp.py
│   │   ├── test_serena_agent.py
│   │   ├── test_symbol_editing.py
│   │   ├── test_symbol.py
│   │   ├── test_task_executor.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
│       ├── fortran
│       │   ├── __init__.py
│       │   └── test_fortran_basic.py
│       ├── fsharp
│       │   └── test_fsharp_basic.py
│       ├── go
│       │   └── test_go_basic.py
│       ├── groovy
│       │   └── test_groovy_basic.py
│       ├── haskell
│       │   ├── __init__.py
│       │   └── test_haskell_basic.py
│       ├── java
│       │   └── test_java_basic.py
│       ├── julia
│       │   └── test_julia_basic.py
│       ├── kotlin
│       │   └── test_kotlin_basic.py
│       ├── lua
│       │   └── test_lua_basic.py
│       ├── markdown
│       │   ├── __init__.py
│       │   └── test_markdown_basic.py
│       ├── matlab
│       │   ├── __init__.py
│       │   └── test_matlab_basic.py
│       ├── nix
│       │   └── test_nix_basic.py
│       ├── pascal
│       │   ├── __init__.py
│       │   └── test_pascal_basic.py
│       ├── perl
│       │   └── test_perl_basic.py
│       ├── php
│       │   └── test_php_basic.py
│       ├── powershell
│       │   ├── __init__.py
│       │   └── test_powershell_basic.py
│       ├── python
│       │   ├── test_python_basic.py
│       │   ├── test_retrieval_with_ignored_dirs.py
│       │   └── test_symbol_retrieval.py
│       ├── r
│       │   ├── __init__.py
│       │   └── test_r_basic.py
│       ├── rego
│       │   └── test_rego_basic.py
│       ├── ruby
│       │   ├── test_ruby_basic.py
│       │   └── test_ruby_symbol_retrieval.py
│       ├── rust
│       │   ├── test_rust_2024_edition.py
│       │   ├── test_rust_analyzer_detection.py
│       │   └── test_rust_basic.py
│       ├── scala
│       │   └── test_scala_language_server.py
│       ├── swift
│       │   └── test_swift_basic.py
│       ├── terraform
│       │   └── test_terraform_basic.py
│       ├── test_lsp_protocol_handler_server.py
│       ├── toml
│       │   ├── __init__.py
│       │   ├── test_toml_basic.py
│       │   ├── test_toml_edge_cases.py
│       │   ├── test_toml_ignored_dirs.py
│       │   └── test_toml_symbol_retrieval.py
│       ├── typescript
│       │   └── test_typescript_basic.py
│       ├── util
│       │   └── test_zip.py
│       ├── vue
│       │   ├── __init__.py
│       │   ├── test_vue_basic.py
│       │   ├── test_vue_error_cases.py
│       │   ├── test_vue_rename.py
│       │   └── test_vue_symbol_retrieval.py
│       ├── yaml_ls
│       │   ├── __init__.py
│       │   └── test_yaml_basic.py
│       └── zig
│           └── test_zig_basic.py
└── uv.lock
```

# Files

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

```python
"""
Tests for the language server symbol-related functionality.

These tests focus on the following methods:
- request_containing_symbol
- request_referencing_symbols
"""

import os

import pytest

from serena.symbol import LanguageServerSymbol
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind

pytestmark = pytest.mark.python


class TestLanguageServerSymbols:
    """Test the language server's symbol-related functionality."""

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a function."""
        # Test for a position inside the create_user method
        file_path = os.path.join("test_repo", "services.py")
        # Line 17 is inside the create_user method body
        containing_symbol = language_server.request_containing_symbol(file_path, 17, 20, include_body=True)

        # Verify that we found the containing symbol
        assert containing_symbol is not None
        assert containing_symbol["name"] == "create_user"
        assert containing_symbol["kind"] == SymbolKind.Method
        if "body" in containing_symbol:
            assert containing_symbol["body"].strip().startswith("def create_user(self")

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_references_to_variables(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a variable."""
        file_path = os.path.join("test_repo", "variables.py")
        # Line 75 contains the field status that is later modified
        ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 74, 4)]

        assert len(ref_symbols) > 0
        ref_lines = [ref["location"]["range"]["start"]["line"] for ref in ref_symbols if "location" in ref and "range" in ref["location"]]
        ref_names = [ref["name"] for ref in ref_symbols]
        assert 87 in ref_lines
        assert 95 in ref_lines
        assert "dataclass_instance" in ref_names
        assert "second_dataclass" in ref_names

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a class."""
        # Test for a position inside the UserService class but outside any method
        file_path = os.path.join("test_repo", "services.py")
        # Line 9 is the class definition line for UserService
        containing_symbol = language_server.request_containing_symbol(file_path, 9, 7)

        # Verify that we found the containing symbol
        assert containing_symbol is not None
        assert containing_symbol["name"] == "UserService"
        assert containing_symbol["kind"] == SymbolKind.Class

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol with nested scopes."""
        # Test for a position inside a method which is inside a class
        file_path = os.path.join("test_repo", "services.py")
        # Line 18 is inside the create_user method inside UserService class
        containing_symbol = language_server.request_containing_symbol(file_path, 18, 25)

        # Verify that we found the innermost containing symbol (the method)
        assert containing_symbol is not None
        assert containing_symbol["name"] == "create_user"
        assert containing_symbol["kind"] == SymbolKind.Method

        # Get the parent containing symbol
        if "location" in containing_symbol and "range" in containing_symbol["location"]:
            parent_symbol = language_server.request_containing_symbol(
                file_path,
                containing_symbol["location"]["range"]["start"]["line"],
                containing_symbol["location"]["range"]["start"]["character"] - 1,
            )

            # Verify that the parent is the class
            assert parent_symbol is not None
            assert parent_symbol["name"] == "UserService"
            assert parent_symbol["kind"] == SymbolKind.Class

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a position with no containing symbol."""
        # Test for a position outside any function/class (e.g., in imports)
        file_path = os.path.join("test_repo", "services.py")
        # Line 1 is in imports, not inside any function or class
        containing_symbol = language_server.request_containing_symbol(file_path, 1, 10)

        # Should return None or an empty dictionary
        assert containing_symbol is None or containing_symbol == {}

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a function."""
        # Test referencing symbols for create_user function
        file_path = os.path.join("test_repo", "services.py")
        # Line 15 contains the create_user function definition
        symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
        create_user_symbol = next((s for s in symbols[0] if s.get("name") == "create_user"), None)
        if not create_user_symbol or "selectionRange" not in create_user_symbol:
            raise AssertionError("create_user symbol or its selectionRange not found")
        sel_start = create_user_symbol["selectionRange"]["start"]
        ref_symbols = [
            ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
        ]
        assert len(ref_symbols) > 0, "No referencing symbols found for create_user (selectionRange)"

        # Verify the structure of referencing symbols
        for symbol in ref_symbols:
            assert "name" in symbol
            assert "kind" in symbol
            if "location" in symbol and "range" in symbol["location"]:
                assert "start" in symbol["location"]["range"]
                assert "end" in symbol["location"]["range"]

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a class."""
        # Test referencing symbols for User class
        file_path = os.path.join("test_repo", "models.py")
        # Line 31 contains the User class definition
        symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
        user_symbol = next((s for s in symbols[0] if s.get("name") == "User"), None)
        if not user_symbol or "selectionRange" not in user_symbol:
            raise AssertionError("User symbol or its selectionRange not found")
        sel_start = user_symbol["selectionRange"]["start"]
        ref_symbols = [
            ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
        ]
        services_references = [
            symbol
            for symbol in ref_symbols
            if "location" in symbol and "uri" in symbol["location"] and "services.py" in symbol["location"]["uri"]
        ]
        assert len(services_references) > 0, "No referencing symbols from services.py for User (selectionRange)"

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a function parameter."""
        # Test referencing symbols for id parameter in get_user
        file_path = os.path.join("test_repo", "services.py")
        # Line 24 contains the get_user method with id parameter
        symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
        get_user_symbol = next((s for s in symbols[0] if s.get("name") == "get_user"), None)
        if not get_user_symbol or "selectionRange" not in get_user_symbol:
            raise AssertionError("get_user symbol or its selectionRange not found")
        sel_start = get_user_symbol["selectionRange"]["start"]
        ref_symbols = [
            ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
        ]
        method_refs = [
            symbol
            for symbol in ref_symbols
            if "location" in symbol and "range" in symbol["location"] and symbol["location"]["range"]["start"]["line"] > sel_start["line"]
        ]
        assert len(method_refs) > 0, "No referencing symbols within method body for get_user (selectionRange)"

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a position with no symbol."""
        # For positions with no symbol, the method might throw an error or return None/empty list
        # We'll modify our test to handle this by using a try-except block

        file_path = os.path.join("test_repo", "services.py")
        # Line 3 is a blank line or comment
        try:
            ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 3, 0)]
            # If we get here, make sure we got an empty result
            assert ref_symbols == [] or ref_symbols is None
        except Exception:
            # The method might raise an exception for invalid positions
            # which is acceptable behavior
            pass

    # Tests for request_defining_symbol
    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a variable usage."""
        # Test finding the definition of a symbol in the create_user method
        file_path = os.path.join("test_repo", "services.py")
        # Line 21 contains self.users[id] = user
        defining_symbol = language_server.request_defining_symbol(file_path, 21, 10)

        # Verify that we found the defining symbol
        # The defining symbol method returns a dictionary with information about the defining symbol
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "create_user"

        # Verify the location and kind of the symbol
        # SymbolKind.Method = 6 for a method
        assert defining_symbol.get("kind") == SymbolKind.Method.value
        if "location" in defining_symbol and "uri" in defining_symbol["location"]:
            assert "services.py" in defining_symbol["location"]["uri"]

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for an imported class."""
        # Test finding the definition of the 'User' class used in the UserService.create_user method
        file_path = os.path.join("test_repo", "services.py")
        # Line 20 references 'User' which was imported from models
        defining_symbol = language_server.request_defining_symbol(file_path, 20, 15)

        # Verify that we found the defining symbol - this should be the User class from models
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "User"

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a method call."""
        # Create an example file path for a file that calls UserService.create_user
        examples_file_path = os.path.join("examples", "user_management.py")

        # Find the line number where create_user is called
        # This could vary, so we'll use a relative position that makes sense
        defining_symbol = language_server.request_defining_symbol(examples_file_path, 10, 30)

        # Verify that we found the defining symbol - should be the create_user method
        # Because this might fail if the structure isn't exactly as expected, we'll use try-except
        try:
            assert defining_symbol is not None
            assert defining_symbol.get("name") == "create_user"
            # The defining symbol should be in the services.py file
            if "location" in defining_symbol and "uri" in defining_symbol["location"]:
                assert "services.py" in defining_symbol["location"]["uri"]
        except AssertionError:
            # If the file structure doesn't match what we expect, we can't guarantee this test
            # will pass, so we'll consider it a warning rather than a failure
            import warnings

            warnings.warn("Could not verify method call definition - file structure may differ from expected")

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a position with no symbol."""
        # Test for a position with no symbol (e.g., whitespace or comment)
        file_path = os.path.join("test_repo", "services.py")
        # Line 3 is a blank line
        defining_symbol = language_server.request_defining_symbol(file_path, 3, 0)

        # Should return None for positions with no symbol
        assert defining_symbol is None

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol where the symbol is a variable."""
        # Test for a position inside a variable definition
        file_path = os.path.join("test_repo", "services.py")
        # Line 74 defines the 'user' variable
        containing_symbol = language_server.request_containing_symbol(file_path, 73, 1)

        # Verify that we found the containing symbol
        assert containing_symbol is not None
        assert containing_symbol["name"] == "user_var_str"
        assert containing_symbol["kind"] == SymbolKind.Variable

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a nested function or closure."""
        # Use the existing nested.py file which contains nested classes and methods
        file_path = os.path.join("test_repo", "nested.py")

        # Test 1: Find definition of nested method - line with 'b = OuterClass().NestedClass().find_me()'
        defining_symbol = language_server.request_defining_symbol(file_path, 15, 35)  # Position of find_me() call

        # This should resolve to the find_me method in the NestedClass
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "find_me"
        assert defining_symbol.get("kind") == SymbolKind.Method.value

        # Test 2: Find definition of the nested class
        defining_symbol = language_server.request_defining_symbol(file_path, 15, 18)  # Position of NestedClass

        # This should resolve to the NestedClass
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "NestedClass"
        assert defining_symbol.get("kind") == SymbolKind.Class.value

        # Test 3: Find definition of a method-local function
        defining_symbol = language_server.request_defining_symbol(file_path, 9, 15)  # Position inside func_within_func

        # This is challenging for many language servers and may fail
        try:
            assert defining_symbol is not None
            assert defining_symbol.get("name") == "func_within_func"
        except (AssertionError, TypeError, KeyError):
            # This is expected to potentially fail in many implementations
            import warnings

            warnings.warn("Could not resolve nested class method definition - implementation limitation")

        # Test 2: Find definition of the nested class
        defining_symbol = language_server.request_defining_symbol(file_path, 15, 18)  # Position of NestedClass

        # This should resolve to the NestedClass
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "NestedClass"
        assert defining_symbol.get("kind") == SymbolKind.Class.value

        # Test 3: Find definition of a method-local function
        defining_symbol = language_server.request_defining_symbol(file_path, 9, 15)  # Position inside func_within_func

        # This is challenging for many language servers and may fail
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "func_within_func"
        assert defining_symbol.get("kind") == SymbolKind.Function.value

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
        """Test the integration between different symbol-related methods."""
        # This test demonstrates using the various symbol methods together
        # by finding a symbol and then checking its definition

        file_path = os.path.join("test_repo", "services.py")

        # First approach: Use a method from the UserService class
        # Step 1: Find a method we know exists
        containing_symbol = language_server.request_containing_symbol(file_path, 15, 8)  # create_user method
        assert containing_symbol is not None
        assert containing_symbol["name"] == "create_user"

        # Step 2: Get the defining symbol for the same position
        # This should be the same method
        defining_symbol = language_server.request_defining_symbol(file_path, 15, 8)
        assert defining_symbol is not None
        assert defining_symbol["name"] == "create_user"

        # Step 3: Verify that they refer to the same symbol
        assert defining_symbol["kind"] == containing_symbol["kind"]
        if "location" in defining_symbol and "location" in containing_symbol:
            assert defining_symbol["location"]["uri"] == containing_symbol["location"]["uri"]

        # The integration test is successful if we've gotten this far,
        # as it demonstrates the integration between request_containing_symbol and request_defining_symbol

        # Try to get the container information for our method, but be flexible
        # since implementations may vary
        container_name = defining_symbol.get("containerName", None)
        if container_name and "UserService" in container_name:
            # If containerName contains UserService, that's a valid implementation
            pass
        else:
            # Try an alternative approach - looking for the containing class
            try:
                # Look for the class symbol in the file
                for line in range(5, 12):  # Approximate range where UserService class should be defined
                    symbol = language_server.request_containing_symbol(file_path, line, 5)  # column 5 should be within class definition
                    if symbol and symbol.get("name") == "UserService" and symbol.get("kind") == SymbolKind.Class.value:
                        # Found the class - this is also a valid implementation
                        break
            except Exception:
                # Just log a warning - this is an alternative verification and not essential
                import warnings

                warnings.warn("Could not verify container hierarchy - implementation detail")

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None:
        """Test that the symbol tree structure is correctly built."""
        # Get all symbols in the test file
        repo_structure = language_server.request_full_symbol_tree()
        assert len(repo_structure) == 1
        # Assert that the root symbol is the test_repo directory
        assert repo_structure[0]["name"] == "test_repo"
        assert repo_structure[0]["kind"] == SymbolKind.Package
        assert "children" in repo_structure[0]
        # Assert that the children are the top-level packages
        child_names = {child["name"] for child in repo_structure[0]["children"]}
        child_kinds = {child["kind"] for child in repo_structure[0]["children"]}
        assert child_names == {"test_repo", "custom_test", "examples", "scripts"}
        assert child_kinds == {SymbolKind.Package}
        examples_package = next(child for child in repo_structure[0]["children"] if child["name"] == "examples")
        # assert that children are __init__ and user_management
        assert {child["name"] for child in examples_package["children"]} == {"__init__", "user_management"}
        assert {child["kind"] for child in examples_package["children"]} == {SymbolKind.File}

        # assert that tree of user_management node is same as retrieved directly
        user_management_node = next(child for child in examples_package["children"] if child["name"] == "user_management")
        if "location" in user_management_node and "relativePath" in user_management_node["location"]:
            user_management_rel_path = user_management_node["location"]["relativePath"]
            assert user_management_rel_path == os.path.join("examples", "user_management.py")
            _, user_management_roots = language_server.request_document_symbols(
                os.path.join("examples", "user_management.py")
            ).get_all_symbols_and_roots()
            assert user_management_roots == user_management_node["children"]

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None:
        """Test that the symbol tree structure is correctly built."""
        # Get all symbols in the test file
        examples_package_roots = language_server.request_full_symbol_tree(within_relative_path="examples")
        assert len(examples_package_roots) == 1
        examples_package = examples_package_roots[0]
        assert examples_package["name"] == "examples"
        assert examples_package["kind"] == SymbolKind.Package
        # assert that children are __init__ and user_management
        assert {child["name"] for child in examples_package["children"]} == {"__init__", "user_management"}
        assert {child["kind"] for child in examples_package["children"]} == {SymbolKind.File}

        # assert that tree of user_management node is same as retrieved directly
        user_management_node = next(child for child in examples_package["children"] if child["name"] == "user_management")
        if "location" in user_management_node and "relativePath" in user_management_node["location"]:
            user_management_rel_path = user_management_node["location"]["relativePath"]
            assert user_management_rel_path == os.path.join("examples", "user_management.py")
            _, user_management_roots = language_server.request_document_symbols(
                os.path.join("examples", "user_management.py")
            ).get_all_symbols_and_roots()
            assert user_management_roots == user_management_node["children"]

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
        """Test that request_dir_overview returns correct symbol information for files in a directory."""
        # Get overview of the examples directory
        overview = language_server.request_dir_overview("test_repo")

        # Verify that we have entries for both files
        assert os.path.join("test_repo", "nested.py") in overview

        # Get the symbols for user_management.py
        services_symbols = overview[os.path.join("test_repo", "services.py")]
        assert len(services_symbols) > 0

        # Check for specific symbols from services.py
        expected_symbols = {
            "UserService",
            "ItemService",
            "create_service_container",
            "user_var_str",
            "user_service",
        }
        retrieved_symbols = {symbol["name"] for symbol in services_symbols if "name" in symbol}
        assert expected_symbols.issubset(retrieved_symbols)

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_request_document_overview(self, language_server: SolidLanguageServer) -> None:
        """Test that request_document_overview returns correct symbol information for a file."""
        # Get overview of the user_management.py file
        overview = language_server.request_document_overview(os.path.join("examples", "user_management.py"))

        # Verify that we have entries for both files
        symbol_names = {LanguageServerSymbol(s_info).name for s_info in overview}
        assert {"UserStats", "UserManager", "process_user_data", "main"}.issubset(symbol_names)

    @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
    def test_containing_symbol_of_var_is_file(self, language_server: SolidLanguageServer) -> None:
        """Test that the containing symbol of a variable is the file itself."""
        # Get the containing symbol of a variable in a file
        file_path = os.path.join("test_repo", "services.py")
        # import of typing
        references_to_typing = [
            ref.symbol
            for ref in language_server.request_referencing_symbols(file_path, 4, 6, include_imports=False, include_file_symbols=True)
        ]
        assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File}
        assert {ref["body"] for ref in references_to_typing} == {""}

        # now include bodies
        references_to_typing = [
            ref.symbol
            for ref in language_server.request_referencing_symbols(
                file_path, 4, 6, include_imports=False, include_file_symbols=True, include_body=True
            )
        ]
        assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File}
        assert references_to_typing[0]["body"]

```

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

```python
# Code generated. DO NOT EDIT.
# LSP v3.17.0
# TODO: Look into use of https://pypi.org/project/ts2python/ to generate the types for https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/

"""
This file provides the python interface corresponding to the requests and notifications defined in Typescript in the language server protocol.
This file is obtained from https://github.com/predragnikolic/OLSP under the MIT License with the following terms:

MIT License

Copyright (c) 2023 Предраг Николић

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

from typing import Any, Union

from solidlsp.lsp_protocol_handler import lsp_types


class LspRequest:
    def __init__(self, send_request: Any) -> None:
        self.send_request = send_request

    async def implementation(
        self, params: lsp_types.ImplementationParams
    ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
        """A request to resolve the implementation locations of a symbol at a given text
        document position. The request's parameter is of type [TextDocumentPositionParams]
        (#TextDocumentPositionParams) the response is of type {@link Definition} or a
        Thenable that resolves to such.
        """
        return await self.send_request("textDocument/implementation", params)

    async def type_definition(
        self, params: lsp_types.TypeDefinitionParams
    ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
        """A request to resolve the type definition locations of a symbol at a given text
        document position. The request's parameter is of type [TextDocumentPositionParams]
        (#TextDocumentPositionParams) the response is of type {@link Definition} or a
        Thenable that resolves to such.
        """
        return await self.send_request("textDocument/typeDefinition", params)

    async def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]:
        """A request to list all color symbols found in a given text document. The request's
        parameter is of type {@link DocumentColorParams} the
        response is of type {@link ColorInformation ColorInformation[]} or a Thenable
        that resolves to such.
        """
        return await self.send_request("textDocument/documentColor", params)

    async def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]:
        """A request to list all presentation for a color. The request's
        parameter is of type {@link ColorPresentationParams} the
        response is of type {@link ColorInformation ColorInformation[]} or a Thenable
        that resolves to such.
        """
        return await self.send_request("textDocument/colorPresentation", params)

    async def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None:
        """A request to provide folding ranges in a document. The request's
        parameter is of type {@link FoldingRangeParams}, the
        response is of type {@link FoldingRangeList} or a Thenable
        that resolves to such.
        """
        return await self.send_request("textDocument/foldingRange", params)

    async def declaration(
        self, params: lsp_types.DeclarationParams
    ) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]:
        """A request to resolve the type definition locations of a symbol at a given text
        document position. The request's parameter is of type [TextDocumentPositionParams]
        (#TextDocumentPositionParams) the response is of type {@link Declaration}
        or a typed array of {@link DeclarationLink} or a Thenable that resolves
        to such.
        """
        return await self.send_request("textDocument/declaration", params)

    async def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None:
        """A request to provide selection ranges in a document. The request's
        parameter is of type {@link SelectionRangeParams}, the
        response is of type {@link SelectionRange SelectionRange[]} or a Thenable
        that resolves to such.
        """
        return await self.send_request("textDocument/selectionRange", params)

    async def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None:
        """A request to result a `CallHierarchyItem` in a document at a given position.
        Can be used as an input to an incoming or outgoing call hierarchy.

        @since 3.16.0
        """
        return await self.send_request("textDocument/prepareCallHierarchy", params)

    async def incoming_calls(
        self, params: lsp_types.CallHierarchyIncomingCallsParams
    ) -> list["lsp_types.CallHierarchyIncomingCall"] | None:
        """A request to resolve the incoming calls for a given `CallHierarchyItem`.

        @since 3.16.0
        """
        return await self.send_request("callHierarchy/incomingCalls", params)

    async def outgoing_calls(
        self, params: lsp_types.CallHierarchyOutgoingCallsParams
    ) -> list["lsp_types.CallHierarchyOutgoingCall"] | None:
        """A request to resolve the outgoing calls for a given `CallHierarchyItem`.

        @since 3.16.0
        """
        return await self.send_request("callHierarchy/outgoingCalls", params)

    async def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]:
        """@since 3.16.0"""
        return await self.send_request("textDocument/semanticTokens/full", params)

    async def semantic_tokens_delta(
        self, params: lsp_types.SemanticTokensDeltaParams
    ) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]:
        """@since 3.16.0"""
        return await self.send_request("textDocument/semanticTokens/full/delta", params)

    async def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]:
        """@since 3.16.0"""
        return await self.send_request("textDocument/semanticTokens/range", params)

    async def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]:
        """A request to provide ranges that can be edited together.

        @since 3.16.0
        """
        return await self.send_request("textDocument/linkedEditingRange", params)

    async def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
        """The will create files request is sent from the client to the server before files are actually
        created as long as the creation is triggered from within the client.

        @since 3.16.0
        """
        return await self.send_request("workspace/willCreateFiles", params)

    async def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
        """The will rename files request is sent from the client to the server before files are actually
        renamed as long as the rename is triggered from within the client.

        @since 3.16.0
        """
        return await self.send_request("workspace/willRenameFiles", params)

    async def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
        """The did delete files notification is sent from the client to the server when
        files were deleted from within the client.

        @since 3.16.0
        """
        return await self.send_request("workspace/willDeleteFiles", params)

    async def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None:
        """A request to get the moniker of a symbol at a given text document position.
        The request parameter is of type {@link TextDocumentPositionParams}.
        The response is of type {@link Moniker Moniker[]} or `null`.
        """
        return await self.send_request("textDocument/moniker", params)

    async def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None:
        """A request to result a `TypeHierarchyItem` in a document at a given position.
        Can be used as an input to a subtypes or supertypes type hierarchy.

        @since 3.17.0
        """
        return await self.send_request("textDocument/prepareTypeHierarchy", params)

    async def type_hierarchy_supertypes(
        self, params: lsp_types.TypeHierarchySupertypesParams
    ) -> list["lsp_types.TypeHierarchyItem"] | None:
        """A request to resolve the supertypes for a given `TypeHierarchyItem`.

        @since 3.17.0
        """
        return await self.send_request("typeHierarchy/supertypes", params)

    async def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None:
        """A request to resolve the subtypes for a given `TypeHierarchyItem`.

        @since 3.17.0
        """
        return await self.send_request("typeHierarchy/subtypes", params)

    async def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None:
        """A request to provide inline values in a document. The request's parameter is of
        type {@link InlineValueParams}, the response is of type
        {@link InlineValue InlineValue[]} or a Thenable that resolves to such.

        @since 3.17.0
        """
        return await self.send_request("textDocument/inlineValue", params)

    async def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None:
        """A request to provide inlay hints in a document. The request's parameter is of
        type {@link InlayHintsParams}, the response is of type
        {@link InlayHint InlayHint[]} or a Thenable that resolves to such.

        @since 3.17.0
        """
        return await self.send_request("textDocument/inlayHint", params)

    async def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint":
        """A request to resolve additional properties for an inlay hint.
        The request's parameter is of type {@link InlayHint}, the response is
        of type {@link InlayHint} or a Thenable that resolves to such.

        @since 3.17.0
        """
        return await self.send_request("inlayHint/resolve", params)

    async def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport":
        """The document diagnostic request definition.

        @since 3.17.0
        """
        return await self.send_request("textDocument/diagnostic", params)

    async def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport":
        """The workspace diagnostic request definition.

        @since 3.17.0
        """
        return await self.send_request("workspace/diagnostic", params)

    async def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult":
        """The initialize request is sent from the client to the server.
        It is sent once as the request after starting up the server.
        The requests parameter is of type {@link InitializeParams}
        the response if of type {@link InitializeResult} of a Thenable that
        resolves to such.
        """
        return await self.send_request("initialize", params)

    async def shutdown(self) -> None:
        """A shutdown request is sent from the client to the server.
        It is sent once when the client decides to shutdown the
        server. The only notification that is sent after a shutdown request
        is the exit event.
        """
        return await self.send_request("shutdown")

    async def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None:
        """A document will save request is sent from the client to the server before
        the document is actually saved. The request can return an array of TextEdits
        which will be applied to the text document before it is saved. Please note that
        clients might drop results if computing the text edits took too long or if a
        server constantly fails on this request. This is done to keep the save fast and
        reliable.
        """
        return await self.send_request("textDocument/willSaveWaitUntil", params)

    async def completion(
        self, params: lsp_types.CompletionParams
    ) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]:
        """Request to request completion at a given text document position. The request's
        parameter is of type {@link TextDocumentPosition} the response
        is of type {@link CompletionItem CompletionItem[]} or {@link CompletionList}
        or a Thenable that resolves to such.

        The request can delay the computation of the {@link CompletionItem.detail `detail`}
        and {@link CompletionItem.documentation `documentation`} properties to the `completionItem/resolve`
        request. However, properties that are needed for the initial sorting and filtering, like `sortText`,
        `filterText`, `insertText`, and `textEdit`, must not be changed during resolve.
        """
        return await self.send_request("textDocument/completion", params)

    async def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem":
        """Request to resolve additional information for a given completion item.The request's
        parameter is of type {@link CompletionItem} the response
        is of type {@link CompletionItem} or a Thenable that resolves to such.
        """
        return await self.send_request("completionItem/resolve", params)

    async def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]:
        """Request to request hover information at a given text document position. The request's
        parameter is of type {@link TextDocumentPosition} the response is of
        type {@link Hover} or a Thenable that resolves to such.
        """
        return await self.send_request("textDocument/hover", params)

    async def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]:
        return await self.send_request("textDocument/signatureHelp", params)

    async def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
        """A request to resolve the definition location of a symbol at a given text
        document position. The request's parameter is of type [TextDocumentPosition]
        (#TextDocumentPosition) the response is of either type {@link Definition}
        or a typed array of {@link DefinitionLink} or a Thenable that resolves
        to such.
        """
        return await self.send_request("textDocument/definition", params)

    async def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None:
        """A request to resolve project-wide references for the symbol denoted
        by the given text document position. The request's parameter is of
        type {@link ReferenceParams} the response is of type
        {@link Location Location[]} or a Thenable that resolves to such.
        """
        return await self.send_request("textDocument/references", params)

    async def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None:
        """Request to resolve a {@link DocumentHighlight} for a given
        text document position. The request's parameter is of type [TextDocumentPosition]
        (#TextDocumentPosition) the request response is of type [DocumentHighlight[]]
        (#DocumentHighlight) or a Thenable that resolves to such.
        """
        return await self.send_request("textDocument/documentHighlight", params)

    async def document_symbol(
        self, params: lsp_types.DocumentSymbolParams
    ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.DocumentSymbol"] | None:
        """A request to list all symbols found in a given text document. The request's
        parameter is of type {@link TextDocumentIdentifier} the
        response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable
        that resolves to such.
        """
        return await self.send_request("textDocument/documentSymbol", params)

    async def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None:
        """A request to provide commands for the given text document and range."""
        return await self.send_request("textDocument/codeAction", params)

    async def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction":
        """Request to resolve additional information for a given code action.The request's
        parameter is of type {@link CodeAction} the response
        is of type {@link CodeAction} or a Thenable that resolves to such.
        """
        return await self.send_request("codeAction/resolve", params)

    async def workspace_symbol(
        self, params: lsp_types.WorkspaceSymbolParams
    ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.WorkspaceSymbol"] | None:
        """A request to list project-wide symbols matching the query string given
        by the {@link WorkspaceSymbolParams}. The response is
        of type {@link SymbolInformation SymbolInformation[]} or a Thenable that
        resolves to such.

        @since 3.17.0 - support for WorkspaceSymbol in the returned data. Clients
         need to advertise support for WorkspaceSymbols via the client capability
         `workspace.symbol.resolveSupport`.
        """
        return await self.send_request("workspace/symbol", params)

    async def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol":
        """A request to resolve the range inside the workspace
        symbol's location.

        @since 3.17.0
        """
        return await self.send_request("workspaceSymbol/resolve", params)

    async def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None:
        """A request to provide code lens for the given text document."""
        return await self.send_request("textDocument/codeLens", params)

    async def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens":
        """A request to resolve a command for a given code lens."""
        return await self.send_request("codeLens/resolve", params)

    async def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None:
        """A request to provide document links"""
        return await self.send_request("textDocument/documentLink", params)

    async def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink":
        """Request to resolve additional information for a given document link. The request's
        parameter is of type {@link DocumentLink} the response
        is of type {@link DocumentLink} or a Thenable that resolves to such.
        """
        return await self.send_request("documentLink/resolve", params)

    async def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None:
        """A request to to format a whole document."""
        return await self.send_request("textDocument/formatting", params)

    async def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None:
        """A request to to format a range in a document."""
        return await self.send_request("textDocument/rangeFormatting", params)

    async def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None:
        """A request to format a document on type."""
        return await self.send_request("textDocument/onTypeFormatting", params)

    async def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]:
        """A request to rename a symbol."""
        return await self.send_request("textDocument/rename", params)

    async def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]:
        """A request to test and perform the setup necessary for a rename.

        @since 3.16 - support for default behavior
        """
        return await self.send_request("textDocument/prepareRename", params)

    async def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]:
        """A request send from the client to the server to execute a command. The request might return
        a workspace edit which the client will apply to the workspace.
        """
        return await self.send_request("workspace/executeCommand", params)


class LspNotification:
    def __init__(self, send_notification: Any) -> None:
        self.send_notification = send_notification

    def did_change_workspace_folders(self, params: lsp_types.DidChangeWorkspaceFoldersParams) -> None:
        """The `workspace/didChangeWorkspaceFolders` notification is sent from the client to the server when the workspace
        folder configuration changes.
        """
        return self.send_notification("workspace/didChangeWorkspaceFolders", params)

    def cancel_work_done_progress(self, params: lsp_types.WorkDoneProgressCancelParams) -> None:
        """The `window/workDoneProgress/cancel` notification is sent from  the client to the server to cancel a progress
        initiated on the server side.
        """
        return self.send_notification("window/workDoneProgress/cancel", params)

    def did_create_files(self, params: lsp_types.CreateFilesParams) -> None:
        """The did create files notification is sent from the client to the server when
        files were created from within the client.

        @since 3.16.0
        """
        return self.send_notification("workspace/didCreateFiles", params)

    def did_rename_files(self, params: lsp_types.RenameFilesParams) -> None:
        """The did rename files notification is sent from the client to the server when
        files were renamed from within the client.

        @since 3.16.0
        """
        return self.send_notification("workspace/didRenameFiles", params)

    def did_delete_files(self, params: lsp_types.DeleteFilesParams) -> None:
        """The will delete files request is sent from the client to the server before files are actually
        deleted as long as the deletion is triggered from within the client.

        @since 3.16.0
        """
        return self.send_notification("workspace/didDeleteFiles", params)

    def did_open_notebook_document(self, params: lsp_types.DidOpenNotebookDocumentParams) -> None:
        """A notification sent when a notebook opens.

        @since 3.17.0
        """
        return self.send_notification("notebookDocument/didOpen", params)

    def did_change_notebook_document(self, params: lsp_types.DidChangeNotebookDocumentParams) -> None:
        return self.send_notification("notebookDocument/didChange", params)

    def did_save_notebook_document(self, params: lsp_types.DidSaveNotebookDocumentParams) -> None:
        """A notification sent when a notebook document is saved.

        @since 3.17.0
        """
        return self.send_notification("notebookDocument/didSave", params)

    def did_close_notebook_document(self, params: lsp_types.DidCloseNotebookDocumentParams) -> None:
        """A notification sent when a notebook closes.

        @since 3.17.0
        """
        return self.send_notification("notebookDocument/didClose", params)

    def initialized(self, params: lsp_types.InitializedParams) -> None:
        """The initialized notification is sent from the client to the
        server after the client is fully initialized and the server
        is allowed to send requests from the server to the client.
        """
        return self.send_notification("initialized", params)

    def exit(self) -> None:
        """The exit event is sent from the client to the server to
        ask the server to exit its process.
        """
        return self.send_notification("exit")

    def workspace_did_change_configuration(self, params: lsp_types.DidChangeConfigurationParams) -> None:
        """The configuration change notification is sent from the client to the server
        when the client's configuration has changed. The notification contains
        the changed configuration as defined by the language client.
        """
        return self.send_notification("workspace/didChangeConfiguration", params)

    def did_open_text_document(self, params: lsp_types.DidOpenTextDocumentParams) -> None:
        """The document open notification is sent from the client to the server to signal
        newly opened text documents. The document's truth is now managed by the client
        and the server must not try to read the document's truth using the document's
        uri. Open in this sense means it is managed by the client. It doesn't necessarily
        mean that its content is presented in an editor. An open notification must not
        be sent more than once without a corresponding close notification send before.
        This means open and close notification must be balanced and the max open count
        is one.
        """
        return self.send_notification("textDocument/didOpen", params)

    def did_change_text_document(self, params: lsp_types.DidChangeTextDocumentParams) -> None:
        """The document change notification is sent from the client to the server to signal
        changes to a text document.
        """
        return self.send_notification("textDocument/didChange", params)

    def did_close_text_document(self, params: lsp_types.DidCloseTextDocumentParams) -> None:
        """The document close notification is sent from the client to the server when
        the document got closed in the client. The document's truth now exists where
        the document's uri points to (e.g. if the document's uri is a file uri the
        truth now exists on disk). As with the open notification the close notification
        is about managing the document's content. Receiving a close notification
        doesn't mean that the document was open in an editor before. A close
        notification requires a previous open notification to be sent.
        """
        return self.send_notification("textDocument/didClose", params)

    def did_save_text_document(self, params: lsp_types.DidSaveTextDocumentParams) -> None:
        """The document save notification is sent from the client to the server when
        the document got saved in the client.
        """
        return self.send_notification("textDocument/didSave", params)

    def will_save_text_document(self, params: lsp_types.WillSaveTextDocumentParams) -> None:
        """A document will save notification is sent from the client to the server before
        the document is actually saved.
        """
        return self.send_notification("textDocument/willSave", params)

    def did_change_watched_files(self, params: lsp_types.DidChangeWatchedFilesParams) -> None:
        """The watched files notification is sent from the client to the server when
        the client detects changes to file watched by the language client.
        """
        return self.send_notification("workspace/didChangeWatchedFiles", params)

    def set_trace(self, params: lsp_types.SetTraceParams) -> None:
        return self.send_notification("$/setTrace", params)

    def cancel_request(self, params: lsp_types.CancelParams) -> None:
        return self.send_notification("$/cancelRequest", params)

    def progress(self, params: lsp_types.ProgressParams) -> None:
        return self.send_notification("$/progress", params)

```

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

```python
"""
The Serena Model Context Protocol (MCP) Server
"""

import dataclasses
import os
import shutil
from collections.abc import Sequence
from copy import deepcopy
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional, Self, TypeVar

import yaml
from ruamel.yaml.comments import CommentedMap
from sensai.util import logging
from sensai.util.logging import LogTime, datetime_tag
from sensai.util.string import ToStringMixin

from serena.constants import (
    DEFAULT_SOURCE_FILE_ENCODING,
    PROJECT_TEMPLATE_FILE,
    REPO_ROOT,
    SERENA_CONFIG_TEMPLATE_FILE,
    SERENA_FILE_ENCODING,
    SERENA_MANAGED_DIR_NAME,
)
from serena.util.general import get_dataclass_default, load_yaml, save_yaml
from serena.util.inspection import determine_programming_language_composition
from solidlsp.ls_config import Language

from ..analytics import RegisteredTokenCountEstimator
from ..util.class_decorators import singleton
from ..util.cli_util import ask_yes_no

if TYPE_CHECKING:
    from ..project import Project

log = logging.getLogger(__name__)
T = TypeVar("T")
DEFAULT_TOOL_TIMEOUT: float = 240
DictType = dict | CommentedMap
TDict = TypeVar("TDict", bound=DictType)


@singleton
class SerenaPaths:
    """
    Provides paths to various Serena-related directories and files.
    """

    def __init__(self) -> None:
        home_dir = os.getenv("SERENA_HOME")
        if home_dir is None or home_dir.strip() == "":
            home_dir = str(Path.home() / SERENA_MANAGED_DIR_NAME)
        else:
            home_dir = home_dir.strip()
        self.serena_user_home_dir: str = home_dir
        """
        the path to the Serena home directory, where the user's configuration/data is stored.
        This is ~/.serena by default, but it can be overridden via the SERENA_HOME environment variable.
        """
        self.user_prompt_templates_dir: str = os.path.join(self.serena_user_home_dir, "prompt_templates")
        """
        directory containing prompt templates defined by the user.
        Prompts defined by the user take precedence over Serena's built-in prompt templates.
        """
        self.user_contexts_dir: str = os.path.join(self.serena_user_home_dir, "contexts")
        """
        directory containing contexts defined by the user. 
        If a name of a context matches a name of a context in SERENAS_OWN_CONTEXT_YAMLS_DIR, 
        the user context will override the default context definition.
        """
        self.user_modes_dir: str = os.path.join(self.serena_user_home_dir, "modes")
        """
        directory containing modes defined by the user.
        If a name of a mode matches a name of a mode in SERENAS_OWN_MODES_YAML_DIR,
        the user mode will override the default mode definition.
        """
        self.news_snippet_id_file: str = os.path.join(self.serena_user_home_dir, "last_read_news_snippet_id.txt")
        """
        file containing the ID of the last read news snippet
        """

    def get_next_log_file_path(self, prefix: str) -> str:
        """
        :param prefix: the filename prefix indicating the type of the log file
        :return: the full path to the log file to use
        """
        log_dir = os.path.join(self.serena_user_home_dir, "logs", datetime.now().strftime("%Y-%m-%d"))
        os.makedirs(log_dir, exist_ok=True)
        return os.path.join(log_dir, prefix + "_" + datetime_tag() + ".txt")

    # TODO: Paths from constants.py should be moved here


@dataclass
class ToolInclusionDefinition:
    """
    Defines which tools to include/exclude in Serena's operation.
    This can mean either
      * defining exclusions/inclusions to apply to an existing set of tools [incremental mode], or
      * defining a fixed set of tools to use [fixed mode].
    """

    excluded_tools: Sequence[str] = ()
    """
    the names of tools to exclude from use [incremental mode]
    """
    included_optional_tools: Sequence[str] = ()
    """
    the names of optional tools to include [incremental mode]
    """
    fixed_tools: Sequence[str] = ()
    """
    the names of tools to use as a fixed set of tools [fixed mode]
    """

    def is_fixed_tool_set(self) -> bool:
        num_fixed = len(self.fixed_tools)
        num_incremental = len(self.excluded_tools) + len(self.included_optional_tools)
        if num_fixed > 0 and num_incremental > 0:
            raise ValueError("Cannot use both fixed_tools and excluded_tools/included_optional_tools at the same time.")
        return num_fixed > 0


class SerenaConfigError(Exception):
    pass


def get_serena_managed_in_project_dir(project_root: str | Path) -> str:
    return os.path.join(project_root, SERENA_MANAGED_DIR_NAME)


class LanguageBackend(Enum):
    LSP = "LSP"
    """
    Use the language server protocol (LSP), spawning freely available language servers
    via the SolidLSP library that is part of Serena
    """
    JETBRAINS = "JetBrains"
    """
    Use the Serena plugin in your JetBrains IDE.
    (requires the plugin to be installed and the project being worked on to be open in your IDE)
    """

    @staticmethod
    def from_str(backend_str: str) -> "LanguageBackend":
        for backend in LanguageBackend:
            if backend.value.lower() == backend_str.lower():
                return backend
        raise ValueError(f"Unknown language backend '{backend_str}': valid values are {[b.value for b in LanguageBackend]}")


@dataclass(kw_only=True)
class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
    project_name: str
    languages: list[Language]
    ignored_paths: list[str] = field(default_factory=list)
    read_only: bool = False
    ignore_all_files_in_gitignore: bool = True
    initial_prompt: str = ""
    encoding: str = DEFAULT_SOURCE_FILE_ENCODING

    SERENA_DEFAULT_PROJECT_FILE = "project.yml"

    def _tostring_includes(self) -> list[str]:
        return ["project_name"]

    @classmethod
    def autogenerate(
        cls,
        project_root: str | Path,
        project_name: str | None = None,
        languages: list[Language] | None = None,
        save_to_disk: bool = True,
        interactive: bool = False,
    ) -> Self:
        """
        Autogenerate a project configuration for a given project root.

        :param project_root: the path to the project root
        :param project_name: the name of the project; if None, the name of the project will be the name of the directory
            containing the project
        :param languages: the languages of the project; if None, they will be determined automatically
        :param save_to_disk: whether to save the project configuration to disk
        :param interactive: whether to run in interactive CLI mode, asking the user for input where appropriate
        :return: the project configuration
        """
        project_root = Path(project_root).resolve()
        if not project_root.exists():
            raise FileNotFoundError(f"Project root not found: {project_root}")
        with LogTime("Project configuration auto-generation", logger=log):
            log.info("Project root: %s", project_root)
            project_name = project_name or project_root.name
            if languages is None:
                # determine languages automatically
                log.info("Determining programming languages used in the project")
                language_composition = determine_programming_language_composition(str(project_root))
                log.info("Language composition: %s", language_composition)
                if len(language_composition) == 0:
                    language_values = ", ".join([lang.value for lang in Language])
                    raise ValueError(
                        f"No source files found in {project_root}\n\n"
                        f"To use Serena with this project, you need to either\n"
                        f"  1. specify a programming language by adding parameters --language <language>\n"
                        f"     when creating the project via the Serena CLI command OR\n"
                        f"  2. add source files in one of the supported languages first.\n\n"
                        f"Supported languages are: {language_values}\n"
                        f"Read the documentation for more information."
                    )
                # sort languages by number of files found
                languages_and_percentages = sorted(
                    language_composition.items(), key=lambda item: (item[1], item[0].get_priority()), reverse=True
                )
                # find the language with the highest percentage and enable it
                top_language_pair = languages_and_percentages[0]
                other_language_pairs = languages_and_percentages[1:]
                languages_to_use: list[str] = [top_language_pair[0].value]
                # if in interactive mode, ask the user which other languages to enable
                if len(other_language_pairs) > 0 and interactive:
                    print(
                        "Detected and enabled main language '%s' (%.2f%% of source files)."
                        % (top_language_pair[0].value, top_language_pair[1])
                    )
                    print(f"Additionally detected {len(other_language_pairs)} other language(s).\n")
                    print("Note: Enable only languages you need symbolic retrieval/editing capabilities for.")
                    print("      Additional language servers use resources and some languages may require additional")
                    print("      system-level installations/configuration (see Serena documentation).")
                    print("\nWhich additional languages do you want to enable?")
                    for lang, perc in other_language_pairs:
                        enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang.value, perc), default=False)
                        if enable:
                            languages_to_use.append(lang.value)
                    print()
                log.info("Using languages: %s", languages_to_use)
            else:
                languages_to_use = [lang.value for lang in languages]
            config_with_comments = cls.load_commented_map(PROJECT_TEMPLATE_FILE)
            config_with_comments["project_name"] = project_name
            config_with_comments["languages"] = languages_to_use
            if save_to_disk:
                project_yml_path = cls.path_to_project_yml(project_root)
                log.info("Saving project configuration to %s", project_yml_path)
                save_yaml(project_yml_path, config_with_comments, preserve_comments=True)
            return cls._from_dict(config_with_comments)

    @classmethod
    def path_to_project_yml(cls, project_root: str | Path) -> str:
        return os.path.join(project_root, cls.rel_path_to_project_yml())

    @classmethod
    def rel_path_to_project_yml(cls) -> str:
        return os.path.join(SERENA_MANAGED_DIR_NAME, cls.SERENA_DEFAULT_PROJECT_FILE)

    @classmethod
    def _apply_defaults_to_dict(cls, data: TDict) -> TDict:
        # apply defaults for new fields
        data["languages"] = data.get("languages", [])
        data["ignored_paths"] = data.get("ignored_paths", [])
        data["excluded_tools"] = data.get("excluded_tools", [])
        data["included_optional_tools"] = data.get("included_optional_tools", [])
        data["read_only"] = data.get("read_only", False)
        data["ignore_all_files_in_gitignore"] = data.get("ignore_all_files_in_gitignore", True)
        data["initial_prompt"] = data.get("initial_prompt", "")
        data["encoding"] = data.get("encoding", DEFAULT_SOURCE_FILE_ENCODING)

        # backward compatibility: handle single "language" field
        if len(data["languages"]) == 0 and "language" in data:
            data["languages"] = [data["language"]]
        if "language" in data:
            del data["language"]

        return data

    @classmethod
    def load_commented_map(cls, yml_path: str) -> CommentedMap:
        """
        Load the project configuration as a CommentedMap, preserving comments and ensuring
        completeness of the configuration by applying default values for missing fields
        and backward compatibility adjustments.

        :param yml_path: the path to the project.yml file
        :return: a CommentedMap representing a full project configuration
        """
        data = load_yaml(yml_path, preserve_comments=True)
        return cls._apply_defaults_to_dict(data)

    @classmethod
    def _from_dict(cls, data: dict[str, Any]) -> Self:
        """
        Create a ProjectConfig instance from a (full) configuration dictionary
        """
        lang_name_mapping = {"javascript": "typescript"}
        languages: list[Language] = []
        for language_str in data["languages"]:
            orig_language_str = language_str
            try:
                language_str = language_str.lower()
                if language_str in lang_name_mapping:
                    language_str = lang_name_mapping[language_str]
                language = Language(language_str)
                languages.append(language)
            except ValueError as e:
                raise ValueError(
                    f"Invalid language: {orig_language_str}.\nValid language_strings are: {[l.value for l in Language]}"
                ) from e

        return cls(
            project_name=data["project_name"],
            languages=languages,
            ignored_paths=data["ignored_paths"],
            excluded_tools=data["excluded_tools"],
            included_optional_tools=data["included_optional_tools"],
            read_only=data["read_only"],
            ignore_all_files_in_gitignore=data["ignore_all_files_in_gitignore"],
            initial_prompt=data["initial_prompt"],
            encoding=data["encoding"],
        )

    def to_yaml_dict(self) -> dict:
        """
        :return: a yaml-serializable dictionary representation of this configuration
        """
        d = dataclasses.asdict(self)
        d["languages"] = [lang.value for lang in self.languages]
        return d

    @classmethod
    def load(cls, project_root: Path | str, autogenerate: bool = False) -> Self:
        """
        Load a ProjectConfig instance from the path to the project root.
        """
        project_root = Path(project_root)
        yaml_path = project_root / cls.rel_path_to_project_yml()
        if not yaml_path.exists():
            if autogenerate:
                return cls.autogenerate(project_root)
            else:
                raise FileNotFoundError(f"Project configuration file not found: {yaml_path}")
        yaml_data = cls.load_commented_map(str(yaml_path))
        if "project_name" not in yaml_data:
            yaml_data["project_name"] = project_root.name
        return cls._from_dict(yaml_data)


class RegisteredProject(ToStringMixin):
    def __init__(self, project_root: str, project_config: "ProjectConfig", project_instance: Optional["Project"] = None) -> None:
        """
        Represents a registered project in the Serena configuration.

        :param project_root: the root directory of the project
        :param project_config: the configuration of the project
        """
        self.project_root = Path(project_root).resolve()
        self.project_config = project_config
        self._project_instance = project_instance

    def _tostring_exclude_private(self) -> bool:
        return True

    @property
    def project_name(self) -> str:
        return self.project_config.project_name

    @classmethod
    def from_project_instance(cls, project_instance: "Project") -> "RegisteredProject":
        return RegisteredProject(
            project_root=project_instance.project_root,
            project_config=project_instance.project_config,
            project_instance=project_instance,
        )

    def matches_root_path(self, path: str | Path) -> bool:
        """
        Check if the given path matches the project root path.

        :param path: the path to check
        :return: True if the path matches the project root, False otherwise
        """
        return self.project_root == Path(path).resolve()

    def get_project_instance(self) -> "Project":
        """
        Returns the project instance for this registered project, loading it if necessary.
        """
        if self._project_instance is None:
            from ..project import Project

            with LogTime(f"Loading project instance for {self}", logger=log):
                self._project_instance = Project(project_root=str(self.project_root), project_config=self.project_config)
        return self._project_instance


@dataclass(kw_only=True)
class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
    """
    Holds the Serena agent configuration, which is typically loaded from a YAML configuration file
    (when instantiated via :method:`from_config_file`), which is updated when projects are added or removed.
    For testing purposes, it can also be instantiated directly with the desired parameters.
    """

    projects: list[RegisteredProject] = field(default_factory=list)
    gui_log_window_enabled: bool = False
    log_level: int = logging.INFO
    trace_lsp_communication: bool = False
    web_dashboard: bool = True
    web_dashboard_open_on_launch: bool = True
    web_dashboard_listen_address: str = "127.0.0.1"
    tool_timeout: float = DEFAULT_TOOL_TIMEOUT
    loaded_commented_yaml: CommentedMap | None = None
    config_file_path: str | None = None
    """
    the path to the configuration file to which updates of the configuration shall be saved;
    if None, the configuration is not saved to disk
    """

    language_backend: LanguageBackend = LanguageBackend.LSP
    """
    the language backend to use for code understanding features
    """

    token_count_estimator: str = RegisteredTokenCountEstimator.CHAR_COUNT.name
    """Only relevant if `record_tool_usage` is True; the name of the token count estimator to use for tool usage statistics.
    See the `RegisteredTokenCountEstimator` enum for available options.
    
    Note: some token estimators (like tiktoken) may require downloading data files
    on the first run, which can take some time and require internet access. Others, like the Anthropic ones, may require an API key
    and rate limits may apply.
    """
    default_max_tool_answer_chars: int = 150_000
    """Used as default for tools where the apply method has a default maximal answer length.
    Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default 
    through the global configuration.
    """
    ls_specific_settings: dict = field(default_factory=dict)
    """Advanced configuration option allowing to configure language server implementation specific options, see SolidLSPSettings for more info."""

    CONFIG_FILE = "serena_config.yml"

    def _tostring_includes(self) -> list[str]:
        return ["config_file_path"]

    @classmethod
    def _generate_config_file(cls, config_file_path: str) -> None:
        """
        Generates a Serena configuration file at the specified path from the template file.

        :param config_file_path: the path where the configuration file should be generated
        """
        log.info(f"Auto-generating Serena configuration file in {config_file_path}")
        loaded_commented_yaml = load_yaml(SERENA_CONFIG_TEMPLATE_FILE, preserve_comments=True)
        save_yaml(config_file_path, loaded_commented_yaml, preserve_comments=True)

    @classmethod
    def _determine_config_file_path(cls) -> str:
        """
        :return: the location where the Serena configuration file is stored/should be stored
        """
        config_path = os.path.join(SerenaPaths().serena_user_home_dir, cls.CONFIG_FILE)

        # if the config file does not exist, check if we can migrate it from the old location
        if not os.path.exists(config_path):
            old_config_path = os.path.join(REPO_ROOT, cls.CONFIG_FILE)
            if os.path.exists(old_config_path):
                log.info(f"Moving Serena configuration file from {old_config_path} to {config_path}")
                os.makedirs(os.path.dirname(config_path), exist_ok=True)
                shutil.move(old_config_path, config_path)

        return config_path

    @classmethod
    def from_config_file(cls, generate_if_missing: bool = True) -> "SerenaConfig":
        """
        Static constructor to create SerenaConfig from the configuration file
        """
        config_file_path = cls._determine_config_file_path()

        # create the configuration file from the template if necessary
        if not os.path.exists(config_file_path):
            if not generate_if_missing:
                raise FileNotFoundError(f"Serena configuration file not found: {config_file_path}")
            log.info(f"Serena configuration file not found at {config_file_path}, autogenerating...")
            cls._generate_config_file(config_file_path)

        # load the configuration
        log.info(f"Loading Serena configuration from {config_file_path}")
        try:
            loaded_commented_yaml = load_yaml(config_file_path, preserve_comments=True)
        except Exception as e:
            raise ValueError(f"Error loading Serena configuration from {config_file_path}: {e}") from e

        # create the configuration instance
        instance = cls(loaded_commented_yaml=loaded_commented_yaml, config_file_path=config_file_path)

        # read projects
        if "projects" not in loaded_commented_yaml:
            raise SerenaConfigError("`projects` key not found in Serena configuration. Please update your `serena_config.yml` file.")

        # load list of known projects
        instance.projects = []
        num_migrations = 0
        for path in loaded_commented_yaml["projects"]:
            path = Path(path).resolve()
            if not path.exists() or (path.is_dir() and not (path / ProjectConfig.rel_path_to_project_yml()).exists()):
                log.warning(f"Project path {path} does not exist or does not contain a project configuration file, skipping.")
                continue
            if path.is_file():
                path = cls._migrate_out_of_project_config_file(path)
                if path is None:
                    continue
                num_migrations += 1
            project_config = ProjectConfig.load(path)
            project = RegisteredProject(
                project_root=str(path),
                project_config=project_config,
            )
            instance.projects.append(project)

        def get_value_or_default(key: str, field_name: str | None = None) -> Any:
            if field_name is None:
                field_name = key
            return loaded_commented_yaml.get(key, get_dataclass_default(SerenaConfig, field_name))

        # determine language backend
        language_backend = get_dataclass_default(SerenaConfig, "language_backend")
        if "language_backend" in loaded_commented_yaml:
            backend_str = loaded_commented_yaml["language_backend"]
            language_backend = LanguageBackend.from_str(backend_str)
        else:
            # backward compatibility (migrate Boolean field "jetbrains")
            if "jetbrains" in loaded_commented_yaml:
                num_migrations += 1
                if loaded_commented_yaml["jetbrains"]:
                    language_backend = LanguageBackend.JETBRAINS
                del loaded_commented_yaml["jetbrains"]
        instance.language_backend = language_backend

        # set other configuration parameters (primitive types)
        instance.gui_log_window_enabled = get_value_or_default("gui_log_window", "gui_log_window_enabled")
        instance.web_dashboard_listen_address = get_value_or_default("web_dashboard_listen_address")
        instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO))
        instance.web_dashboard = get_value_or_default("web_dashboard")
        instance.web_dashboard_open_on_launch = get_value_or_default("web_dashboard_open_on_launch")
        instance.tool_timeout = get_value_or_default("tool_timeout")
        instance.trace_lsp_communication = get_value_or_default("trace_lsp_communication")
        instance.excluded_tools = get_value_or_default("excluded_tools")
        instance.included_optional_tools = get_value_or_default("included_optional_tools")
        instance.token_count_estimator = get_value_or_default("token_count_estimator")
        instance.default_max_tool_answer_chars = get_value_or_default("default_max_tool_answer_chars")
        instance.ls_specific_settings = get_value_or_default("ls_specific_settings")

        # re-save the configuration file if any migrations were performed
        if num_migrations > 0:
            log.info("Legacy configuration was migrated; re-saving configuration file")
            instance.save()

        return instance

    @classmethod
    def _migrate_out_of_project_config_file(cls, path: Path) -> Path | None:
        """
        Migrates a legacy project configuration file (which is a YAML file containing the project root) to the
        in-project configuration file (project.yml) inside the project root directory.

        :param path: the path to the legacy project configuration file
        :return: the project root path if the migration was successful, None otherwise.
        """
        log.info(f"Found legacy project configuration file {path}, migrating to in-project configuration.")
        try:
            with open(path, encoding=SERENA_FILE_ENCODING) as f:
                project_config_data = yaml.safe_load(f)
            if "project_name" not in project_config_data:
                project_name = path.stem
                with open(path, "a", encoding=SERENA_FILE_ENCODING) as f:
                    f.write(f"\nproject_name: {project_name}")
            project_root = project_config_data["project_root"]
            shutil.move(str(path), str(Path(project_root) / ProjectConfig.rel_path_to_project_yml()))
            return Path(project_root).resolve()
        except Exception as e:
            log.error(f"Error migrating configuration file: {e}")
            return None

    @cached_property
    def project_paths(self) -> list[str]:
        return sorted(str(project.project_root) for project in self.projects)

    @cached_property
    def project_names(self) -> list[str]:
        return sorted(project.project_config.project_name for project in self.projects)

    def get_project(self, project_root_or_name: str) -> Optional["Project"]:
        # look for project by name
        project_candidates = []
        for project in self.projects:
            if project.project_config.project_name == project_root_or_name:
                project_candidates.append(project)
        if len(project_candidates) == 1:
            return project_candidates[0].get_project_instance()
        elif len(project_candidates) > 1:
            raise ValueError(
                f"Multiple projects found with name '{project_root_or_name}'. Please activate it by location instead. "
                f"Locations: {[p.project_root for p in project_candidates]}"
            )
        # no project found by name; check if it's a path
        if os.path.isdir(project_root_or_name):
            for project in self.projects:
                if project.matches_root_path(project_root_or_name):
                    return project.get_project_instance()
        return None

    def add_project_from_path(self, project_root: Path | str) -> "Project":
        """
        Add a project to the Serena configuration from a given path. Will raise a FileExistsError if a
        project already exists at the path.

        :param project_root: the path to the project to add
        :return: the project that was added
        """
        from ..project import Project

        project_root = Path(project_root).resolve()
        if not project_root.exists():
            raise FileNotFoundError(f"Error: Path does not exist: {project_root}")
        if not project_root.is_dir():
            raise FileNotFoundError(f"Error: Path is not a directory: {project_root}")

        for already_registered_project in self.projects:
            if str(already_registered_project.project_root) == str(project_root):
                raise FileExistsError(
                    f"Project with path {project_root} was already added with name '{already_registered_project.project_name}'."
                )

        project_config = ProjectConfig.load(project_root, autogenerate=True)

        new_project = Project(project_root=str(project_root), project_config=project_config, is_newly_created=True)
        self.projects.append(RegisteredProject.from_project_instance(new_project))
        self.save()

        return new_project

    def remove_project(self, project_name: str) -> None:
        # find the index of the project with the desired name and remove it
        for i, project in enumerate(list(self.projects)):
            if project.project_name == project_name:
                del self.projects[i]
                break
        else:
            raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}")
        self.save()

    def save(self) -> None:
        """
        Saves the configuration to the file from which it was loaded (if any)
        """
        if self.config_file_path is None:
            return

        assert self.loaded_commented_yaml is not None, "Cannot save configuration without loaded YAML"

        loaded_original_yaml = deepcopy(self.loaded_commented_yaml)

        # convert project objects into list of paths
        loaded_original_yaml["projects"] = sorted({str(project.project_root) for project in self.projects})

        # convert language backend to string
        loaded_original_yaml["language_backend"] = self.language_backend.value

        save_yaml(self.config_file_path, loaded_original_yaml, preserve_comments=True)

```

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

```python
import json
import logging
import os
from abc import ABC, abstractmethod
from collections.abc import Callable, Iterator, Sequence
from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING, Any, Self, Union

from sensai.util.string import ToStringMixin

import serena.tools.jetbrains_types as jb
from solidlsp import SolidLanguageServer
from solidlsp.ls import ReferenceInSymbol as LSPReferenceInSymbol
from solidlsp.ls_types import Position, SymbolKind, UnifiedSymbolInformation

from .ls_manager import LanguageServerManager
from .project import Project

if TYPE_CHECKING:
    from .agent import SerenaAgent

log = logging.getLogger(__name__)
NAME_PATH_SEP = "/"


@dataclass
class LanguageServerSymbolLocation:
    """
    Represents the (start) location of a symbol identifier, which, within Serena, uniquely identifies the symbol.
    """

    relative_path: str | None
    """
    the relative path of the file containing the symbol; if None, the symbol is defined outside of the project's scope
    """
    line: int | None
    """
    the line number in which the symbol identifier is defined (if the symbol is a function, class, etc.);
    may be None for some types of symbols (e.g. SymbolKind.File)
    """
    column: int | None
    """
    the column number in which the symbol identifier is defined (if the symbol is a function, class, etc.);
    may be None for some types of symbols (e.g. SymbolKind.File)
    """

    def __post_init__(self) -> None:
        if self.relative_path is not None:
            self.relative_path = self.relative_path.replace("/", os.path.sep)

    def to_dict(self, include_relative_path: bool = True) -> dict[str, Any]:
        result = asdict(self)
        if not include_relative_path:
            result.pop("relative_path", None)
        return result

    def has_position_in_file(self) -> bool:
        return self.relative_path is not None and self.line is not None and self.column is not None


@dataclass
class PositionInFile:
    """
    Represents a character position within a file
    """

    line: int
    """
    the 0-based line number in the file
    """
    col: int
    """
    the 0-based column
    """

    def to_lsp_position(self) -> Position:
        """
        Convert to LSP Position.
        """
        return Position(line=self.line, character=self.col)


class Symbol(ToStringMixin, ABC):
    @abstractmethod
    def get_body_start_position(self) -> PositionInFile | None:
        pass

    @abstractmethod
    def get_body_end_position(self) -> PositionInFile | None:
        pass

    def get_body_start_position_or_raise(self) -> PositionInFile:
        """
        Get the start position of the symbol body, raising an error if it is not defined.
        """
        pos = self.get_body_start_position()
        if pos is None:
            raise ValueError(f"Body start position is not defined for {self}")
        return pos

    def get_body_end_position_or_raise(self) -> PositionInFile:
        """
        Get the end position of the symbol body, raising an error if it is not defined.
        """
        pos = self.get_body_end_position()
        if pos is None:
            raise ValueError(f"Body end position is not defined for {self}")
        return pos

    @abstractmethod
    def is_neighbouring_definition_separated_by_empty_line(self) -> bool:
        """
        :return: whether a symbol definition of this symbol's kind is usually separated from the
            previous/next definition by at least one empty line.
        """


class NamePathMatcher(ToStringMixin):
    """
    Matches name paths of symbols against search patterns.

    A name path is a path in the symbol tree *within a source file*.
    For example, the method `my_method` defined in class `MyClass` would have the name path `MyClass/my_method`.
    If a symbol is overloaded (e.g., in Java), a 0-based index is appended (e.g. "MyClass/my_method[0]") to
    uniquely identify it.

    A matching pattern can be:
     * a simple name (e.g. "method"), which will match any symbol with that name
     * a relative path like "class/method", which will match any symbol with that name path suffix
     * an absolute name path "/class/method" (absolute name path), which requires an exact match of the full name path within the source file.
    Append an index `[i]` to match a specific overload only, e.g. "MyClass/my_method[1]".
    """

    def __init__(self, name_path_pattern: str, substring_matching: bool) -> None:
        """
        :param name_path_pattern: the name path expression to match against
        :param substring_matching: whether to use substring matching for the last segment
        """
        assert name_path_pattern, "name_path must not be empty"
        self._expr = name_path_pattern
        self._substring_matching = substring_matching
        self._is_absolute_pattern = name_path_pattern.startswith(NAME_PATH_SEP)
        self._pattern_parts = name_path_pattern.lstrip(NAME_PATH_SEP).rstrip(NAME_PATH_SEP).split(NAME_PATH_SEP)

        # extract overload index "[idx]" if present at end of last part
        self._overload_idx: int | None = None
        last_part = self._pattern_parts[-1]
        if last_part.endswith("]") and "[" in last_part:
            bracket_idx = last_part.rfind("[")
            index_part = last_part[bracket_idx + 1 : -1]
            if index_part.isdigit():
                self._pattern_parts[-1] = last_part[:bracket_idx]
                self._overload_idx = int(index_part)

    def _tostring_includes(self) -> list[str]:
        return ["_expr"]

    def matches_ls_symbol(self, symbol: "LanguageServerSymbol") -> bool:
        return self.matches_components(symbol.get_name_path_parts(), symbol.overload_idx)

    def matches_components(self, symbol_name_path_parts: list[str], overload_idx: int | None) -> bool:
        # filtering based on ancestors
        if len(self._pattern_parts) > len(symbol_name_path_parts):
            # can't possibly match if pattern has more parts than symbol
            return False
        if self._is_absolute_pattern and len(self._pattern_parts) != len(symbol_name_path_parts):
            # for absolute patterns, the number of parts must match exactly
            return False
        if symbol_name_path_parts[-len(self._pattern_parts) : -1] != self._pattern_parts[:-1]:
            # ancestors must match
            return False

        # matching the last part of the symbol name
        name_to_match = self._pattern_parts[-1]
        symbol_name = symbol_name_path_parts[-1]
        if self._substring_matching:
            if name_to_match not in symbol_name:
                return False
        else:
            if name_to_match != symbol_name:
                return False

        # check for matching overload index
        if self._overload_idx is not None:
            if overload_idx != self._overload_idx:
                return False

        return True


class LanguageServerSymbol(Symbol, ToStringMixin):
    def __init__(self, symbol_root_from_ls: UnifiedSymbolInformation) -> None:
        self.symbol_root = symbol_root_from_ls

    def _tostring_includes(self) -> list[str]:
        return []

    def _tostring_additional_entries(self) -> dict[str, Any]:
        return dict(name=self.name, kind=self.kind, num_children=len(self.symbol_root["children"]))

    @property
    def name(self) -> str:
        return self.symbol_root["name"]

    @property
    def kind(self) -> str:
        return SymbolKind(self.symbol_kind).name

    @property
    def symbol_kind(self) -> SymbolKind:
        return self.symbol_root["kind"]

    def is_low_level(self) -> bool:
        """
        :return: whether the symbol is a low-level symbol (variable, constant, etc.), which typically represents data
            rather than structure and therefore is not relevant in a high-level overview of the code.
        """
        return self.symbol_kind >= SymbolKind.Variable.value

    @property
    def overload_idx(self) -> int | None:
        return self.symbol_root.get("overload_idx")

    def is_neighbouring_definition_separated_by_empty_line(self) -> bool:
        return self.symbol_kind in (SymbolKind.Function, SymbolKind.Method, SymbolKind.Class, SymbolKind.Interface, SymbolKind.Struct)

    @property
    def relative_path(self) -> str | None:
        location = self.symbol_root.get("location")
        if location:
            return location.get("relativePath")
        return None

    @property
    def location(self) -> LanguageServerSymbolLocation:
        """
        :return: the start location of the actual symbol identifier
        """
        return LanguageServerSymbolLocation(relative_path=self.relative_path, line=self.line, column=self.column)

    @property
    def body_start_position(self) -> Position | None:
        location = self.symbol_root.get("location")
        if location:
            range_info = location.get("range")
            if range_info:
                start_pos = range_info.get("start")
                if start_pos:
                    return start_pos
        return None

    @property
    def body_end_position(self) -> Position | None:
        location = self.symbol_root.get("location")
        if location:
            range_info = location.get("range")
            if range_info:
                end_pos = range_info.get("end")
                if end_pos:
                    return end_pos
        return None

    def get_body_start_position(self) -> PositionInFile | None:
        start_pos = self.body_start_position
        if start_pos is None:
            return None
        return PositionInFile(line=start_pos["line"], col=start_pos["character"])

    def get_body_end_position(self) -> PositionInFile | None:
        end_pos = self.body_end_position
        if end_pos is None:
            return None
        return PositionInFile(line=end_pos["line"], col=end_pos["character"])

    def get_body_line_numbers(self) -> tuple[int | None, int | None]:
        start_pos = self.body_start_position
        end_pos = self.body_end_position
        start_line = start_pos["line"] if start_pos else None
        end_line = end_pos["line"] if end_pos else None
        return start_line, end_line

    @property
    def line(self) -> int | None:
        """
        :return: the line in which the symbol identifier is defined.
        """
        if "selectionRange" in self.symbol_root:
            return self.symbol_root["selectionRange"]["start"]["line"]
        else:
            # line is expected to be undefined for some types of symbols (e.g. SymbolKind.File)
            return None

    @property
    def column(self) -> int | None:
        if "selectionRange" in self.symbol_root:
            return self.symbol_root["selectionRange"]["start"]["character"]
        else:
            # precise location is expected to be undefined for some types of symbols (e.g. SymbolKind.File)
            return None

    @property
    def body(self) -> str | None:
        return self.symbol_root.get("body")

    def get_name_path(self) -> str:
        """
        Get the name path of the symbol, e.g. "class/method/inner_function" or
        "class/method[1]" (overloaded method with identifying index).
        """
        name_path = NAME_PATH_SEP.join(self.get_name_path_parts())
        if "overload_idx" in self.symbol_root:
            name_path += f"[{self.symbol_root['overload_idx']}]"
        return name_path

    def get_name_path_parts(self) -> list[str]:
        """
        Get the parts of the name path of the symbol (e.g. ["class", "method", "inner_function"]).
        """
        ancestors_within_file = list(self.iter_ancestors(up_to_symbol_kind=SymbolKind.File))
        ancestors_within_file.reverse()
        return [a.name for a in ancestors_within_file] + [self.name]

    def iter_children(self) -> Iterator[Self]:
        for c in self.symbol_root["children"]:
            yield self.__class__(c)

    def iter_ancestors(self, up_to_symbol_kind: SymbolKind | None = None) -> Iterator[Self]:
        """
        Iterate over all ancestors of the symbol, starting with the parent and going up to the root or
        the given symbol kind.

        :param up_to_symbol_kind: if provided, iteration will stop *before* the first ancestor of the given kind.
            A typical use case is to pass `SymbolKind.File` or `SymbolKind.Package`.
        """
        parent = self.get_parent()
        if parent is not None:
            if up_to_symbol_kind is None or parent.symbol_kind != up_to_symbol_kind:
                yield parent
                yield from parent.iter_ancestors(up_to_symbol_kind=up_to_symbol_kind)

    def get_parent(self) -> Self | None:
        parent_root = self.symbol_root.get("parent")
        if parent_root is None:
            return None
        return self.__class__(parent_root)

    def find(
        self,
        name_path_pattern: str,
        substring_matching: bool = False,
        include_kinds: Sequence[SymbolKind] | None = None,
        exclude_kinds: Sequence[SymbolKind] | None = None,
    ) -> list[Self]:
        """
        Find all symbols within the symbol's subtree that match the given name path pattern.

        :param name_path_pattern: the name path pattern to match against (see class :class:`NamePathMatcher` for details)
        :param substring_matching: whether to use substring matching (as opposed to exact matching)
            of the last segment of `name_path` against the symbol name.
        :param include_kinds: an optional sequence of ints representing the LSP symbol kind.
            If provided, only symbols of the given kinds will be included in the result.
        :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result.
        """
        result = []
        name_path_matcher = NamePathMatcher(name_path_pattern, substring_matching)

        def should_include(s: "LanguageServerSymbol") -> bool:
            if include_kinds is not None and s.symbol_kind not in include_kinds:
                return False
            if exclude_kinds is not None and s.symbol_kind in exclude_kinds:
                return False
            return name_path_matcher.matches_ls_symbol(s)

        def traverse(s: "LanguageServerSymbol") -> None:
            if should_include(s):
                result.append(s)
            for c in s.iter_children():
                traverse(c)

        traverse(self)
        return result

    def to_dict(
        self,
        kind: bool = False,
        location: bool = False,
        depth: int = 0,
        include_body: bool = False,
        include_children_body: bool = False,
        include_relative_path: bool = True,
        child_inclusion_predicate: Callable[[Self], bool] | None = None,
    ) -> dict[str, Any]:
        """
        Converts the symbol to a dictionary.

        :param kind: whether to include the kind of the symbol
        :param location: whether to include the location of the symbol
        :param depth: the depth up to which to include child symbols (0 = do not include children)
        :param include_body: whether to include the body of the top-level symbol.
        :param include_children_body: whether to also include the body of the children.
            Note that the body of the children is part of the body of the parent symbol,
            so there is usually no need to set this to True unless you want process the output
            and pass the children without passing the parent body to the LM.
        :param include_relative_path: whether to include the relative path of the symbol in the location
            entry. Relative paths of the symbol's children are always excluded.
        :param child_inclusion_predicate: an optional predicate that decides whether a child symbol
            should be included.
        :return: a dictionary representation of the symbol
        """
        result: dict[str, Any] = {"name": self.name, "name_path": self.get_name_path()}

        if kind:
            result["kind"] = self.kind

        if location:
            result["location"] = self.location.to_dict(include_relative_path=include_relative_path)
            body_start_line, body_end_line = self.get_body_line_numbers()
            result["body_location"] = {"start_line": body_start_line, "end_line": body_end_line}

        if include_body:
            if self.body is None:
                log.warning("Requested body for symbol, but it is not present. The symbol might have been loaded with include_body=False.")
            result["body"] = self.body

        if child_inclusion_predicate is None:
            child_inclusion_predicate = lambda s: True

        def included_children(s: Self) -> list[dict[str, Any]]:
            children = []
            for c in s.iter_children():
                if not child_inclusion_predicate(c):
                    continue
                children.append(
                    c.to_dict(
                        kind=kind,
                        location=location,
                        depth=depth - 1,
                        child_inclusion_predicate=child_inclusion_predicate,
                        include_body=include_children_body,
                        include_children_body=include_children_body,
                        # all children have the same relative path as the parent
                        include_relative_path=False,
                    )
                )
            return children

        if depth > 0:
            children = included_children(self)
            if len(children) > 0:
                result["children"] = included_children(self)

        return result


@dataclass
class ReferenceInLanguageServerSymbol(ToStringMixin):
    """
    Represents the location of a reference to another symbol within a symbol/file.

    The contained symbol is the symbol within which the reference is located,
    not the symbol that is referenced.
    """

    symbol: LanguageServerSymbol
    """
    the symbol within which the reference is located
    """
    line: int
    """
    the line number in which the reference is located (0-based)
    """
    character: int
    """
    the column number in which the reference is located (0-based)
    """

    @classmethod
    def from_lsp_reference(cls, reference: LSPReferenceInSymbol) -> Self:
        return cls(symbol=LanguageServerSymbol(reference.symbol), line=reference.line, character=reference.character)

    def get_relative_path(self) -> str | None:
        return self.symbol.location.relative_path


class LanguageServerSymbolRetriever:
    def __init__(self, ls: SolidLanguageServer | LanguageServerManager, agent: Union["SerenaAgent", None] = None) -> None:
        """
        :param ls: the language server or language server manager to use for symbol retrieval and editing operations.
        :param agent: the agent to use (only needed for marking files as modified). You can pass None if you don't
            need an agent to be aware of file modifications performed by the symbol manager.
        """
        if isinstance(ls, SolidLanguageServer):
            ls_manager = LanguageServerManager({ls.language: ls})
        else:
            ls_manager = ls
        assert isinstance(ls_manager, LanguageServerManager)
        self._ls_manager: LanguageServerManager = ls_manager
        self.agent = agent

    def _request_info(self, relative_file_path: str, line: int, column: int) -> str | None:
        """Retrieves information (in a sanitized format) about the symbol at the desired location,
        typically containing the docstring and signature.

        Returns None if no information is available.
        """
        lang_server = self.get_language_server(relative_file_path)
        hover_info = lang_server.request_hover(relative_file_path=relative_file_path, line=line, column=column)
        if hover_info is None:
            return None
        contents = hover_info["contents"]
        # Handle various response formats
        if isinstance(contents, list):
            # Array format: extract all parts and join them
            stripped_parts = []
            for part in contents:
                if isinstance(part, str) and (stripped_part := part.strip()):
                    stripped_parts.append(stripped_part)
                else:
                    # should be a dict with "value" key
                    stripped_parts.append(part["value"].strip())  # type: ignore
            return "\n".join(stripped_parts) if stripped_parts else None
        if isinstance(contents, dict) and (stripped_contents := contents.get("value", "").strip()):
            return stripped_contents
        if isinstance(contents, str) and (stripped_contents := contents.strip()):
            return stripped_contents
        return None

    def request_info_for_symbol(self, symbol: LanguageServerSymbol) -> str | None:
        if None in [symbol.relative_path, symbol.line, symbol.column]:
            return None
        return self._request_info(relative_file_path=symbol.relative_path, line=symbol.line, column=symbol.column)  # type: ignore[arg-type]

    def get_root_path(self) -> str:
        return self._ls_manager.get_root_path()

    def get_language_server(self, relative_path: str) -> SolidLanguageServer:
        return self._ls_manager.get_language_server(relative_path)

    def find(
        self,
        name_path_pattern: str,
        include_kinds: Sequence[SymbolKind] | None = None,
        exclude_kinds: Sequence[SymbolKind] | None = None,
        substring_matching: bool = False,
        within_relative_path: str | None = None,
    ) -> list[LanguageServerSymbol]:
        """
        Finds all symbols that match the given name path pattern (see class :class:`NamePathMatcher` for details),
        optionally limited to a specific file and filtered by kind.
        """
        symbols: list[LanguageServerSymbol] = []
        for lang_server in self._ls_manager.iter_language_servers():
            symbol_roots = lang_server.request_full_symbol_tree(within_relative_path=within_relative_path)
            for root in symbol_roots:
                symbols.extend(
                    LanguageServerSymbol(root).find(
                        name_path_pattern, include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching
                    )
                )
        return symbols

    def find_unique(
        self,
        name_path_pattern: str,
        include_kinds: Sequence[SymbolKind] | None = None,
        exclude_kinds: Sequence[SymbolKind] | None = None,
        substring_matching: bool = False,
        within_relative_path: str | None = None,
    ) -> LanguageServerSymbol:
        symbol_candidates = self.find(
            name_path_pattern,
            include_kinds=include_kinds,
            exclude_kinds=exclude_kinds,
            substring_matching=substring_matching,
            within_relative_path=within_relative_path,
        )
        if len(symbol_candidates) == 1:
            return symbol_candidates[0]
        elif len(symbol_candidates) == 0:
            raise ValueError(f"No symbol matching '{name_path_pattern}' found")
        else:
            # There are multiple candidates.
            # If only one of the candidates has the given pattern as its exact name path, return that one
            exact_matches = [s for s in symbol_candidates if s.get_name_path() == name_path_pattern]
            if len(exact_matches) == 1:
                return exact_matches[0]
            # otherwise, raise an error
            include_rel_path = within_relative_path is not None
            raise ValueError(
                f"Found multiple {len(symbol_candidates)} symbols matching '{name_path_pattern}'. "
                "They are: \n"
                + json.dumps([s.to_dict(kind=True, include_relative_path=include_rel_path) for s in symbol_candidates], indent=2)
            )

    def find_by_location(self, location: LanguageServerSymbolLocation) -> LanguageServerSymbol | None:
        if location.relative_path is None:
            return None
        lang_server = self.get_language_server(location.relative_path)
        document_symbols = lang_server.request_document_symbols(location.relative_path)
        for symbol_dict in document_symbols.iter_symbols():
            symbol = LanguageServerSymbol(symbol_dict)
            if symbol.location == location:
                return symbol
        return None

    def find_referencing_symbols(
        self,
        name_path: str,
        relative_file_path: str,
        include_body: bool = False,
        include_kinds: Sequence[SymbolKind] | None = None,
        exclude_kinds: Sequence[SymbolKind] | None = None,
    ) -> list[ReferenceInLanguageServerSymbol]:
        """
        Find all symbols that reference the specified symbol, which is assumed to be unique.

        :param name_path: the name path of the symbol to find. (While this can be a matching pattern, it should
            usually be the full path to ensure uniqueness.)
        :param relative_file_path: the relative path of the file in which the referenced symbol is defined.
        :param include_body: whether to include the body of all symbols in the result.
            Not recommended, as the referencing symbols will often be files, and thus the bodies will be very long.
        :param include_kinds: which kinds of symbols to include in the result.
        :param exclude_kinds: which kinds of symbols to exclude from the result.
        """
        symbol = self.find_unique(name_path, substring_matching=False, within_relative_path=relative_file_path)
        return self.find_referencing_symbols_by_location(
            symbol.location, include_body=include_body, include_kinds=include_kinds, exclude_kinds=exclude_kinds
        )

    def find_referencing_symbols_by_location(
        self,
        symbol_location: LanguageServerSymbolLocation,
        include_body: bool = False,
        include_kinds: Sequence[SymbolKind] | None = None,
        exclude_kinds: Sequence[SymbolKind] | None = None,
    ) -> list[ReferenceInLanguageServerSymbol]:
        """
        Find all symbols that reference the symbol at the given location.

        :param symbol_location: the location of the symbol for which to find references.
            Does not need to include an end_line, as it is unused in the search.
        :param include_body: whether to include the body of all symbols in the result.
            Not recommended, as the referencing symbols will often be files, and thus the bodies will be very long.
            Note: you can filter out the bodies of the children if you set include_children_body=False
            in the to_dict method.
        :param include_kinds: an optional sequence of ints representing the LSP symbol kind.
            If provided, only symbols of the given kinds will be included in the result.
        :param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result.
            Takes precedence over include_kinds.
        :return: a list of symbols that reference the given symbol
        """
        if not symbol_location.has_position_in_file():
            raise ValueError("Symbol location does not contain a valid position in a file")
        assert symbol_location.relative_path is not None
        assert symbol_location.line is not None
        assert symbol_location.column is not None
        lang_server = self.get_language_server(symbol_location.relative_path)
        references = lang_server.request_referencing_symbols(
            relative_file_path=symbol_location.relative_path,
            line=symbol_location.line,
            column=symbol_location.column,
            include_imports=False,
            include_self=False,
            include_body=include_body,
            include_file_symbols=True,
        )

        if include_kinds is not None:
            references = [s for s in references if s.symbol["kind"] in include_kinds]

        if exclude_kinds is not None:
            references = [s for s in references if s.symbol["kind"] not in exclude_kinds]

        return [ReferenceInLanguageServerSymbol.from_lsp_reference(r) for r in references]

    def get_symbol_overview(self, relative_path: str, depth: int = 0) -> dict[str, list[dict]]:
        """
        :param relative_path: the path of the file or directory for which to get the symbol overview
        :param depth: the depth up to which to include child symbols (0 = only top-level symbols)
        :return: a mapping from file paths to lists of symbol dictionaries.
            For the case where a file is passed, the mapping will contain a single entry.
        """
        lang_server = self.get_language_server(relative_path)
        path_to_unified_symbols = lang_server.request_overview(relative_path)

        def child_inclusion_predicate(s: LanguageServerSymbol) -> bool:
            return not s.is_low_level()

        result = {}
        for file_path, unified_symbols in path_to_unified_symbols.items():
            symbols_in_file = []
            for us in unified_symbols:
                symbol = LanguageServerSymbol(us)
                symbols_in_file.append(
                    symbol.to_dict(
                        depth=depth,
                        kind=True,
                        include_relative_path=False,
                        location=False,
                        child_inclusion_predicate=child_inclusion_predicate,
                    )
                )
            result[file_path] = symbols_in_file

        return result


class JetBrainsSymbol(Symbol):
    def __init__(self, symbol_dict: jb.SymbolDTO, project: Project) -> None:
        """
        :param symbol_dict: dictionary as returned by the JetBrains plugin client.
        """
        self._project = project
        self._dict = symbol_dict
        self._cached_file_content: str | None = None
        self._cached_body_start_position: PositionInFile | None = None
        self._cached_body_end_position: PositionInFile | None = None

    def _tostring_includes(self) -> list[str]:
        return []

    def _tostring_additional_entries(self) -> dict[str, Any]:
        return dict(name_path=self.get_name_path(), relative_path=self.get_relative_path(), type=self._dict["type"])

    def get_name_path(self) -> str:
        return self._dict["name_path"]

    def get_relative_path(self) -> str:
        return self._dict["relative_path"]

    def get_file_content(self) -> str:
        if self._cached_file_content is None:
            path = os.path.join(self._project.project_root, self.get_relative_path())
            with open(path, encoding=self._project.project_config.encoding) as f:
                self._cached_file_content = f.read()
        return self._cached_file_content

    def is_position_in_file_available(self) -> bool:
        return "text_range" in self._dict

    def get_body_start_position(self) -> PositionInFile | None:
        if not self.is_position_in_file_available():
            return None
        if self._cached_body_start_position is None:
            pos = self._dict["text_range"]["start_pos"]
            line, col = pos["line"], pos["col"]
            self._cached_body_start_position = PositionInFile(line=line, col=col)
        return self._cached_body_start_position

    def get_body_end_position(self) -> PositionInFile | None:
        if not self.is_position_in_file_available():
            return None
        if self._cached_body_end_position is None:
            pos = self._dict["text_range"]["end_pos"]
            line, col = pos["line"], pos["col"]
            self._cached_body_end_position = PositionInFile(line=line, col=col)
        return self._cached_body_end_position

    def is_neighbouring_definition_separated_by_empty_line(self) -> bool:
        # NOTE: Symbol types cannot really be differentiated, because types are not handled in a language-agnostic way.
        return False

```

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

```python
"""
CSharp Language Server using Microsoft.CodeAnalysis.LanguageServer (Official Roslyn-based LSP server)
"""

import json
import logging
import os
import platform
import shutil
import subprocess
import tarfile
import threading
import urllib.request
import zipfile
from collections.abc import Iterable
from pathlib import Path
from typing import Any, cast

from overrides import override

from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_utils import PathUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from solidlsp.util.zip import SafeZipExtractor

from .common import RuntimeDependency, RuntimeDependencyCollection

log = logging.getLogger(__name__)

_RUNTIME_DEPENDENCIES = [
    RuntimeDependency(
        id="CSharpLanguageServer",
        description="Microsoft.CodeAnalysis.LanguageServer for Windows (x64)",
        package_name="Microsoft.CodeAnalysis.LanguageServer.win-x64",
        package_version="5.0.0-1.25329.6",
        platform_id="win-x64",
        archive_type="nupkg",
        binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
        extract_path="content/LanguageServer/win-x64",
    ),
    RuntimeDependency(
        id="CSharpLanguageServer",
        description="Microsoft.CodeAnalysis.LanguageServer for Windows (ARM64)",
        package_name="Microsoft.CodeAnalysis.LanguageServer.win-arm64",
        package_version="5.0.0-1.25329.6",
        platform_id="win-arm64",
        archive_type="nupkg",
        binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
        extract_path="content/LanguageServer/win-arm64",
    ),
    RuntimeDependency(
        id="CSharpLanguageServer",
        description="Microsoft.CodeAnalysis.LanguageServer for macOS (x64)",
        package_name="Microsoft.CodeAnalysis.LanguageServer.osx-x64",
        package_version="5.0.0-1.25329.6",
        platform_id="osx-x64",
        archive_type="nupkg",
        binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
        extract_path="content/LanguageServer/osx-x64",
    ),
    RuntimeDependency(
        id="CSharpLanguageServer",
        description="Microsoft.CodeAnalysis.LanguageServer for macOS (ARM64)",
        package_name="Microsoft.CodeAnalysis.LanguageServer.osx-arm64",
        package_version="5.0.0-1.25329.6",
        platform_id="osx-arm64",
        archive_type="nupkg",
        binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
        extract_path="content/LanguageServer/osx-arm64",
    ),
    RuntimeDependency(
        id="CSharpLanguageServer",
        description="Microsoft.CodeAnalysis.LanguageServer for Linux (x64)",
        package_name="Microsoft.CodeAnalysis.LanguageServer.linux-x64",
        package_version="5.0.0-1.25329.6",
        platform_id="linux-x64",
        archive_type="nupkg",
        binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
        extract_path="content/LanguageServer/linux-x64",
    ),
    RuntimeDependency(
        id="CSharpLanguageServer",
        description="Microsoft.CodeAnalysis.LanguageServer for Linux (ARM64)",
        package_name="Microsoft.CodeAnalysis.LanguageServer.linux-arm64",
        package_version="5.0.0-1.25329.6",
        platform_id="linux-arm64",
        archive_type="nupkg",
        binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
        extract_path="content/LanguageServer/linux-arm64",
    ),
    RuntimeDependency(
        id="DotNetRuntime",
        description=".NET 9 Runtime for Windows (x64)",
        url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-x64.zip",
        platform_id="win-x64",
        archive_type="zip",
        binary_name="dotnet.exe",
    ),
    RuntimeDependency(
        id="DotNetRuntime",
        description=".NET 9 Runtime for Linux (x64)",
        url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-x64.tar.gz",
        platform_id="linux-x64",
        archive_type="tar.gz",
        binary_name="dotnet",
    ),
    RuntimeDependency(
        id="DotNetRuntime",
        description=".NET 9 Runtime for Linux (ARM64)",
        url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-arm64.tar.gz",
        platform_id="linux-arm64",
        archive_type="tar.gz",
        binary_name="dotnet",
    ),
    RuntimeDependency(
        id="DotNetRuntime",
        description=".NET 9 Runtime for macOS (x64)",
        url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-osx-x64.tar.gz",
        platform_id="osx-x64",
        archive_type="tar.gz",
        binary_name="dotnet",
    ),
    RuntimeDependency(
        id="DotNetRuntime",
        description=".NET 9 Runtime for macOS (ARM64)",
        url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-osx-arm64.tar.gz",
        platform_id="osx-arm64",
        archive_type="tar.gz",
        binary_name="dotnet",
    ),
]


def breadth_first_file_scan(root_dir: str) -> Iterable[str]:
    """
    Perform a breadth-first scan of files in the given directory.
    Yields file paths in breadth-first order.
    """
    queue = [root_dir]
    while queue:
        current_dir = queue.pop(0)
        try:
            for item in os.listdir(current_dir):
                if item.startswith("."):
                    continue
                item_path = os.path.join(current_dir, item)
                if os.path.isdir(item_path):
                    queue.append(item_path)
                elif os.path.isfile(item_path):
                    yield item_path
        except (PermissionError, OSError):
            # Skip directories we can't access
            pass


def find_solution_or_project_file(root_dir: str) -> str | None:
    """
    Find the first .sln file in breadth-first order.
    If no .sln file is found, look for a .csproj file.
    """
    sln_file = None
    csproj_file = None

    for filename in breadth_first_file_scan(root_dir):
        if filename.endswith(".sln") and sln_file is None:
            sln_file = filename
        elif filename.endswith(".csproj") and csproj_file is None:
            csproj_file = filename

        # If we found a .sln file, return it immediately
        if sln_file:
            return sln_file

    # If no .sln file was found, return the first .csproj file
    return csproj_file


class CSharpLanguageServer(SolidLanguageServer):
    """
    Provides C# specific instantiation of the LanguageServer class using `Microsoft.CodeAnalysis.LanguageServer`,
    the official Roslyn-based language server from Microsoft.

    You can pass a list of runtime dependency overrides in ls_specific_settings["csharp"]. This is a list of
    dicts, each containing at least the "id" key, and optionally "platform_id" to uniquely identify the dependency to override.
    For example, to override the URL of the .NET runtime on windows-x64, add the entry:

    ```
        {
            "id": "DotNetRuntime",
            "platform_id": "win-x64",
            "url": "https://example.com/custom-dotnet-runtime.zip"
        }
    ```

    See the `_RUNTIME_DEPENDENCIES` variable above for the available dependency ids and platform_ids.
    """

    def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
        """
        Creates a CSharpLanguageServer instance. This class is not meant to be instantiated directly.
        Use LanguageServer.create() instead.
        """
        dotnet_path, language_server_path = self._ensure_server_installed(config, solidlsp_settings)

        # Find solution or project file
        solution_or_project = find_solution_or_project_file(repository_root_path)

        # Create log directory
        log_dir = Path(self.ls_resources_dir(solidlsp_settings)) / "logs"
        log_dir.mkdir(parents=True, exist_ok=True)

        # Build command using dotnet directly
        cmd = [dotnet_path, language_server_path, "--logLevel=Information", f"--extensionLogDirectory={log_dir}", "--stdio"]

        # The language server will discover the solution/project from the workspace root
        if solution_or_project:
            log.info(f"Found solution/project file: {solution_or_project}")
        else:
            log.warning("No .sln or .csproj file found, language server will attempt auto-discovery")

        log.debug(f"Language server command: {' '.join(cmd)}")

        super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", solidlsp_settings)

        self.initialization_complete = threading.Event()

    @override
    def is_ignored_dirname(self, dirname: str) -> bool:
        return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]

    @classmethod
    def _ensure_server_installed(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[str, str]:
        """
        Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available.
        Returns a tuple of (dotnet_path, language_server_dll_path).
        """
        language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance())
        runtime_dependency_overrides = cast(list[dict[str, Any]], language_specific_config.get("runtime_dependencies", []))

        log.debug("Resolving runtime dependencies")

        runtime_dependencies = RuntimeDependencyCollection(
            _RUNTIME_DEPENDENCIES,
            overrides=runtime_dependency_overrides,
        )

        log.debug(
            f"Available runtime dependencies: {runtime_dependencies.get_dependencies_for_current_platform}",
        )

        # Find the dependencies for our platform
        lang_server_dep = runtime_dependencies.get_single_dep_for_current_platform("CSharpLanguageServer")
        dotnet_runtime_dep = runtime_dependencies.get_single_dep_for_current_platform("DotNetRuntime")
        dotnet_path = CSharpLanguageServer._ensure_dotnet_runtime(dotnet_runtime_dep, solidlsp_settings)
        server_dll_path = CSharpLanguageServer._ensure_language_server(lang_server_dep, solidlsp_settings)

        return dotnet_path, server_dll_path

    @classmethod
    def _ensure_dotnet_runtime(cls, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str:
        """Ensure .NET runtime is available and return the dotnet executable path."""
        # TODO: use RuntimeDependency util methods instead of custom validation/download logic

        # Check if dotnet is already available on the system
        system_dotnet = shutil.which("dotnet")
        if system_dotnet:
            # Check if it's .NET 9
            try:
                result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
                if "Microsoft.NETCore.App 9." in result.stdout:
                    log.info("Found system .NET 9 runtime")
                    return system_dotnet
            except subprocess.CalledProcessError:
                pass

        # Download .NET 9 runtime using config
        return cls._ensure_dotnet_runtime_from_config(dotnet_runtime_dep, solidlsp_settings)

    @classmethod
    def _ensure_language_server(cls, lang_server_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str:
        """Ensure language server is available and return the DLL path."""
        package_name = lang_server_dep.package_name
        package_version = lang_server_dep.package_version

        server_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / f"{package_name}.{package_version}"
        assert lang_server_dep.binary_name is not None
        server_dll = server_dir / lang_server_dep.binary_name

        if server_dll.exists():
            log.info(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}")
            return str(server_dll)

        # Download and install the language server
        log.info(f"Downloading {package_name} version {package_version}...")
        assert package_version is not None
        assert package_name is not None
        package_path = cls._download_nuget_package_direct(package_name, package_version, solidlsp_settings)

        # Extract and install
        cls._extract_language_server(lang_server_dep, package_path, server_dir)

        if not server_dll.exists():
            raise SolidLSPException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction")

        # Make executable on Unix systems
        if platform.system().lower() != "windows":
            server_dll.chmod(0o755)

        log.info(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}")
        return str(server_dll)

    @staticmethod
    def _extract_language_server(lang_server_dep: RuntimeDependency, package_path: Path, server_dir: Path) -> None:
        """Extract language server files from downloaded package."""
        extract_path = lang_server_dep.extract_path or "lib/net9.0"
        source_dir = package_path / extract_path

        if not source_dir.exists():
            # Try alternative locations
            for possible_dir in [
                package_path / "tools" / "net9.0" / "any",
                package_path / "lib" / "net9.0",
                package_path / "contentFiles" / "any" / "net9.0",
            ]:
                if possible_dir.exists():
                    source_dir = possible_dir
                    break
            else:
                raise SolidLSPException(f"Could not find language server files in package. Searched in {package_path}")

        # Copy files to cache directory
        server_dir.mkdir(parents=True, exist_ok=True)
        shutil.copytree(source_dir, server_dir, dirs_exist_ok=True)

    @classmethod
    def _download_nuget_package_direct(cls, package_name: str, package_version: str, solidlsp_settings: SolidLSPSettings) -> Path:
        """
        Download a NuGet package directly from the Azure NuGet feed.
        Returns the path to the extracted package directory.
        """
        azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json"

        # Create temporary directory for package download
        temp_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "temp_downloads"
        temp_dir.mkdir(parents=True, exist_ok=True)

        try:
            # First, get the service index from the Azure feed
            log.debug("Fetching NuGet service index from Azure feed...")
            with urllib.request.urlopen(azure_feed_url) as response:
                service_index = json.loads(response.read().decode())

            # Find the package base address (for downloading packages)
            package_base_address = None
            for resource in service_index.get("resources", []):
                if resource.get("@type") == "PackageBaseAddress/3.0.0":
                    package_base_address = resource.get("@id")
                    break

            if not package_base_address:
                raise SolidLSPException("Could not find package base address in Azure NuGet feed")

            # Construct the download URL for the specific package
            package_id_lower = package_name.lower()
            package_version_lower = package_version.lower()
            package_url = f"{package_base_address.rstrip('/')}/{package_id_lower}/{package_version_lower}/{package_id_lower}.{package_version_lower}.nupkg"

            log.debug(f"Downloading package from: {package_url}")

            # Download the .nupkg file
            nupkg_file = temp_dir / f"{package_name}.{package_version}.nupkg"
            urllib.request.urlretrieve(package_url, nupkg_file)

            # Extract the .nupkg file (it's just a zip file)
            package_extract_dir = temp_dir / f"{package_name}.{package_version}"
            package_extract_dir.mkdir(exist_ok=True)

            # Use SafeZipExtractor to handle long paths and skip errors
            extractor = SafeZipExtractor(archive_path=nupkg_file, extract_dir=package_extract_dir, verbose=False)
            extractor.extract_all()

            # Clean up the nupkg file
            nupkg_file.unlink()

            log.info(f"Successfully downloaded and extracted {package_name} version {package_version}")
            return package_extract_dir

        except Exception as e:
            raise SolidLSPException(
                f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}"
            ) from e

    @classmethod
    def _ensure_dotnet_runtime_from_config(cls, dotnet_runtime_dep: RuntimeDependency, solidlsp_settings: SolidLSPSettings) -> str:
        """
        Ensure .NET 9 runtime is available using runtime dependency configuration.
        Returns the path to the dotnet executable.
        """
        # TODO: use RuntimeDependency util methods instead of custom download logic

        # Check if dotnet is already available on the system
        system_dotnet = shutil.which("dotnet")
        if system_dotnet:
            # Check if it's .NET 9
            try:
                result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
                if "Microsoft.NETCore.App 9." in result.stdout:
                    log.info("Found system .NET 9 runtime")
                    return system_dotnet
            except subprocess.CalledProcessError:
                pass

        # Download .NET 9 runtime using config
        dotnet_dir = Path(cls.ls_resources_dir(solidlsp_settings)) / "dotnet-runtime-9.0"
        assert dotnet_runtime_dep.binary_name is not None, "Runtime dependency must have a binary_name"
        dotnet_exe = dotnet_dir / dotnet_runtime_dep.binary_name

        if dotnet_exe.exists():
            log.info(f"Using cached .NET runtime from {dotnet_exe}")
            return str(dotnet_exe)

        # Download .NET runtime
        log.info("Downloading .NET 9 runtime...")
        dotnet_dir.mkdir(parents=True, exist_ok=True)

        custom_settings = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance())
        custom_dotnet_runtime_url = custom_settings.get("dotnet_runtime_url")
        if custom_dotnet_runtime_url is not None:
            log.info(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}")
            url = custom_dotnet_runtime_url
        else:
            url = dotnet_runtime_dep.url

        archive_type = dotnet_runtime_dep.archive_type

        # Download the runtime
        download_path = dotnet_dir / f"dotnet-runtime.{archive_type}"
        try:
            log.debug(f"Downloading from {url}")
            urllib.request.urlretrieve(url, download_path)

            # Extract the archive
            if archive_type == "zip":
                with zipfile.ZipFile(download_path, "r") as zip_ref:
                    zip_ref.extractall(dotnet_dir)
            else:
                # tar.gz
                with tarfile.open(download_path, "r:gz") as tar_ref:
                    tar_ref.extractall(dotnet_dir)

            # Remove the archive
            download_path.unlink()

            # Make dotnet executable on Unix
            if platform.system().lower() != "windows":
                dotnet_exe.chmod(0o755)

            log.info(f"Successfully installed .NET 9 runtime to {dotnet_exe}")
            return str(dotnet_exe)

        except Exception as e:
            raise SolidLSPException(f"Failed to download .NET 9 runtime from {url}: {e}") from e

    def _get_initialize_params(self) -> InitializeParams:
        """
        Returns the initialize params for the Microsoft.CodeAnalysis.LanguageServer.
        """
        root_uri = PathUtils.path_to_uri(self.repository_root_path)
        root_name = os.path.basename(self.repository_root_path)
        return cast(
            InitializeParams,
            {
                "workspaceFolders": [{"uri": root_uri, "name": root_name}],
                "processId": os.getpid(),
                "rootPath": self.repository_root_path,
                "rootUri": root_uri,
                "capabilities": {
                    "window": {
                        "workDoneProgress": True,
                        "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}},
                        "showDocument": {"support": True},
                    },
                    "workspace": {
                        "applyEdit": True,
                        "workspaceEdit": {"documentChanges": True},
                        "didChangeConfiguration": {"dynamicRegistration": True},
                        "didChangeWatchedFiles": {"dynamicRegistration": True},
                        "symbol": {
                            "dynamicRegistration": True,
                            "symbolKind": {"valueSet": list(range(1, 27))},
                        },
                        "executeCommand": {"dynamicRegistration": True},
                        "configuration": True,
                        "workspaceFolders": True,
                        "workDoneProgress": True,
                    },
                    "textDocument": {
                        "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
                        "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
                        "signatureHelp": {
                            "dynamicRegistration": True,
                            "signatureInformation": {
                                "documentationFormat": ["markdown", "plaintext"],
                                "parameterInformation": {"labelOffsetSupport": True},
                            },
                        },
                        "definition": {"dynamicRegistration": True},
                        "references": {"dynamicRegistration": True},
                        "documentSymbol": {
                            "dynamicRegistration": True,
                            "symbolKind": {"valueSet": list(range(1, 27))},
                            "hierarchicalDocumentSymbolSupport": True,
                        },
                    },
                },
            },
        )

    def _start_server(self) -> None:
        def do_nothing(params: dict) -> None:
            return

        def window_log_message(msg: dict) -> None:
            """Log messages from the language server."""
            message_text = msg.get("message", "")
            level = msg.get("type", 4)  # Default to Log level

            # Map LSP message types to Python logging levels
            level_map = {1: logging.ERROR, 2: logging.WARNING, 3: logging.INFO, 4: logging.DEBUG}  # Error  # Warning  # Info  # Log

            log.log(level_map.get(level, logging.DEBUG), f"LSP: {message_text}")

        def handle_progress(params: dict) -> None:
            """Handle progress notifications from the language server."""
            token = params.get("token", "")
            value = params.get("value", {})

            # Log raw progress for debugging
            log.debug(f"Progress notification received: {params}")

            # Handle different progress notification types
            kind = value.get("kind")

            if kind == "begin":
                title = value.get("title", "Operation in progress")
                message = value.get("message", "")
                percentage = value.get("percentage")

                if percentage is not None:
                    log.debug(f"Progress [{token}]: {title} - {message} ({percentage}%)")
                else:
                    log.info(f"Progress [{token}]: {title} - {message}")

            elif kind == "report":
                message = value.get("message", "")
                percentage = value.get("percentage")

                if percentage is not None:
                    log.info(f"Progress [{token}]: {message} ({percentage}%)")
                elif message:
                    log.info(f"Progress [{token}]: {message}")

            elif kind == "end":
                message = value.get("message", "Operation completed")
                log.info(f"Progress [{token}]: {message}")

        def handle_workspace_configuration(params: dict) -> list:
            """Handle workspace/configuration requests from the server."""
            items = params.get("items", [])
            result: list[Any] = []

            for item in items:
                section = item.get("section", "")

                # Provide default values based on the configuration section
                if section.startswith(("dotnet", "csharp")):
                    # Default configuration for C# settings
                    if "enable" in section or "show" in section or "suppress" in section or "navigate" in section:
                        # Boolean settings
                        result.append(False)
                    elif "scope" in section:
                        # Scope settings - use appropriate enum values
                        if "analyzer_diagnostics_scope" in section:
                            result.append("openFiles")  # BackgroundAnalysisScope
                        elif "compiler_diagnostics_scope" in section:
                            result.append("openFiles")  # CompilerDiagnosticsScope
                        else:
                            result.append("openFiles")
                    elif section == "dotnet_member_insertion_location":
                        # ImplementTypeInsertionBehavior enum
                        result.append("with_other_members_of_the_same_kind")
                    elif section == "dotnet_property_generation_behavior":
                        # ImplementTypePropertyGenerationBehavior enum
                        result.append("prefer_throwing_properties")
                    elif "location" in section or "behavior" in section:
                        # Other enum settings - return null to avoid parsing errors
                        result.append(None)
                    else:
                        # Default for other dotnet/csharp settings
                        result.append(None)
                elif section == "tab_width" or section == "indent_size":
                    # Tab and indent settings
                    result.append(4)
                elif section == "insert_final_newline":
                    # Editor settings
                    result.append(True)
                else:
                    # Unknown configuration - return null
                    result.append(None)

            return result

        def handle_work_done_progress_create(params: dict) -> None:
            """Handle work done progress create requests."""
            # Just acknowledge the request
            return

        def handle_register_capability(params: dict) -> None:
            """Handle client/registerCapability requests."""
            # Just acknowledge the request - we don't need to track these for now
            return

        def handle_project_needs_restore(params: dict) -> None:
            return

        def handle_workspace_indexing_complete(params: dict) -> None:
            self.completions_available.set()

        # Set up notification handlers
        self.server.on_notification("window/logMessage", window_log_message)
        self.server.on_notification("$/progress", handle_progress)
        self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
        self.server.on_notification("workspace/projectInitializationComplete", handle_workspace_indexing_complete)
        self.server.on_request("workspace/configuration", handle_workspace_configuration)
        self.server.on_request("window/workDoneProgress/create", handle_work_done_progress_create)
        self.server.on_request("client/registerCapability", handle_register_capability)
        self.server.on_request("workspace/_roslyn_projectNeedsRestore", handle_project_needs_restore)

        log.info("Starting Microsoft.CodeAnalysis.LanguageServer process")

        try:
            self.server.start()
        except Exception as e:
            log.info(f"Failed to start language server process: {e}", logging.ERROR)
            raise SolidLSPException(f"Failed to start C# language server: {e}")

        # Send initialization
        initialize_params = self._get_initialize_params()

        log.info("Sending initialize request to language server")
        try:
            init_response = self.server.send.initialize(initialize_params)
            log.info(f"Received initialize response: {init_response}")
        except Exception as e:
            raise SolidLSPException(f"Failed to initialize C# language server for {self.repository_root_path}: {e}") from e

        # Apply diagnostic capabilities
        self._force_pull_diagnostics(init_response)

        # Verify required capabilities
        capabilities = init_response.get("capabilities", {})
        required_capabilities = [
            "textDocumentSync",
            "definitionProvider",
            "referencesProvider",
            "documentSymbolProvider",
        ]
        missing = [cap for cap in required_capabilities if cap not in capabilities]
        if missing:
            raise RuntimeError(
                f"Language server is missing required capabilities: {', '.join(missing)}. "
                "Initialization failed. Please ensure the correct version of Microsoft.CodeAnalysis.LanguageServer is installed and the .NET runtime is working."
            )

        # Complete initialization
        self.server.notify.initialized({})

        # Open solution and project files
        self._open_solution_and_projects()

        self.initialization_complete.set()

        log.info(
            "Microsoft.CodeAnalysis.LanguageServer initialized and ready\n"
            "Waiting for language server to index project files...\n"
            "This may take a while for large projects"
        )

        if self.completions_available.wait(30):  # Wait up to 30 seconds for indexing
            log.info("Indexing complete")
        else:
            log.warning("Timeout waiting for indexing to complete, proceeding anyway")
            self.completions_available.set()

    def _force_pull_diagnostics(self, init_response: dict | InitializeResult) -> None:
        """
        Apply the diagnostic capabilities hack.
        Forces the server to support pull diagnostics.
        """
        capabilities = init_response.get("capabilities", {})
        diagnostic_provider: Any = capabilities.get("diagnosticProvider", {})

        # Add the diagnostic capabilities hack
        if isinstance(diagnostic_provider, dict):
            diagnostic_provider.update(
                {
                    "interFileDependencies": True,
                    "workDoneProgress": True,
                    "workspaceDiagnostics": True,
                }
            )
            log.debug("Applied diagnostic capabilities hack for better C# diagnostics")

    def _open_solution_and_projects(self) -> None:
        """
        Open solution and project files using notifications.
        """
        # Find solution file
        solution_file = None
        for filename in breadth_first_file_scan(self.repository_root_path):
            if filename.endswith(".sln"):
                solution_file = filename
                break

        # Send solution/open notification if solution file found
        if solution_file:
            solution_uri = PathUtils.path_to_uri(solution_file)
            self.server.notify.send_notification("solution/open", {"solution": solution_uri})
            log.debug(f"Opened solution file: {solution_file}")

        # Find and open project files
        project_files = []
        for filename in breadth_first_file_scan(self.repository_root_path):
            if filename.endswith(".csproj"):
                project_files.append(filename)

        # Send project/open notifications for each project file
        if project_files:
            project_uris = [PathUtils.path_to_uri(project_file) for project_file in project_files]
            self.server.notify.send_notification("project/open", {"projects": project_uris})
            log.debug(f"Opened project files: {project_files}")

    @override
    def _get_wait_time_for_cross_file_referencing(self) -> float:
        return 2

```

--------------------------------------------------------------------------------
/test/solidlsp/ruby/test_ruby_symbol_retrieval.py:
--------------------------------------------------------------------------------

```python
"""
Tests for the Ruby language server symbol-related functionality.

These tests focus on the following methods:
- request_containing_symbol
- request_referencing_symbols
- request_defining_symbol
- request_document_symbols integration
"""

import os

import pytest

from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind

pytestmark = pytest.mark.ruby


class TestRubyLanguageServerSymbols:
    """Test the Ruby language server's symbol-related functionality."""

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_containing_symbol_method(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a method."""
        # Test for a position inside the create_user method
        file_path = os.path.join("services.rb")
        # Look for a position inside the create_user method body
        containing_symbol = language_server.request_containing_symbol(file_path, 11, 10, include_body=True)

        # Verify that we found the containing symbol
        assert containing_symbol is not None, "Should find containing symbol for method position"
        assert containing_symbol["name"] == "create_user", f"Expected 'create_user', got '{containing_symbol['name']}'"
        assert (
            containing_symbol["kind"] == SymbolKind.Method.value
        ), f"Expected Method kind ({SymbolKind.Method.value}), got {containing_symbol['kind']}"

        # Verify location information
        assert "location" in containing_symbol, "Containing symbol should have location information"
        location = containing_symbol["location"]
        assert "range" in location, "Location should contain range information"
        assert "start" in location["range"], "Range should have start position"
        assert "end" in location["range"], "Range should have end position"

        # Verify container information
        if "containerName" in containing_symbol:
            assert containing_symbol["containerName"] in [
                "Services::UserService",
                "UserService",
            ], f"Expected UserService container, got '{containing_symbol['containerName']}'"

        # Verify body content if available
        if "body" in containing_symbol:
            body = containing_symbol["body"]
            assert "def create_user" in body, "Method body should contain method definition"
            assert len(body.strip()) > 0, "Method body should not be empty"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a class."""
        # Test for a position inside the UserService class but outside any method
        file_path = os.path.join("services.rb")
        # Line around the class definition
        containing_symbol = language_server.request_containing_symbol(file_path, 5, 5)

        # Verify that we found the containing symbol
        assert containing_symbol is not None, "Should find containing symbol for class position"
        assert containing_symbol["name"] == "UserService", f"Expected 'UserService', got '{containing_symbol['name']}'"
        assert (
            containing_symbol["kind"] == SymbolKind.Class.value
        ), f"Expected Class kind ({SymbolKind.Class.value}), got {containing_symbol['kind']}"

        # Verify location information exists
        assert "location" in containing_symbol, "Class symbol should have location information"
        location = containing_symbol["location"]
        assert "range" in location, "Location should contain range"
        assert "start" in location["range"] and "end" in location["range"], "Range should have start and end positions"

        # Verify the class is properly nested in the Services module
        if "containerName" in containing_symbol:
            assert (
                containing_symbol["containerName"] == "Services"
            ), f"Expected 'Services' as container, got '{containing_symbol['containerName']}'"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_containing_symbol_module(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a module context."""
        # Test that we can find the Services module in document symbols
        file_path = os.path.join("services.rb")
        symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()

        # Verify Services module appears in document symbols
        services_module = None
        for symbol in symbols:
            if symbol.get("name") == "Services" and symbol.get("kind") == SymbolKind.Module:
                services_module = symbol
                break

        assert services_module is not None, "Services module not found in document symbols"

        # Test that UserService class has Services as container
        # Position inside UserService class
        containing_symbol = language_server.request_containing_symbol(file_path, 4, 8)
        assert containing_symbol is not None
        assert containing_symbol["name"] == "UserService"
        assert containing_symbol["kind"] == SymbolKind.Class
        # Verify the module context is preserved in containerName (if supported by the language server)
        # ruby-lsp doesn't provide containerName, but Solargraph does
        if "containerName" in containing_symbol:
            assert containing_symbol.get("containerName") == "Services"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_containing_symbol_nested_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol with nested classes."""
        # Test for a position inside a nested class method
        file_path = os.path.join("nested.rb")
        # Position inside NestedClass.find_me method
        containing_symbol = language_server.request_containing_symbol(file_path, 20, 10)

        # Verify that we found the innermost containing symbol
        assert containing_symbol is not None
        assert containing_symbol["name"] == "find_me"
        assert containing_symbol["kind"] == SymbolKind.Method

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a position with no containing symbol."""
        # Test for a position outside any class/method (e.g., in requires)
        file_path = os.path.join("services.rb")
        # Line 1 is a require statement, not inside any class or method
        containing_symbol = language_server.request_containing_symbol(file_path, 1, 5)

        # Should return None or an empty dictionary
        assert containing_symbol is None or containing_symbol == {}

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_referencing_symbols_method(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a method."""
        # Test referencing symbols for create_user method
        file_path = os.path.join("services.rb")
        # Line containing the create_user method definition
        symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
        create_user_symbol = None

        # Find create_user method in the document symbols (Ruby returns flat list)
        for symbol in symbols:
            if symbol.get("name") == "create_user":
                create_user_symbol = symbol
                break

        if not create_user_symbol or "selectionRange" not in create_user_symbol:
            pytest.skip("create_user symbol or its selectionRange not found")

        sel_start = create_user_symbol["selectionRange"]["start"]
        ref_symbols = [
            ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
        ]

        # We might not have references in our simple test setup, so just verify structure
        for symbol in ref_symbols:
            assert "name" in symbol
            assert "kind" in symbol

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a class."""
        # Test referencing symbols for User class
        file_path = os.path.join("models.rb")
        # Find User class in document symbols
        symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
        user_symbol = None

        for symbol in symbols:
            if symbol.get("name") == "User":
                user_symbol = symbol
                break

        if not user_symbol or "selectionRange" not in user_symbol:
            pytest.skip("User symbol or its selectionRange not found")

        sel_start = user_symbol["selectionRange"]["start"]
        ref_symbols = [
            ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
        ]

        # Verify structure of referencing symbols
        for symbol in ref_symbols:
            assert "name" in symbol
            assert "kind" in symbol
            if "location" in symbol and "range" in symbol["location"]:
                assert "start" in symbol["location"]["range"]
                assert "end" in symbol["location"]["range"]

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a variable usage."""
        # Test finding the definition of a variable in a method
        file_path = os.path.join("services.rb")
        # Look for @users variable usage
        defining_symbol = language_server.request_defining_symbol(file_path, 12, 10)

        # This test might fail if the language server doesn't support it well
        if defining_symbol is not None:
            assert "name" in defining_symbol
            assert "kind" in defining_symbol

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_defining_symbol_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a class reference."""
        # Test finding the definition of the User class used in services
        file_path = os.path.join("services.rb")
        # Line that references User class
        defining_symbol = language_server.request_defining_symbol(file_path, 11, 15)

        # This might not work perfectly in all Ruby language servers
        if defining_symbol is not None:
            assert "name" in defining_symbol
            # The name might be "User" or the method that contains it
            assert defining_symbol.get("name") is not None

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a position with no symbol."""
        # Test for a position with no symbol (e.g., whitespace or comment)
        file_path = os.path.join("services.rb")
        # Line 3 is likely a blank line or comment
        defining_symbol = language_server.request_defining_symbol(file_path, 3, 0)

        # Should return None for positions with no symbol
        assert defining_symbol is None or defining_symbol == {}

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_defining_symbol_nested_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for nested class access."""
        # Test finding definition of NestedClass
        file_path = os.path.join("nested.rb")
        # Position where NestedClass is referenced
        defining_symbol = language_server.request_defining_symbol(file_path, 44, 25)

        # This is challenging for many language servers
        if defining_symbol is not None:
            assert "name" in defining_symbol
            assert defining_symbol.get("name") in ["NestedClass", "OuterClass"]

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
        """Test the integration between different symbol-related methods."""
        file_path = os.path.join("models.rb")

        # Step 1: Find a method we know exists
        containing_symbol = language_server.request_containing_symbol(file_path, 8, 5)  # inside initialize method
        if containing_symbol is not None:
            assert containing_symbol["name"] == "initialize"

            # Step 2: Get the defining symbol for the same position
            defining_symbol = language_server.request_defining_symbol(file_path, 8, 5)
            if defining_symbol is not None:
                assert defining_symbol["name"] == "initialize"

                # Step 3: Verify that they refer to the same symbol type
                assert defining_symbol["kind"] == containing_symbol["kind"]

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_symbol_tree_structure_basic(self, language_server: SolidLanguageServer) -> None:
        """Test that the symbol tree structure includes Ruby symbols."""
        # Get all symbols in the test repository
        repo_structure = language_server.request_full_symbol_tree()
        assert len(repo_structure) >= 1

        # Look for our Ruby files in the structure
        found_ruby_files = False
        for root in repo_structure:
            if "children" in root:
                for child in root["children"]:
                    if child.get("name") in ["models", "services", "nested"]:
                        found_ruby_files = True
                        break

        # We should find at least some Ruby files in the symbol tree
        assert found_ruby_files, "Ruby files not found in symbol tree"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_document_symbols_detailed(self, language_server: SolidLanguageServer) -> None:
        """Test document symbols for detailed Ruby file structure."""
        file_path = os.path.join("models.rb")
        symbols, roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()

        # Verify we have symbols
        assert len(symbols) > 0 or len(roots) > 0

        # Look for expected class names
        symbol_names = set()
        all_symbols = symbols if symbols else roots

        for symbol in all_symbols:
            symbol_names.add(symbol.get("name"))
            # Add children names too
            if "children" in symbol:
                for child in symbol["children"]:
                    symbol_names.add(child.get("name"))

        # We should find at least some of our defined classes/methods
        expected_symbols = {"User", "Item", "Order", "ItemHelpers"}
        found_symbols = symbol_names.intersection(expected_symbols)
        assert len(found_symbols) > 0, f"Expected symbols not found. Found: {symbol_names}"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_module_and_class_hierarchy(self, language_server: SolidLanguageServer) -> None:
        """Test symbol detection for modules and nested class hierarchies."""
        file_path = os.path.join("nested.rb")
        symbols, roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()

        # Verify we can detect the nested structure
        assert len(symbols) > 0 or len(roots) > 0

        # Look for OuterClass and its nested elements
        symbol_names = set()
        all_symbols = symbols if symbols else roots

        for symbol in all_symbols:
            symbol_names.add(symbol.get("name"))
            if "children" in symbol:
                for child in symbol["children"]:
                    symbol_names.add(child.get("name"))
                    # Check deeply nested too
                    if "children" in child:
                        for grandchild in child["children"]:
                            symbol_names.add(grandchild.get("name"))

        # Should find the outer class at minimum
        assert "OuterClass" in symbol_names, f"OuterClass not found in symbols: {symbol_names}"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_references_to_variables(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a variable with detailed verification."""
        file_path = os.path.join("variables.rb")
        # Test references to @status variable in DataContainer class (around line 9)
        ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 8, 4)]

        if len(ref_symbols) > 0:
            # Verify we have references
            assert len(ref_symbols) > 0, "Should find references to @status variable"

            # Check that we have location information
            ref_with_locations = [ref for ref in ref_symbols if "location" in ref and "range" in ref["location"]]
            assert len(ref_with_locations) > 0, "References should include location information"

            # Verify line numbers are reasonable (should be within the file)
            ref_lines = [ref["location"]["range"]["start"]["line"] for ref in ref_with_locations]
            assert all(line >= 0 for line in ref_lines), "Reference lines should be valid"

            # Check for specific reference locations we expect
            # Lines where @status is modified/accessed
            expected_line_ranges = [(20, 40), (45, 70)]  # Approximate ranges
            found_in_expected_range = any(any(start <= line <= end for start, end in expected_line_ranges) for line in ref_lines)
            assert found_in_expected_range, f"Expected references in ranges {expected_line_ranges}, found lines: {ref_lines}"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a method parameter."""
        # Test referencing symbols for a method parameter in get_user method
        file_path = os.path.join("services.rb")
        # Find get_user method and test parameter references
        symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
        get_user_symbol = None

        for symbol in symbols:
            if symbol.get("name") == "get_user":
                get_user_symbol = symbol
                break

        if not get_user_symbol or "selectionRange" not in get_user_symbol:
            pytest.skip("get_user symbol or its selectionRange not found")

        # Test parameter reference within method body
        method_start_line = get_user_symbol["selectionRange"]["start"]["line"]
        ref_symbols = [
            ref.symbol
            for ref in language_server.request_referencing_symbols(file_path, method_start_line + 1, 10)  # Position within method body
        ]

        # Verify structure of referencing symbols
        for symbol in ref_symbols:
            assert "name" in symbol, "Symbol should have name"
            assert "kind" in symbol, "Symbol should have kind"
            if "location" in symbol and "range" in symbol["location"]:
                range_info = symbol["location"]["range"]
                assert "start" in range_info, "Range should have start"
                assert "end" in range_info, "Range should have end"
                # Verify line number is valid (references can be before method definition too)
                assert range_info["start"]["line"] >= 0, "Reference line should be valid"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a position with no symbol."""
        # Test for a position with no symbol (comment or blank line)
        file_path = os.path.join("services.rb")

        # Try multiple positions that should have no symbols
        test_positions = [(1, 0), (2, 0)]  # Comment/require lines

        for line, char in test_positions:
            try:
                ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, line, char)]
                # If we get here, make sure we got an empty result or minimal results
                if ref_symbols:
                    # Some language servers might return minimal info, verify it's reasonable
                    assert len(ref_symbols) <= 3, f"Expected few/no references at line {line}, got {len(ref_symbols)}"

            except Exception as e:
                # Some language servers throw exceptions for invalid positions, which is acceptable
                assert (
                    "symbol" in str(e).lower() or "position" in str(e).lower() or "reference" in str(e).lower()
                ), f"Exception should be related to symbol/position/reference issues, got: {e}"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
        """Test that request_dir_overview returns correct symbol information for files in a directory."""
        # Get overview of the test repo directory
        overview = language_server.request_dir_overview(".")

        # Verify that we have entries for our main files
        expected_files = ["services.rb", "models.rb", "variables.rb", "nested.rb"]
        found_files = []

        for file_path in overview.keys():
            for expected in expected_files:
                if expected in file_path:
                    found_files.append(expected)
                    break

        assert len(found_files) >= 2, f"Should find at least 2 expected files, found: {found_files}"

        # Test specific symbols from services.rb if it exists
        services_file_key = None
        for file_path in overview.keys():
            if "services.rb" in file_path:
                services_file_key = file_path
                break

        if services_file_key:
            services_symbols = overview[services_file_key]
            assert len(services_symbols) > 0, "services.rb should have symbols"

            # Check for expected symbols with detailed verification
            symbol_names = [s[0] for s in services_symbols if isinstance(s, tuple) and len(s) > 0]
            if not symbol_names:  # If not tuples, try different format
                symbol_names = [s.get("name") for s in services_symbols if hasattr(s, "get")]

            expected_symbols = ["Services", "UserService", "ItemService"]
            found_expected = [name for name in expected_symbols if name in symbol_names]
            assert len(found_expected) >= 1, f"Should find at least one expected symbol, found: {found_expected} in {symbol_names}"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_document_overview(self, language_server: SolidLanguageServer) -> None:
        """Test that request_document_overview returns correct symbol information for a file."""
        # Get overview of the user_management.rb file
        file_path = os.path.join("examples", "user_management.rb")
        overview = language_server.request_document_overview(file_path)

        # Verify that we have symbol information
        assert len(overview) > 0, "Document overview should contain symbols"

        # Look for expected symbols from the file
        symbol_names = set()
        for s_info in overview:
            if isinstance(s_info, tuple) and len(s_info) > 0:
                symbol_names.add(s_info[0])
            elif hasattr(s_info, "get"):
                symbol_names.add(s_info.get("name"))
            elif isinstance(s_info, str):
                symbol_names.add(s_info)

        # We should find some of our defined classes/methods
        expected_symbols = {"UserStats", "UserManager", "process_user_data", "main"}
        found_symbols = symbol_names.intersection(expected_symbols)
        assert len(found_symbols) > 0, f"Expected to find some symbols from {expected_symbols}, found: {symbol_names}"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol where the target is a variable."""
        # Test for a position inside a variable definition or usage
        file_path = os.path.join("variables.rb")
        # Position around a variable assignment (e.g., @status = "pending")
        containing_symbol = language_server.request_containing_symbol(file_path, 10, 5)

        # Verify that we found a containing symbol (likely the method or class)
        if containing_symbol is not None:
            assert "name" in containing_symbol, "Containing symbol should have a name"
            assert "kind" in containing_symbol, "Containing symbol should have a kind"
            # The containing symbol should be a method, class, or similar construct
            expected_kinds = [SymbolKind.Method, SymbolKind.Class, SymbolKind.Function, SymbolKind.Constructor]
            assert containing_symbol["kind"] in [
                k.value for k in expected_kinds
            ], f"Expected containing symbol to be method/class/function, got kind: {containing_symbol['kind']}"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a function (not method)."""
        # Test for a position inside a standalone function
        file_path = os.path.join("variables.rb")
        # Position inside the demonstrate_variable_usage function
        containing_symbol = language_server.request_containing_symbol(file_path, 100, 10)

        if containing_symbol is not None:
            assert containing_symbol["name"] in [
                "demonstrate_variable_usage",
                "main",
            ], f"Expected function name, got: {containing_symbol['name']}"
            assert containing_symbol["kind"] in [
                SymbolKind.Function.value,
                SymbolKind.Method.value,
            ], f"Expected function or method kind, got: {containing_symbol['kind']}"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol with nested scopes."""
        # Test for a position inside a method which is inside a class
        file_path = os.path.join("services.rb")
        # Position inside create_user method within UserService class
        containing_symbol = language_server.request_containing_symbol(file_path, 12, 15)

        # Verify that we found the innermost containing symbol (the method)
        assert containing_symbol is not None
        assert containing_symbol["name"] == "create_user"
        assert containing_symbol["kind"] == SymbolKind.Method

        # Verify the container context is preserved
        if "containerName" in containing_symbol:
            assert "UserService" in containing_symbol["containerName"]

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None:
        """Test that the symbol tree structure correctly handles subdirectories."""
        # Get symbols within the examples subdirectory
        examples_structure = language_server.request_full_symbol_tree(within_relative_path="examples")

        if len(examples_structure) > 0:
            # Should find the examples directory structure
            assert len(examples_structure) >= 1, "Should find examples directory structure"

            # Look for the user_management file in the structure
            found_user_management = False
            for root in examples_structure:
                if "children" in root:
                    for child in root["children"]:
                        if "user_management" in child.get("name", ""):
                            found_user_management = True
                            # Verify the structure includes symbol information
                            if "children" in child:
                                child_names = [c.get("name") for c in child["children"]]
                                expected_names = ["UserStats", "UserManager", "process_user_data"]
                                found_expected = [name for name in expected_names if name in child_names]
                                assert (
                                    len(found_expected) > 0
                                ), f"Should find symbols in user_management, expected {expected_names}, found {child_names}"
                            break

            if not found_user_management:
                pytest.skip("user_management file not found in examples subdirectory structure")

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for an imported/required class."""
        # Test finding the definition of a class used from another file
        file_path = os.path.join("examples", "user_management.rb")
        # Position where Services::UserService is referenced
        defining_symbol = language_server.request_defining_symbol(file_path, 25, 20)

        # This might not work perfectly in all Ruby language servers due to require complexity
        if defining_symbol is not None:
            assert "name" in defining_symbol
            # The defining symbol should relate to UserService or Services
            # The defining symbol should relate to UserService, Services, or the containing class
            # Different language servers may resolve this differently
            expected_names = ["UserService", "Services", "new", "UserManager"]
            assert defining_symbol.get("name") in expected_names, f"Expected one of {expected_names}, got: {defining_symbol.get('name')}"

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a method call."""
        # Test finding the definition of a method being called
        file_path = os.path.join("examples", "user_management.rb")
        # Position at a method call like create_user
        defining_symbol = language_server.request_defining_symbol(file_path, 30, 15)

        # Verify that we can find method definitions
        if defining_symbol is not None:
            assert "name" in defining_symbol
            assert "kind" in defining_symbol
            # Should be a method or constructor
            assert defining_symbol.get("kind") in [SymbolKind.Method.value, SymbolKind.Constructor.value, SymbolKind.Function.value]

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a nested function or block."""
        # Test finding definition within nested contexts
        file_path = os.path.join("nested.rb")
        # Position inside or referencing nested functionality
        defining_symbol = language_server.request_defining_symbol(file_path, 15, 10)

        # This is challenging for many language servers
        if defining_symbol is not None:
            assert "name" in defining_symbol
            assert "kind" in defining_symbol
            # Could be method, function, or variable depending on implementation
            valid_kinds = [SymbolKind.Method.value, SymbolKind.Function.value, SymbolKind.Variable.value, SymbolKind.Class.value]
            assert defining_symbol.get("kind") in valid_kinds

    @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
    def test_containing_symbol_of_var_is_file(self, language_server: SolidLanguageServer) -> None:
        """Test that the containing symbol of a file-level variable is handled appropriately."""
        # Test behavior with file-level variables or constants
        file_path = os.path.join("variables.rb")
        # Position at file-level variable/constant
        containing_symbol = language_server.request_containing_symbol(file_path, 5, 5)

        # Different language servers handle file-level symbols differently
        # Some return None, others return file-level containers
        if containing_symbol is not None:
            # If we get a symbol, verify its structure
            assert "name" in containing_symbol
            assert "kind" in containing_symbol

```
Page 12/17FirstPrevNextLast