#
tokens: 41933/50000 4/410 files (page 16/21)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 16 of 21. Use http://codebase.md/oraios/serena?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .devcontainer
│   └── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── config.yml
│   │   ├── feature_request.md
│   │   └── issue--bug--performance-problem--question-.md
│   └── workflows
│       ├── codespell.yml
│       ├── docker.yml
│       ├── 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
│   │   │   │   ├── 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
│   │   │   ├── 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
│   └── 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/ruby/test_ruby_symbol_retrieval.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the Ruby language server symbol-related functionality.
  3 | 
  4 | These tests focus on the following methods:
  5 | - request_containing_symbol
  6 | - request_referencing_symbols
  7 | - request_defining_symbol
  8 | - request_document_symbols integration
  9 | """
 10 | 
 11 | import os
 12 | 
 13 | import pytest
 14 | 
 15 | from solidlsp import SolidLanguageServer
 16 | from solidlsp.ls_config import Language
 17 | from solidlsp.ls_types import SymbolKind
 18 | 
 19 | pytestmark = pytest.mark.ruby
 20 | 
 21 | 
 22 | class TestRubyLanguageServerSymbols:
 23 |     """Test the Ruby language server's symbol-related functionality."""
 24 | 
 25 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
 26 |     def test_request_containing_symbol_method(self, language_server: SolidLanguageServer) -> None:
 27 |         """Test request_containing_symbol for a method."""
 28 |         # Test for a position inside the create_user method
 29 |         file_path = os.path.join("services.rb")
 30 |         # Look for a position inside the create_user method body
 31 |         containing_symbol = language_server.request_containing_symbol(file_path, 11, 10, include_body=True)
 32 | 
 33 |         # Verify that we found the containing symbol
 34 |         assert containing_symbol is not None, "Should find containing symbol for method position"
 35 |         assert containing_symbol["name"] == "create_user", f"Expected 'create_user', got '{containing_symbol['name']}'"
 36 |         assert (
 37 |             containing_symbol["kind"] == SymbolKind.Method.value
 38 |         ), f"Expected Method kind ({SymbolKind.Method.value}), got {containing_symbol['kind']}"
 39 | 
 40 |         # Verify location information
 41 |         assert "location" in containing_symbol, "Containing symbol should have location information"
 42 |         location = containing_symbol["location"]
 43 |         assert "range" in location, "Location should contain range information"
 44 |         assert "start" in location["range"], "Range should have start position"
 45 |         assert "end" in location["range"], "Range should have end position"
 46 | 
 47 |         # Verify container information
 48 |         if "containerName" in containing_symbol:
 49 |             assert containing_symbol["containerName"] in [
 50 |                 "Services::UserService",
 51 |                 "UserService",
 52 |             ], f"Expected UserService container, got '{containing_symbol['containerName']}'"
 53 | 
 54 |         # Verify body content if available
 55 |         if "body" in containing_symbol:
 56 |             body = containing_symbol["body"]
 57 |             assert "def create_user" in body, "Method body should contain method definition"
 58 |             assert len(body.strip()) > 0, "Method body should not be empty"
 59 | 
 60 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
 61 |     def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
 62 |         """Test request_containing_symbol for a class."""
 63 |         # Test for a position inside the UserService class but outside any method
 64 |         file_path = os.path.join("services.rb")
 65 |         # Line around the class definition
 66 |         containing_symbol = language_server.request_containing_symbol(file_path, 5, 5)
 67 | 
 68 |         # Verify that we found the containing symbol
 69 |         assert containing_symbol is not None, "Should find containing symbol for class position"
 70 |         assert containing_symbol["name"] == "UserService", f"Expected 'UserService', got '{containing_symbol['name']}'"
 71 |         assert (
 72 |             containing_symbol["kind"] == SymbolKind.Class.value
 73 |         ), f"Expected Class kind ({SymbolKind.Class.value}), got {containing_symbol['kind']}"
 74 | 
 75 |         # Verify location information exists
 76 |         assert "location" in containing_symbol, "Class symbol should have location information"
 77 |         location = containing_symbol["location"]
 78 |         assert "range" in location, "Location should contain range"
 79 |         assert "start" in location["range"] and "end" in location["range"], "Range should have start and end positions"
 80 | 
 81 |         # Verify the class is properly nested in the Services module
 82 |         if "containerName" in containing_symbol:
 83 |             assert (
 84 |                 containing_symbol["containerName"] == "Services"
 85 |             ), f"Expected 'Services' as container, got '{containing_symbol['containerName']}'"
 86 | 
 87 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
 88 |     def test_request_containing_symbol_module(self, language_server: SolidLanguageServer) -> None:
 89 |         """Test request_containing_symbol for a module context."""
 90 |         # Test that we can find the Services module in document symbols
 91 |         file_path = os.path.join("services.rb")
 92 |         symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
 93 | 
 94 |         # Verify Services module appears in document symbols
 95 |         services_module = None
 96 |         for symbol in symbols:
 97 |             if symbol.get("name") == "Services" and symbol.get("kind") == SymbolKind.Module:
 98 |                 services_module = symbol
 99 |                 break
