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 |
```