100 | 
101 |         assert services_module is not None, "Services module not found in document symbols"
102 | 
103 |         # Test that UserService class has Services as container
104 |         # Position inside UserService class
105 |         containing_symbol = language_server.request_containing_symbol(file_path, 4, 8)
106 |         assert containing_symbol is not None
107 |         assert containing_symbol["name"] == "UserService"
108 |         assert containing_symbol["kind"] == SymbolKind.Class
109 |         # Verify the module context is preserved in containerName (if supported by the language server)
110 |         # ruby-lsp doesn't provide containerName, but Solargraph does
111 |         if "containerName" in containing_symbol:
112 |             assert containing_symbol.get("containerName") == "Services"
113 | 
114 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
115 |     def test_request_containing_symbol_nested_class(self, language_server: SolidLanguageServer) -> None:
116 |         """Test request_containing_symbol with nested classes."""
117 |         # Test for a position inside a nested class method
118 |         file_path = os.path.join("nested.rb")
119 |         # Position inside NestedClass.find_me method
120 |         containing_symbol = language_server.request_containing_symbol(file_path, 20, 10)
121 | 
122 |         # Verify that we found the innermost containing symbol
123 |         assert containing_symbol is not None
124 |         assert containing_symbol["name"] == "find_me"
125 |         assert containing_symbol["kind"] == SymbolKind.Method
126 | 
127 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
128 |     def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None:
129 |         """Test request_containing_symbol for a position with no containing symbol."""
130 |         # Test for a position outside any class/method (e.g., in requires)
131 |         file_path = os.path.join("services.rb")
132 |         # Line 1 is a require statement, not inside any class or method
133 |         containing_symbol = language_server.request_containing_symbol(file_path, 1, 5)
134 | 
135 |         # Should return None or an empty dictionary
136 |         assert containing_symbol is None or containing_symbol == {}
137 | 
138 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
139 |     def test_request_referencing_symbols_method(self, language_server: SolidLanguageServer) -> None:
140 |         """Test request_referencing_symbols for a method."""
141 |         # Test referencing symbols for create_user method
142 |         file_path = os.path.join("services.rb")
143 |         # Line containing the create_user method definition
144 |         symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
145 |         create_user_symbol = None
146 | 
147 |         # Find create_user method in the document symbols (Ruby returns flat list)
148 |         for symbol in symbols:
149 |             if symbol.get("name") == "create_user":
150 |                 create_user_symbol = symbol
151 |                 break
152 | 
153 |         if not create_user_symbol or "selectionRange" not in create_user_symbol:
154 |             pytest.skip("create_user symbol or its selectionRange not found")
155 | 
156 |         sel_start = create_user_symbol["selectionRange"]["start"]
157 |         ref_symbols = [
158 |             ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
159 |         ]
160 | 
161 |         # We might not have references in our simple test setup, so just verify structure
162 |         for symbol in ref_symbols:
163 |             assert "name" in symbol
164 |             assert "kind" in symbol
165 | 
166 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
167 |     def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None:
168 |         """Test request_referencing_symbols for a class."""
169 |         # Test referencing symbols for User class
170 |         file_path = os.path.join("models.rb")
171 |         # Find User class in document symbols
172 |         symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
173 |         user_symbol = None
174 | 
175 |         for symbol in symbols:
176 |             if symbol.get("name") == "User":
177 |                 user_symbol = symbol
178 |                 break
179 | 
180 |         if not user_symbol or "selectionRange" not in user_symbol:
181 |             pytest.skip("User symbol or its selectionRange not found")
182 | 
183 |         sel_start = user_symbol["selectionRange"]["start"]
184 |         ref_symbols = [
185 |             ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
186 |         ]
187 | 
188 |         # Verify structure of referencing symbols
189 |         for symbol in ref_symbols:
190 |             assert "name" in symbol
191 |             assert "kind" in symbol
192 |             if "location" in symbol and "range" in symbol["location"]:
193 |                 assert "start" in symbol["location"]["range"]
194 |                 assert "end" in symbol["location"]["range"]
195 | 
196 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
197 |     def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
198 |         """Test request_defining_symbol for a variable usage."""
199 |         # Test finding the definition of a variable in a method
200 |         file_path = os.path.join("services.rb")
201 |         # Look for @users variable usage
202 |         defining_symbol = language_server.request_defining_symbol(file_path, 12, 10)
203 | 
204 |         # This test might fail if the language server doesn't support it well
205 |         if defining_symbol is not None:
206 |             assert "name" in defining_symbol
207 |             assert "kind" in defining_symbol
208 | 
209 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
210 |     def test_request_defining_symbol_class(self, language_server: SolidLanguageServer) -> None:
211 |         """Test request_defining_symbol for a class reference."""
212 |         # Test finding the definition of the User class used in services
213 |         file_path = os.path.join("services.rb")
214 |         # Line that references User class
215 |         defining_symbol = language_server.request_defining_symbol(file_path, 11, 15)
216 | 
217 |         # This might not work perfectly in all Ruby language servers
218 |         if defining_symbol is not None:
219 |             assert "name" in defining_symbol
220 |             # The name might be "User" or the method that contains it
221 |             assert defining_symbol.get("name") is not None
222 | 
223 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
224 |     def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
225 |         """Test request_defining_symbol for a position with no symbol."""
226 |         # Test for a position with no symbol (e.g., whitespace or comment)
227 |         file_path = os.path.join("services.rb")
228 |         # Line 3 is likely a blank line or comment
229 |         defining_symbol = language_server.request_defining_symbol(file_path, 3, 0)
230 | 
231 |         # Should return None for positions with no symbol
232 |         assert defining_symbol is None or defining_symbol == {}
233 | 
234 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
235 |     def test_request_defining_symbol_nested_class(self, language_server: SolidLanguageServer) -> None:
236 |         """Test request_defining_symbol for nested class access."""
237 |         # Test finding definition of NestedClass
238 |         file_path = os.path.join("nested.rb")
239 |         # Position where NestedClass is referenced
240 |         defining_symbol = language_server.request_defining_symbol(file_path, 44, 25)
241 | 
242 |         # This is challenging for many language servers
243 |         if defining_symbol is not None:
244 |             assert "name" in defining_symbol
245 |             assert defining_symbol.get("name") in ["NestedClass", "OuterClass"]
246 | 
247 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
248 |     def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
249 |         """Test the integration between different symbol-related methods."""
250 |         file_path = os.path.join("models.rb")
251 | 
252 |         # Step 1: Find a method we know exists
253 |         containing_symbol = language_server.request_containing_symbol(file_path, 8, 5)  # inside initialize method
254 |         if containing_symbol is not None:
255 |             assert containing_symbol["name"] == "initialize"
256 | 
257 |             # Step 2: Get the defining symbol for the same position
258 |             defining_symbol = language_server.request_defining_symbol(file_path, 8, 5)
259 |             if defining_symbol is not None:
260 |                 assert defining_symbol["name"] == "initialize"
261 | 
262 |                 # Step 3: Verify that they refer to the same symbol type
263 |                 assert defining_symbol["kind"] == containing_symbol["kind"]
264 | 
265 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
266 |     def test_symbol_tree_structure_basic(self, language_server: SolidLanguageServer) -> None:
267 |         """Test that the symbol tree structure includes Ruby symbols."""
268 |         # Get all symbols in the test repository
269 |         repo_structure = language_server.request_full_symbol_tree()
270 |         assert len(repo_structure) >= 1
271 | 
272 |         # Look for our Ruby files in the structure
273 |         found_ruby_files = False
274 |         for root in repo_structure:
275 |             if "children" in root:
276 |                 for child in root["children"]:
277 |                     if child.get("name") in ["models", "services", "nested"]:
278 |                         found_ruby_files = True
279 |                         break
280 | 
281 |         # We should find at least some Ruby files in the symbol tree
282 |         assert found_ruby_files, "Ruby files not found in symbol tree"
283 | 
284 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
285 |     def test_document_symbols_detailed(self, language_server: SolidLanguageServer) -> None:
286 |         """Test document symbols for detailed Ruby file structure."""
287 |         file_path = os.path.join("models.rb")
288 |         symbols, roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
289 | 
290 |         # Verify we have symbols
291 |         assert len(symbols) > 0 or len(roots) > 0
292 | 
293 |         # Look for expected class names
294 |         symbol_names = set()
295 |         all_symbols = symbols if symbols else roots
296 | 
297 |         for symbol in all_symbols:
298 |             symbol_names.add(symbol.get("name"))
299 |             # Add children names too
300 |             if "children" in symbol:
301 |                 for child in symbol["children"]:
302 |                     symbol_names.add(child.get("name"))
303 | 
304 |         # We should find at least some of our defined classes/methods
305 |         expected_symbols = {"User", "Item", "Order", "ItemHelpers"}
306 |         found_symbols = symbol_names.intersection(expected_symbols)
307 |         assert len(found_symbols) > 0, f"Expected symbols not found. Found: {symbol_names}"
308 | 
309 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
310 |     def test_module_and_class_hierarchy(self, language_server: SolidLanguageServer) -> None:
311 |         """Test symbol detection for modules and nested class hierarchies."""
312 |         file_path = os.path.join("nested.rb")
313 |         symbols, roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
314 | 
315 |         # Verify we can detect the nested structure
316 |         assert len(symbols) > 0 or len(roots) > 0
317 | 
318 |         # Look for OuterClass and its nested elements
319 |         symbol_names = set()
320 |         all_symbols = symbols if symbols else roots
321 | 
322 |         for symbol in all_symbols:
323 |             symbol_names.add(symbol.get("name"))
324 |             if "children" in symbol:
325 |                 for child in symbol["children"]:
326 |                     symbol_names.add(child.get("name"))
327 |                     # Check deeply nested too
328 |                     if "children" in child:
329 |                         for grandchild in child["children"]:
330 |                             symbol_names.add(grandchild.get("name"))
331 | 
332 |         # Should find the outer class at minimum
333 |         assert "OuterClass" in symbol_names, f"OuterClass not found in symbols: {symbol_names}"
334 | 
335 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
336 |     def test_references_to_variables(self, language_server: SolidLanguageServer) -> None:
337 |         """Test request_referencing_symbols for a variable with detailed verification."""
338 |         file_path = os.path.join("variables.rb")
339 |         # Test references to @status variable in DataContainer class (around line 9)
340 |         ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 8, 4)]
341 | 
342 |         if len(ref_symbols) > 0:
343 |             # Verify we have references
344 |             assert len(ref_symbols) > 0, "Should find references to @status variable"
345 | 
346 |             # Check that we have location information
347 |             ref_with_locations = [ref for ref in ref_symbols if "location" in ref and "range" in ref["location"]]
348 |             assert len(ref_with_locations) > 0, "References should include location information"
349 | 
350 |             # Verify line numbers are reasonable (should be within the file)
351 |             ref_lines = [ref["location"]["range"]["start"]["line"] for ref in ref_with_locations]
352 |             assert all(line >= 0 for line in ref_lines), "Reference lines should be valid"
353 | 
354 |             # Check for specific reference locations we expect
355 |             # Lines where @status is modified/accessed
356 |             expected_line_ranges = [(20, 40), (45, 70)]  # Approximate ranges
357 |             found_in_expected_range = any(any(start <= line <= end for start, end in expected_line_ranges) for line in ref_lines)
358 |             assert found_in_expected_range, f"Expected references in ranges {expected_line_ranges}, found lines: {ref_lines}"
359 | 
360 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
361 |     def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None:
362 |         """Test request_referencing_symbols for a method parameter."""
363 |         # Test referencing symbols for a method parameter in get_user method
364 |         file_path = os.path.join("services.rb")
365 |         # Find get_user method and test parameter references
366 |         symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
367 |         get_user_symbol = None
368 | 
369 |         for symbol in symbols:
370 |             if symbol.get("name") == "get_user":
371 |                 get_user_symbol = symbol
372 |                 break
373 | 
374 |         if not get_user_symbol or "selectionRange" not in get_user_symbol:
375 |             pytest.skip("get_user symbol or its selectionRange not found")
376 | 
377 |         # Test parameter reference within method body
378 |         method_start_line = get_user_symbol["selectionRange"]["start"]["line"]
379 |         ref_symbols = [
380 |             ref.symbol
381 |             for ref in language_server.request_referencing_symbols(file_path, method_start_line + 1, 10)  # Position within method body
382 |         ]
383 | 
384 |         # Verify structure of referencing symbols
385 |         for symbol in ref_symbols:
386 |             assert "name" in symbol, "Symbol should have name"
387 |             assert "kind" in symbol, "Symbol should have kind"
388 |             if "location" in symbol and "range" in symbol["location"]:
389 |                 range_info = symbol["location"]["range"]
390 |                 assert "start" in range_info, "Range should have start"
391 |                 assert "end" in range_info, "Range should have end"
392 |                 # Verify line number is valid (references can be before method definition too)
393 |                 assert range_info["start"]["line"] >= 0, "Reference line should be valid"
394 | 
395 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
396 |     def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None:
397 |         """Test request_referencing_symbols for a position with no symbol."""
398 |         # Test for a position with no symbol (comment or blank line)
399 |         file_path = os.path.join("services.rb")
400 | 
401 |         # Try multiple positions that should have no symbols
402 |         test_positions = [(1, 0), (2, 0)]  # Comment/require lines
403 | 
404 |         for line, char in test_positions:
405 |             try:
406 |                 ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, line, char)]
407 |                 # If we get here, make sure we got an empty result or minimal results
408 |                 if ref_symbols:
409 |                     # Some language servers might return minimal info, verify it's reasonable
410 |                     assert len(ref_symbols) <= 3, f"Expected few/no references at line {line}, got {len(ref_symbols)}"
411 | 
412 |             except Exception as e:
413 |                 # Some language servers throw exceptions for invalid positions, which is acceptable
414 |                 assert (
415 |                     "symbol" in str(e).lower() or "position" in str(e).lower() or "reference" in str(e).lower()
416 |                 ), f"Exception should be related to symbol/position/reference issues, got: {e}"
417 | 
418 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
419 |     def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
420 |         """Test that request_dir_overview returns correct symbol information for files in a directory."""
421 |         # Get overview of the test repo directory
422 |         overview = language_server.request_dir_overview(".")
423 | 
424 |         # Verify that we have entries for our main files
425 |         expected_files = ["services.rb", "models.rb", "variables.rb", "nested.rb"]
426 |         found_files = []
427 | 
428 |         for file_path in overview.keys():
429 |             for expected in expected_files:
430 |                 if expected in file_path:
431 |                     found_files.append(expected)
432 |                     break
433 | 
434 |         assert len(found_files) >= 2, f"Should find at least 2 expected files, found: {found_files}"
435 | 
436 |         # Test specific symbols from services.rb if it exists
437 |         services_file_key = None
438 |         for file_path in overview.keys():
439 |             if "services.rb" in file_path:
440 |                 services_file_key = file_path
441 |                 break
442 | 
443 |         if services_file_key:
444 |             services_symbols = overview[services_file_key]
445 |             assert len(services_symbols) > 0, "services.rb should have symbols"
446 | 
447 |             # Check for expected symbols with detailed verification
448 |             symbol_names = [s[0] for s in services_symbols if isinstance(s, tuple) and len(s) > 0]
449 |             if not symbol_names:  # If not tuples, try different format
450 |                 symbol_names = [s.get("name") for s in services_symbols if hasattr(s, "get")]
451 | 
452 |             expected_symbols = ["Services", "UserService", "ItemService"]
453 |             found_expected = [name for name in expected_symbols if name in symbol_names]
454 |             assert len(found_expected) >= 1, f"Should find at least one expected symbol, found: {found_expected} in {symbol_names}"
455 | 
456 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
457 |     def test_request_document_overview(self, language_server: SolidLanguageServer) -> None:
458 |         """Test that request_document_overview returns correct symbol information for a file."""
459 |         # Get overview of the user_management.rb file
460 |         file_path = os.path.join("examples", "user_management.rb")
461 |         overview = language_server.request_document_overview(file_path)
462 | 
463 |         # Verify that we have symbol information
464 |         assert len(overview) > 0, "Document overview should contain symbols"
465 | 
466 |         # Look for expected symbols from the file
467 |         symbol_names = set()
468 |         for s_info in overview:
469 |             if isinstance(s_info, tuple) and len(s_info) > 0:
470 |                 symbol_names.add(s_info[0])
471 |             elif hasattr(s_info, "get"):
472 |                 symbol_names.add(s_info.get("name"))
473 |             elif isinstance(s_info, str):
474 |                 symbol_names.add(s_info)
475 | 
476 |         # We should find some of our defined classes/methods
477 |         expected_symbols = {"UserStats", "UserManager", "process_user_data", "main"}
478 |         found_symbols = symbol_names.intersection(expected_symbols)
479 |         assert len(found_symbols) > 0, f"Expected to find some symbols from {expected_symbols}, found: {symbol_names}"
480 | 
481 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
482 |     def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None:
483 |         """Test request_containing_symbol where the target is a variable."""
484 |         # Test for a position inside a variable definition or usage
485 |         file_path = os.path.join("variables.rb")
486 |         # Position around a variable assignment (e.g., @status = "pending")
487 |         containing_symbol = language_server.request_containing_symbol(file_path, 10, 5)
488 | 
489 |         # Verify that we found a containing symbol (likely the method or class)
490 |         if containing_symbol is not None:
491 |             assert "name" in containing_symbol, "Containing symbol should have a name"
492 |             assert "kind" in containing_symbol, "Containing symbol should have a kind"
493 |             # The containing symbol should be a method, class, or similar construct
494 |             expected_kinds = [SymbolKind.Method, SymbolKind.Class, SymbolKind.Function, SymbolKind.Constructor]
495 |             assert containing_symbol["kind"] in [
496 |                 k.value for k in expected_kinds
497 |             ], f"Expected containing symbol to be method/class/function, got kind: {containing_symbol['kind']}"
498 | 
499 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
500 |     def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
501 |         """Test request_containing_symbol for a function (not method)."""
502 |         # Test for a position inside a standalone function
503 |         file_path = os.path.join("variables.rb")
504 |         # Position inside the demonstrate_variable_usage function
505 |         containing_symbol = language_server.request_containing_symbol(file_path, 100, 10)
506 | 
507 |         if containing_symbol is not None:
508 |             assert containing_symbol["name"] in [
509 |                 "demonstrate_variable_usage",
510 |                 "main",
511 |             ], f"Expected function name, got: {containing_symbol['name']}"
512 |             assert containing_symbol["kind"] in [
513 |                 SymbolKind.Function.value,
514 |                 SymbolKind.Method.value,
515 |             ], f"Expected function or method kind, got: {containing_symbol['kind']}"
516 | 
517 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
518 |     def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
519 |         """Test request_containing_symbol with nested scopes."""
520 |         # Test for a position inside a method which is inside a class
521 |         file_path = os.path.join("services.rb")
522 |         # Position inside create_user method within UserService class
523 |         containing_symbol = language_server.request_containing_symbol(file_path, 12, 15)
524 | 
525 |         # Verify that we found the innermost containing symbol (the method)
526 |         assert containing_symbol is not None
527 |         assert containing_symbol["name"] == "create_user"
528 |         assert containing_symbol["kind"] == SymbolKind.Method
529 | 
530 |         # Verify the container context is preserved
531 |         if "containerName" in containing_symbol:
532 |             assert "UserService" in containing_symbol["containerName"]
533 | 
534 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
535 |     def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None:
536 |         """Test that the symbol tree structure correctly handles subdirectories."""
537 |         # Get symbols within the examples subdirectory
538 |         examples_structure = language_server.request_full_symbol_tree(within_relative_path="examples")
539 | 
540 |         if len(examples_structure) > 0:
541 |             # Should find the examples directory structure
542 |             assert len(examples_structure) >= 1, "Should find examples directory structure"
543 | 
544 |             # Look for the user_management file in the structure
545 |             found_user_management = False
546 |             for root in examples_structure:
547 |                 if "children" in root:
548 |                     for child in root["children"]:
549 |                         if "user_management" in child.get("name", ""):
550 |                             found_user_management = True
551 |                             # Verify the structure includes symbol information
552 |                             if "children" in child:
553 |                                 child_names = [c.get("name") for c in child["children"]]
554 |                                 expected_names = ["UserStats", "UserManager", "process_user_data"]
555 |                                 found_expected = [name for name in expected_names if name in child_names]
556 |                                 assert (
557 |                                     len(found_expected) > 0
558 |                                 ), f"Should find symbols in user_management, expected {expected_names}, found {child_names}"
559 |                             break
560 | 
561 |             if not found_user_management:
562 |                 pytest.skip("user_management file not found in examples subdirectory structure")
563 | 
564 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
565 |     def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
566 |         """Test request_defining_symbol for an imported/required class."""
567 |         # Test finding the definition of a class used from another file
568 |         file_path = os.path.join("examples", "user_management.rb")
569 |         # Position where Services::UserService is referenced
570 |         defining_symbol = language_server.request_defining_symbol(file_path, 25, 20)
571 | 
572 |         # This might not work perfectly in all Ruby language servers due to require complexity
573 |         if defining_symbol is not None:
574 |             assert "name" in defining_symbol
575 |             # The defining symbol should relate to UserService or Services
576 |             # The defining symbol should relate to UserService, Services, or the containing class
577 |             # Different language servers may resolve this differently
578 |             expected_names = ["UserService", "Services", "new", "UserManager"]
579 |             assert defining_symbol.get("name") in expected_names, f"Expected one of {expected_names}, got: {defining_symbol.get('name')}"
580 | 
581 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
582 |     def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None:
583 |         """Test request_defining_symbol for a method call."""
584 |         # Test finding the definition of a method being called
585 |         file_path = os.path.join("examples", "user_management.rb")
586 |         # Position at a method call like create_user
587 |         defining_symbol = language_server.request_defining_symbol(file_path, 30, 15)
588 | 
589 |         # Verify that we can find method definitions
590 |         if defining_symbol is not None:
591 |             assert "name" in defining_symbol
592 |             assert "kind" in defining_symbol
593 |             # Should be a method or constructor
594 |             assert defining_symbol.get("kind") in [SymbolKind.Method.value, SymbolKind.Constructor.value, SymbolKind.Function.value]
595 | 
596 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
597 |     def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None:
598 |         """Test request_defining_symbol for a nested function or block."""
599 |         # Test finding definition within nested contexts
600 |         file_path = os.path.join("nested.rb")
601 |         # Position inside or referencing nested functionality
602 |         defining_symbol = language_server.request_defining_symbol(file_path, 15, 10)
603 | 
604 |         # This is challenging for many language servers
605 |         if defining_symbol is not None:
606 |             assert "name" in defining_symbol
607 |             assert "kind" in defining_symbol
608 |             # Could be method, function, or variable depending on implementation
609 |             valid_kinds = [SymbolKind.Method.value, SymbolKind.Function.value, SymbolKind.Variable.value, SymbolKind.Class.value]
610 |             assert defining_symbol.get("kind") in valid_kinds
611 | 
612 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
613 |     def test_containing_symbol_of_var_is_file(self, language_server: SolidLanguageServer) -> None:
614 |         """Test that the containing symbol of a file-level variable is handled appropriately."""
615 |         # Test behavior with file-level variables or constants
616 |         file_path = os.path.join("variables.rb")
617 |         # Position at file-level variable/constant
618 |         containing_symbol = language_server.request_containing_symbol(file_path, 5, 5)
619 | 
620 |         # Different language servers handle file-level symbols differently
621 |         # Some return None, others return file-level containers
622 |         if containing_symbol is not None:
623 |             # If we get a symbol, verify its structure
624 |             assert "name" in containing_symbol
625 |             assert "kind" in containing_symbol
626 | 
```

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

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

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

```python
  1 | """
  2 | The Serena Model Context Protocol (MCP) Server
  3 | """
  4 | 
  5 | import os
  6 | import platform
  7 | import subprocess
  8 | import sys
  9 | from collections.abc import Callable
 10 | from logging import Logger
 11 | from typing import TYPE_CHECKING, Optional, TypeVar
 12 | 
 13 | from sensai.util import logging
 14 | from sensai.util.logging import LogTime
 15 | 
 16 | from interprompt.jinja_template import JinjaTemplate
 17 | from serena import serena_version
 18 | from serena.analytics import RegisteredTokenCountEstimator, ToolUsageStats
 19 | from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
 20 | from serena.config.serena_config import LanguageBackend, SerenaConfig, ToolInclusionDefinition
 21 | from serena.dashboard import SerenaDashboardAPI
 22 | from serena.ls_manager import LanguageServerManager
 23 | from serena.project import Project
 24 | from serena.prompt_factory import SerenaPromptFactory
 25 | from serena.task_executor import TaskExecutor
 26 | from serena.tools import ActivateProjectTool, GetCurrentConfigTool, OpenDashboardTool, ReplaceContentTool, Tool, ToolMarker, ToolRegistry
 27 | from serena.util.gui import system_has_usable_display
 28 | from serena.util.inspection import iter_subclasses
 29 | from serena.util.logging import MemoryLogHandler
 30 | from solidlsp.ls_config import Language
 31 | 
 32 | if TYPE_CHECKING:
 33 |     from serena.gui_log_viewer import GuiLogViewer
 34 | 
 35 | log = logging.getLogger(__name__)
 36 | TTool = TypeVar("TTool", bound="Tool")
 37 | T = TypeVar("T")
 38 | SUCCESS_RESULT = "OK"
 39 | 
 40 | 
 41 | class ProjectNotFoundError(Exception):
 42 |     pass
 43 | 
 44 | 
 45 | class AvailableTools:
 46 |     """
 47 |     Represents the set of available/exposed tools of a SerenaAgent.
 48 |     """
 49 | 
 50 |     def __init__(self, tools: list[Tool]):
 51 |         """
 52 |         :param tools: the list of available tools
 53 |         """
 54 |         self.tools = tools
 55 |         self.tool_names = [tool.get_name_from_cls() for tool in tools]
 56 |         self.tool_marker_names = set()
 57 |         for marker_class in iter_subclasses(ToolMarker):
 58 |             for tool in tools:
 59 |                 if isinstance(tool, marker_class):
 60 |                     self.tool_marker_names.add(marker_class.__name__)
 61 | 
 62 |     def __len__(self) -> int:
 63 |         return len(self.tools)
 64 | 
 65 | 
 66 | class ToolSet:
 67 |     """
 68 |     Represents a set of tools by their names.
 69 |     """
 70 | 
 71 |     LEGACY_TOOL_NAME_MAPPING = {"replace_regex": ReplaceContentTool.get_name_from_cls()}
 72 |     """
 73 |     maps legacy tool names to their new names for backward compatibility
 74 |     """
 75 | 
 76 |     def __init__(self, tool_names: set[str]) -> None:
 77 |         self._tool_names = tool_names
 78 | 
 79 |     @classmethod
 80 |     def default(cls) -> "ToolSet":
 81 |         """
 82 |         :return: the default tool set, which contains all tools that are enabled by default
 83 |         """
 84 |         from serena.tools import ToolRegistry
 85 | 
 86 |         return cls(set(ToolRegistry().get_tool_names_default_enabled()))
 87 | 
 88 |     def apply(self, *tool_inclusion_definitions: "ToolInclusionDefinition") -> "ToolSet":
 89 |         """
 90 |         Applies one or more tool inclusion definitions to this tool set,
 91 |         resulting in a new tool set.
 92 | 
 93 |         :param tool_inclusion_definitions: the definitions to apply
 94 |         :return: a new tool set with the definitions applied
 95 |         """
 96 |         from serena.tools import ToolRegistry
 97 | 
 98 |         def get_updated_tool_name(tool_name: str) -> str:
 99 |             """Retrieves the updated tool name if the provided tool name is deprecated, logging a warning."""
100 |             if tool_name in self.LEGACY_TOOL_NAME_MAPPING:
101 |                 new_tool_name = self.LEGACY_TOOL_NAME_MAPPING[tool_name]
102 |                 log.warning("Tool name '%s' is deprecated, please use '%s' instead", tool_name, new_tool_name)
103 |                 return new_tool_name
104 |             return tool_name
105 | 
106 |         registry = ToolRegistry()
107 |         tool_names = set(self._tool_names)
108 |         for definition in tool_inclusion_definitions:
109 |             if definition.is_fixed_tool_set():
110 |                 tool_names = set()
111 |                 for fixed_tool in definition.fixed_tools:
112 |                     fixed_tool = get_updated_tool_name(fixed_tool)
113 |                     if not registry.is_valid_tool_name(fixed_tool):
114 |                         raise ValueError(f"Invalid tool name '{fixed_tool}' provided for fixed tool set")
115 |                     tool_names.add(fixed_tool)
116 |                 log.info(f"{definition} defined a fixed tool set with {len(tool_names)} tools: {', '.join(tool_names)}")
117 |             else:
118 |                 included_tools = []
119 |                 excluded_tools = []
120 |                 for included_tool in definition.included_optional_tools:
121 |                     included_tool = get_updated_tool_name(included_tool)
122 |                     if not registry.is_valid_tool_name(included_tool):
123 |                         raise ValueError(f"Invalid tool name '{included_tool}' provided for inclusion")
124 |                     if included_tool not in tool_names:
125 |                         tool_names.add(included_tool)
126 |                         included_tools.append(included_tool)
127 |                 for excluded_tool in definition.excluded_tools:
128 |                     excluded_tool = get_updated_tool_name(excluded_tool)
129 |                     if not registry.is_valid_tool_name(excluded_tool):
130 |                         raise ValueError(f"Invalid tool name '{excluded_tool}' provided for exclusion")
131 |                     if excluded_tool in tool_names:
132 |                         tool_names.remove(excluded_tool)
133 |                         excluded_tools.append(excluded_tool)
134 |                 if included_tools:
135 |                     log.info(f"{definition} included {len(included_tools)} tools: {', '.join(included_tools)}")
136 |                 if excluded_tools:
137 |                     log.info(f"{definition} excluded {len(excluded_tools)} tools: {', '.join(excluded_tools)}")
138 |         return ToolSet(tool_names)
139 | 
140 |     def without_editing_tools(self) -> "ToolSet":
141 |         """
142 |         :return: a new tool set that excludes all tools that can edit
143 |         """
144 |         from serena.tools import ToolRegistry
145 | 
146 |         registry = ToolRegistry()
147 |         tool_names = set(self._tool_names)
148 |         for tool_name in self._tool_names:
149 |             if registry.get_tool_class_by_name(tool_name).can_edit():
150 |                 tool_names.remove(tool_name)
151 |         return ToolSet(tool_names)
152 | 
153 |     def get_tool_names(self) -> set[str]:
154 |         """
155 |         Returns the names of the tools that are currently included in the tool set.
156 |         """
157 |         return self._tool_names
158 | 
159 |     def includes_name(self, tool_name: str) -> bool:
160 |         return tool_name in self._tool_names
161 | 
162 | 
163 | class SerenaAgent:
164 |     def __init__(
165 |         self,
166 |         project: str | None = None,
167 |         project_activation_callback: Callable[[], None] | None = None,
168 |         serena_config: SerenaConfig | None = None,
169 |         context: SerenaAgentContext | None = None,
170 |         modes: list[SerenaAgentMode] | None = None,
171 |         memory_log_handler: MemoryLogHandler | None = None,
172 |     ):
173 |         """
174 |         :param project: the project to load immediately or None to not load any project; may be a path to the project or a name of
175 |             an already registered project;
176 |         :param project_activation_callback: a callback function to be called when a project is activated.
177 |         :param serena_config: the Serena configuration or None to read the configuration from the default location.
178 |         :param context: the context in which the agent is operating, None for default context.
179 |             The context may adjust prompts, tool availability, and tool descriptions.
180 |         :param modes: list of modes in which the agent is operating (they will be combined), None for default modes.
181 |             The modes may adjust prompts, tool availability, and tool descriptions.
182 |         :param memory_log_handler: a MemoryLogHandler instance from which to read log messages; if None, a new one will be created
183 |             if necessary.
184 |         """
185 |         # obtain serena configuration using the decoupled factory function
186 |         self.serena_config = serena_config or SerenaConfig.from_config_file()
187 | 
188 |         # project-specific instances, which will be initialized upon project activation
189 |         self._active_project: Project | None = None
190 | 
191 |         # dashboard URL (set when dashboard is started)
192 |         self._dashboard_url: str | None = None
193 | 
194 |         # adjust log level
195 |         serena_log_level = self.serena_config.log_level
196 |         if Logger.root.level != serena_log_level:
197 |             log.info(f"Changing the root logger level to {serena_log_level}")
198 |             Logger.root.setLevel(serena_log_level)
199 | 
200 |         def get_memory_log_handler() -> MemoryLogHandler:
201 |             nonlocal memory_log_handler
202 |             if memory_log_handler is None:
203 |                 memory_log_handler = MemoryLogHandler(level=serena_log_level)
204 |                 Logger.root.addHandler(memory_log_handler)
205 |             return memory_log_handler
206 | 
207 |         # open GUI log window if enabled
208 |         self._gui_log_viewer: Optional["GuiLogViewer"] = None
209 |         if self.serena_config.gui_log_window_enabled:
210 |             log.info("Opening GUI window")
211 |             if platform.system() == "Darwin":
212 |                 log.warning("GUI log window is not supported on macOS")
213 |             else:
214 |                 # even importing on macOS may fail if tkinter dependencies are unavailable (depends on Python interpreter installation
215 |                 # which uv used as a base, unfortunately)
216 |                 from serena.gui_log_viewer import GuiLogViewer
217 | 
218 |                 self._gui_log_viewer = GuiLogViewer("dashboard", title="Serena Logs", memory_log_handler=get_memory_log_handler())
219 |                 self._gui_log_viewer.start()
220 |         else:
221 |             log.debug("GUI window is disabled")
222 | 
223 |         # set the agent context
224 |         if context is None:
225 |             context = SerenaAgentContext.load_default()
226 |         self._context = context
227 | 
228 |         # instantiate all tool classes
229 |         self._all_tools: dict[type[Tool], Tool] = {tool_class: tool_class(self) for tool_class in ToolRegistry().get_all_tool_classes()}
230 |         tool_names = [tool.get_name_from_cls() for tool in self._all_tools.values()]
231 | 
232 |         # If GUI log window is enabled, set the tool names for highlighting
233 |         if self._gui_log_viewer is not None:
234 |             self._gui_log_viewer.set_tool_names(tool_names)
235 | 
236 |         token_count_estimator = RegisteredTokenCountEstimator[self.serena_config.token_count_estimator]
237 |         log.info(f"Will record tool usage statistics with token count estimator: {token_count_estimator.name}.")
238 |         self._tool_usage_stats = ToolUsageStats(token_count_estimator)
239 | 
240 |         # log fundamental information
241 |         log.info(
242 |             f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()}; "
243 |             f"language backend={self.serena_config.language_backend.name})"
244 |         )
245 |         log.info("Configuration file: %s", self.serena_config.config_file_path)
246 |         log.info("Available projects: {}".format(", ".join(self.serena_config.project_names)))
247 |         log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name_from_cls() for tool in self._all_tools.values()])}")
248 | 
249 |         self._check_shell_settings()
250 | 
251 |         # determine the base toolset defining the set of exposed tools (which e.g. the MCP shall see),
252 |         # determined by the
253 |         #   * dashboard availability/opening on launch
254 |         #   * Serena config,
255 |         #   * the context (which is fixed for the session)
256 |         #   * single-project mode reductions (if applicable)
257 |         #   * JetBrains mode
258 |         tool_inclusion_definitions: list[ToolInclusionDefinition] = []
259 |         if (
260 |             self.serena_config.web_dashboard
261 |             and not self.serena_config.web_dashboard_open_on_launch
262 |             and not self.serena_config.gui_log_window_enabled
263 |         ):
264 |             tool_inclusion_definitions.append(ToolInclusionDefinition(included_optional_tools=[OpenDashboardTool.get_name_from_cls()]))
265 |         tool_inclusion_definitions.append(self.serena_config)
266 |         tool_inclusion_definitions.append(self._context)
267 |         if self._context.single_project:
268 |             tool_inclusion_definitions.extend(self._single_project_context_tool_inclusion_definitions(project))
269 |         if self.serena_config.language_backend == LanguageBackend.JETBRAINS:
270 |             tool_inclusion_definitions.append(SerenaAgentMode.from_name_internal("jetbrains"))
271 | 
272 |         self._base_tool_set = ToolSet.default().apply(*tool_inclusion_definitions)
273 |         self._exposed_tools = AvailableTools([t for t in self._all_tools.values() if self._base_tool_set.includes_name(t.get_name())])
274 |         log.info(f"Number of exposed tools: {len(self._exposed_tools)}")
275 | 
276 |         # create executor for starting the language server and running tools in another thread
277 |         # This executor is used to achieve linear task execution
278 |         self._task_executor = TaskExecutor("SerenaAgentTaskExecutor")
279 | 
280 |         # Initialize the prompt factory
281 |         self.prompt_factory = SerenaPromptFactory()
282 |         self._project_activation_callback = project_activation_callback
283 | 
284 |         # set the active modes
285 |         if modes is None:
286 |             modes = SerenaAgentMode.load_default_modes()
287 |         self._modes = modes
288 | 
289 |         self._active_tools: dict[type[Tool], Tool] = {}
290 |         self._update_active_tools()
291 | 
292 |         # activate a project configuration (if provided or if there is only a single project available)
293 |         if project is not None:
294 |             try:
295 |                 self.activate_project_from_path_or_name(project)
296 |             except Exception as e:
297 |                 log.error(f"Error activating project '{project}' at startup: {e}", exc_info=e)
298 | 
299 |         # start the dashboard (web frontend), registering its log handler
300 |         # should be the last thing to happen in the initialization since the dashboard
301 |         # may access various parts of the agent
302 |         if self.serena_config.web_dashboard:
303 |             self._dashboard_thread, port = SerenaDashboardAPI(
304 |                 get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats
305 |             ).run_in_thread(host=self.serena_config.web_dashboard_listen_address)
306 |             dashboard_host = self.serena_config.web_dashboard_listen_address
307 |             if dashboard_host == "0.0.0.0":
308 |                 dashboard_host = "localhost"
309 |             dashboard_url = f"http://{dashboard_host}:{port}/dashboard/index.html"
310 |             self._dashboard_url = dashboard_url
311 |             log.info("Serena web dashboard started at %s", dashboard_url)
312 |             if self.serena_config.web_dashboard_open_on_launch:
313 |                 self.open_dashboard()
314 |             # inform the GUI window (if any)
315 |             if self._gui_log_viewer is not None:
316 |                 self._gui_log_viewer.set_dashboard_url(dashboard_url)
317 | 
318 |     def get_current_tasks(self) -> list[TaskExecutor.TaskInfo]:
319 |         """
320 |         Gets the list of tasks currently running or queued for execution.
321 |         The function returns a list of thread-safe TaskInfo objects (specifically created for the caller).
322 | 
323 |         :return: the list of tasks in the execution order (running task first)
324 |         """
325 |         return self._task_executor.get_current_tasks()
326 | 
327 |     def get_last_executed_task(self) -> TaskExecutor.TaskInfo | None:
328 |         """
329 |         Gets the last executed task.
330 | 
331 |         :return: the last executed task info or None if no task has been executed yet
332 |         """
333 |         return self._task_executor.get_last_executed_task()
334 | 
335 |     def get_language_server_manager(self) -> LanguageServerManager | None:
336 |         if self._active_project is not None:
337 |             return self._active_project.language_server_manager
338 |         return None
339 | 
340 |     def get_language_server_manager_or_raise(self) -> LanguageServerManager:
341 |         language_server_manager = self.get_language_server_manager()
342 |         if language_server_manager is None:
343 |             raise Exception(
344 |                 "The language server manager is not initialized, indicating a problem during project activation. "
345 |                 "Inform the user, telling them to inspect Serena's logs in order to determine the issue. "
346 |                 "IMPORTANT: Wait for further instructions before you continue!"
347 |             )
348 |         return language_server_manager
349 | 
350 |     def get_context(self) -> SerenaAgentContext:
351 |         return self._context
352 | 
353 |     def get_tool_description_override(self, tool_name: str) -> str | None:
354 |         return self._context.tool_description_overrides.get(tool_name, None)
355 | 
356 |     def _check_shell_settings(self) -> None:
357 |         # On Windows, Claude Code sets COMSPEC to Git-Bash (often even with a path containing spaces),
358 |         # which causes all sorts of trouble, preventing language servers from being launched correctly.
359 |         # So we make sure that COMSPEC is unset if it has been set to bash specifically.
360 |         if platform.system() == "Windows":
361 |             comspec = os.environ.get("COMSPEC", "")
362 |             if "bash" in comspec:
363 |                 os.environ["COMSPEC"] = ""  # force use of default shell
364 |                 log.info("Adjusting COMSPEC environment variable to use the default shell instead of '%s'", comspec)
365 | 
366 |     def _single_project_context_tool_inclusion_definitions(self, project_root_or_name: str | None) -> list[ToolInclusionDefinition]:
367 |         """
368 |         In the IDE assistant context, the agent is assumed to work on a single project, and we thus
369 |         want to apply that project's tool exclusions/inclusions from the get-go, limiting the set
370 |         of tools that will be exposed to the client.
371 |         Furthermore, we disable tools that are only relevant for project activation.
372 |         So if the project exists, we apply all the aforementioned exclusions.
373 | 
374 |         :param project_root_or_name: the project root path or project name
375 |         :return:
376 |         """
377 |         tool_inclusion_definitions = []
378 |         if project_root_or_name is not None:
379 |             # Note: Auto-generation is disabled, because the result must be returned instantaneously
380 |             #   (project generation could take too much time), so as not to delay MCP server startup
381 |             #   and provide responses to the client immediately.
382 |             project = self.load_project_from_path_or_name(project_root_or_name, autogenerate=False)
383 |             if project is not None:
384 |                 log.info(
385 |                     "Applying tool inclusion/exclusion definitions for single-project context based on project '%s'", project.project_name
386 |                 )
387 |                 tool_inclusion_definitions.append(
388 |                     ToolInclusionDefinition(
389 |                         excluded_tools=[ActivateProjectTool.get_name_from_cls(), GetCurrentConfigTool.get_name_from_cls()]
390 |                     )
391 |                 )
392 |                 tool_inclusion_definitions.append(project.project_config)
393 |         return tool_inclusion_definitions
394 | 
395 |     def record_tool_usage(self, input_kwargs: dict, tool_result: str | dict, tool: Tool) -> None:
396 |         """
397 |         Record the usage of a tool with the given input and output strings if tool usage statistics recording is enabled.
398 |         """
399 |         tool_name = tool.get_name()
400 |         input_str = str(input_kwargs)
401 |         output_str = str(tool_result)
402 |         log.debug(f"Recording tool usage for tool '{tool_name}'")
403 |         self._tool_usage_stats.record_tool_usage(tool_name, input_str, output_str)
404 | 
405 |     def get_dashboard_url(self) -> str | None:
406 |         """
407 |         :return: the URL of the web dashboard, or None if the dashboard is not running
408 |         """
409 |         return self._dashboard_url
410 | 
411 |     def open_dashboard(self) -> bool:
412 |         """
413 |         Opens the Serena web dashboard in the default web browser.
414 | 
415 |         :return: a message indicating success or failure
416 |         """
417 |         if self._dashboard_url is None:
418 |             raise Exception("Dashboard is not running.")
419 | 
420 |         if not system_has_usable_display():
421 |             log.warning("Not opening the Serena web dashboard because no usable display was detected.")
422 |             return False
423 | 
424 |         # Use a subprocess to avoid any output from webbrowser.open being written to stdout
425 |         subprocess.Popen(
426 |             [sys.executable, "-c", f"import webbrowser; webbrowser.open({self._dashboard_url!r})"],
427 |             stdin=subprocess.DEVNULL,
428 |             stdout=subprocess.DEVNULL,
429 |             stderr=subprocess.DEVNULL,
430 |             start_new_session=True,  # Detach from parent process
431 |         )
432 |         return True
433 | 
434 |     def get_project_root(self) -> str:
435 |         """
436 |         :return: the root directory of the active project (if any); raises a ValueError if there is no active project
437 |         """
438 |         project = self.get_active_project()
439 |         if project is None:
440 |             raise ValueError("Cannot get project root if no project is active.")
441 |         return project.project_root
442 | 
443 |     def get_exposed_tool_instances(self) -> list["Tool"]:
444 |         """
445 |         :return: the tool instances which are exposed (e.g. to the MCP client).
446 |             Note that the set of exposed tools is fixed for the session, as
447 |             clients don't react to changes in the set of tools, so this is the superset
448 |             of tools that can be offered during the session.
449 |             If a client should attempt to use a tool that is dynamically disabled
450 |             (e.g. because a project is activated that disables it), it will receive an error.
451 |         """
452 |         return list(self._exposed_tools.tools)
453 | 
454 |     def get_active_project(self) -> Project | None:
455 |         """
456 |         :return: the active project or None if no project is active
457 |         """
458 |         return self._active_project
459 | 
460 |     def get_active_project_or_raise(self) -> Project:
461 |         """
462 |         :return: the active project or raises an exception if no project is active
463 |         """
464 |         project = self.get_active_project()
465 |         if project is None:
466 |             raise ValueError("No active project. Please activate a project first.")
467 |         return project
468 | 
469 |     def set_modes(self, modes: list[SerenaAgentMode]) -> None:
470 |         """
471 |         Set the current mode configurations.
472 | 
473 |         :param modes: List of mode names or paths to use
474 |         """
475 |         self._modes = modes
476 |         self._update_active_tools()
477 | 
478 |         log.info(f"Set modes to {[mode.name for mode in modes]}")
479 | 
480 |     def get_active_modes(self) -> list[SerenaAgentMode]:
481 |         """
482 |         :return: the list of active modes
483 |         """
484 |         return list(self._modes)
485 | 
486 |     def _format_prompt(self, prompt_template: str) -> str:
487 |         template = JinjaTemplate(prompt_template)
488 |         return template.render(available_tools=self._exposed_tools.tool_names, available_markers=self._exposed_tools.tool_marker_names)
489 | 
490 |     def create_system_prompt(self) -> str:
491 |         available_markers = self._exposed_tools.tool_marker_names
492 |         log.info("Generating system prompt with available_tools=(see exposed tools), available_markers=%s", available_markers)
493 |         system_prompt = self.prompt_factory.create_system_prompt(
494 |             context_system_prompt=self._format_prompt(self._context.prompt),
495 |             mode_system_prompts=[self._format_prompt(mode.prompt) for mode in self._modes],
496 |             available_tools=self._exposed_tools.tool_names,
497 |             available_markers=available_markers,
498 |         )
499 | 
500 |         # If a project is active at startup, append its activation message
501 |         if self._active_project is not None:
502 |             system_prompt += "\n\n" + self._active_project.get_activation_message()
503 | 
504 |         log.info("System prompt:\n%s", system_prompt)
505 |         return system_prompt
506 | 
507 |     def _update_active_tools(self) -> None:
508 |         """
509 |         Update the active tools based on enabled modes and the active project.
510 |         The base tool set already takes the Serena configuration and the context into account
511 |         (as well as any internal modes that are not handled dynamically, such as JetBrains mode).
512 |         """
513 |         tool_set = self._base_tool_set.apply(*self._modes)
514 |         if self._active_project is not None:
515 |             tool_set = tool_set.apply(self._active_project.project_config)
516 |             if self._active_project.project_config.read_only:
517 |                 tool_set = tool_set.without_editing_tools()
518 | 
519 |         self._active_tools = {
520 |             tool_class: tool_instance
521 |             for tool_class, tool_instance in self._all_tools.items()
522 |             if tool_set.includes_name(tool_instance.get_name())
523 |         }
524 | 
525 |         log.info(f"Active tools ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}")
526 | 
527 |     def issue_task(
528 |         self, task: Callable[[], T], name: str | None = None, logged: bool = True, timeout: float | None = None
529 |     ) -> TaskExecutor.Task[T]:
530 |         """
531 |         Issue a task to the executor for asynchronous execution.
532 |         It is ensured that tasks are executed in the order they are issued, one after another.
533 | 
534 |         :param task: the task to execute
535 |         :param name: the name of the task for logging purposes; if None, use the task function's name
536 |         :param logged: whether to log management of the task; if False, only errors will be logged
537 |         :param timeout: the maximum time to wait for task completion in seconds, or None to wait indefinitely
538 |         :return: the task object, through which the task's future result can be accessed
539 |         """
540 |         return self._task_executor.issue_task(task, name=name, logged=logged, timeout=timeout)
541 | 
542 |     def execute_task(self, task: Callable[[], T], name: str | None = None, logged: bool = True, timeout: float | None = None) -> T:
543 |         """
544 |         Executes the given task synchronously via the agent's task executor.
545 |         This is useful for tasks that need to be executed immediately and whose results are needed right away.
546 | 
547 |         :param task: the task to execute
548 |         :param name: the name of the task for logging purposes; if None, use the task function's name
549 |         :param logged: whether to log management of the task; if False, only errors will be logged
550 |         :param timeout: the maximum time to wait for task completion in seconds, or None to wait indefinitely
551 |         :return: the result of the task execution
552 |         """
553 |         return self._task_executor.execute_task(task, name=name, logged=logged, timeout=timeout)
554 | 
555 |     def is_using_language_server(self) -> bool:
556 |         """
557 |         :return: whether this agent uses language server-based code analysis
558 |         """
559 |         return self.serena_config.language_backend == LanguageBackend.LSP
560 | 
561 |     def _activate_project(self, project: Project) -> None:
562 |         log.info(f"Activating {project.project_name} at {project.project_root}")
563 |         self._active_project = project
564 |         self._update_active_tools()
565 | 
566 |         def init_language_server_manager() -> None:
567 |             # start the language server
568 |             with LogTime("Language server initialization", logger=log):
569 |                 self.reset_language_server_manager()
570 | 
571 |         # initialize the language server in the background (if in language server mode)
572 |         if self.is_using_language_server():
573 |             self.issue_task(init_language_server_manager)
574 | 
575 |         if self._project_activation_callback is not None:
576 |             self._project_activation_callback()
577 | 
578 |     def load_project_from_path_or_name(self, project_root_or_name: str, autogenerate: bool) -> Project | None:
579 |         """
580 |         Get a project instance from a path or a name.
581 | 
582 |         :param project_root_or_name: the path to the project root or the name of the project
583 |         :param autogenerate: whether to autogenerate the project for the case where first argument is a directory
584 |             which does not yet contain a Serena project configuration file
585 |         :return: the project instance if it was found/could be created, None otherwise
586 |         """
587 |         project_instance: Project | None = self.serena_config.get_project(project_root_or_name)
588 |         if project_instance is not None:
589 |             log.info(f"Found registered project '{project_instance.project_name}' at path {project_instance.project_root}")
590 |         elif autogenerate and os.path.isdir(project_root_or_name):
591 |             project_instance = self.serena_config.add_project_from_path(project_root_or_name)
592 |             log.info(f"Added new project {project_instance.project_name} for path {project_instance.project_root}")
593 |         return project_instance
594 | 
595 |     def activate_project_from_path_or_name(self, project_root_or_name: str) -> Project:
596 |         """
597 |         Activate a project from a path or a name.
598 |         If the project was already registered, it will just be activated.
599 |         If the argument is a path at which no Serena project previously existed, the project will be created beforehand.
600 |         Raises ProjectNotFoundError if the project could neither be found nor created.
601 | 
602 |         :return: a tuple of the project instance and a Boolean indicating whether the project was newly
603 |             created
604 |         """
605 |         project_instance: Project | None = self.load_project_from_path_or_name(project_root_or_name, autogenerate=True)
606 |         if project_instance is None:
607 |             raise ProjectNotFoundError(
608 |                 f"Project '{project_root_or_name}' not found: Not a valid project name or directory. "
609 |                 f"Existing project names: {self.serena_config.project_names}"
610 |             )
611 |         self._activate_project(project_instance)
612 |         return project_instance
613 | 
614 |     def get_active_tool_classes(self) -> list[type["Tool"]]:
615 |         """
616 |         :return: the list of active tool classes for the current project
617 |         """
618 |         return list(self._active_tools.keys())
619 | 
620 |     def get_active_tool_names(self) -> list[str]:
621 |         """
622 |         :return: the list of names of the active tools for the current project
623 |         """
624 |         return sorted([tool.get_name_from_cls() for tool in self.get_active_tool_classes()])
625 | 
626 |     def tool_is_active(self, tool_class: type["Tool"] | str) -> bool:
627 |         """
628 |         :param tool_class: the class or name of the tool to check
629 |         :return: True if the tool is active, False otherwise
630 |         """
631 |         if isinstance(tool_class, str):
632 |             return tool_class in self.get_active_tool_names()
633 |         else:
634 |             return tool_class in self.get_active_tool_classes()
635 | 
636 |     def get_current_config_overview(self) -> str:
637 |         """
638 |         :return: a string overview of the current configuration, including the active and available configuration options
639 |         """
640 |         result_str = "Current configuration:\n"
641 |         result_str += f"Serena version: {serena_version()}\n"
642 |         result_str += f"Loglevel: {self.serena_config.log_level}, trace_lsp_communication={self.serena_config.trace_lsp_communication}\n"
643 |         if self._active_project is not None:
644 |             result_str += f"Active project: {self._active_project.project_name}\n"
645 |         else:
646 |             result_str += "No active project\n"
647 |         result_str += "Available projects:\n" + "\n".join(list(self.serena_config.project_names)) + "\n"
648 |         result_str += f"Active context: {self._context.name}\n"
649 | 
650 |         # Active modes
651 |         active_mode_names = [mode.name for mode in self.get_active_modes()]
652 |         result_str += "Active modes: {}\n".format(", ".join(active_mode_names)) + "\n"
653 | 
654 |         # Available but not active modes
655 |         all_available_modes = SerenaAgentMode.list_registered_mode_names()
656 |         inactive_modes = [mode for mode in all_available_modes if mode not in active_mode_names]
657 |         if inactive_modes:
658 |             result_str += "Available but not active modes: {}\n".format(", ".join(inactive_modes)) + "\n"
659 | 
660 |         # Active tools
661 |         result_str += "Active tools (after all exclusions from the project, context, and modes):\n"
662 |         active_tool_names = self.get_active_tool_names()
663 |         # print the tool names in chunks
664 |         chunk_size = 4
665 |         for i in range(0, len(active_tool_names), chunk_size):
666 |             chunk = active_tool_names[i : i + chunk_size]
667 |             result_str += "  " + ", ".join(chunk) + "\n"
668 | 
669 |         # Available but not active tools
670 |         all_tool_names = sorted([tool.get_name_from_cls() for tool in self._all_tools.values()])
671 |         inactive_tool_names = [tool for tool in all_tool_names if tool not in active_tool_names]
672 |         if inactive_tool_names:
673 |             result_str += "Available but not active tools:\n"
674 |             for i in range(0, len(inactive_tool_names), chunk_size):
675 |                 chunk = inactive_tool_names[i : i + chunk_size]
676 |                 result_str += "  " + ", ".join(chunk) + "\n"
677 | 
678 |         return result_str
679 | 
680 |     def reset_language_server_manager(self) -> None:
681 |         """
682 |         Starts/resets the language server manager for the current project
683 |         """
684 |         tool_timeout = self.serena_config.tool_timeout
685 |         if tool_timeout is None or tool_timeout < 0:
686 |             ls_timeout = None
687 |         else:
688 |             if tool_timeout < 10:
689 |                 raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds")
690 |             ls_timeout = tool_timeout - 5  # the LS timeout is for a single call, it should be smaller than the tool timeout
691 | 
692 |         # instantiate and start the necessary language servers
693 |         self.get_active_project_or_raise().create_language_server_manager(
694 |             log_level=self.serena_config.log_level,
695 |             ls_timeout=ls_timeout,
696 |             trace_lsp_communication=self.serena_config.trace_lsp_communication,
697 |             ls_specific_settings=self.serena_config.ls_specific_settings,
698 |         )
699 | 
700 |     def add_language(self, language: Language) -> None:
701 |         """
702 |         Adds a new language to the active project, spawning the respective language server and updating the project configuration.
703 |         The addition is scheduled via the agent's task executor and executed synchronously, i.e. the method returns
704 |         when the addition is complete.
705 | 
706 |         :param language: the language to add
707 |         """
708 |         self.execute_task(lambda: self.get_active_project_or_raise().add_language(language), name=f"AddLanguage:{language.value}")
709 | 
710 |     def remove_language(self, language: Language) -> None:
711 |         """
712 |         Removes a language from the active project, shutting down the respective language server and updating the project configuration.
713 |         The removal is scheduled via the agent's task executor and executed asynchronously.
714 | 
715 |         :param language: the language to remove
716 |         """
717 |         self.issue_task(lambda: self.get_active_project_or_raise().remove_language(language), name=f"RemoveLanguage:{language.value}")
718 | 
719 |     def get_tool(self, tool_class: type[TTool]) -> TTool:
720 |         return self._all_tools[tool_class]  # type: ignore
721 | 
722 |     def print_tool_overview(self) -> None:
723 |         ToolRegistry().print_tool_overview(self._active_tools.values())
724 | 
725 |     def __del__(self) -> None:
726 |         self.shutdown()
727 | 
728 |     def shutdown(self, timeout: float = 2.0) -> None:
729 |         """
730 |         Shuts down the agent, freeing resources and stopping background tasks.
731 |         """
732 |         if not hasattr(self, "_is_initialized"):
733 |             return
734 |         log.info("SerenaAgent is shutting down ...")
735 |         if self._active_project is not None:
736 |             self._active_project.shutdown(timeout=timeout)
737 |             self._active_project = None
738 |         if self._gui_log_viewer:
739 |             log.info("Stopping the GUI log window ...")
740 |             self._gui_log_viewer.stop()
741 |             self._gui_log_viewer = None
742 | 
743 |     def get_tool_by_name(self, tool_name: str) -> Tool:
744 |         tool_class = ToolRegistry().get_tool_class_by_name(tool_name)
745 |         return self.get_tool(tool_class)
746 | 
```

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

```python
  1 | """
  2 | Vue Language Server implementation using @vue/language-server (Volar) with companion TypeScript LS.
  3 | Operates in hybrid mode: Vue LS handles .vue files, TypeScript LS handles .ts/.js files.
  4 | """
  5 | 
  6 | import logging
  7 | import os
  8 | import pathlib
  9 | import shutil
 10 | import threading
 11 | from pathlib import Path
 12 | from time import sleep
 13 | from typing import Any
 14 | 
 15 | from overrides import override
 16 | 
 17 | from solidlsp import ls_types
 18 | from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
 19 | from solidlsp.language_servers.typescript_language_server import (
 20 |     TypeScriptLanguageServer,
 21 |     prefer_non_node_modules_definition,
 22 | )
 23 | from solidlsp.ls import LSPFileBuffer, SolidLanguageServer
 24 | from solidlsp.ls_config import Language, LanguageServerConfig
 25 | from solidlsp.ls_exceptions import SolidLSPException
 26 | from solidlsp.ls_types import Location
 27 | from solidlsp.ls_utils import PathUtils
 28 | from solidlsp.lsp_protocol_handler import lsp_types
 29 | from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, ExecuteCommandParams, InitializeParams, SymbolInformation
 30 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 31 | from solidlsp.settings import SolidLSPSettings
 32 | 
 33 | log = logging.getLogger(__name__)
 34 | 
 35 | 
 36 | class VueTypeScriptServer(TypeScriptLanguageServer):
 37 |     """TypeScript LS configured with @vue/typescript-plugin for Vue file support."""
 38 | 
 39 |     _pending_ts_ls_executable: list[str] | None = None
 40 | 
 41 |     @classmethod
 42 |     @override
 43 |     def get_language_enum_instance(cls) -> Language:
 44 |         """Return TYPESCRIPT since this is a TypeScript language server variant.
 45 | 
 46 |         Note: VueTypeScriptServer is a companion server that uses TypeScript's language server
 47 |         with the Vue TypeScript plugin. It reports as TYPESCRIPT to maintain compatibility
 48 |         with the TypeScript language server infrastructure.
 49 |         """
 50 |         return Language.TYPESCRIPT
 51 | 
 52 |     @classmethod
 53 |     @override
 54 |     def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]:
 55 |         if cls._pending_ts_ls_executable is not None:
 56 |             return cls._pending_ts_ls_executable
 57 |         return ["typescript-language-server", "--stdio"]
 58 | 
 59 |     @override
 60 |     def _get_language_id_for_file(self, relative_file_path: str) -> str:
 61 |         """Return the correct language ID for files.
 62 | 
 63 |         Vue files must be opened with language ID "vue" for the @vue/typescript-plugin
 64 |         to process them correctly. The plugin is configured with "languages": ["vue"]
 65 |         in the initialization options.
 66 |         """
 67 |         ext = os.path.splitext(relative_file_path)[1].lower()
 68 |         if ext == ".vue":
 69 |             return "vue"
 70 |         elif ext in (".ts", ".tsx", ".mts", ".cts"):
 71 |             return "typescript"
 72 |         elif ext in (".js", ".jsx", ".mjs", ".cjs"):
 73 |             return "javascript"
 74 |         else:
 75 |             return "typescript"
 76 | 
 77 |     def __init__(
 78 |         self,
 79 |         config: LanguageServerConfig,
 80 |         repository_root_path: str,
 81 |         solidlsp_settings: SolidLSPSettings,
 82 |         vue_plugin_path: str,
 83 |         tsdk_path: str,
 84 |         ts_ls_executable_path: list[str],
 85 |     ):
 86 |         self._vue_plugin_path = vue_plugin_path
 87 |         self._custom_tsdk_path = tsdk_path
 88 |         VueTypeScriptServer._pending_ts_ls_executable = ts_ls_executable_path
 89 |         super().__init__(config, repository_root_path, solidlsp_settings)
 90 |         VueTypeScriptServer._pending_ts_ls_executable = None
 91 | 
 92 |     @override
 93 |     def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
 94 |         params = super()._get_initialize_params(repository_absolute_path)
 95 | 
 96 |         params["initializationOptions"] = {
 97 |             "plugins": [
 98 |                 {
 99 |                     "name": "@vue/typescript-plugin",
100 |                     "location": self._vue_plugin_path,
101 |                     "languages": ["vue"],
102 |                 }
103 |             ],
104 |             "tsserver": {
105 |                 "path": self._custom_tsdk_path,
106 |             },
107 |         }
108 | 
109 |         if "workspace" in params["capabilities"]:
110 |             params["capabilities"]["workspace"]["executeCommand"] = {"dynamicRegistration": True}
111 | 
112 |         return params
113 | 
114 |     @override
115 |     def _start_server(self) -> None:
116 |         def workspace_configuration_handler(params: dict) -> list:
117 |             items = params.get("items", [])
118 |             return [{} for _ in items]
119 | 
120 |         self.server.on_request("workspace/configuration", workspace_configuration_handler)
121 |         super()._start_server()
122 | 
123 | 
124 | class VueLanguageServer(SolidLanguageServer):
125 |     """
126 |     Language server for Vue Single File Components using @vue/language-server (Volar) with companion TypeScript LS.
127 | 
128 |     You can pass the following entries in ls_specific_settings["vue"]:
129 |         - vue_language_server_version: Version of @vue/language-server to install (default: "3.1.5")
130 | 
131 |     Note: TypeScript versions are configured via ls_specific_settings["typescript"]:
132 |         - typescript_version: Version of TypeScript to install (default: "5.9.3")
133 |         - typescript_language_server_version: Version of typescript-language-server to install (default: "5.1.3")
134 |     """
135 | 
136 |     TS_SERVER_READY_TIMEOUT = 5.0
137 |     VUE_SERVER_READY_TIMEOUT = 3.0
138 |     # Windows requires more time due to slower I/O and process operations.
139 |     VUE_INDEXING_WAIT_TIME = 4.0 if os.name == "nt" else 2.0
140 | 
141 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
142 |         vue_lsp_executable_path, self.tsdk_path, self._ts_ls_cmd = self._setup_runtime_dependencies(config, solidlsp_settings)
143 |         self._vue_ls_dir = os.path.join(self.ls_resources_dir(solidlsp_settings), "vue-lsp")
144 |         super().__init__(
145 |             config,
146 |             repository_root_path,
147 |             ProcessLaunchInfo(cmd=vue_lsp_executable_path, cwd=repository_root_path),
148 |             "vue",
149 |             solidlsp_settings,
150 |         )
151 |         self.server_ready = threading.Event()
152 |         self.initialize_searcher_command_available = threading.Event()
153 |         self._ts_server: VueTypeScriptServer | None = None
154 |         self._ts_server_started = False
155 |         self._vue_files_indexed = False
156 |         self._indexed_vue_file_uris: list[str] = []
157 | 
158 |     @override
159 |     def is_ignored_dirname(self, dirname: str) -> bool:
160 |         return super().is_ignored_dirname(dirname) or dirname in [
161 |             "node_modules",
162 |             "dist",
163 |             "build",
164 |             "coverage",
165 |             ".nuxt",
166 |             ".output",
167 |         ]
168 | 
169 |     @override
170 |     def _get_language_id_for_file(self, relative_file_path: str) -> str:
171 |         ext = os.path.splitext(relative_file_path)[1].lower()
172 |         if ext == ".vue":
173 |             return "vue"
174 |         elif ext in (".ts", ".tsx", ".mts", ".cts"):
175 |             return "typescript"
176 |         elif ext in (".js", ".jsx", ".mjs", ".cjs"):
177 |             return "javascript"
178 |         else:
179 |             return "vue"
180 | 
181 |     def _is_typescript_file(self, file_path: str) -> bool:
182 |         ext = os.path.splitext(file_path)[1].lower()
183 |         return ext in (".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs")
184 | 
185 |     def _find_all_vue_files(self) -> list[str]:
186 |         vue_files = []
187 |         repo_path = Path(self.repository_root_path)
188 | 
189 |         for vue_file in repo_path.rglob("*.vue"):
190 |             try:
191 |                 relative_path = str(vue_file.relative_to(repo_path))
192 |                 if "node_modules" not in relative_path and not relative_path.startswith("."):
193 |                     vue_files.append(relative_path)
194 |             except Exception as e:
195 |                 log.debug(f"Error processing Vue file {vue_file}: {e}")
196 | 
197 |         return vue_files
198 | 
199 |     def _ensure_vue_files_indexed_on_ts_server(self) -> None:
200 |         if self._vue_files_indexed:
201 |             return
202 | 
203 |         assert self._ts_server is not None
204 |         log.info("Indexing .vue files on TypeScript server for cross-file references")
205 |         vue_files = self._find_all_vue_files()
206 |         log.debug(f"Found {len(vue_files)} .vue files to index")
207 | 
208 |         for vue_file in vue_files:
209 |             try:
210 |                 with self._ts_server.open_file(vue_file) as file_buffer:
211 |                     file_buffer.ref_count += 1
212 |                     self._indexed_vue_file_uris.append(file_buffer.uri)
213 |             except Exception as e:
214 |                 log.debug(f"Failed to open {vue_file} on TS server: {e}")
215 | 
216 |         self._vue_files_indexed = True
217 |         log.info("Vue file indexing on TypeScript server complete")
218 | 
219 |         sleep(self._get_vue_indexing_wait_time())
220 |         log.debug("Wait period after Vue file indexing complete")
221 | 
222 |     def _get_vue_indexing_wait_time(self) -> float:
223 |         return self.VUE_INDEXING_WAIT_TIME
224 | 
225 |     def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
226 |         uri = PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))
227 |         request_params = {
228 |             "textDocument": {"uri": uri},
229 |             "position": {"line": line, "character": column},
230 |             "context": {"includeDeclaration": False},
231 |         }
232 | 
233 |         return self.server.send.references(request_params)  # type: ignore[arg-type]
234 | 
235 |     def _send_ts_references_request(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
236 |         assert self._ts_server is not None
237 |         uri = PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))
238 |         request_params = {
239 |             "textDocument": {"uri": uri},
240 |             "position": {"line": line, "character": column},
241 |             "context": {"includeDeclaration": True},
242 |         }
243 | 
244 |         with self._ts_server.open_file(relative_file_path):
245 |             response = self._ts_server.handler.send.references(request_params)  # type: ignore[arg-type]
246 | 
247 |         result: list[ls_types.Location] = []
248 |         if response is not None:
249 |             for item in response:
250 |                 abs_path = PathUtils.uri_to_path(item["uri"])
251 |                 if not Path(abs_path).is_relative_to(self.repository_root_path):
252 |                     log.debug(f"Found reference outside repository: {abs_path}, skipping")
253 |                     continue
254 | 
255 |                 rel_path = Path(abs_path).relative_to(self.repository_root_path)
256 |                 if self.is_ignored_path(str(rel_path)):
257 |                     log.debug(f"Ignoring reference in {rel_path}")
258 |                     continue
259 | 
260 |                 new_item: dict = {}
261 |                 new_item.update(item)  # type: ignore[arg-type]
262 |                 new_item["absolutePath"] = str(abs_path)
263 |                 new_item["relativePath"] = str(rel_path)
264 |                 result.append(ls_types.Location(**new_item))  # type: ignore
265 | 
266 |         return result
267 | 
268 |     def request_file_references(self, relative_file_path: str) -> list:
269 |         if not self.server_started:
270 |             log.error("request_file_references called before Language Server started")
271 |             raise SolidLSPException("Language Server not started")
272 | 
273 |         absolute_file_path = os.path.join(self.repository_root_path, relative_file_path)
274 |         uri = PathUtils.path_to_uri(absolute_file_path)
275 | 
276 |         request_params = {"textDocument": {"uri": uri}}
277 | 
278 |         log.info(f"Sending volar/client/findFileReference request for {relative_file_path}")
279 |         log.info(f"Request URI: {uri}")
280 |         log.info(f"Request params: {request_params}")
281 | 
282 |         try:
283 |             with self.open_file(relative_file_path):
284 |                 log.debug(f"Sending volar/client/findFileReference for {relative_file_path}")
285 |                 log.debug(f"Request params: {request_params}")
286 | 
287 |                 response = self.server.send_request("volar/client/findFileReference", request_params)
288 | 
289 |                 log.debug(f"Received response type: {type(response)}")
290 | 
291 |             log.info(f"Received file references response: {response}")
292 |             log.info(f"Response type: {type(response)}")
293 | 
294 |             if response is None:
295 |                 log.debug(f"No file references found for {relative_file_path}")
296 |                 return []
297 | 
298 |             # Response should be an array of Location objects
299 |             if not isinstance(response, list):
300 |                 log.warning(f"Unexpected response format from volar/client/findFileReference: {type(response)}")
301 |                 return []
302 | 
303 |             ret: list[Location] = []
304 |             for item in response:
305 |                 if not isinstance(item, dict) or "uri" not in item:
306 |                     log.debug(f"Skipping invalid location item: {item}")
307 |                     continue
308 | 
309 |                 abs_path = PathUtils.uri_to_path(item["uri"])  # type: ignore[arg-type]
310 |                 if not Path(abs_path).is_relative_to(self.repository_root_path):
311 |                     log.warning(f"Found file reference outside repository: {abs_path}, skipping")
312 |                     continue
313 | 
314 |                 rel_path = Path(abs_path).relative_to(self.repository_root_path)
315 |                 if self.is_ignored_path(str(rel_path)):
316 |                     log.debug(f"Ignoring file reference in {rel_path}")
317 |                     continue
318 | 
319 |                 new_item: dict = {}
320 |                 new_item.update(item)  # type: ignore[arg-type]
321 |                 new_item["absolutePath"] = str(abs_path)
322 |                 new_item["relativePath"] = str(rel_path)
323 |                 ret.append(Location(**new_item))  # type: ignore
324 | 
325 |             log.debug(f"Found {len(ret)} file references for {relative_file_path}")
326 |             return ret
327 | 
328 |         except Exception as e:
329 |             log.warning(f"Error requesting file references for {relative_file_path}: {e}")
330 |             return []
331 | 
332 |     @override
333 |     def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
334 |         if not self.server_started:
335 |             log.error("request_references called before Language Server started")
336 |             raise SolidLSPException("Language Server not started")
337 | 
338 |         if not self._has_waited_for_cross_file_references:
339 |             sleep(self._get_wait_time_for_cross_file_referencing())
340 |             self._has_waited_for_cross_file_references = True
341 | 
342 |         self._ensure_vue_files_indexed_on_ts_server()
343 |         symbol_refs = self._send_ts_references_request(relative_file_path, line=line, column=column)
344 | 
345 |         if relative_file_path.endswith(".vue"):
346 |             log.info(f"Attempting to find file-level references for Vue component {relative_file_path}")
347 |             file_refs = self.request_file_references(relative_file_path)
348 |             log.info(f"file_refs result: {len(file_refs)} references found")
349 | 
350 |             seen = set()
351 |             for ref in symbol_refs:
352 |                 key = (ref["uri"], ref["range"]["start"]["line"], ref["range"]["start"]["character"])
353 |                 seen.add(key)
354 | 
355 |             for file_ref in file_refs:
356 |                 key = (file_ref["uri"], file_ref["range"]["start"]["line"], file_ref["range"]["start"]["character"])
357 |                 if key not in seen:
358 |                     symbol_refs.append(file_ref)
359 |                     seen.add(key)
360 | 
361 |             log.info(f"Total references for {relative_file_path}: {len(symbol_refs)} (symbol refs + file refs, deduplicated)")
362 | 
363 |         return symbol_refs
364 | 
365 |     @override
366 |     def request_definition(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
367 |         if not self.server_started:
368 |             log.error("request_definition called before Language Server started")
369 |             raise SolidLSPException("Language Server not started")
370 | 
371 |         assert self._ts_server is not None
372 |         with self._ts_server.open_file(relative_file_path):
373 |             return self._ts_server.request_definition(relative_file_path, line, column)
374 | 
375 |     @override
376 |     def request_rename_symbol_edit(self, relative_file_path: str, line: int, column: int, new_name: str) -> ls_types.WorkspaceEdit | None:
377 |         if not self.server_started:
378 |             log.error("request_rename_symbol_edit called before Language Server started")
379 |             raise SolidLSPException("Language Server not started")
380 | 
381 |         assert self._ts_server is not None
382 |         with self._ts_server.open_file(relative_file_path):
383 |             return self._ts_server.request_rename_symbol_edit(relative_file_path, line, column, new_name)
384 | 
385 |     @classmethod
386 |     def _setup_runtime_dependencies(
387 |         cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
388 |     ) -> tuple[list[str], str, list[str]]:
389 |         is_node_installed = shutil.which("node") is not None
390 |         assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
391 |         is_npm_installed = shutil.which("npm") is not None
392 |         assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
393 | 
394 |         # Get TypeScript version settings from TypeScript language server settings
395 |         typescript_config = solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT)
396 |         typescript_version = typescript_config.get("typescript_version", "5.9.3")
397 |         typescript_language_server_version = typescript_config.get("typescript_language_server_version", "5.1.3")
398 |         vue_config = solidlsp_settings.get_ls_specific_settings(Language.VUE)
399 |         vue_language_server_version = vue_config.get("vue_language_server_version", "3.1.5")
400 | 
401 |         deps = RuntimeDependencyCollection(
402 |             [
403 |                 RuntimeDependency(
404 |                     id="vue-language-server",
405 |                     description="Vue language server package (Volar)",
406 |                     command=["npm", "install", "--prefix", "./", f"@vue/language-server@{vue_language_server_version}"],
407 |                     platform_id="any",
408 |                 ),
409 |                 RuntimeDependency(
410 |                     id="typescript",
411 |                     description="TypeScript (required for tsdk)",
412 |                     command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"],
413 |                     platform_id="any",
414 |                 ),
415 |                 RuntimeDependency(
416 |                     id="typescript-language-server",
417 |                     description="TypeScript language server (for Vue LS 3.x tsserver forwarding)",
418 |                     command=[
419 |                         "npm",
420 |                         "install",
421 |                         "--prefix",
422 |                         "./",
423 |                         f"typescript-language-server@{typescript_language_server_version}",
424 |                     ],
425 |                     platform_id="any",
426 |                 ),
427 |             ]
428 |         )
429 | 
430 |         vue_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "vue-lsp")
431 |         vue_executable_path = os.path.join(vue_ls_dir, "node_modules", ".bin", "vue-language-server")
432 |         ts_ls_executable_path = os.path.join(vue_ls_dir, "node_modules", ".bin", "typescript-language-server")
433 | 
434 |         if os.name == "nt":
435 |             vue_executable_path += ".cmd"
436 |             ts_ls_executable_path += ".cmd"
437 | 
438 |         tsdk_path = os.path.join(vue_ls_dir, "node_modules", "typescript", "lib")
439 | 
440 |         # Check if installation is needed based on executables AND version
441 |         version_file = os.path.join(vue_ls_dir, ".installed_version")
442 |         expected_version = f"{vue_language_server_version}_{typescript_version}_{typescript_language_server_version}"
443 | 
444 |         needs_install = False
445 |         if not os.path.exists(vue_executable_path) or not os.path.exists(ts_ls_executable_path):
446 |             log.info("Vue/TypeScript Language Server executables not found.")
447 |             needs_install = True
448 |         elif os.path.exists(version_file):
449 |             with open(version_file) as f:
450 |                 installed_version = f.read().strip()
451 |             if installed_version != expected_version:
452 |                 log.info(
453 |                     f"Vue Language Server version mismatch: installed={installed_version}, expected={expected_version}. Reinstalling..."
454 |                 )
455 |                 needs_install = True
456 |         else:
457 |             # No version file exists, assume old installation needs refresh
458 |             log.info("Vue Language Server version file not found. Reinstalling to ensure correct version...")
459 |             needs_install = True
460 | 
461 |         if needs_install:
462 |             log.info("Installing Vue/TypeScript Language Server dependencies...")
463 |             deps.install(vue_ls_dir)
464 |             # Write version marker file
465 |             with open(version_file, "w") as f:
466 |                 f.write(expected_version)
467 |             log.info("Vue language server dependencies installed successfully")
468 | 
469 |         if not os.path.exists(vue_executable_path):
470 |             raise FileNotFoundError(
471 |                 f"vue-language-server executable not found at {vue_executable_path}, something went wrong with the installation."
472 |             )
473 | 
474 |         if not os.path.exists(ts_ls_executable_path):
475 |             raise FileNotFoundError(
476 |                 f"typescript-language-server executable not found at {ts_ls_executable_path}, something went wrong with the installation."
477 |             )
478 | 
479 |         return [vue_executable_path, "--stdio"], tsdk_path, [ts_ls_executable_path, "--stdio"]
480 | 
481 |     def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
482 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
483 |         initialize_params = {
484 |             "locale": "en",
485 |             "capabilities": {
486 |                 "textDocument": {
487 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
488 |                     "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
489 |                     "definition": {"dynamicRegistration": True, "linkSupport": True},
490 |                     "references": {"dynamicRegistration": True},
491 |                     "documentSymbol": {
492 |                         "dynamicRegistration": True,
493 |                         "hierarchicalDocumentSymbolSupport": True,
494 |                         "symbolKind": {"valueSet": list(range(1, 27))},
495 |                     },
496 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
497 |                     "signatureHelp": {"dynamicRegistration": True},
498 |                     "codeAction": {"dynamicRegistration": True},
499 |                     "rename": {"dynamicRegistration": True, "prepareSupport": True},
500 |                 },
501 |                 "workspace": {
502 |                     "workspaceFolders": True,
503 |                     "didChangeConfiguration": {"dynamicRegistration": True},
504 |                     "symbol": {"dynamicRegistration": True},
505 |                 },
506 |             },
507 |             "processId": os.getpid(),
508 |             "rootPath": repository_absolute_path,
509 |             "rootUri": root_uri,
510 |             "workspaceFolders": [
511 |                 {
512 |                     "uri": root_uri,
513 |                     "name": os.path.basename(repository_absolute_path),
514 |                 }
515 |             ],
516 |             "initializationOptions": {
517 |                 "vue": {
518 |                     "hybridMode": True,
519 |                 },
520 |                 "typescript": {
521 |                     "tsdk": self.tsdk_path,
522 |                 },
523 |             },
524 |         }
525 |         return initialize_params  # type: ignore
526 | 
527 |     def _start_typescript_server(self) -> None:
528 |         try:
529 |             vue_ts_plugin_path = os.path.join(self._vue_ls_dir, "node_modules", "@vue", "typescript-plugin")
530 | 
531 |             ts_config = LanguageServerConfig(
532 |                 code_language=Language.TYPESCRIPT,
533 |                 trace_lsp_communication=False,
534 |             )
535 | 
536 |             log.info("Creating companion VueTypeScriptServer")
537 |             self._ts_server = VueTypeScriptServer(
538 |                 config=ts_config,
539 |                 repository_root_path=self.repository_root_path,
540 |                 solidlsp_settings=self._solidlsp_settings,
541 |                 vue_plugin_path=vue_ts_plugin_path,
542 |                 tsdk_path=self.tsdk_path,
543 |                 ts_ls_executable_path=self._ts_ls_cmd,
544 |             )
545 | 
546 |             log.info("Starting companion TypeScript server")
547 |             self._ts_server.start()
548 | 
549 |             log.info("Waiting for companion TypeScript server to be ready...")
550 |             if not self._ts_server.server_ready.wait(timeout=self.TS_SERVER_READY_TIMEOUT):
551 |                 log.warning(
552 |                     f"Timeout waiting for companion TypeScript server to be ready after {self.TS_SERVER_READY_TIMEOUT} seconds, proceeding anyway"
553 |                 )
554 |                 self._ts_server.server_ready.set()
555 | 
556 |             self._ts_server_started = True
557 |             log.info("Companion TypeScript server ready")
558 |         except Exception as e:
559 |             log.error(f"Error starting TypeScript server: {e}")
560 |             self._ts_server = None
561 |             self._ts_server_started = False
562 |             raise
563 | 
564 |     def _forward_tsserver_request(self, method: str, params: dict) -> Any:
565 |         if self._ts_server is None:
566 |             log.error("Cannot forward tsserver request - TypeScript server not started")
567 |             return None
568 | 
569 |         try:
570 |             execute_params: ExecuteCommandParams = {
571 |                 "command": "typescript.tsserverRequest",
572 |                 "arguments": [method, params, {"isAsync": True, "lowPriority": True}],
573 |             }
574 |             result = self._ts_server.handler.send.execute_command(execute_params)
575 |             log.debug(f"TypeScript server raw response for {method}: {result}")
576 | 
577 |             if isinstance(result, dict) and "body" in result:
578 |                 return result["body"]
579 |             return result
580 |         except Exception as e:
581 |             log.error(f"Error forwarding tsserver request {method}: {e}")
582 |             return None
583 | 
584 |     def _cleanup_indexed_vue_files(self) -> None:
585 |         if not self._indexed_vue_file_uris or self._ts_server is None:
586 |             return
587 | 
588 |         log.debug(f"Cleaning up {len(self._indexed_vue_file_uris)} indexed Vue files")
589 |         for uri in self._indexed_vue_file_uris:
590 |             try:
591 |                 if uri in self._ts_server.open_file_buffers:
592 |                     file_buffer = self._ts_server.open_file_buffers[uri]
593 |                     file_buffer.ref_count -= 1
594 | 
595 |                     if file_buffer.ref_count == 0:
596 |                         self._ts_server.server.notify.did_close_text_document({"textDocument": {"uri": uri}})
597 |                         del self._ts_server.open_file_buffers[uri]
598 |                         log.debug(f"Closed indexed Vue file: {uri}")
599 |             except Exception as e:
600 |                 log.debug(f"Error closing indexed Vue file {uri}: {e}")
601 | 
602 |         self._indexed_vue_file_uris.clear()
603 | 
604 |     def _stop_typescript_server(self) -> None:
605 |         if self._ts_server is not None:
606 |             try:
607 |                 log.info("Stopping companion TypeScript server")
608 |                 self._ts_server.stop()
609 |             except Exception as e:
610 |                 log.warning(f"Error stopping TypeScript server: {e}")
611 |             finally:
612 |                 self._ts_server = None
613 |                 self._ts_server_started = False
614 | 
615 |     @override
616 |     def _start_server(self) -> None:
617 |         self._start_typescript_server()
618 | 
619 |         def register_capability_handler(params: dict) -> None:
620 |             assert "registrations" in params
621 |             for registration in params["registrations"]:
622 |                 if registration["method"] == "workspace/executeCommand":
623 |                     self.initialize_searcher_command_available.set()
624 |             return
625 | 
626 |         def configuration_handler(params: dict) -> list:
627 |             items = params.get("items", [])
628 |             return [{} for _ in items]
629 | 
630 |         def do_nothing(params: dict) -> None:
631 |             return
632 | 
633 |         def window_log_message(msg: dict) -> None:
634 |             log.info(f"LSP: window/logMessage: {msg}")
635 |             message_text = msg.get("message", "")
636 |             if "initialized" in message_text.lower() or "ready" in message_text.lower():
637 |                 log.info("Vue language server ready signal detected")
638 |                 self.server_ready.set()
639 |                 self.completions_available.set()
640 | 
641 |         def tsserver_request_notification_handler(params: list) -> None:
642 |             try:
643 |                 if params and len(params) > 0 and len(params[0]) >= 2:
644 |                     request_id = params[0][0]
645 |                     method = params[0][1]
646 |                     method_params = params[0][2] if len(params[0]) > 2 else {}
647 |                     log.debug(f"Received tsserver/request: id={request_id}, method={method}")
648 | 
649 |                     if method == "_vue:projectInfo":
650 |                         file_path = method_params.get("file", "")
651 |                         tsconfig_path = self._find_tsconfig_for_file(file_path)
652 |                         result = {"configFileName": tsconfig_path} if tsconfig_path else None
653 |                         response = [[request_id, result]]
654 |                         self.server.notify.send_notification("tsserver/response", response)
655 |                         log.debug(f"Sent tsserver/response for projectInfo: {tsconfig_path}")
656 |                     else:
657 |                         result = self._forward_tsserver_request(method, method_params)
658 |                         response = [[request_id, result]]
659 |                         self.server.notify.send_notification("tsserver/response", response)
660 |                         log.debug(f"Forwarded tsserver/response for {method}: {result}")
661 |                 else:
662 |                     log.warning(f"Unexpected tsserver/request params format: {params}")
663 |             except Exception as e:
664 |                 log.error(f"Error handling tsserver/request: {e}")
665 | 
666 |         self.server.on_request("client/registerCapability", register_capability_handler)
667 |         self.server.on_request("workspace/configuration", configuration_handler)
668 |         self.server.on_notification("tsserver/request", tsserver_request_notification_handler)
669 |         self.server.on_notification("window/logMessage", window_log_message)
670 |         self.server.on_notification("$/progress", do_nothing)
671 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
672 | 
673 |         log.info("Starting Vue server process")
674 |         self.server.start()
675 |         initialize_params = self._get_initialize_params(self.repository_root_path)
676 | 
677 |         log.info("Sending initialize request from LSP client to LSP server and awaiting response")
678 |         init_response = self.server.send.initialize(initialize_params)
679 |         log.debug(f"Received initialize response from Vue server: {init_response}")
680 | 
681 |         assert init_response["capabilities"]["textDocumentSync"] in [1, 2]
682 | 
683 |         self.server.notify.initialized({})
684 | 
685 |         log.info("Waiting for Vue language server to be ready...")
686 |         if not self.server_ready.wait(timeout=self.VUE_SERVER_READY_TIMEOUT):
687 |             log.info("Timeout waiting for Vue server ready signal, proceeding anyway")
688 |             self.server_ready.set()
689 |             self.completions_available.set()
690 |         else:
691 |             log.info("Vue server initialization complete")
692 | 
693 |     def _find_tsconfig_for_file(self, file_path: str) -> str | None:
694 |         if not file_path:
695 |             tsconfig_path = os.path.join(self.repository_root_path, "tsconfig.json")
696 |             return tsconfig_path if os.path.exists(tsconfig_path) else None
697 | 
698 |         current_dir = os.path.dirname(file_path)
699 |         repo_root = os.path.abspath(self.repository_root_path)
700 | 
701 |         while current_dir and current_dir.startswith(repo_root):
702 |             tsconfig_path = os.path.join(current_dir, "tsconfig.json")
703 |             if os.path.exists(tsconfig_path):
704 |                 return tsconfig_path
705 |             parent = os.path.dirname(current_dir)
706 |             if parent == current_dir:
707 |                 break
708 |             current_dir = parent
709 | 
710 |         tsconfig_path = os.path.join(repo_root, "tsconfig.json")
711 |         return tsconfig_path if os.path.exists(tsconfig_path) else None
712 | 
713 |     @override
714 |     def _get_wait_time_for_cross_file_referencing(self) -> float:
715 |         return 5.0
716 | 
717 |     @override
718 |     def stop(self, shutdown_timeout: float = 5.0) -> None:
719 |         self._cleanup_indexed_vue_files()
720 |         self._stop_typescript_server()
721 |         super().stop(shutdown_timeout)
722 | 
723 |     @override
724 |     def _get_preferred_definition(self, definitions: list[ls_types.Location]) -> ls_types.Location:
725 |         return prefer_non_node_modules_definition(definitions)
726 | 
727 |     @override
728 |     def _request_document_symbols(
729 |         self, relative_file_path: str, file_data: LSPFileBuffer | None
730 |     ) -> list[SymbolInformation] | list[DocumentSymbol] | None:
731 |         """
732 |         Override to filter out shorthand property references in Vue files.
733 | 
734 |         In Vue, when using shorthand syntax in defineExpose like `defineExpose({ pressCount })`,
735 |         the Vue LSP returns both:
736 |         - The Variable definition (e.g., `const pressCount = ref(0)`)
737 |         - A Property symbol for the shorthand reference (e.g., `pressCount` in defineExpose)
738 | 
739 |         This causes duplicate symbols with the same name, which breaks symbol lookup.
740 |         We filter out Property symbols that have a matching Variable with the same name
741 |         at a different location (the definition), keeping only the definition.
742 |         """
743 |         symbols = super()._request_document_symbols(relative_file_path, file_data)
744 | 
745 |         if symbols is None or len(symbols) == 0:
746 |             return symbols
747 | 
748 |         # Only process DocumentSymbol format (hierarchical symbols with children)
749 |         # SymbolInformation format doesn't have the same issue
750 |         if not isinstance(symbols[0], dict) or "range" not in symbols[0]:
751 |             return symbols
752 | 
753 |         return self._filter_shorthand_property_duplicates(symbols)
754 | 
755 |     def _filter_shorthand_property_duplicates(
756 |         self, symbols: list[DocumentSymbol] | list[SymbolInformation]
757 |     ) -> list[DocumentSymbol] | list[SymbolInformation]:
758 |         """
759 |         Filter out Property symbols that have a matching Variable symbol with the same name.
760 | 
761 |         This handles Vue's shorthand property syntax in defineExpose, where the same
762 |         identifier appears as both a Variable definition and a Property reference.
763 |         """
764 |         VARIABLE_KIND = 13  # SymbolKind.Variable
765 |         PROPERTY_KIND = 7  # SymbolKind.Property
766 | 
767 |         def filter_symbols(syms: list[dict]) -> list[dict]:
768 |             # Collect all Variable symbol names with their line numbers
769 |             variable_names: dict[str, set[int]] = {}
770 |             for sym in syms:
771 |                 if sym.get("kind") == VARIABLE_KIND:
772 |                     name = sym.get("name", "")
773 |                     line = sym.get("range", {}).get("start", {}).get("line", -1)
774 |                     if name not in variable_names:
775 |                         variable_names[name] = set()
776 |                     variable_names[name].add(line)
777 | 
778 |             # Filter: keep symbols that are either:
779 |             # 1. Not a Property, or
780 |             # 2. A Property without a matching Variable name at a different location
781 |             filtered = []
782 |             for sym in syms:
783 |                 name = sym.get("name", "")
784 |                 kind = sym.get("kind")
785 |                 line = sym.get("range", {}).get("start", {}).get("line", -1)
786 | 
787 |                 # If it's a Property with a matching Variable name at a DIFFERENT line, skip it
788 |                 if kind == PROPERTY_KIND and name in variable_names:
789 |                     # Check if there's a Variable definition at a different line
790 |                     var_lines = variable_names[name]
791 |                     if any(var_line != line for var_line in var_lines):
792 |                         # This is a shorthand reference, skip it
793 |                         log.debug(
794 |                             f"Filtering shorthand property reference '{name}' at line {line} "
795 |                             f"(Variable definition exists at line(s) {var_lines})"
796 |                         )
797 |                         continue
798 | 
799 |                 # Recursively filter children
800 |                 children = sym.get("children", [])
801 |                 if children:
802 |                     sym = dict(sym)  # Create a copy to avoid mutating the original
803 |                     sym["children"] = filter_symbols(children)
804 | 
805 |                 filtered.append(sym)
806 | 
807 |             return filtered
808 | 
809 |         return filter_symbols(list(symbols))  # type: ignore
810 | 
```
Page 16/21FirstPrevNextLast