This is page 12 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
│ │ │ │ ├── news
│ │ │ │ │ └── 20260111.html
│ │ │ │ ├── serena-icon-16.png
│ │ │ │ ├── serena-icon-32.png
│ │ │ │ ├── serena-icon-48.png
│ │ │ │ ├── serena-logo-dark-mode.svg
│ │ │ │ ├── serena-logo.svg
│ │ │ │ ├── serena-logs-dark-mode.png
│ │ │ │ └── serena-logs.png
│ │ │ ├── project.template.yml
│ │ │ └── serena_config.template.yml
│ │ ├── symbol.py
│ │ ├── task_executor.py
│ │ ├── text_utils.py
│ │ ├── tools
│ │ │ ├── __init__.py
│ │ │ ├── cmd_tools.py
│ │ │ ├── config_tools.py
│ │ │ ├── file_tools.py
│ │ │ ├── jetbrains_plugin_client.py
│ │ │ ├── jetbrains_tools.py
│ │ │ ├── jetbrains_types.py
│ │ │ ├── memory_tools.py
│ │ │ ├── symbol_tools.py
│ │ │ ├── tools_base.py
│ │ │ └── workflow_tools.py
│ │ └── util
│ │ ├── class_decorators.py
│ │ ├── cli_util.py
│ │ ├── exception.py
│ │ ├── file_system.py
│ │ ├── general.py
│ │ ├── git.py
│ │ ├── gui.py
│ │ ├── inspection.py
│ │ ├── logging.py
│ │ ├── shell.py
│ │ ├── thread.py
│ │ └── version.py
│ └── solidlsp
│ ├── __init__.py
│ ├── .gitignore
│ ├── language_servers
│ │ ├── al_language_server.py
│ │ ├── bash_language_server.py
│ │ ├── clangd_language_server.py
│ │ ├── clojure_lsp.py
│ │ ├── common.py
│ │ ├── csharp_language_server.py
│ │ ├── dart_language_server.py
│ │ ├── eclipse_jdtls.py
│ │ ├── elixir_tools
│ │ │ ├── __init__.py
│ │ │ ├── elixir_tools.py
│ │ │ └── README.md
│ │ ├── elm_language_server.py
│ │ ├── erlang_language_server.py
│ │ ├── fortran_language_server.py
│ │ ├── fsharp_language_server.py
│ │ ├── gopls.py
│ │ ├── groovy_language_server.py
│ │ ├── haskell_language_server.py
│ │ ├── intelephense.py
│ │ ├── jedi_server.py
│ │ ├── julia_server.py
│ │ ├── kotlin_language_server.py
│ │ ├── lua_ls.py
│ │ ├── marksman.py
│ │ ├── matlab_language_server.py
│ │ ├── nixd_ls.py
│ │ ├── omnisharp
│ │ │ ├── initialize_params.json
│ │ │ ├── runtime_dependencies.json
│ │ │ └── workspace_did_change_configuration.json
│ │ ├── omnisharp.py
│ │ ├── pascal_server.py
│ │ ├── perl_language_server.py
│ │ ├── powershell_language_server.py
│ │ ├── pyright_server.py
│ │ ├── r_language_server.py
│ │ ├── regal_server.py
│ │ ├── ruby_lsp.py
│ │ ├── rust_analyzer.py
│ │ ├── scala_language_server.py
│ │ ├── solargraph.py
│ │ ├── sourcekit_lsp.py
│ │ ├── taplo_server.py
│ │ ├── terraform_ls.py
│ │ ├── typescript_language_server.py
│ │ ├── vts_language_server.py
│ │ ├── vue_language_server.py
│ │ ├── yaml_language_server.py
│ │ └── zls.py
│ ├── ls_config.py
│ ├── ls_exceptions.py
│ ├── ls_handler.py
│ ├── ls_request.py
│ ├── ls_types.py
│ ├── ls_utils.py
│ ├── ls.py
│ ├── lsp_protocol_handler
│ │ ├── lsp_constants.py
│ │ ├── lsp_requests.py
│ │ ├── lsp_types.py
│ │ └── server.py
│ ├── settings.py
│ └── util
│ ├── cache.py
│ ├── subprocess_util.py
│ └── zip.py
├── sync.py
├── test
│ ├── __init__.py
│ ├── conftest.py
│ ├── resources
│ │ └── repos
│ │ ├── al
│ │ │ └── test_repo
│ │ │ ├── app.json
│ │ │ └── src
│ │ │ ├── Codeunits
│ │ │ │ ├── CustomerMgt.Codeunit.al
│ │ │ │ └── PaymentProcessorImpl.Codeunit.al
│ │ │ ├── Enums
│ │ │ │ └── CustomerType.Enum.al
│ │ │ ├── Interfaces
│ │ │ │ └── IPaymentProcessor.Interface.al
│ │ │ ├── Pages
│ │ │ │ ├── CustomerCard.Page.al
│ │ │ │ └── CustomerList.Page.al
│ │ │ ├── TableExtensions
│ │ │ │ └── Item.TableExt.al
│ │ │ └── Tables
│ │ │ └── Customer.Table.al
│ │ ├── bash
│ │ │ └── test_repo
│ │ │ ├── config.sh
│ │ │ ├── main.sh
│ │ │ └── utils.sh
│ │ ├── clojure
│ │ │ └── test_repo
│ │ │ ├── deps.edn
│ │ │ └── src
│ │ │ └── test_app
│ │ │ ├── core.clj
│ │ │ └── utils.clj
│ │ ├── csharp
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── Models
│ │ │ │ └── Person.cs
│ │ │ ├── Program.cs
│ │ │ ├── serena.sln
│ │ │ └── TestProject.csproj
│ │ ├── dart
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── lib
│ │ │ │ ├── helper.dart
│ │ │ │ ├── main.dart
│ │ │ │ └── models.dart
│ │ │ └── pubspec.yaml
│ │ ├── elixir
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── lib
│ │ │ │ ├── examples.ex
│ │ │ │ ├── ignored_dir
│ │ │ │ │ └── ignored_module.ex
│ │ │ │ ├── models.ex
│ │ │ │ ├── services.ex
│ │ │ │ ├── test_repo.ex
│ │ │ │ └── utils.ex
│ │ │ ├── mix.exs
│ │ │ ├── mix.lock
│ │ │ ├── scripts
│ │ │ │ └── build_script.ex
│ │ │ └── test
│ │ │ ├── models_test.exs
│ │ │ └── test_repo_test.exs
│ │ ├── elm
│ │ │ └── test_repo
│ │ │ ├── elm.json
│ │ │ ├── Main.elm
│ │ │ └── Utils.elm
│ │ ├── erlang
│ │ │ └── test_repo
│ │ │ ├── hello.erl
│ │ │ ├── ignored_dir
│ │ │ │ └── ignored_module.erl
│ │ │ ├── include
│ │ │ │ ├── records.hrl
│ │ │ │ └── types.hrl
│ │ │ ├── math_utils.erl
│ │ │ ├── rebar.config
│ │ │ ├── src
│ │ │ │ ├── app.erl
│ │ │ │ ├── models.erl
│ │ │ │ ├── services.erl
│ │ │ │ └── utils.erl
│ │ │ └── test
│ │ │ ├── models_tests.erl
│ │ │ └── utils_tests.erl
│ │ ├── fortran
│ │ │ └── test_repo
│ │ │ ├── main.f90
│ │ │ └── modules
│ │ │ ├── geometry.f90
│ │ │ └── math_utils.f90
│ │ ├── fsharp
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── Calculator.fs
│ │ │ ├── Models
│ │ │ │ └── Person.fs
│ │ │ ├── Program.fs
│ │ │ ├── README.md
│ │ │ └── TestProject.fsproj
│ │ ├── go
│ │ │ └── test_repo
│ │ │ └── main.go
│ │ ├── groovy
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── build.gradle
│ │ │ └── src
│ │ │ └── main
│ │ │ └── groovy
│ │ │ └── com
│ │ │ └── example
│ │ │ ├── Main.groovy
│ │ │ ├── Model.groovy
│ │ │ ├── ModelUser.groovy
│ │ │ └── Utils.groovy
│ │ ├── haskell
│ │ │ └── test_repo
│ │ │ ├── app
│ │ │ │ └── Main.hs
│ │ │ ├── haskell-test-repo.cabal
│ │ │ ├── package.yaml
│ │ │ ├── src
│ │ │ │ ├── Calculator.hs
│ │ │ │ └── Helper.hs
│ │ │ └── stack.yaml
│ │ ├── java
│ │ │ └── test_repo
│ │ │ ├── pom.xml
│ │ │ └── src
│ │ │ └── main
│ │ │ └── java
│ │ │ └── test_repo
│ │ │ ├── Main.java
│ │ │ ├── Model.java
│ │ │ ├── ModelUser.java
│ │ │ └── Utils.java
│ │ ├── julia
│ │ │ └── test_repo
│ │ │ ├── lib
│ │ │ │ └── helper.jl
│ │ │ └── main.jl
│ │ ├── kotlin
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── build.gradle.kts
│ │ │ └── src
│ │ │ └── main
│ │ │ └── kotlin
│ │ │ └── test_repo
│ │ │ ├── Main.kt
│ │ │ ├── Model.kt
│ │ │ ├── ModelUser.kt
│ │ │ └── Utils.kt
│ │ ├── lua
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── main.lua
│ │ │ ├── src
│ │ │ │ ├── calculator.lua
│ │ │ │ └── utils.lua
│ │ │ └── tests
│ │ │ └── test_calculator.lua
│ │ ├── markdown
│ │ │ └── test_repo
│ │ │ ├── api.md
│ │ │ ├── CONTRIBUTING.md
│ │ │ ├── guide.md
│ │ │ └── README.md
│ │ ├── matlab
│ │ │ └── test_repo
│ │ │ ├── Calculator.m
│ │ │ └── main.m
│ │ ├── nix
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── default.nix
│ │ │ ├── flake.nix
│ │ │ ├── lib
│ │ │ │ └── utils.nix
│ │ │ ├── modules
│ │ │ │ └── example.nix
│ │ │ └── scripts
│ │ │ └── hello.sh
│ │ ├── pascal
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── lib
│ │ │ │ └── helper.pas
│ │ │ └── main.pas
│ │ ├── perl
│ │ │ └── test_repo
│ │ │ ├── helper.pl
│ │ │ └── main.pl
│ │ ├── php
│ │ │ └── test_repo
│ │ │ ├── helper.php
│ │ │ ├── index.php
│ │ │ └── simple_var.php
│ │ ├── powershell
│ │ │ └── test_repo
│ │ │ ├── main.ps1
│ │ │ ├── PowerShellEditorServices.json
│ │ │ └── utils.ps1
│ │ ├── python
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── custom_test
│ │ │ │ ├── __init__.py
│ │ │ │ └── advanced_features.py
│ │ │ ├── examples
│ │ │ │ ├── __init__.py
│ │ │ │ └── user_management.py
│ │ │ ├── ignore_this_dir_with_postfix
│ │ │ │ └── ignored_module.py
│ │ │ ├── scripts
│ │ │ │ ├── __init__.py
│ │ │ │ └── run_app.py
│ │ │ └── test_repo
│ │ │ ├── __init__.py
│ │ │ ├── complex_types.py
│ │ │ ├── models.py
│ │ │ ├── name_collisions.py
│ │ │ ├── nested_base.py
│ │ │ ├── nested.py
│ │ │ ├── overloaded.py
│ │ │ ├── services.py
│ │ │ ├── utils.py
│ │ │ └── variables.py
│ │ ├── r
│ │ │ └── test_repo
│ │ │ ├── .Rbuildignore
│ │ │ ├── DESCRIPTION
│ │ │ ├── examples
│ │ │ │ └── analysis.R
│ │ │ ├── NAMESPACE
│ │ │ └── R
│ │ │ ├── models.R
│ │ │ └── utils.R
│ │ ├── rego
│ │ │ └── test_repo
│ │ │ ├── policies
│ │ │ │ ├── authz.rego
│ │ │ │ └── validation.rego
│ │ │ └── utils
│ │ │ └── helpers.rego
│ │ ├── ruby
│ │ │ └── test_repo
│ │ │ ├── .solargraph.yml
│ │ │ ├── examples
│ │ │ │ └── user_management.rb
│ │ │ ├── lib.rb
│ │ │ ├── main.rb
│ │ │ ├── models.rb
│ │ │ ├── nested.rb
│ │ │ ├── services.rb
│ │ │ └── variables.rb
│ │ ├── rust
│ │ │ ├── test_repo
│ │ │ │ ├── Cargo.lock
│ │ │ │ ├── Cargo.toml
│ │ │ │ └── src
│ │ │ │ ├── lib.rs
│ │ │ │ └── main.rs
│ │ │ └── test_repo_2024
│ │ │ ├── Cargo.lock
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── main.rs
│ │ ├── scala
│ │ │ ├── build.sbt
│ │ │ ├── project
│ │ │ │ ├── build.properties
│ │ │ │ ├── metals.sbt
│ │ │ │ └── plugins.sbt
│ │ │ └── src
│ │ │ └── main
│ │ │ └── scala
│ │ │ └── com
│ │ │ └── example
│ │ │ ├── Main.scala
│ │ │ └── Utils.scala
│ │ ├── swift
│ │ │ └── test_repo
│ │ │ ├── Package.swift
│ │ │ └── src
│ │ │ ├── main.swift
│ │ │ └── utils.swift
│ │ ├── terraform
│ │ │ └── test_repo
│ │ │ ├── data.tf
│ │ │ ├── main.tf
│ │ │ ├── outputs.tf
│ │ │ └── variables.tf
│ │ ├── toml
│ │ │ └── test_repo
│ │ │ ├── Cargo.toml
│ │ │ ├── config.toml
│ │ │ └── pyproject.toml
│ │ ├── typescript
│ │ │ └── test_repo
│ │ │ ├── .serena
│ │ │ │ └── project.yml
│ │ │ ├── index.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── use_helper.ts
│ │ │ └── ws_manager.js
│ │ ├── vue
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── src
│ │ │ │ ├── App.vue
│ │ │ │ ├── components
│ │ │ │ │ ├── CalculatorButton.vue
│ │ │ │ │ ├── CalculatorDisplay.vue
│ │ │ │ │ └── CalculatorInput.vue
│ │ │ │ ├── composables
│ │ │ │ │ ├── useFormatter.ts
│ │ │ │ │ └── useTheme.ts
│ │ │ │ ├── main.ts
│ │ │ │ ├── stores
│ │ │ │ │ └── calculator.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── tsconfig.node.json
│ │ │ └── vite.config.ts
│ │ ├── yaml
│ │ │ └── test_repo
│ │ │ ├── config.yaml
│ │ │ ├── data.yaml
│ │ │ └── services.yml
│ │ └── zig
│ │ └── test_repo
│ │ ├── .gitignore
│ │ ├── build.zig
│ │ ├── src
│ │ │ ├── calculator.zig
│ │ │ ├── main.zig
│ │ │ └── math_utils.zig
│ │ └── zls.json
│ ├── serena
│ │ ├── __init__.py
│ │ ├── __snapshots__
│ │ │ └── test_symbol_editing.ambr
│ │ ├── config
│ │ │ ├── __init__.py
│ │ │ └── test_serena_config.py
│ │ ├── test_cli_project_commands.py
│ │ ├── test_edit_marker.py
│ │ ├── test_mcp.py
│ │ ├── test_serena_agent.py
│ │ ├── test_symbol_editing.py
│ │ ├── test_symbol.py
│ │ ├── test_task_executor.py
│ │ ├── test_text_utils.py
│ │ ├── test_tool_parameter_types.py
│ │ └── util
│ │ ├── test_exception.py
│ │ └── test_file_system.py
│ └── solidlsp
│ ├── al
│ │ └── test_al_basic.py
│ ├── bash
│ │ ├── __init__.py
│ │ └── test_bash_basic.py
│ ├── clojure
│ │ ├── __init__.py
│ │ └── test_clojure_basic.py
│ ├── csharp
│ │ └── test_csharp_basic.py
│ ├── dart
│ │ ├── __init__.py
│ │ └── test_dart_basic.py
│ ├── elixir
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_elixir_basic.py
│ │ ├── test_elixir_ignored_dirs.py
│ │ ├── test_elixir_integration.py
│ │ └── test_elixir_symbol_retrieval.py
│ ├── elm
│ │ └── test_elm_basic.py
│ ├── erlang
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_erlang_basic.py
│ │ ├── test_erlang_ignored_dirs.py
│ │ └── test_erlang_symbol_retrieval.py
│ ├── fortran
│ │ ├── __init__.py
│ │ └── test_fortran_basic.py
│ ├── fsharp
│ │ └── test_fsharp_basic.py
│ ├── go
│ │ └── test_go_basic.py
│ ├── groovy
│ │ └── test_groovy_basic.py
│ ├── haskell
│ │ ├── __init__.py
│ │ └── test_haskell_basic.py
│ ├── java
│ │ └── test_java_basic.py
│ ├── julia
│ │ └── test_julia_basic.py
│ ├── kotlin
│ │ └── test_kotlin_basic.py
│ ├── lua
│ │ └── test_lua_basic.py
│ ├── markdown
│ │ ├── __init__.py
│ │ └── test_markdown_basic.py
│ ├── matlab
│ │ ├── __init__.py
│ │ └── test_matlab_basic.py
│ ├── nix
│ │ └── test_nix_basic.py
│ ├── pascal
│ │ ├── __init__.py
│ │ └── test_pascal_basic.py
│ ├── perl
│ │ └── test_perl_basic.py
│ ├── php
│ │ └── test_php_basic.py
│ ├── powershell
│ │ ├── __init__.py
│ │ └── test_powershell_basic.py
│ ├── python
│ │ ├── test_python_basic.py
│ │ ├── test_retrieval_with_ignored_dirs.py
│ │ └── test_symbol_retrieval.py
│ ├── r
│ │ ├── __init__.py
│ │ └── test_r_basic.py
│ ├── rego
│ │ └── test_rego_basic.py
│ ├── ruby
│ │ ├── test_ruby_basic.py
│ │ └── test_ruby_symbol_retrieval.py
│ ├── rust
│ │ ├── test_rust_2024_edition.py
│ │ ├── test_rust_analyzer_detection.py
│ │ └── test_rust_basic.py
│ ├── scala
│ │ └── test_scala_language_server.py
│ ├── swift
│ │ └── test_swift_basic.py
│ ├── terraform
│ │ └── test_terraform_basic.py
│ ├── test_lsp_protocol_handler_server.py
│ ├── toml
│ │ ├── __init__.py
│ │ ├── test_toml_basic.py
│ │ ├── test_toml_edge_cases.py
│ │ ├── test_toml_ignored_dirs.py
│ │ └── test_toml_symbol_retrieval.py
│ ├── typescript
│ │ └── test_typescript_basic.py
│ ├── util
│ │ └── test_zip.py
│ ├── vue
│ │ ├── __init__.py
│ │ ├── test_vue_basic.py
│ │ ├── test_vue_error_cases.py
│ │ ├── test_vue_rename.py
│ │ └── test_vue_symbol_retrieval.py
│ ├── yaml_ls
│ │ ├── __init__.py
│ │ └── test_yaml_basic.py
│ └── zig
│ └── test_zig_basic.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/test/solidlsp/vue/test_vue_basic.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 |
3 | import pytest
4 |
5 | from solidlsp import SolidLanguageServer
6 | from solidlsp.ls_config import Language
7 | from solidlsp.ls_utils import SymbolUtils
8 |
9 |
10 | @pytest.mark.vue
11 | class TestVueLanguageServer:
12 | @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
13 | def test_vue_files_in_symbol_tree(self, language_server: SolidLanguageServer) -> None:
14 | symbols = language_server.request_full_symbol_tree()
15 | assert SymbolUtils.symbol_tree_contains_name(symbols, "App"), "App not found in symbol tree"
16 | assert SymbolUtils.symbol_tree_contains_name(symbols, "CalculatorButton"), "CalculatorButton not found in symbol tree"
17 | assert SymbolUtils.symbol_tree_contains_name(symbols, "CalculatorInput"), "CalculatorInput not found in symbol tree"
18 | assert SymbolUtils.symbol_tree_contains_name(symbols, "CalculatorDisplay"), "CalculatorDisplay not found in symbol tree"
19 |
20 | @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
21 | def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
22 | store_file = os.path.join("src", "stores", "calculator.ts")
23 | symbols = language_server.request_document_symbols(store_file).get_all_symbols_and_roots()
24 |
25 | # Find useCalculatorStore function
26 | store_symbol = None
27 | for sym in symbols[0]:
28 | if sym.get("name") == "useCalculatorStore":
29 | store_symbol = sym
30 | break
31 |
32 | assert store_symbol is not None, "useCalculatorStore function not found"
33 |
34 | # Get references
35 | sel_start = store_symbol["selectionRange"]["start"]
36 | refs = language_server.request_references(store_file, sel_start["line"], sel_start["character"])
37 |
38 | # Should have multiple references: definition + usage in App.vue, CalculatorInput.vue, CalculatorDisplay.vue
39 | assert len(refs) >= 4, f"useCalculatorStore should have at least 4 references (definition + 3 usages), got {len(refs)}"
40 |
41 | # Verify we have references from .vue files
42 | vue_refs = [ref for ref in refs if ".vue" in ref.get("relativePath", "")]
43 | assert len(vue_refs) >= 3, f"Should have at least 3 Vue component references, got {len(vue_refs)}"
44 |
45 |
46 | @pytest.mark.vue
47 | class TestVueDualLspArchitecture:
48 | @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
49 | def test_typescript_server_coordination(self, language_server: SolidLanguageServer) -> None:
50 | ts_file = os.path.join("src", "stores", "calculator.ts")
51 | ts_symbols = language_server.request_document_symbols(ts_file).get_all_symbols_and_roots()
52 | ts_symbol_names = [s.get("name") for s in ts_symbols[0]]
53 |
54 | assert len(ts_symbols[0]) >= 5, f"TypeScript server should return multiple symbols for calculator.ts, got {len(ts_symbols[0])}"
55 | assert "useCalculatorStore" in ts_symbol_names, "TypeScript server should extract store function"
56 |
57 | # Verify Vue server can parse .vue files
58 | vue_file = os.path.join("src", "App.vue")
59 | vue_symbols = language_server.request_document_symbols(vue_file).get_all_symbols_and_roots()
60 | vue_symbol_names = [s.get("name") for s in vue_symbols[0]]
61 |
62 | assert len(vue_symbols[0]) >= 15, f"Vue server should return at least 15 symbols for App.vue, got {len(vue_symbols[0])}"
63 | assert "appTitle" in vue_symbol_names, "Vue server should extract ref declarations from script setup"
64 |
65 | @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
66 | def test_cross_file_references_vue_to_typescript(self, language_server: SolidLanguageServer) -> None:
67 | store_file = os.path.join("src", "stores", "calculator.ts")
68 | store_symbols = language_server.request_document_symbols(store_file).get_all_symbols_and_roots()
69 |
70 | store_symbol = None
71 | for sym in store_symbols[0]:
72 | if sym.get("name") == "useCalculatorStore":
73 | store_symbol = sym
74 | break
75 |
76 | if not store_symbol or "selectionRange" not in store_symbol:
77 | pytest.skip("useCalculatorStore symbol not found - test fixture may need updating")
78 |
79 | # Request references for this symbol
80 | sel_start = store_symbol["selectionRange"]["start"]
81 | refs = language_server.request_references(store_file, sel_start["line"], sel_start["character"])
82 |
83 | # Verify we found references: definition + usage in App.vue, CalculatorInput.vue, CalculatorDisplay.vue
84 | assert len(refs) >= 4, f"useCalculatorStore should have at least 4 references (definition + 3 usages), found {len(refs)} references"
85 |
86 | # Verify references include .vue files (components that import the store)
87 | vue_refs = [ref for ref in refs if ".vue" in ref.get("uri", "")]
88 | assert (
89 | len(vue_refs) >= 3
90 | ), f"Should find at least 3 references in Vue components, found {len(vue_refs)}: {[ref.get('uri', '') for ref in vue_refs]}"
91 |
92 | # Verify specific components that use the store
93 | expected_vue_files = ["App.vue", "CalculatorInput.vue", "CalculatorDisplay.vue"]
94 | found_components = []
95 | for expected_file in expected_vue_files:
96 | matching_refs = [ref for ref in vue_refs if expected_file in ref.get("uri", "")]
97 | if matching_refs:
98 | found_components.append(expected_file)
99 |
100 | assert len(found_components) > 0, (
101 | f"Should find references in at least one component that uses the store. "
102 | f"Expected any of {expected_vue_files}, found references in: {[ref.get('uri', '') for ref in vue_refs]}"
103 | )
104 |
105 | @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
106 | def test_cross_file_references_typescript_to_vue(self, language_server: SolidLanguageServer) -> None:
107 | types_file = os.path.join("src", "types", "index.ts")
108 | types_symbols = language_server.request_document_symbols(types_file).get_all_symbols_and_roots()
109 | types_symbol_names = [s.get("name") for s in types_symbols[0]]
110 |
111 | # Operation type is used in calculator.ts and CalculatorInput.vue
112 | assert "Operation" in types_symbol_names, "Operation type should exist in types file"
113 |
114 | operation_symbol = None
115 | for sym in types_symbols[0]:
116 | if sym.get("name") == "Operation":
117 | operation_symbol = sym
118 | break
119 |
120 | if not operation_symbol or "selectionRange" not in operation_symbol:
121 | pytest.skip("Operation type symbol not found - test fixture may need updating")
122 |
123 | # Request references for the Operation type
124 | sel_start = operation_symbol["selectionRange"]["start"]
125 | refs = language_server.request_references(types_file, sel_start["line"], sel_start["character"])
126 |
127 | # Verify we found references: definition + usage in calculator.ts and Vue files
128 | assert len(refs) >= 2, f"Operation type should have at least 2 references (definition + usages), found {len(refs)} references"
129 |
130 | # The Operation type should be referenced in both .ts files (calculator.ts) and potentially .vue files
131 | all_ref_uris = [ref.get("uri", "") for ref in refs]
132 | has_ts_refs = any(".ts" in uri and "types" not in uri for uri in all_ref_uris)
133 |
134 | assert (
135 | has_ts_refs
136 | ), f"Operation type should be referenced in TypeScript files like calculator.ts. Found references in: {all_ref_uris}"
137 |
138 | @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
139 | def test_reference_deduplication(self, language_server: SolidLanguageServer) -> None:
140 | store_file = os.path.join("src", "stores", "calculator.ts")
141 | store_symbols = language_server.request_document_symbols(store_file).get_all_symbols_and_roots()
142 |
143 | # Find a commonly-used symbol (useCalculatorStore)
144 | store_symbol = None
145 | for sym in store_symbols[0]:
146 | if sym.get("name") == "useCalculatorStore":
147 | store_symbol = sym
148 | break
149 |
150 | if not store_symbol or "selectionRange" not in store_symbol:
151 | pytest.skip("useCalculatorStore symbol not found - test fixture may need updating")
152 |
153 | # Request references
154 | sel_start = store_symbol["selectionRange"]["start"]
155 | refs = language_server.request_references(store_file, sel_start["line"], sel_start["character"])
156 |
157 | # Check for duplicate references (same file, line, and character)
158 | seen_locations = set()
159 | duplicates = []
160 |
161 | for ref in refs:
162 | # Create a unique key for this reference location
163 | uri = ref.get("uri", "")
164 | if "range" in ref:
165 | line = ref["range"]["start"]["line"]
166 | character = ref["range"]["start"]["character"]
167 | location_key = (uri, line, character)
168 |
169 | if location_key in seen_locations:
170 | duplicates.append(location_key)
171 | else:
172 | seen_locations.add(location_key)
173 |
174 | assert len(duplicates) == 0, (
175 | f"Found {len(duplicates)} duplicate reference locations. "
176 | f"The dual-LSP architecture should deduplicate references from both servers. "
177 | f"Duplicates: {duplicates}"
178 | )
179 |
180 |
181 | @pytest.mark.vue
182 | class TestVueEdgeCases:
183 | @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
184 | def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None:
185 | full_tree = language_server.request_full_symbol_tree()
186 |
187 | # Helper to extract all file paths from symbol tree
188 | def extract_paths_from_tree(symbols, paths=None):
189 | """Recursively extract file paths from symbol tree."""
190 | if paths is None:
191 | paths = []
192 |
193 | if isinstance(symbols, list):
194 | for symbol in symbols:
195 | extract_paths_from_tree(symbol, paths)
196 | elif isinstance(symbols, dict):
197 | # Check if this symbol has a location
198 | if "location" in symbols and "uri" in symbols["location"]:
199 | uri = symbols["location"]["uri"]
200 | # Extract the path after file://
201 | if uri.startswith("file://"):
202 | file_path = uri[7:] # Remove "file://"
203 | paths.append(file_path)
204 |
205 | # Recurse into children
206 | if "children" in symbols:
207 | extract_paths_from_tree(symbols["children"], paths)
208 |
209 | return paths
210 |
211 | all_paths = extract_paths_from_tree(full_tree)
212 |
213 | # Verify we have files from expected directories
214 | # Note: Symbol tree may include duplicate paths (one per symbol in file)
215 | components_files = list({p for p in all_paths if "components" in p and ".vue" in p})
216 | stores_files = list({p for p in all_paths if "stores" in p and ".ts" in p})
217 | composables_files = list({p for p in all_paths if "composables" in p and ".ts" in p})
218 |
219 | assert len(components_files) == 3, (
220 | f"Symbol tree should include exactly 3 unique Vue components (CalculatorButton, CalculatorInput, CalculatorDisplay). "
221 | f"Found {len(components_files)} unique component files: {[p.split('/')[-1] for p in sorted(components_files)]}"
222 | )
223 |
224 | assert len(stores_files) == 1, (
225 | f"Symbol tree should include exactly 1 unique store file (calculator.ts). "
226 | f"Found {len(stores_files)} unique store files: {[p.split('/')[-1] for p in sorted(stores_files)]}"
227 | )
228 |
229 | assert len(composables_files) == 2, (
230 | f"Symbol tree should include exactly 2 unique composable files (useFormatter.ts, useTheme.ts). "
231 | f"Found {len(composables_files)} unique composable files: {[p.split('/')[-1] for p in sorted(composables_files)]}"
232 | )
233 |
234 | # Verify specific expected files exist in the tree
235 | expected_files = [
236 | "CalculatorButton.vue",
237 | "CalculatorInput.vue",
238 | "CalculatorDisplay.vue",
239 | "App.vue",
240 | "calculator.ts",
241 | "useFormatter.ts",
242 | "useTheme.ts",
243 | ]
244 |
245 | for expected_file in expected_files:
246 | matching_files = [p for p in all_paths if expected_file in p]
247 | assert len(matching_files) > 0, f"Expected file '{expected_file}' should be in symbol tree. All paths: {all_paths}"
248 |
249 | @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
250 | def test_document_overview(self, language_server: SolidLanguageServer) -> None:
251 | app_file = os.path.join("src", "App.vue")
252 | overview = language_server.request_document_overview(app_file)
253 |
254 | # Overview should return a list of top-level symbols
255 | assert isinstance(overview, list), f"Overview should be a list, got: {type(overview)}"
256 | assert len(overview) >= 1, f"App.vue should have at least 1 top-level symbol in overview, got {len(overview)}"
257 |
258 | # Extract symbol names from overview
259 | symbol_names = [s.get("name") for s in overview if isinstance(s, dict)]
260 |
261 | # Vue LSP returns SFC structure (template/script/style sections) for .vue files
262 | # This is expected behavior - overview shows the file's high-level structure
263 | assert (
264 | len(symbol_names) >= 1
265 | ), f"Should have at least 1 symbol name in overview (e.g., 'App' or SFC section), got {len(symbol_names)}: {symbol_names}"
266 |
267 | # Test overview for a TypeScript file
268 | store_file = os.path.join("src", "stores", "calculator.ts")
269 | store_overview = language_server.request_document_overview(store_file)
270 |
271 | assert isinstance(store_overview, list), f"Store overview should be a list, got: {type(store_overview)}"
272 | assert len(store_overview) >= 1, f"calculator.ts should have at least 1 top-level symbol in overview, got {len(store_overview)}"
273 |
274 | store_symbol_names = [s.get("name") for s in store_overview if isinstance(s, dict)]
275 | assert (
276 | "useCalculatorStore" in store_symbol_names
277 | ), f"useCalculatorStore should be in store file overview. Found {len(store_symbol_names)} symbols: {store_symbol_names}"
278 |
279 | # Test overview for another Vue component
280 | button_file = os.path.join("src", "components", "CalculatorButton.vue")
281 | button_overview = language_server.request_document_overview(button_file)
282 |
283 | assert isinstance(button_overview, list), f"Button overview should be a list, got: {type(button_overview)}"
284 | assert (
285 | len(button_overview) >= 1
286 | ), f"CalculatorButton.vue should have at least 1 top-level symbol in overview, got {len(button_overview)}"
287 |
288 | # For Vue files, overview provides SFC structure which is useful for navigation
289 | # The detailed symbols are available via request_document_symbols
290 | button_symbol_names = [s.get("name") for s in button_overview if isinstance(s, dict)]
291 | assert len(button_symbol_names) >= 1, (
292 | f"CalculatorButton.vue should have at least 1 symbol in overview (e.g., 'CalculatorButton' or SFC section). "
293 | f"Found {len(button_symbol_names)} symbols: {button_symbol_names}"
294 | )
295 |
296 | @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
297 | def test_directory_overview(self, language_server: SolidLanguageServer) -> None:
298 | components_dir = os.path.join("src", "components")
299 | dir_overview = language_server.request_dir_overview(components_dir)
300 |
301 | # Directory overview should be a dict mapping file paths to symbol lists
302 | assert isinstance(dir_overview, dict), f"Directory overview should be a dict, got: {type(dir_overview)}"
303 | assert len(dir_overview) == 3, f"src/components directory should have exactly 3 files in overview, got {len(dir_overview)}"
304 |
305 | # Verify all component files are included
306 | expected_components = ["CalculatorButton.vue", "CalculatorInput.vue", "CalculatorDisplay.vue"]
307 |
308 | for expected_component in expected_components:
309 | # Find files that match this component name
310 | matching_files = [path for path in dir_overview.keys() if expected_component in path]
311 | assert len(matching_files) == 1, (
312 | f"Component '{expected_component}' should appear exactly once in directory overview. "
313 | f"Found {len(matching_files)} matches. All files: {list(dir_overview.keys())}"
314 | )
315 |
316 | # Verify the matched file has symbols
317 | file_path = matching_files[0]
318 | symbols = dir_overview[file_path]
319 | assert isinstance(symbols, list), f"Symbols for {file_path} should be a list, got {type(symbols)}"
320 | assert len(symbols) >= 1, f"Component {expected_component} should have at least 1 symbol in overview, got {len(symbols)}"
321 |
322 | # Test overview for stores directory
323 | stores_dir = os.path.join("src", "stores")
324 | stores_overview = language_server.request_dir_overview(stores_dir)
325 |
326 | assert isinstance(stores_overview, dict), f"Stores overview should be a dict, got: {type(stores_overview)}"
327 | assert (
328 | len(stores_overview) == 1
329 | ), f"src/stores directory should have exactly 1 file (calculator.ts) in overview, got {len(stores_overview)}"
330 |
331 | # Verify calculator.ts is included
332 | calculator_files = [path for path in stores_overview.keys() if "calculator.ts" in path]
333 | assert len(calculator_files) == 1, (
334 | f"calculator.ts should appear exactly once in stores directory overview. "
335 | f"Found {len(calculator_files)} matches. All files: {list(stores_overview.keys())}"
336 | )
337 |
338 | # Verify the store file has symbols
339 | store_path = calculator_files[0]
340 | store_symbols = stores_overview[store_path]
341 | store_symbol_names = [s.get("name") for s in store_symbols if isinstance(s, dict)]
342 | assert (
343 | "useCalculatorStore" in store_symbol_names
344 | ), f"calculator.ts should have useCalculatorStore in overview. Found {len(store_symbol_names)} symbols: {store_symbol_names}"
345 |
346 | # Test overview for composables directory
347 | composables_dir = os.path.join("src", "composables")
348 | composables_overview = language_server.request_dir_overview(composables_dir)
349 |
350 | assert isinstance(composables_overview, dict), f"Composables overview should be a dict, got: {type(composables_overview)}"
351 | assert (
352 | len(composables_overview) == 2
353 | ), f"src/composables directory should have exactly 2 files in overview, got {len(composables_overview)}"
354 |
355 | # Verify composable files are included
356 | expected_composables = ["useFormatter.ts", "useTheme.ts"]
357 | for expected_composable in expected_composables:
358 | matching_files = [path for path in composables_overview.keys() if expected_composable in path]
359 | assert len(matching_files) == 1, (
360 | f"Composable '{expected_composable}' should appear exactly once in directory overview. "
361 | f"Found {len(matching_files)} matches. All files: {list(composables_overview.keys())}"
362 | )
363 |
```
--------------------------------------------------------------------------------
/test/serena/test_serena_agent.py:
--------------------------------------------------------------------------------
```python
1 | import json
2 | import logging
3 | import os
4 | import re
5 | import time
6 | from collections.abc import Iterator
7 | from contextlib import contextmanager
8 | from typing import Literal
9 |
10 | import pytest
11 |
12 | from serena.agent import SerenaAgent
13 | from serena.config.serena_config import ProjectConfig, RegisteredProject, SerenaConfig
14 | from serena.project import Project
15 | from serena.tools import SUCCESS_RESULT, FindReferencingSymbolsTool, FindSymbolTool, ReplaceContentTool, ReplaceSymbolBodyTool
16 | from solidlsp.ls_config import Language
17 | from solidlsp.ls_types import SymbolKind
18 | from test.conftest import get_repo_path, language_tests_enabled
19 | from test.solidlsp import clojure as clj
20 |
21 |
22 | @pytest.fixture
23 | def serena_config():
24 | """Create an in-memory configuration for tests with test repositories pre-registered."""
25 | # Create test projects for all supported languages
26 | test_projects = []
27 | for language in [
28 | Language.PYTHON,
29 | Language.GO,
30 | Language.JAVA,
31 | Language.KOTLIN,
32 | Language.RUST,
33 | Language.TYPESCRIPT,
34 | Language.PHP,
35 | Language.CSHARP,
36 | Language.CLOJURE,
37 | Language.FSHARP,
38 | Language.POWERSHELL,
39 | ]:
40 | repo_path = get_repo_path(language)
41 | if repo_path.exists():
42 | project_name = f"test_repo_{language}"
43 | project = Project(
44 | project_root=str(repo_path),
45 | project_config=ProjectConfig(
46 | project_name=project_name,
47 | languages=[language],
48 | ignored_paths=[],
49 | excluded_tools=set(),
50 | read_only=False,
51 | ignore_all_files_in_gitignore=True,
52 | initial_prompt="",
53 | encoding="utf-8",
54 | ),
55 | )
56 | test_projects.append(RegisteredProject.from_project_instance(project))
57 |
58 | config = SerenaConfig(gui_log_window_enabled=False, web_dashboard=False, log_level=logging.ERROR)
59 | config.projects = test_projects
60 | return config
61 |
62 |
63 | def read_project_file(project: Project, relative_path: str) -> str:
64 | """Utility function to read a file from the project."""
65 | file_path = os.path.join(project.project_root, relative_path)
66 | with open(file_path, encoding=project.project_config.encoding) as f:
67 | return f.read()
68 |
69 |
70 | @contextmanager
71 | def project_file_modification_context(serena_agent: SerenaAgent, relative_path: str) -> Iterator[None]:
72 | """Context manager to modify a project file and revert the changes after use."""
73 | project = serena_agent.get_active_project()
74 | file_path = os.path.join(project.project_root, relative_path)
75 |
76 | # Read the original content
77 | original_content = read_project_file(project, relative_path)
78 |
79 | try:
80 | yield
81 | finally:
82 | # Revert to the original content
83 | with open(file_path, "w", encoding=project.project_config.encoding) as f:
84 | f.write(original_content)
85 |
86 |
87 | @pytest.fixture
88 | def serena_agent(request: pytest.FixtureRequest, serena_config) -> Iterator[SerenaAgent]:
89 | language = Language(request.param)
90 | if not language_tests_enabled(language):
91 | pytest.skip(f"Tests for language {language} are not enabled.")
92 |
93 | project_name = f"test_repo_{language}"
94 |
95 | agent = SerenaAgent(project=project_name, serena_config=serena_config)
96 |
97 | # wait for agent to be ready
98 | agent.execute_task(lambda: None)
99 |
100 | yield agent
101 |
102 | # explicitly shut down to free resources
103 | agent.shutdown(timeout=5)
104 |
105 |
106 | class TestSerenaAgent:
107 | @pytest.mark.parametrize(
108 | "serena_agent,symbol_name,expected_kind,expected_file",
109 | [
110 | pytest.param(Language.PYTHON, "User", "Class", "models.py", marks=pytest.mark.python),
111 | pytest.param(Language.GO, "Helper", "Function", "main.go", marks=pytest.mark.go),
112 | pytest.param(Language.JAVA, "Model", "Class", "Model.java", marks=pytest.mark.java),
113 | pytest.param(Language.KOTLIN, "Model", "Struct", "Model.kt", marks=pytest.mark.kotlin),
114 | pytest.param(Language.RUST, "add", "Function", "lib.rs", marks=pytest.mark.rust),
115 | pytest.param(Language.TYPESCRIPT, "DemoClass", "Class", "index.ts", marks=pytest.mark.typescript),
116 | pytest.param(Language.PHP, "helperFunction", "Function", "helper.php", marks=pytest.mark.php),
117 | pytest.param(Language.CLOJURE, "greet", "Function", clj.CORE_PATH, marks=pytest.mark.clojure),
118 | pytest.param(Language.CSHARP, "Calculator", "Class", "Program.cs", marks=pytest.mark.csharp),
119 | pytest.param(Language.FSHARP, "Calculator", "Module", "Calculator.fs", marks=pytest.mark.fsharp),
120 | pytest.param(Language.POWERSHELL, "function Greet-User ()", "Function", "main.ps1", marks=pytest.mark.powershell),
121 | ],
122 | indirect=["serena_agent"],
123 | )
124 | def test_find_symbol(self, serena_agent: SerenaAgent, symbol_name: str, expected_kind: str, expected_file: str):
125 | agent = serena_agent
126 | find_symbol_tool = agent.get_tool(FindSymbolTool)
127 | result = find_symbol_tool.apply_ex(name_path_pattern=symbol_name)
128 |
129 | symbols = json.loads(result)
130 | assert any(
131 | symbol_name in s["name_path"] and expected_kind.lower() in s["kind"].lower() and expected_file in s["relative_path"]
132 | for s in symbols
133 | ), f"Expected to find {symbol_name} ({expected_kind}) in {expected_file}"
134 | # testing retrieval of symbol info
135 | if serena_agent.get_active_lsp_languages() == [Language.KOTLIN]:
136 | # kotlin LS doesn't seem to provide hover info right now, at least for the struct we test this on
137 | return
138 | for s in symbols:
139 | if s["kind"] in (SymbolKind.File.name, SymbolKind.Module.name):
140 | # we ignore file and module symbols for the info test
141 | continue
142 | symbol_info = s.get("info")
143 | assert symbol_info, f"Expected symbol info to be present for symbol: {s}"
144 | assert (
145 | symbol_name in s["info"]
146 | ), f"[{serena_agent.get_active_lsp_languages()[0]}] Expected symbol info to contain symbol name {symbol_name}. Info: {s['info']}"
147 | # special additional test for Java, since Eclipse returns hover in a complex format and we want to make sure to get it right
148 | if s["kind"] == SymbolKind.Class.name and serena_agent.get_active_lsp_languages() == [Language.JAVA]:
149 | assert "A simple model class" in symbol_info, f"Java class docstring not found in symbol info: {s}"
150 |
151 | @pytest.mark.parametrize(
152 | "serena_agent,symbol_name,def_file,ref_file",
153 | [
154 | pytest.param(
155 | Language.PYTHON,
156 | "User",
157 | os.path.join("test_repo", "models.py"),
158 | os.path.join("test_repo", "services.py"),
159 | marks=pytest.mark.python,
160 | ),
161 | pytest.param(Language.GO, "Helper", "main.go", "main.go", marks=pytest.mark.go),
162 | pytest.param(
163 | Language.JAVA,
164 | "Model",
165 | os.path.join("src", "main", "java", "test_repo", "Model.java"),
166 | os.path.join("src", "main", "java", "test_repo", "Main.java"),
167 | marks=pytest.mark.java,
168 | ),
169 | pytest.param(
170 | Language.KOTLIN,
171 | "Model",
172 | os.path.join("src", "main", "kotlin", "test_repo", "Model.kt"),
173 | os.path.join("src", "main", "kotlin", "test_repo", "Main.kt"),
174 | marks=pytest.mark.kotlin,
175 | ),
176 | pytest.param(Language.RUST, "add", os.path.join("src", "lib.rs"), os.path.join("src", "main.rs"), marks=pytest.mark.rust),
177 | pytest.param(Language.TYPESCRIPT, "helperFunction", "index.ts", "use_helper.ts", marks=pytest.mark.typescript),
178 | pytest.param(Language.PHP, "helperFunction", "helper.php", "index.php", marks=pytest.mark.php),
179 | pytest.param(
180 | Language.CLOJURE,
181 | "multiply",
182 | clj.CORE_PATH,
183 | clj.UTILS_PATH,
184 | marks=pytest.mark.clojure,
185 | ),
186 | pytest.param(Language.CSHARP, "Calculator", "Program.cs", "Program.cs", marks=pytest.mark.csharp),
187 | pytest.param(Language.FSHARP, "add", "Calculator.fs", "Program.fs", marks=pytest.mark.fsharp),
188 | pytest.param(Language.POWERSHELL, "function Greet-User ()", "main.ps1", "main.ps1", marks=pytest.mark.powershell),
189 | ],
190 | indirect=["serena_agent"],
191 | )
192 | def test_find_symbol_references(self, serena_agent: SerenaAgent, symbol_name: str, def_file: str, ref_file: str) -> None:
193 | agent = serena_agent
194 |
195 | # Find the symbol location first
196 | find_symbol_tool = agent.get_tool(FindSymbolTool)
197 | result = find_symbol_tool.apply_ex(name_path_pattern=symbol_name, relative_path=def_file)
198 |
199 | time.sleep(1)
200 | symbols = json.loads(result)
201 | # Find the definition
202 | def_symbol = symbols[0]
203 |
204 | # Now find references
205 | find_refs_tool = agent.get_tool(FindReferencingSymbolsTool)
206 | result = find_refs_tool.apply_ex(name_path=def_symbol["name_path"], relative_path=def_symbol["relative_path"])
207 |
208 | refs = json.loads(result)
209 | assert any(
210 | ref["relative_path"] == ref_file for ref in refs
211 | ), f"Expected to find reference to {symbol_name} in {ref_file}. refs={refs}"
212 |
213 | @pytest.mark.parametrize(
214 | "serena_agent,name_path,substring_matching,expected_symbol_name,expected_kind,expected_file",
215 | [
216 | pytest.param(
217 | Language.PYTHON,
218 | "OuterClass/NestedClass",
219 | False,
220 | "NestedClass",
221 | "Class",
222 | os.path.join("test_repo", "nested.py"),
223 | id="exact_qualname_class",
224 | marks=pytest.mark.python,
225 | ),
226 | pytest.param(
227 | Language.PYTHON,
228 | "OuterClass/NestedClass/find_me",
229 | False,
230 | "find_me",
231 | "Method",
232 | os.path.join("test_repo", "nested.py"),
233 | id="exact_qualname_method",
234 | marks=pytest.mark.python,
235 | ),
236 | pytest.param(
237 | Language.PYTHON,
238 | "OuterClass/NestedCl", # Substring for NestedClass
239 | True,
240 | "NestedClass",
241 | "Class",
242 | os.path.join("test_repo", "nested.py"),
243 | id="substring_qualname_class",
244 | marks=pytest.mark.python,
245 | ),
246 | pytest.param(
247 | Language.PYTHON,
248 | "OuterClass/NestedClass/find_m", # Substring for find_me
249 | True,
250 | "find_me",
251 | "Method",
252 | os.path.join("test_repo", "nested.py"),
253 | id="substring_qualname_method",
254 | marks=pytest.mark.python,
255 | ),
256 | pytest.param(
257 | Language.PYTHON,
258 | "/OuterClass", # Absolute path
259 | False,
260 | "OuterClass",
261 | "Class",
262 | os.path.join("test_repo", "nested.py"),
263 | id="absolute_qualname_class",
264 | marks=pytest.mark.python,
265 | ),
266 | pytest.param(
267 | Language.PYTHON,
268 | "/OuterClass/NestedClass/find_m", # Absolute path with substring
269 | True,
270 | "find_me",
271 | "Method",
272 | os.path.join("test_repo", "nested.py"),
273 | id="absolute_substring_qualname_method",
274 | marks=pytest.mark.python,
275 | ),
276 | ],
277 | indirect=["serena_agent"],
278 | )
279 | def test_find_symbol_name_path(
280 | self,
281 | serena_agent,
282 | name_path: str,
283 | substring_matching: bool,
284 | expected_symbol_name: str,
285 | expected_kind: str,
286 | expected_file: str,
287 | ):
288 | agent = serena_agent
289 |
290 | find_symbol_tool = agent.get_tool(FindSymbolTool)
291 | result = find_symbol_tool.apply_ex(
292 | name_path_pattern=name_path,
293 | depth=0,
294 | relative_path=None,
295 | include_body=False,
296 | include_kinds=None,
297 | exclude_kinds=None,
298 | substring_matching=substring_matching,
299 | )
300 |
301 | symbols = json.loads(result)
302 | assert any(
303 | expected_symbol_name == s["name_path"].split("/")[-1]
304 | and expected_kind.lower() in s["kind"].lower()
305 | and expected_file in s["relative_path"]
306 | for s in symbols
307 | ), f"Expected to find {name_path} ({expected_kind}) in {expected_file}. Symbols: {symbols}"
308 |
309 | @pytest.mark.parametrize(
310 | "serena_agent,name_path",
311 | [
312 | pytest.param(
313 | Language.PYTHON,
314 | "/NestedClass", # Absolute path, NestedClass is not top-level
315 | id="absolute_path_non_top_level_no_match",
316 | marks=pytest.mark.python,
317 | ),
318 | pytest.param(
319 | Language.PYTHON,
320 | "/NoSuchParent/NestedClass", # Absolute path with non-existent parent
321 | id="absolute_path_non_existent_parent_no_match",
322 | marks=pytest.mark.python,
323 | ),
324 | ],
325 | indirect=["serena_agent"],
326 | )
327 | def test_find_symbol_name_path_no_match(
328 | self,
329 | serena_agent,
330 | name_path: str,
331 | ):
332 | agent = serena_agent
333 |
334 | find_symbol_tool = agent.get_tool(FindSymbolTool)
335 | result = find_symbol_tool.apply_ex(
336 | name_path_pattern=name_path,
337 | depth=0,
338 | substring_matching=True,
339 | )
340 |
341 | symbols = json.loads(result)
342 | assert not symbols, f"Expected to find no symbols for {name_path}. Symbols found: {symbols}"
343 |
344 | @pytest.mark.parametrize(
345 | "serena_agent,name_path,num_expected",
346 | [
347 | pytest.param(
348 | Language.JAVA,
349 | "Model/getName",
350 | 2,
351 | id="overloaded_java_method",
352 | marks=pytest.mark.java,
353 | ),
354 | ],
355 | indirect=["serena_agent"],
356 | )
357 | def test_find_symbol_overloaded_function(self, serena_agent: SerenaAgent, name_path: str, num_expected: int):
358 | """
359 | Tests whether the FindSymbolTool can find all overloads of a function/method
360 | (provided that the overload id remains unspecified in the name path)
361 | """
362 | agent = serena_agent
363 |
364 | find_symbol_tool = agent.get_tool(FindSymbolTool)
365 | result = find_symbol_tool.apply_ex(
366 | name_path_pattern=name_path,
367 | depth=0,
368 | substring_matching=False,
369 | )
370 |
371 | symbols = json.loads(result)
372 | assert (
373 | len(symbols) == num_expected
374 | ), f"Expected to find {num_expected} symbols for overloaded function {name_path}. Symbols found: {symbols}"
375 |
376 | @pytest.mark.parametrize(
377 | "serena_agent,name_path,relative_path",
378 | [
379 | pytest.param(
380 | Language.JAVA,
381 | "Model/getName",
382 | os.path.join("src", "main", "java", "test_repo", "Model.java"),
383 | id="overloaded_java_method",
384 | marks=pytest.mark.java,
385 | ),
386 | ],
387 | indirect=["serena_agent"],
388 | )
389 | def test_non_unique_symbol_reference_error(self, serena_agent: SerenaAgent, name_path: str, relative_path: str):
390 | """
391 | Tests whether the tools operating on a well-defined symbol raises an error when the symbol reference is non-unique.
392 | We exemplarily test a retrieval tool (FindReferencingSymbolsTool) and an editing tool (ReplaceSymbolBodyTool).
393 | """
394 | match_text = "multiple"
395 |
396 | find_refs_tool = serena_agent.get_tool(FindReferencingSymbolsTool)
397 | with pytest.raises(ValueError, match=match_text):
398 | find_refs_tool.apply(name_path=name_path, relative_path=relative_path)
399 |
400 | replace_symbol_body_tool = serena_agent.get_tool(ReplaceSymbolBodyTool)
401 | with pytest.raises(ValueError, match=match_text):
402 | replace_symbol_body_tool.apply(name_path=name_path, relative_path=relative_path, body="")
403 |
404 | @pytest.mark.parametrize(
405 | "serena_agent",
406 | [
407 | pytest.param(
408 | Language.TYPESCRIPT,
409 | marks=pytest.mark.typescript,
410 | ),
411 | ],
412 | indirect=["serena_agent"],
413 | )
414 | def test_replace_content_regex_with_wildcard_ok(self, serena_agent: SerenaAgent):
415 | """
416 | Tests a regex-based content replacement that has a unique match
417 | """
418 | relative_path = "ws_manager.js"
419 | with project_file_modification_context(serena_agent, relative_path):
420 | replace_content_tool = serena_agent.get_tool(ReplaceContentTool)
421 | result = replace_content_tool.apply(
422 | needle=r'catch \(error\) \{\s*console.error\("Failed to connect.*?\}',
423 | repl='catch(error) { console.log("Never mind"); }',
424 | relative_path=relative_path,
425 | mode="regex",
426 | )
427 | assert result == SUCCESS_RESULT
428 |
429 | @pytest.mark.parametrize(
430 | "serena_agent",
431 | [
432 | pytest.param(
433 | Language.TYPESCRIPT,
434 | marks=pytest.mark.typescript,
435 | ),
436 | ],
437 | indirect=["serena_agent"],
438 | )
439 | @pytest.mark.parametrize("mode", ["literal", "regex"])
440 | def test_replace_content_with_backslashes(self, serena_agent: SerenaAgent, mode: Literal["literal", "regex"]):
441 | """
442 | Tests a content replacement where the needle and replacement strings contain backslashes.
443 | This is a regression test for escaping issues.
444 | """
445 | relative_path = "ws_manager.js"
446 | needle = r'console.log("WebSocketManager initializing\nStatus OK");'
447 | repl = r'console.log("WebSocketManager initialized\nAll systems go!");'
448 | replace_content_tool = serena_agent.get_tool(ReplaceContentTool)
449 | mode: Literal["literal", "regex"]
450 | with project_file_modification_context(serena_agent, relative_path):
451 | result = replace_content_tool.apply(
452 | needle=re.escape(needle) if mode == "regex" else needle,
453 | repl=repl,
454 | relative_path=relative_path,
455 | mode=mode,
456 | )
457 | assert result == SUCCESS_RESULT
458 | new_content = read_project_file(serena_agent.get_active_project(), relative_path)
459 | assert repl in new_content
460 |
461 | @pytest.mark.parametrize(
462 | "serena_agent",
463 | [
464 | pytest.param(
465 | Language.TYPESCRIPT,
466 | marks=pytest.mark.typescript,
467 | ),
468 | ],
469 | indirect=["serena_agent"],
470 | )
471 | def test_replace_content_regex_with_wildcard_ambiguous(self, serena_agent: SerenaAgent):
472 | """
473 | Tests that an ambiguous replacement where there is a larger match that internally contains
474 | a smaller match triggers an exception
475 | """
476 | replace_content_tool = serena_agent.get_tool(ReplaceContentTool)
477 | with pytest.raises(ValueError, match="ambiguous"):
478 | replace_content_tool.apply(
479 | needle=r'catch \(error\) \{.*?this\.updateConnectionStatus\("Connection failed", false\);.*?\}',
480 | repl='catch(error) { console.log("Never mind"); }',
481 | relative_path="ws_manager.js",
482 | mode="regex",
483 | )
484 |
```
--------------------------------------------------------------------------------
/src/serena/code_editor.py:
--------------------------------------------------------------------------------
```python
1 | import json
2 | import logging
3 | import os
4 | from abc import ABC, abstractmethod
5 | from collections.abc import Iterable, Iterator, Reversible
6 | from contextlib import contextmanager
7 | from typing import TYPE_CHECKING, Generic, Optional, TypeVar, cast
8 |
9 | from serena.symbol import JetBrainsSymbol, LanguageServerSymbol, LanguageServerSymbolRetriever, PositionInFile, Symbol
10 | from solidlsp import SolidLanguageServer, ls_types
11 | from solidlsp.ls import LSPFileBuffer
12 | from solidlsp.ls_utils import PathUtils, TextUtils
13 |
14 | from .constants import DEFAULT_SOURCE_FILE_ENCODING
15 | from .project import Project
16 | from .tools.jetbrains_plugin_client import JetBrainsPluginClient
17 |
18 | if TYPE_CHECKING:
19 | from .agent import SerenaAgent
20 |
21 |
22 | log = logging.getLogger(__name__)
23 | TSymbol = TypeVar("TSymbol", bound=Symbol)
24 |
25 |
26 | class CodeEditor(Generic[TSymbol], ABC):
27 | def __init__(self, project_root: str, agent: Optional["SerenaAgent"] = None) -> None:
28 | self.project_root = project_root
29 | self.agent = agent
30 |
31 | # set encoding based on active project, if available
32 | encoding = DEFAULT_SOURCE_FILE_ENCODING
33 | if agent is not None:
34 | project = agent.get_active_project()
35 | if project is not None:
36 | encoding = project.project_config.encoding
37 | self.encoding = encoding
38 |
39 | class EditedFile(ABC):
40 | def __init__(self, relative_path: str) -> None:
41 | self.relative_path = relative_path
42 |
43 | @abstractmethod
44 | def get_contents(self) -> str:
45 | """
46 | :return: the contents of the file.
47 | """
48 |
49 | @abstractmethod
50 | def set_contents(self, contents: str) -> None:
51 | """
52 | Fully resets the contents of the file.
53 |
54 | :param contents: the new contents
55 | """
56 |
57 | @abstractmethod
58 | def delete_text_between_positions(self, start_pos: PositionInFile, end_pos: PositionInFile) -> None:
59 | pass
60 |
61 | @abstractmethod
62 | def insert_text_at_position(self, pos: PositionInFile, text: str) -> None:
63 | pass
64 |
65 | @contextmanager
66 | def _open_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]:
67 | """
68 | Context manager for opening a file
69 | """
70 | raise NotImplementedError("This method must be overridden for each subclass")
71 |
72 | @contextmanager
73 | def edited_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]:
74 | """
75 | Context manager for editing a file.
76 | """
77 | with self._open_file_context(relative_path) as edited_file:
78 | yield edited_file
79 | # save the file
80 | self._save_edited_file(edited_file)
81 |
82 | def _save_edited_file(self, edited_file: "CodeEditor.EditedFile") -> None:
83 | abs_path = os.path.join(self.project_root, edited_file.relative_path)
84 | with open(abs_path, "w", encoding=self.encoding) as f:
85 | f.write(edited_file.get_contents())
86 |
87 | @abstractmethod
88 | def _find_unique_symbol(self, name_path: str, relative_file_path: str) -> TSymbol:
89 | """
90 | Finds the unique symbol with the given name in the given file.
91 | If no such symbol exists, raises a ValueError.
92 |
93 | :param name_path: the name path
94 | :param relative_file_path: the relative path of the file in which to search for the symbol.
95 | :return: the unique symbol
96 | """
97 |
98 | def replace_body(self, name_path: str, relative_file_path: str, body: str) -> None:
99 | """
100 | Replaces the body of the symbol with the given name_path in the given file.
101 |
102 | :param name_path: the name path of the symbol to replace.
103 | :param relative_file_path: the relative path of the file in which the symbol is defined.
104 | :param body: the new body
105 | """
106 | symbol = self._find_unique_symbol(name_path, relative_file_path)
107 | start_pos = symbol.get_body_start_position_or_raise()
108 | end_pos = symbol.get_body_end_position_or_raise()
109 |
110 | with self.edited_file_context(relative_file_path) as edited_file:
111 | # make sure the replacement adds no additional newlines (before or after) - all newlines
112 | # and whitespace before/after should remain the same, so we strip it entirely
113 | body = body.strip()
114 |
115 | edited_file.delete_text_between_positions(start_pos, end_pos)
116 | edited_file.insert_text_at_position(start_pos, body)
117 |
118 | @staticmethod
119 | def _count_leading_newlines(text: Iterable) -> int:
120 | cnt = 0
121 | for c in text:
122 | if c == "\n":
123 | cnt += 1
124 | elif c == "\r":
125 | continue
126 | else:
127 | break
128 | return cnt
129 |
130 | @classmethod
131 | def _count_trailing_newlines(cls, text: Reversible) -> int:
132 | return cls._count_leading_newlines(reversed(text))
133 |
134 | def insert_after_symbol(self, name_path: str, relative_file_path: str, body: str) -> None:
135 | """
136 | Inserts content after the symbol with the given name in the given file.
137 | """
138 | symbol = self._find_unique_symbol(name_path, relative_file_path)
139 |
140 | # make sure body always ends with at least one newline
141 | if not body.endswith("\n"):
142 | body += "\n"
143 |
144 | pos = symbol.get_body_end_position_or_raise()
145 |
146 | # start at the beginning of the next line
147 | col = 0
148 | line = pos.line + 1
149 |
150 | # make sure a suitable number of leading empty lines is used (at least 0/1 depending on the symbol type,
151 | # otherwise as many as the caller wanted to insert)
152 | original_leading_newlines = self._count_leading_newlines(body)
153 | body = body.lstrip("\r\n")
154 | min_empty_lines = 0
155 | if symbol.is_neighbouring_definition_separated_by_empty_line():
156 | min_empty_lines = 1
157 | num_leading_empty_lines = max(min_empty_lines, original_leading_newlines)
158 | if num_leading_empty_lines:
159 | body = ("\n" * num_leading_empty_lines) + body
160 |
161 | # make sure the one line break succeeding the original symbol, which we repurposed as prefix via
162 | # `line += 1`, is replaced
163 | body = body.rstrip("\r\n") + "\n"
164 |
165 | with self.edited_file_context(relative_file_path) as edited_file:
166 | edited_file.insert_text_at_position(PositionInFile(line, col), body)
167 |
168 | def insert_before_symbol(self, name_path: str, relative_file_path: str, body: str) -> None:
169 | """
170 | Inserts content before the symbol with the given name in the given file.
171 | """
172 | symbol = self._find_unique_symbol(name_path, relative_file_path)
173 | symbol_start_pos = symbol.get_body_start_position_or_raise()
174 |
175 | # insert position is the start of line where the symbol is defined
176 | line = symbol_start_pos.line
177 | col = 0
178 |
179 | original_trailing_empty_lines = self._count_trailing_newlines(body) - 1
180 |
181 | # ensure eol is present at end
182 | body = body.rstrip() + "\n"
183 |
184 | # add suitable number of trailing empty lines after the body (at least 0/1 depending on the symbol type,
185 | # otherwise as many as the caller wanted to insert)
186 | min_trailing_empty_lines = 0
187 | if symbol.is_neighbouring_definition_separated_by_empty_line():
188 | min_trailing_empty_lines = 1
189 | num_trailing_newlines = max(min_trailing_empty_lines, original_trailing_empty_lines)
190 | body += "\n" * num_trailing_newlines
191 |
192 | # apply edit
193 | with self.edited_file_context(relative_file_path) as edited_file:
194 | edited_file.insert_text_at_position(PositionInFile(line=line, col=col), body)
195 |
196 | def insert_at_line(self, relative_path: str, line: int, content: str) -> None:
197 | """
198 | Inserts content at the given line in the given file.
199 |
200 | :param relative_path: the relative path of the file in which to insert content
201 | :param line: the 0-based index of the line to insert content at
202 | :param content: the content to insert
203 | """
204 | with self.edited_file_context(relative_path) as edited_file:
205 | edited_file.insert_text_at_position(PositionInFile(line, 0), content)
206 |
207 | def delete_lines(self, relative_path: str, start_line: int, end_line: int) -> None:
208 | """
209 | Deletes lines in the given file.
210 |
211 | :param relative_path: the relative path of the file in which to delete lines
212 | :param start_line: the 0-based index of the first line to delete (inclusive)
213 | :param end_line: the 0-based index of the last line to delete (inclusive)
214 | """
215 | start_col = 0
216 | end_line_for_delete = end_line + 1
217 | end_col = 0
218 | with self.edited_file_context(relative_path) as edited_file:
219 | start_pos = PositionInFile(line=start_line, col=start_col)
220 | end_pos = PositionInFile(line=end_line_for_delete, col=end_col)
221 | edited_file.delete_text_between_positions(start_pos, end_pos)
222 |
223 | def delete_symbol(self, name_path: str, relative_file_path: str) -> None:
224 | """
225 | Deletes the symbol with the given name in the given file.
226 | """
227 | symbol = self._find_unique_symbol(name_path, relative_file_path)
228 | start_pos = symbol.get_body_start_position_or_raise()
229 | end_pos = symbol.get_body_end_position_or_raise()
230 | with self.edited_file_context(relative_file_path) as edited_file:
231 | edited_file.delete_text_between_positions(start_pos, end_pos)
232 |
233 | @abstractmethod
234 | def rename_symbol(self, name_path: str, relative_file_path: str, new_name: str) -> str:
235 | """
236 | Renames the symbol with the given name throughout the codebase.
237 |
238 | :param name_path: the name path of the symbol to rename
239 | :param relative_file_path: the relative path of the file containing the symbol
240 | :param new_name: the new name for the symbol
241 | :return: a status message
242 | """
243 |
244 |
245 | class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]):
246 | def __init__(self, symbol_retriever: LanguageServerSymbolRetriever, agent: Optional["SerenaAgent"] = None):
247 | super().__init__(project_root=symbol_retriever.get_root_path(), agent=agent)
248 | self._symbol_retriever = symbol_retriever
249 |
250 | def _get_language_server(self, relative_path: str) -> SolidLanguageServer:
251 | return self._symbol_retriever.get_language_server(relative_path)
252 |
253 | class EditedFile(CodeEditor.EditedFile):
254 | def __init__(self, lang_server: SolidLanguageServer, relative_path: str, file_buffer: LSPFileBuffer):
255 | super().__init__(relative_path)
256 | self._lang_server = lang_server
257 | self._file_buffer = file_buffer
258 |
259 | def get_contents(self) -> str:
260 | return self._file_buffer.contents
261 |
262 | def set_contents(self, contents: str) -> None:
263 | self._file_buffer.contents = contents
264 |
265 | def delete_text_between_positions(self, start_pos: PositionInFile, end_pos: PositionInFile) -> None:
266 | self._lang_server.delete_text_between_positions(self.relative_path, start_pos.to_lsp_position(), end_pos.to_lsp_position())
267 |
268 | def insert_text_at_position(self, pos: PositionInFile, text: str) -> None:
269 | self._lang_server.insert_text_at_position(self.relative_path, pos.line, pos.col, text)
270 |
271 | def apply_text_edits(self, text_edits: list[ls_types.TextEdit]) -> None:
272 | return self._lang_server.apply_text_edits_to_file(self.relative_path, text_edits)
273 |
274 | @contextmanager
275 | def _open_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]:
276 | lang_server = self._get_language_server(relative_path)
277 | with lang_server.open_file(relative_path) as file_buffer:
278 | yield self.EditedFile(lang_server, relative_path, file_buffer)
279 |
280 | def _get_code_file_content(self, relative_path: str) -> str:
281 | """Get the content of a file using the language server."""
282 | lang_server = self._get_language_server(relative_path)
283 | return lang_server.language_server.retrieve_full_file_content(relative_path)
284 |
285 | def _find_unique_symbol(self, name_path: str, relative_file_path: str) -> LanguageServerSymbol:
286 | return self._symbol_retriever.find_unique(name_path, within_relative_path=relative_file_path)
287 |
288 | def _relative_path_from_uri(self, uri: str) -> str:
289 | return os.path.relpath(PathUtils.uri_to_path(uri), self.project_root)
290 |
291 | class EditOperation(ABC):
292 | @abstractmethod
293 | def apply(self) -> None:
294 | pass
295 |
296 | class EditOperationFileTextEdits(EditOperation):
297 | def __init__(self, code_editor: "LanguageServerCodeEditor", file_uri: str, text_edits: list[ls_types.TextEdit]):
298 | self._code_editor = code_editor
299 | self._relative_path = code_editor._relative_path_from_uri(file_uri)
300 | self._text_edits = text_edits
301 |
302 | def apply(self) -> None:
303 | with self._code_editor.edited_file_context(self._relative_path) as edited_file:
304 | edited_file = cast(LanguageServerCodeEditor.EditedFile, edited_file)
305 | edited_file.apply_text_edits(self._text_edits)
306 |
307 | class EditOperationRenameFile(EditOperation):
308 | def __init__(self, code_editor: "LanguageServerCodeEditor", old_uri: str, new_uri: str):
309 | self._code_editor = code_editor
310 | self._old_relative_path = code_editor._relative_path_from_uri(old_uri)
311 | self._new_relative_path = code_editor._relative_path_from_uri(new_uri)
312 |
313 | def apply(self) -> None:
314 | old_abs_path = os.path.join(self._code_editor.project_root, self._old_relative_path)
315 | new_abs_path = os.path.join(self._code_editor.project_root, self._new_relative_path)
316 | os.rename(old_abs_path, new_abs_path)
317 |
318 | def _workspace_edit_to_edit_operations(self, workspace_edit: ls_types.WorkspaceEdit) -> list["LanguageServerCodeEditor.EditOperation"]:
319 | operations: list[LanguageServerCodeEditor.EditOperation] = []
320 |
321 | if "changes" in workspace_edit:
322 | for uri, edits in workspace_edit["changes"].items():
323 | operations.append(self.EditOperationFileTextEdits(self, uri, edits))
324 |
325 | if "documentChanges" in workspace_edit:
326 | for change in workspace_edit["documentChanges"]:
327 | if "textDocument" in change and "edits" in change:
328 | operations.append(self.EditOperationFileTextEdits(self, change["textDocument"]["uri"], change["edits"]))
329 | elif "kind" in change:
330 | if change["kind"] == "rename":
331 | operations.append(self.EditOperationRenameFile(self, change["oldUri"], change["newUri"]))
332 | else:
333 | raise ValueError(f"Unhandled document change kind: {change}; Please report to Serena developers.")
334 | else:
335 | raise ValueError(f"Unhandled document change format: {change}; Please report to Serena developers.")
336 |
337 | return operations
338 |
339 | def _apply_workspace_edit(self, workspace_edit: ls_types.WorkspaceEdit) -> int:
340 | """
341 | Applies a WorkspaceEdit
342 |
343 | :param workspace_edit: the edit to apply
344 | :return: number of edit operations applied
345 | """
346 | operations = self._workspace_edit_to_edit_operations(workspace_edit)
347 | for operation in operations:
348 | operation.apply()
349 | return len(operations)
350 |
351 | def rename_symbol(self, name_path: str, relative_file_path: str, new_name: str) -> str:
352 | symbol = self._find_unique_symbol(name_path, relative_file_path)
353 | if not symbol.location.has_position_in_file():
354 | raise ValueError(f"Symbol '{name_path}' does not have a valid position in file for renaming")
355 |
356 | # After has_position_in_file check, line and column are guaranteed to be non-None
357 | assert symbol.location.line is not None
358 | assert symbol.location.column is not None
359 |
360 | lang_server = self._get_language_server(relative_file_path)
361 | rename_result = lang_server.request_rename_symbol_edit(
362 | relative_file_path=relative_file_path, line=symbol.location.line, column=symbol.location.column, new_name=new_name
363 | )
364 | if rename_result is None:
365 | raise ValueError(
366 | f"Language server for {lang_server.language_id} returned no rename edits for symbol '{name_path}'. "
367 | f"The symbol might not support renaming."
368 | )
369 | num_changes = self._apply_workspace_edit(rename_result)
370 |
371 | if num_changes == 0:
372 | raise ValueError(
373 | f"Renaming symbol '{name_path}' to '{new_name}' resulted in no changes being applied; renaming may not be supported."
374 | )
375 |
376 | msg = f"Successfully renamed '{name_path}' to '{new_name}' ({num_changes} changes applied)"
377 | return msg
378 |
379 |
380 | class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]):
381 | def __init__(self, project: Project, agent: Optional["SerenaAgent"] = None) -> None:
382 | self._project = project
383 | super().__init__(project_root=project.project_root, agent=agent)
384 |
385 | class EditedFile(CodeEditor.EditedFile):
386 | def __init__(self, relative_path: str, project: Project):
387 | super().__init__(relative_path)
388 | path = os.path.join(project.project_root, relative_path)
389 | log.info("Editing file: %s", path)
390 | with open(path, encoding=project.project_config.encoding) as f:
391 | self._content = f.read()
392 |
393 | def get_contents(self) -> str:
394 | return self._content
395 |
396 | def set_contents(self, contents: str) -> None:
397 | self._content = contents
398 |
399 | def delete_text_between_positions(self, start_pos: PositionInFile, end_pos: PositionInFile) -> None:
400 | self._content, _ = TextUtils.delete_text_between_positions(
401 | self._content, start_pos.line, start_pos.col, end_pos.line, end_pos.col
402 | )
403 |
404 | def insert_text_at_position(self, pos: PositionInFile, text: str) -> None:
405 | self._content, _, _ = TextUtils.insert_text_at_position(self._content, pos.line, pos.col, text)
406 |
407 | @contextmanager
408 | def _open_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]:
409 | yield self.EditedFile(relative_path, self._project)
410 |
411 | def _save_edited_file(self, edited_file: "CodeEditor.EditedFile") -> None:
412 | super()._save_edited_file(edited_file)
413 | with JetBrainsPluginClient.from_project(self._project) as client:
414 | client.refresh_file(edited_file.relative_path)
415 |
416 | def _find_unique_symbol(self, name_path: str, relative_file_path: str) -> JetBrainsSymbol:
417 | with JetBrainsPluginClient.from_project(self._project) as client:
418 | result = client.find_symbol(name_path, relative_path=relative_file_path, include_body=False, depth=0, include_location=True)
419 | symbols = result["symbols"]
420 | if not symbols:
421 | raise ValueError(f"No symbol with name {name_path} found in file {relative_file_path}")
422 | if len(symbols) > 1:
423 | raise ValueError(
424 | f"Found multiple {len(symbols)} symbols with name {name_path} in file {relative_file_path}: "
425 | + json.dumps(symbols, indent=2)
426 | )
427 | return JetBrainsSymbol(symbols[0], self._project)
428 |
429 | def rename_symbol(self, name_path: str, relative_file_path: str, new_name: str) -> str:
430 | with JetBrainsPluginClient.from_project(self._project) as client:
431 | client.rename_symbol(
432 | name_path=name_path,
433 | relative_path=relative_file_path,
434 | new_name=new_name,
435 | rename_in_comments=False,
436 | rename_in_text_occurrences=False,
437 | )
438 | return "Success"
439 |
```
--------------------------------------------------------------------------------
/.github/workflows/pytest.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Tests on CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 |
9 | concurrency:
10 | group: ci-${{ github.workflow }}-${{ github.ref }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | cpu:
15 | name: Tests on ${{ matrix.os }}
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | os: [ubuntu-latest, windows-latest, macos-latest]
21 | python-version: ["3.11"]
22 | steps:
23 | - uses: actions/checkout@v3
24 | - name: Free disk space
25 | if: runner.os == 'Linux'
26 | run: |
27 | df -h
28 | sudo rm -rf /usr/local/lib/android
29 | sudo rm -rf /usr/share/dotnet
30 | sudo rm -rf /opt/ghc
31 | sudo rm -rf /opt/hostedtoolcache
32 | sudo apt-get clean
33 | sudo apt-get autoremove -y
34 | docker system prune -af || true
35 | df -h
36 | - name: Set up Python ${{ matrix.python-version }}
37 | uses: actions/setup-python@v4
38 | with:
39 | python-version: "${{ matrix.python-version }}"
40 | - uses: actions/setup-go@v5
41 | with:
42 | go-version: ">=1.17.0"
43 | - name: Set up Node.js
44 | uses: actions/setup-node@v4
45 | with:
46 | node-version: '20.x'
47 | - name: Ensure cached directory exist before calling cache-related actions
48 | shell: bash
49 | run: |
50 | mkdir -p $HOME/.serena/language_servers/static
51 | mkdir -p $HOME/.cache/go-build
52 | mkdir -p $HOME/go/bin
53 | - name: Install uv
54 | shell: bash
55 | run: curl -LsSf https://astral.sh/uv/install.sh | sh
56 | - name: Cache uv virtualenv
57 | id: cache-uv
58 | uses: actions/cache@v3
59 | with:
60 | path: .venv
61 | key: uv-venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
62 | - name: Create virtual environment
63 | shell: bash
64 | run: |
65 | if [ ! -d ".venv" ]; then
66 | uv venv
67 | fi
68 | - name: Install dependencies
69 | shell: bash
70 | run: uv pip install -e ".[dev]"
71 | - name: Check formatting
72 | shell: bash
73 | run: uv run poe lint
74 | # Add Go bin directory to PATH for this workflow
75 | # GITHUB_PATH is a special file that GitHub Actions uses to modify PATH
76 | # Writing to this file adds the directory to the PATH for subsequent steps
77 | - name: Cache Go binaries
78 | id: cache-go-binaries
79 | uses: actions/cache@v3
80 | with:
81 | path: |
82 | ~/go/bin
83 | ~/.cache/go-build
84 | key: go-binaries-${{ runner.os }}-gopls-latest
85 | - name: Install gopls
86 | if: steps.cache-go-binaries.outputs.cache-hit != 'true'
87 | shell: bash
88 | run: go install golang.org/x/tools/gopls@latest
89 | - name: Set up Elixir
90 | if: runner.os != 'Windows'
91 | uses: erlef/setup-beam@v1
92 | with:
93 | elixir-version: "1.19.3"
94 | otp-version: "28"
95 | # Erlang currently not tested in CI, random hangings on macos, always hangs on ubuntu
96 | # In local tests, erlang seems to work though
97 | # - name: Install Erlang Language Server
98 | # if: runner.os != 'Windows'
99 | # shell: bash
100 | # run: |
101 | # # Install rebar3 if not already available
102 | # which rebar3 || (curl -fsSL https://github.com/erlang/rebar3/releases/download/3.23.0/rebar3 -o /tmp/rebar3 && chmod +x /tmp/rebar3 && sudo mv /tmp/rebar3 /usr/local/bin/rebar3)
103 | # # Clone and build erlang_ls
104 | # git clone https://github.com/erlang-ls/erlang_ls.git /tmp/erlang_ls
105 | # cd /tmp/erlang_ls
106 | # make install PREFIX=/usr/local
107 | # # Ensure erlang_ls is in PATH
108 | # echo "$HOME/.local/bin" >> $GITHUB_PATH
109 | - name: Install clojure tools
110 | uses: DeLaGuardo/[email protected]
111 | with:
112 | cli: latest
113 | - name: Setup Java (for JVM based languages)
114 | uses: actions/setup-java@v4
115 | with:
116 | distribution: 'temurin'
117 | java-version: '17'
118 | - name: Setup .NET SDK (for F# and C# languages)
119 | uses: actions/setup-dotnet@v4
120 | with:
121 | dotnet-version: '8.0.x'
122 | - name: Install Terraform
123 | uses: hashicorp/setup-terraform@v3
124 | with:
125 | terraform_version: "1.5.0"
126 | terraform_wrapper: false
127 | # - name: Install swift
128 | # if: runner.os != 'Windows'
129 | # uses: swift-actions/setup-swift@v2
130 | # Installation of swift with the action screws with installation of ruby on macOS for some reason
131 | # We can try again when version 3 of the action is released, where they will also use swiftly
132 | # Until then, we use custom code to install swift. Sourcekit-lsp is installed automatically with swift
133 | - name: Install Swift with swiftly (macOS)
134 | if: runner.os == 'macOS'
135 | run: |
136 | echo "=== Installing swiftly on macOS ==="
137 | curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg && \
138 | installer -pkg swiftly.pkg -target CurrentUserHomeDirectory && \
139 | ~/.swiftly/bin/swiftly init --quiet-shell-followup && \
140 | . "${SWIFTLY_HOME_DIR:-$HOME/.swiftly}/env.sh" && \
141 | hash -r
142 | swiftly install --use 6.1.2
143 | swiftly use 6.1.2
144 | echo "~/.swiftly/bin" >> $GITHUB_PATH
145 | echo "Swiftly installed successfully"
146 | # Verify sourcekit-lsp is working before proceeding
147 | echo "=== Verifying sourcekit-lsp installation ==="
148 | which sourcekit-lsp || echo "Warning: sourcekit-lsp not found in PATH"
149 | sourcekit-lsp --help || echo "Warning: sourcekit-lsp not responding"
150 | - name: Install Swift with swiftly (Ubuntu)
151 | if: runner.os == 'Linux'
152 | run: |
153 | echo "=== Installing swiftly on Ubuntu ==="
154 | # Install dependencies BEFORE Swift to avoid exit code 1
155 | sudo apt-get update
156 | sudo apt-get -y install libcurl4-openssl-dev
157 | curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz && \
158 | tar zxf swiftly-$(uname -m).tar.gz && \
159 | ./swiftly init --quiet-shell-followup && \
160 | . "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh" && \
161 | hash -r
162 | swiftly install --use 6.1.2
163 | swiftly use 6.1.2
164 | echo "=== Adding Swift toolchain to PATH ==="
165 | echo "$HOME/.local/share/swiftly/bin" >> $GITHUB_PATH
166 | echo "Swiftly installed successfully!"
167 | # Verify sourcekit-lsp is working before proceeding
168 | echo "=== Verifying sourcekit-lsp installation ==="
169 | which sourcekit-lsp || echo "Warning: sourcekit-lsp not found in PATH"
170 | sourcekit-lsp --help || echo "Warning: sourcekit-lsp not responding"
171 | - name: Install Ruby
172 | uses: ruby/setup-ruby@v1
173 | with:
174 | ruby-version: '3.4'
175 | - name: Install Ruby language server
176 | shell: bash
177 | run: gem install ruby-lsp
178 | - name: Install R
179 | uses: r-lib/actions/setup-r@v2
180 | with:
181 | r-version: '4.4.2'
182 | use-public-rspm: true
183 | - name: Install R language server
184 | shell: bash
185 | run: |
186 | Rscript -e "install.packages('languageserver', repos='https://cloud.r-project.org')"
187 | - name: Set up Julia
188 | uses: julia-actions/setup-julia@v2
189 | with:
190 | version: '1.10'
191 | - name: Install Julia LanguageServer
192 | shell: bash
193 | run: julia -e 'using Pkg; Pkg.add("LanguageServer")'
194 | - name: Setup Haskell toolchain
195 | if: runner.os != 'Windows'
196 | uses: haskell/ghcup-setup@v1
197 | with:
198 | ghc: '9.12.2'
199 | cabal: '3.10.3.0'
200 | hls: '2.11.0.0'
201 | - name: Verify Haskell tools
202 | if: runner.os != 'Windows'
203 | run: |
204 | echo "Verifying installed Haskell tools:"
205 | which ghc && ghc --version
206 | which cabal && cabal --version
207 | # HLS verification - non-blocking in case of version incompatibility
208 | if command -v haskell-language-server-wrapper &>/dev/null; then
209 | echo "Found haskell-language-server-wrapper"
210 | haskell-language-server-wrapper --version || echo "WARNING: HLS wrapper found but version check failed"
211 | elif command -v haskell-language-server &>/dev/null; then
212 | echo "Found haskell-language-server"
213 | haskell-language-server --version || echo "WARNING: HLS found but version check failed"
214 | else
215 | echo "WARNING: HLS not found (may be incompatible with GHC 9.12.2)"
216 | echo "This is not a critical error - tests will use HLS if available at runtime"
217 | fi
218 | shell: bash
219 | - name: Pre-build Haskell test project for HLS
220 | if: runner.os != 'Windows'
221 | run: |
222 | cd test/resources/repos/haskell/test_repo
223 | cabal update
224 | cabal build --only-dependencies
225 | cabal build
226 | echo "Haskell test project built successfully"
227 | shell: bash
228 | - name: Install Zig
229 | uses: goto-bus-stop/setup-zig@v2
230 | with:
231 | version: 0.14.1
232 | - name: Install ZLS (Zig Language Server)
233 | shell: bash
234 | run: |
235 | if [[ "${{ runner.os }}" == "Linux" ]]; then
236 | wget https://github.com/zigtools/zls/releases/download/0.14.0/zls-x86_64-linux.tar.xz
237 | tar -xf zls-x86_64-linux.tar.xz
238 | sudo mv zls /usr/local/bin/
239 | rm zls-x86_64-linux.tar.xz
240 | elif [[ "${{ runner.os }}" == "macOS" ]]; then
241 | wget https://github.com/zigtools/zls/releases/download/0.14.0/zls-x86_64-macos.tar.xz
242 | tar -xf zls-x86_64-macos.tar.xz
243 | sudo mv zls /usr/local/bin/
244 | rm zls-x86_64-macos.tar.xz
245 | elif [[ "${{ runner.os }}" == "Windows" ]]; then
246 | curl -L -o zls.zip https://github.com/zigtools/zls/releases/download/0.14.0/zls-x86_64-windows.zip
247 | unzip -o zls.zip
248 | mkdir -p "$HOME/bin"
249 | mv zls.exe "$HOME/bin/"
250 | echo "$HOME/bin" >> $GITHUB_PATH
251 | rm zls.zip
252 | fi
253 | - name: Install Lua Language Server
254 | shell: bash
255 | run: |
256 | LUA_LS_VERSION="3.15.0"
257 | LUA_LS_DIR="$HOME/.serena/language_servers/lua"
258 | mkdir -p "$LUA_LS_DIR"
259 |
260 | if [[ "${{ runner.os }}" == "Linux" ]]; then
261 | if [[ "$(uname -m)" == "x86_64" ]]; then
262 | wget https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-linux-x64.tar.gz
263 | tar -xzf lua-language-server-${LUA_LS_VERSION}-linux-x64.tar.gz -C "$LUA_LS_DIR"
264 | else
265 | wget https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-linux-arm64.tar.gz
266 | tar -xzf lua-language-server-${LUA_LS_VERSION}-linux-arm64.tar.gz -C "$LUA_LS_DIR"
267 | fi
268 | chmod +x "$LUA_LS_DIR/bin/lua-language-server"
269 | # Create wrapper script instead of symlink to ensure supporting files are found
270 | echo '#!/bin/bash' | sudo tee /usr/local/bin/lua-language-server > /dev/null
271 | echo 'cd "${HOME}/.serena/language_servers/lua/bin"' | sudo tee -a /usr/local/bin/lua-language-server > /dev/null
272 | echo 'exec ./lua-language-server "$@"' | sudo tee -a /usr/local/bin/lua-language-server > /dev/null
273 | sudo chmod +x /usr/local/bin/lua-language-server
274 | rm lua-language-server-*.tar.gz
275 | elif [[ "${{ runner.os }}" == "macOS" ]]; then
276 | if [[ "$(uname -m)" == "x86_64" ]]; then
277 | wget https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-darwin-x64.tar.gz
278 | tar -xzf lua-language-server-${LUA_LS_VERSION}-darwin-x64.tar.gz -C "$LUA_LS_DIR"
279 | else
280 | wget https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-darwin-arm64.tar.gz
281 | tar -xzf lua-language-server-${LUA_LS_VERSION}-darwin-arm64.tar.gz -C "$LUA_LS_DIR"
282 | fi
283 | chmod +x "$LUA_LS_DIR/bin/lua-language-server"
284 | # Create wrapper script instead of symlink to ensure supporting files are found
285 | echo '#!/bin/bash' | sudo tee /usr/local/bin/lua-language-server > /dev/null
286 | echo 'cd "${HOME}/.serena/language_servers/lua/bin"' | sudo tee -a /usr/local/bin/lua-language-server > /dev/null
287 | echo 'exec ./lua-language-server "$@"' | sudo tee -a /usr/local/bin/lua-language-server > /dev/null
288 | sudo chmod +x /usr/local/bin/lua-language-server
289 | rm lua-language-server-*.tar.gz
290 | elif [[ "${{ runner.os }}" == "Windows" ]]; then
291 | curl -L -o lua-ls.zip https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-win32-x64.zip
292 | unzip -o lua-ls.zip -d "$LUA_LS_DIR"
293 | # For Windows, we'll add the bin directory directly to PATH
294 | # The lua-language-server.exe can find its supporting files relative to its location
295 | echo "$LUA_LS_DIR/bin" >> $GITHUB_PATH
296 | rm lua-ls.zip
297 | fi
298 | - name: Install Perl::LanguageServer
299 | if: runner.os != 'Windows'
300 | shell: bash
301 | run: |
302 | if [[ "${{ runner.os }}" == "Linux" ]]; then
303 | sudo apt-get update
304 | sudo apt-get install -y cpanminus build-essential libanyevent-perl libio-aio-perl
305 | elif [[ "${{ runner.os }}" == "macOS" ]]; then
306 | brew install cpanminus
307 | fi
308 | PERL_MM_USE_DEFAULT=1 cpanm --notest --force Perl::LanguageServer
309 | # Set up Perl local::lib environment for subsequent steps
310 | echo "PERL5LIB=$HOME/perl5/lib/perl5${PERL5LIB:+:${PERL5LIB}}" >> $GITHUB_ENV
311 | echo "PERL_LOCAL_LIB_ROOT=$HOME/perl5${PERL_LOCAL_LIB_ROOT:+:${PERL_LOCAL_LIB_ROOT}}" >> $GITHUB_ENV
312 | echo "PERL_MB_OPT=--install_base \"$HOME/perl5\"" >> $GITHUB_ENV
313 | echo "PERL_MM_OPT=INSTALL_BASE=$HOME/perl5" >> $GITHUB_ENV
314 | echo "$HOME/perl5/bin" >> $GITHUB_PATH
315 | - name: Install Elm
316 | shell: bash
317 | run: npm install -g [email protected]
318 | - name: Install Nix
319 | if: runner.os != 'Windows' # Nix doesn't support Windows natively
320 | uses: cachix/install-nix-action@v30
321 | with:
322 | nix_path: nixpkgs=channel:nixos-unstable
323 | - name: Install nixd (Nix Language Server)
324 | if: runner.os != 'Windows' # Skip on Windows since Nix isn't available
325 | shell: bash
326 | run: |
327 | # Install nixd using nix
328 | nix profile install github:nix-community/nixd
329 |
330 | # Verify nixd is installed and working
331 | if ! command -v nixd &> /dev/null; then
332 | echo "nixd installation failed or not in PATH"
333 | exit 1
334 | fi
335 |
336 | echo "$HOME/.nix-profile/bin" >> $GITHUB_PATH
337 | - name: Verify Nix package build
338 | if: runner.os != 'Windows' # Nix only supported on Linux/macOS
339 | shell: bash
340 | run: |
341 | # Verify the flake builds successfully
342 | nix build --no-link
343 | - name: Install Regal (Rego Language Server)
344 | shell: bash
345 | run: |
346 | REGAL_VERSION="0.36.1"
347 |
348 | if [[ "${{ runner.os }}" == "Linux" ]]; then
349 | if [[ "$(uname -m)" == "x86_64" ]]; then
350 | curl -L -o regal https://github.com/StyraInc/regal/releases/download/v${REGAL_VERSION}/regal_Linux_x86_64
351 | else
352 | curl -L -o regal https://github.com/StyraInc/regal/releases/download/v${REGAL_VERSION}/regal_Linux_arm64
353 | fi
354 | chmod +x regal
355 | sudo mv regal /usr/local/bin/
356 | elif [[ "${{ runner.os }}" == "macOS" ]]; then
357 | if [[ "$(uname -m)" == "x86_64" ]]; then
358 | curl -L -o regal https://github.com/StyraInc/regal/releases/download/v${REGAL_VERSION}/regal_Darwin_x86_64
359 | else
360 | curl -L -o regal https://github.com/StyraInc/regal/releases/download/v${REGAL_VERSION}/regal_Darwin_arm64
361 | fi
362 | chmod +x regal
363 | sudo mv regal /usr/local/bin/
364 | elif [[ "${{ runner.os }}" == "Windows" ]]; then
365 | curl -L -o regal.exe https://github.com/StyraInc/regal/releases/download/v${REGAL_VERSION}/regal_Windows_x86_64.exe
366 | mkdir -p "$HOME/bin"
367 | mv regal.exe "$HOME/bin/"
368 | echo "$HOME/bin" >> $GITHUB_PATH
369 | fi
370 | - name: Install Free Pascal Compiler
371 | shell: bash
372 | run: |
373 | if [[ "${{ runner.os }}" == "Linux" ]]; then
374 | sudo apt-get update
375 | sudo apt-get install -y fpc fpc-source
376 | # Set environment variables for pasls
377 | echo "PP=/usr/bin/fpc" >> $GITHUB_ENV
378 | # Find FPC source directory (version may vary)
379 | FPCDIR=$(ls -d /usr/share/fpcsrc/*/ 2>/dev/null | head -1 || echo "/usr/share/fpcsrc")
380 | echo "FPCDIR=$FPCDIR" >> $GITHUB_ENV
381 | elif [[ "${{ runner.os }}" == "macOS" ]]; then
382 | brew install fpc
383 | # Download FPC source from SourceForge (fpc-src-laz cask is incompatible with ARM64)
384 | FPC_VERSION="3.2.2"
385 | curl -L -o fpc-source.tar.gz "https://sourceforge.net/projects/freepascal/files/Source/${FPC_VERSION}/fpc-${FPC_VERSION}.source.tar.gz/download"
386 | mkdir -p "$HOME/fpcsrc"
387 | tar -xzf fpc-source.tar.gz -C "$HOME/fpcsrc"
388 | rm fpc-source.tar.gz
389 | # Set environment variables for pasls
390 | echo "PP=$(which fpc)" >> $GITHUB_ENV
391 | echo "FPCDIR=$HOME/fpcsrc/fpc-${FPC_VERSION}" >> $GITHUB_ENV
392 | elif [[ "${{ runner.os }}" == "Windows" ]]; then
393 | FPC_VERSION="3.2.2"
394 | # Download freepascal-ootb (includes FPC compiler)
395 | curl -L -o fpc-ootb.zip https://github.com/fredvs/freepascal-ootb/releases/download/${FPC_VERSION}/fpc-ootb-322-x86_64-win64.zip
396 | mkdir -p "$HOME/fpc"
397 | unzip -q fpc-ootb.zip -d "$HOME/fpc"
398 | rm fpc-ootb.zip
399 | # Download FPC source from SourceForge (fpc-ootb only has compiled units, not source)
400 | curl -L -o fpc-source.zip "https://sourceforge.net/projects/freepascal/files/Source/${FPC_VERSION}/fpc-${FPC_VERSION}.source.zip/download"
401 | mkdir -p "$HOME/fpcsrc"
402 | unzip -q fpc-source.zip -d "$HOME/fpcsrc"
403 | rm fpc-source.zip
404 | # Find fpc executable (fpc-ootb uses fpc-ootb.exe as the compiler)
405 | echo "=== FPC directory structure ==="
406 | find "$HOME/fpc" -name "*.exe" -type f 2>/dev/null | head -10
407 | FPC_EXE=$(find "$HOME/fpc" -name "fpc-ootb-64.exe" -type f 2>/dev/null | head -1)
408 | echo "Found FPC executable: $FPC_EXE"
409 | echo "Found FPC source dir: $HOME/fpcsrc/fpc-${FPC_VERSION}"
410 | # Set environment variables for pasls
411 | echo "PP=$FPC_EXE" >> $GITHUB_ENV
412 | echo "FPCDIR=$HOME/fpcsrc/fpc-${FPC_VERSION}" >> $GITHUB_ENV
413 | # Add FPC bin directory to PATH
414 | FPC_BIN_DIR=$(dirname "$FPC_EXE")
415 | echo "$FPC_BIN_DIR" >> $GITHUB_PATH
416 | fi
417 | - name: Verify FPC installation
418 | shell: bash
419 | run: |
420 | echo "PP=$PP"
421 | echo "FPCDIR=$FPCDIR"
422 | fpc -v || echo "FPC not in PATH, using PP directly"
423 | - name: Cache language servers
424 | id: cache-language-servers
425 | uses: actions/cache@v3
426 | with:
427 | path: ~/.serena/language_servers/static
428 | key: language-servers-${{ runner.os }}-v1
429 | restore-keys: |
430 | language-servers-${{ runner.os }}-
431 | - name: Report free disk space
432 | if: runner.os == 'Linux'
433 | run: |
434 | echo "Free disk space before tests:"
435 | df -h
436 | - name: Test with pytest
437 | shell: bash
438 | run: uv run poe test
439 | - name: Type-checking with mypy
440 | shell: bash
441 | run: uv run poe type-check
442 |
```
--------------------------------------------------------------------------------
/src/solidlsp/ls_request.py:
--------------------------------------------------------------------------------
```python
1 | from typing import TYPE_CHECKING, Any, Union
2 |
3 | from solidlsp.lsp_protocol_handler import lsp_types
4 |
5 | if TYPE_CHECKING:
6 | from .ls_handler import SolidLanguageServerHandler
7 |
8 |
9 | class LanguageServerRequest:
10 | def __init__(self, handler: "SolidLanguageServerHandler"):
11 | self.handler = handler
12 |
13 | def _send_request(self, method: str, params: Any | None = None) -> Any:
14 | return self.handler.send_request(method, params)
15 |
16 | def implementation(self, params: lsp_types.ImplementationParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
17 | """A request to resolve the implementation locations of a symbol at a given text
18 | document position. The request's parameter is of type [TextDocumentPositionParams]
19 | (#TextDocumentPositionParams) the response is of type {@link Definition} or a
20 | Thenable that resolves to such.
21 | """
22 | return self._send_request("textDocument/implementation", params)
23 |
24 | def type_definition(
25 | self, params: lsp_types.TypeDefinitionParams
26 | ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
27 | """A request to resolve the type definition locations of a symbol at a given text
28 | document position. The request's parameter is of type [TextDocumentPositionParams]
29 | (#TextDocumentPositionParams) the response is of type {@link Definition} or a
30 | Thenable that resolves to such.
31 | """
32 | return self._send_request("textDocument/typeDefinition", params)
33 |
34 | def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]:
35 | """A request to list all color symbols found in a given text document. The request's
36 | parameter is of type {@link DocumentColorParams} the
37 | response is of type {@link ColorInformation ColorInformation[]} or a Thenable
38 | that resolves to such.
39 | """
40 | return self._send_request("textDocument/documentColor", params)
41 |
42 | def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]:
43 | """A request to list all presentation for a color. The request's
44 | parameter is of type {@link ColorPresentationParams} the
45 | response is of type {@link ColorInformation ColorInformation[]} or a Thenable
46 | that resolves to such.
47 | """
48 | return self._send_request("textDocument/colorPresentation", params)
49 |
50 | def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None:
51 | """A request to provide folding ranges in a document. The request's
52 | parameter is of type {@link FoldingRangeParams}, the
53 | response is of type {@link FoldingRangeList} or a Thenable
54 | that resolves to such.
55 | """
56 | return self._send_request("textDocument/foldingRange", params)
57 |
58 | def declaration(self, params: lsp_types.DeclarationParams) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]:
59 | """A request to resolve the type definition locations of a symbol at a given text
60 | document position. The request's parameter is of type [TextDocumentPositionParams]
61 | (#TextDocumentPositionParams) the response is of type {@link Declaration}
62 | or a typed array of {@link DeclarationLink} or a Thenable that resolves
63 | to such.
64 | """
65 | return self._send_request("textDocument/declaration", params)
66 |
67 | def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None:
68 | """A request to provide selection ranges in a document. The request's
69 | parameter is of type {@link SelectionRangeParams}, the
70 | response is of type {@link SelectionRange SelectionRange[]} or a Thenable
71 | that resolves to such.
72 | """
73 | return self._send_request("textDocument/selectionRange", params)
74 |
75 | def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None:
76 | """A request to result a `CallHierarchyItem` in a document at a given position.
77 | Can be used as an input to an incoming or outgoing call hierarchy.
78 |
79 | @since 3.16.0
80 | """
81 | return self._send_request("textDocument/prepareCallHierarchy", params)
82 |
83 | def incoming_calls(self, params: lsp_types.CallHierarchyIncomingCallsParams) -> list["lsp_types.CallHierarchyIncomingCall"] | None:
84 | """A request to resolve the incoming calls for a given `CallHierarchyItem`.
85 |
86 | @since 3.16.0
87 | """
88 | return self._send_request("callHierarchy/incomingCalls", params)
89 |
90 | def outgoing_calls(self, params: lsp_types.CallHierarchyOutgoingCallsParams) -> list["lsp_types.CallHierarchyOutgoingCall"] | None:
91 | """A request to resolve the outgoing calls for a given `CallHierarchyItem`.
92 |
93 | @since 3.16.0
94 | """
95 | return self._send_request("callHierarchy/outgoingCalls", params)
96 |
97 | def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]:
98 | """@since 3.16.0"""
99 | return self._send_request("textDocument/semanticTokens/full", params)
100 |
101 | def semantic_tokens_delta(
102 | self, params: lsp_types.SemanticTokensDeltaParams
103 | ) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]:
104 | """@since 3.16.0"""
105 | return self._send_request("textDocument/semanticTokens/full/delta", params)
106 |
107 | def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]:
108 | """@since 3.16.0"""
109 | return self._send_request("textDocument/semanticTokens/range", params)
110 |
111 | def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]:
112 | """A request to provide ranges that can be edited together.
113 |
114 | @since 3.16.0
115 | """
116 | return self._send_request("textDocument/linkedEditingRange", params)
117 |
118 | def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
119 | """The will create files request is sent from the client to the server before files are actually
120 | created as long as the creation is triggered from within the client.
121 |
122 | @since 3.16.0
123 | """
124 | return self._send_request("workspace/willCreateFiles", params)
125 |
126 | def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
127 | """The will rename files request is sent from the client to the server before files are actually
128 | renamed as long as the rename is triggered from within the client.
129 |
130 | @since 3.16.0
131 | """
132 | return self._send_request("workspace/willRenameFiles", params)
133 |
134 | def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
135 | """The did delete files notification is sent from the client to the server when
136 | files were deleted from within the client.
137 |
138 | @since 3.16.0
139 | """
140 | return self._send_request("workspace/willDeleteFiles", params)
141 |
142 | def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None:
143 | """A request to get the moniker of a symbol at a given text document position.
144 | The request parameter is of type {@link TextDocumentPositionParams}.
145 | The response is of type {@link Moniker Moniker[]} or `null`.
146 | """
147 | return self._send_request("textDocument/moniker", params)
148 |
149 | def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None:
150 | """A request to result a `TypeHierarchyItem` in a document at a given position.
151 | Can be used as an input to a subtypes or supertypes type hierarchy.
152 |
153 | @since 3.17.0
154 | """
155 | return self._send_request("textDocument/prepareTypeHierarchy", params)
156 |
157 | def type_hierarchy_supertypes(self, params: lsp_types.TypeHierarchySupertypesParams) -> list["lsp_types.TypeHierarchyItem"] | None:
158 | """A request to resolve the supertypes for a given `TypeHierarchyItem`.
159 |
160 | @since 3.17.0
161 | """
162 | return self._send_request("typeHierarchy/supertypes", params)
163 |
164 | def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None:
165 | """A request to resolve the subtypes for a given `TypeHierarchyItem`.
166 |
167 | @since 3.17.0
168 | """
169 | return self._send_request("typeHierarchy/subtypes", params)
170 |
171 | def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None:
172 | """A request to provide inline values in a document. The request's parameter is of
173 | type {@link InlineValueParams}, the response is of type
174 | {@link InlineValue InlineValue[]} or a Thenable that resolves to such.
175 |
176 | @since 3.17.0
177 | """
178 | return self._send_request("textDocument/inlineValue", params)
179 |
180 | def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None:
181 | """A request to provide inlay hints in a document. The request's parameter is of
182 | type {@link InlayHintsParams}, the response is of type
183 | {@link InlayHint InlayHint[]} or a Thenable that resolves to such.
184 |
185 | @since 3.17.0
186 | """
187 | return self._send_request("textDocument/inlayHint", params)
188 |
189 | def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint":
190 | """A request to resolve additional properties for an inlay hint.
191 | The request's parameter is of type {@link InlayHint}, the response is
192 | of type {@link InlayHint} or a Thenable that resolves to such.
193 |
194 | @since 3.17.0
195 | """
196 | return self._send_request("inlayHint/resolve", params)
197 |
198 | def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport":
199 | """The document diagnostic request definition.
200 |
201 | @since 3.17.0
202 | """
203 | return self._send_request("textDocument/diagnostic", params)
204 |
205 | def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport":
206 | """The workspace diagnostic request definition.
207 |
208 | @since 3.17.0
209 | """
210 | return self._send_request("workspace/diagnostic", params)
211 |
212 | def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult":
213 | """The initialize request is sent from the client to the server.
214 | It is sent once as the request after starting up the server.
215 | The requests parameter is of type {@link InitializeParams}
216 | the response if of type {@link InitializeResult} of a Thenable that
217 | resolves to such.
218 | """
219 | return self._send_request("initialize", params)
220 |
221 | def shutdown(self) -> None:
222 | """A shutdown request is sent from the client to the server.
223 | It is sent once when the client decides to shutdown the
224 | server. The only notification that is sent after a shutdown request
225 | is the exit event.
226 | """
227 | return self._send_request("shutdown")
228 |
229 | def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None:
230 | """A document will save request is sent from the client to the server before
231 | the document is actually saved. The request can return an array of TextEdits
232 | which will be applied to the text document before it is saved. Please note that
233 | clients might drop results if computing the text edits took too long or if a
234 | server constantly fails on this request. This is done to keep the save fast and
235 | reliable.
236 | """
237 | return self._send_request("textDocument/willSaveWaitUntil", params)
238 |
239 | def completion(self, params: lsp_types.CompletionParams) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]:
240 | """Request to request completion at a given text document position. The request's
241 | parameter is of type {@link TextDocumentPosition} the response
242 | is of type {@link CompletionItem CompletionItem[]} or {@link CompletionList}
243 | or a Thenable that resolves to such.
244 |
245 | The request can delay the computation of the {@link CompletionItem.detail `detail`}
246 | and {@link CompletionItem.documentation `documentation`} properties to the `completionItem/resolve`
247 | request. However, properties that are needed for the initial sorting and filtering, like `sortText`,
248 | `filterText`, `insertText`, and `textEdit`, must not be changed during resolve.
249 | """
250 | return self._send_request("textDocument/completion", params)
251 |
252 | def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem":
253 | """Request to resolve additional information for a given completion item.The request's
254 | parameter is of type {@link CompletionItem} the response
255 | is of type {@link CompletionItem} or a Thenable that resolves to such.
256 | """
257 | return self._send_request("completionItem/resolve", params)
258 |
259 | def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]:
260 | """Request to request hover information at a given text document position. The request's
261 | parameter is of type {@link TextDocumentPosition} the response is of
262 | type {@link Hover} or a Thenable that resolves to such.
263 | """
264 | return self._send_request("textDocument/hover", params)
265 |
266 | def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]:
267 | return self._send_request("textDocument/signatureHelp", params)
268 |
269 | def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
270 | """A request to resolve the definition location of a symbol at a given text
271 | document position. The request's parameter is of type [TextDocumentPosition]
272 | (#TextDocumentPosition) the response is of either type {@link Definition}
273 | or a typed array of {@link DefinitionLink} or a Thenable that resolves
274 | to such.
275 | """
276 | return self._send_request("textDocument/definition", params)
277 |
278 | def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None:
279 | """A request to resolve project-wide references for the symbol denoted
280 | by the given text document position. The request's parameter is of
281 | type {@link ReferenceParams} the response is of type
282 | {@link Location Location[]} or a Thenable that resolves to such.
283 | """
284 | return self._send_request("textDocument/references", params)
285 |
286 | def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None:
287 | """Request to resolve a {@link DocumentHighlight} for a given
288 | text document position. The request's parameter is of type [TextDocumentPosition]
289 | (#TextDocumentPosition) the request response is of type [DocumentHighlight[]]
290 | (#DocumentHighlight) or a Thenable that resolves to such.
291 | """
292 | return self._send_request("textDocument/documentHighlight", params)
293 |
294 | def document_symbol(
295 | self, params: lsp_types.DocumentSymbolParams
296 | ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.DocumentSymbol"] | None:
297 | """A request to list all symbols found in a given text document. The request's
298 | parameter is of type {@link TextDocumentIdentifier} the
299 | response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable
300 | that resolves to such.
301 | """
302 | return self._send_request("textDocument/documentSymbol", params)
303 |
304 | def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None:
305 | """A request to provide commands for the given text document and range."""
306 | return self._send_request("textDocument/codeAction", params)
307 |
308 | def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction":
309 | """Request to resolve additional information for a given code action.The request's
310 | parameter is of type {@link CodeAction} the response
311 | is of type {@link CodeAction} or a Thenable that resolves to such.
312 | """
313 | return self._send_request("codeAction/resolve", params)
314 |
315 | def workspace_symbol(
316 | self, params: lsp_types.WorkspaceSymbolParams
317 | ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.WorkspaceSymbol"] | None:
318 | """A request to list project-wide symbols matching the query string given
319 | by the {@link WorkspaceSymbolParams}. The response is
320 | of type {@link SymbolInformation SymbolInformation[]} or a Thenable that
321 | resolves to such.
322 |
323 | @since 3.17.0 - support for WorkspaceSymbol in the returned data. Clients
324 | need to advertise support for WorkspaceSymbols via the client capability
325 | `workspace.symbol.resolveSupport`.
326 | """
327 | return self._send_request("workspace/symbol", params)
328 |
329 | def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol":
330 | """A request to resolve the range inside the workspace
331 | symbol's location.
332 |
333 | @since 3.17.0
334 | """
335 | return self._send_request("workspaceSymbol/resolve", params)
336 |
337 | def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None:
338 | """A request to provide code lens for the given text document."""
339 | return self._send_request("textDocument/codeLens", params)
340 |
341 | def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens":
342 | """A request to resolve a command for a given code lens."""
343 | return self._send_request("codeLens/resolve", params)
344 |
345 | def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None:
346 | """A request to provide document links"""
347 | return self._send_request("textDocument/documentLink", params)
348 |
349 | def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink":
350 | """Request to resolve additional information for a given document link. The request's
351 | parameter is of type {@link DocumentLink} the response
352 | is of type {@link DocumentLink} or a Thenable that resolves to such.
353 | """
354 | return self._send_request("documentLink/resolve", params)
355 |
356 | def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None:
357 | """A request to to format a whole document."""
358 | return self._send_request("textDocument/formatting", params)
359 |
360 | def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None:
361 | """A request to to format a range in a document."""
362 | return self._send_request("textDocument/rangeFormatting", params)
363 |
364 | def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None:
365 | """A request to format a document on type."""
366 | return self._send_request("textDocument/onTypeFormatting", params)
367 |
368 | def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]:
369 | """A request to rename a symbol."""
370 | return self._send_request("textDocument/rename", params)
371 |
372 | def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]:
373 | """A request to test and perform the setup necessary for a rename.
374 |
375 | @since 3.16 - support for default behavior
376 | """
377 | return self._send_request("textDocument/prepareRename", params)
378 |
379 | def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]:
380 | """A request send from the client to the server to execute a command. The request might return
381 | a workspace edit which the client will apply to the workspace.
382 | """
383 | return self._send_request("workspace/executeCommand", params)
384 |
```
--------------------------------------------------------------------------------
/test/solidlsp/dart/test_dart_basic.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | from pathlib import Path
3 |
4 | import pytest
5 |
6 | from solidlsp import SolidLanguageServer
7 | from solidlsp.ls_config import Language
8 | from solidlsp.ls_types import SymbolKind
9 | from solidlsp.ls_utils import SymbolUtils
10 |
11 |
12 | @pytest.mark.dart
13 | class TestDartLanguageServer:
14 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
15 | @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
16 | def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
17 | """Test that the language server starts and stops successfully."""
18 | # The fixture already handles start and stop
19 | assert language_server.is_running()
20 | assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
21 |
22 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
23 | @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
24 | def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
25 | """Test finding definition of a method within the same file."""
26 | # In lib/main.dart:
27 | # Line 105: final result1 = calc.add(5, 3); // Reference to add method
28 | # Line 12: int add(int a, int b) { // Definition of add method
29 | # Find definition of 'add' method from its usage
30 | main_dart_path = str(repo_path / "lib" / "main.dart")
31 |
32 | # Position: calc.add(5, 3) - cursor on 'add'
33 | # Line 105 (1-indexed) = line 104 (0-indexed), char position around 22
34 | definition_location_list = language_server.request_definition(main_dart_path, 104, 22)
35 |
36 | assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
37 | assert len(definition_location_list) >= 1
38 | definition_location = definition_location_list[0]
39 | assert definition_location["uri"].endswith("main.dart")
40 | # Definition of add method should be around line 11 (0-indexed)
41 | # But language server may return different positions
42 | assert definition_location["range"]["start"]["line"] >= 0
43 |
44 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
45 | @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
46 | def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
47 | """Test finding definition across different files."""
48 | # Test finding definition of MathHelper class which is in helper.dart
49 | # In lib/main.dart line 50: MathHelper.power(step1, 2)
50 | main_dart_path = str(repo_path / "lib" / "main.dart")
51 |
52 | # Position: MathHelper.power(step1, 2) - cursor on 'MathHelper'
53 | # Line 50 (1-indexed) = line 49 (0-indexed), char position around 18
54 | definition_location_list = language_server.request_definition(main_dart_path, 49, 18)
55 |
56 | # Skip the test if language server doesn't find cross-file references
57 | # This is acceptable for a basic test - the important thing is that LS is working
58 | if not definition_location_list:
59 | pytest.skip("Language server doesn't support cross-file definition lookup for this case")
60 |
61 | assert len(definition_location_list) >= 1
62 | definition_location = definition_location_list[0]
63 | assert definition_location["uri"].endswith("helper.dart")
64 | assert definition_location["range"]["start"]["line"] >= 0
65 |
66 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
67 | @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
68 | def test_find_definition_class_method(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
69 | """Test finding definition of a class method."""
70 | # In lib/main.dart:
71 | # Line 50: final step2 = MathHelper.power(step1, 2); // Reference to MathHelper.power method
72 | # In lib/helper.dart:
73 | # Line 14: static double power(double base, int exponent) { // Definition of power method
74 | main_dart_path = str(repo_path / "lib" / "main.dart")
75 |
76 | # Position: MathHelper.power(step1, 2) - cursor on 'power'
77 | # Line 50 (1-indexed) = line 49 (0-indexed), char position around 30
78 | definition_location_list = language_server.request_definition(main_dart_path, 49, 30)
79 |
80 | assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
81 | assert len(definition_location_list) >= 1
82 | definition_location = definition_location_list[0]
83 | assert definition_location["uri"].endswith("helper.dart")
84 | # Definition of power method should be around line 13 (0-indexed)
85 | assert 12 <= definition_location["range"]["start"]["line"] <= 16
86 |
87 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
88 | @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
89 | def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
90 | """Test finding references to a method within the same file."""
91 | main_dart_path = str(repo_path / "lib" / "main.dart")
92 |
93 | # Find references to the 'add' method from its definition
94 | # Line 12: int add(int a, int b) { // Definition of add method
95 | # Line 105: final result1 = calc.add(5, 3); // Usage of add method
96 | references = language_server.request_references(main_dart_path, 11, 6) # cursor on 'add' in definition
97 |
98 | assert references, f"Expected non-empty references but got {references=}"
99 | # Should find at least the usage of add method
100 | assert len(references) >= 1
101 |
102 | # Check that we have a reference in main.dart
103 | main_dart_references = [ref for ref in references if ref["uri"].endswith("main.dart")]
104 | assert len(main_dart_references) >= 1
105 |
106 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
107 | @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
108 | def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
109 | """Test finding references across different files."""
110 | helper_dart_path = str(repo_path / "lib" / "helper.dart")
111 |
112 | # Find references to the 'subtract' function from its definition in helper.dart
113 | # Definition is in helper.dart, usage is in main.dart
114 | references = language_server.request_references(helper_dart_path, 4, 4) # cursor on 'subtract' in definition
115 |
116 | assert references, f"Expected non-empty references for subtract function but got {references=}"
117 |
118 | # Should find references in main.dart
119 | main_dart_references = [ref for ref in references if ref["uri"].endswith("main.dart")]
120 | assert len(main_dart_references) >= 1
121 |
122 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
123 | @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
124 | def test_find_definition_constructor(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
125 | """Test finding definition of a constructor call."""
126 | main_dart_path = str(repo_path / "lib" / "main.dart")
127 |
128 | # In lib/main.dart:
129 | # Line 104: final calc = Calculator(); // Reference to Calculator constructor
130 | # Line 4: class Calculator { // Definition of Calculator class
131 | definition_location_list = language_server.request_definition(main_dart_path, 103, 18) # cursor on 'Calculator'
132 |
133 | assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
134 | assert len(definition_location_list) >= 1
135 | definition_location = definition_location_list[0]
136 | assert definition_location["uri"].endswith("main.dart")
137 | # Definition of Calculator class should be around line 3 (0-indexed)
138 | assert 3 <= definition_location["range"]["start"]["line"] <= 7
139 |
140 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
141 | @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
142 | def test_find_definition_import(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
143 | """Test finding definition through imports."""
144 | models_dart_path = str(repo_path / "lib" / "models.dart")
145 |
146 | # Test finding definition of User class name where it's used
147 | # In lib/models.dart line 27 (constructor): User(this.id, this.name, this.email, this._age);
148 | definition_location_list = language_server.request_definition(models_dart_path, 26, 2) # cursor on 'User' in constructor
149 |
150 | # Skip if language server doesn't find definition in this case
151 | if not definition_location_list:
152 | pytest.skip("Language server doesn't support definition lookup for this case")
153 |
154 | assert len(definition_location_list) >= 1
155 | definition_location = definition_location_list[0]
156 | # Language server might return SDK files instead of local files
157 | # This is acceptable behavior - the important thing is that it found a definition
158 | assert "dart" in definition_location["uri"].lower()
159 |
160 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
161 | def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
162 | """Test finding symbols in the full symbol tree."""
163 | symbols = language_server.request_full_symbol_tree()
164 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator class not found in symbol tree"
165 | assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add method not found in symbol tree"
166 | assert SymbolUtils.symbol_tree_contains_name(symbols, "subtract"), "subtract function not found in symbol tree"
167 | assert SymbolUtils.symbol_tree_contains_name(symbols, "MathHelper"), "MathHelper class not found in symbol tree"
168 | assert SymbolUtils.symbol_tree_contains_name(symbols, "User"), "User class not found in symbol tree"
169 |
170 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
171 | def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
172 | """Test finding references using symbol selection range."""
173 | file_path = os.path.join("lib", "main.dart")
174 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
175 |
176 | # Handle nested symbol structure - symbols can be nested in lists
177 | symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
178 |
179 | # Find the 'add' method symbol in Calculator class
180 | add_symbol = None
181 | for sym in symbol_list:
182 | if sym.get("name") == "add":
183 | add_symbol = sym
184 | break
185 | # Check for nested symbols (methods inside classes)
186 | if "children" in sym and sym.get("name") == "Calculator":
187 | for child in sym["children"]:
188 | if child.get("name") == "add":
189 | add_symbol = child
190 | break
191 | if add_symbol:
192 | break
193 |
194 | assert add_symbol is not None, "Could not find 'add' method symbol in main.dart"
195 | sel_start = add_symbol["selectionRange"]["start"]
196 | refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
197 |
198 | # Check that we found references - at least one should be in main.dart
199 | assert any(
200 | "main.dart" in ref.get("relativePath", "") or "main.dart" in ref.get("uri", "") for ref in refs
201 | ), "main.dart should reference add method (tried all positions in selectionRange)"
202 |
203 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
204 | def test_request_containing_symbol_method(self, language_server: SolidLanguageServer) -> None:
205 | """Test request_containing_symbol for a method."""
206 | file_path = os.path.join("lib", "main.dart")
207 | # Line 14 is inside the add method body (around 'final result = a + b;')
208 | containing_symbol = language_server.request_containing_symbol(file_path, 13, 10, include_body=True)
209 |
210 | # Verify that we found the containing symbol
211 | if containing_symbol is not None:
212 | assert containing_symbol["name"] == "add"
213 | assert containing_symbol["kind"] == SymbolKind.Method
214 | if "body" in containing_symbol:
215 | assert "add" in containing_symbol["body"] or "final result" in containing_symbol["body"]
216 |
217 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
218 | def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
219 | """Test request_containing_symbol for a class."""
220 | file_path = os.path.join("lib", "main.dart")
221 | # Line 4 is the Calculator class definition line
222 | containing_symbol = language_server.request_containing_symbol(file_path, 4, 6)
223 |
224 | # Verify that we found the containing symbol
225 | if containing_symbol is not None:
226 | assert containing_symbol["name"] == "Calculator"
227 | assert containing_symbol["kind"] == SymbolKind.Class
228 |
229 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
230 | def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
231 | """Test request_containing_symbol with nested scopes."""
232 | file_path = os.path.join("lib", "main.dart")
233 | # Line 14 is inside the add method inside Calculator class
234 | containing_symbol = language_server.request_containing_symbol(file_path, 13, 20)
235 |
236 | # Verify that we found the innermost containing symbol (the method)
237 | if containing_symbol is not None:
238 | assert containing_symbol["name"] == "add"
239 | assert containing_symbol["kind"] == SymbolKind.Method
240 |
241 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
242 | def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
243 | """Test request_defining_symbol for a variable usage."""
244 | file_path = os.path.join("lib", "main.dart")
245 | # Line 14 contains 'final result = a + b;' - test position on 'result'
246 | defining_symbol = language_server.request_defining_symbol(file_path, 13, 10)
247 |
248 | # The defining symbol might be the variable itself or the containing method
249 | # This is acceptable behavior - different language servers handle this differently
250 | if defining_symbol is not None:
251 | assert defining_symbol.get("name") in ["result", "add"]
252 | if defining_symbol.get("name") == "add":
253 | assert defining_symbol.get("kind") == SymbolKind.Method.value
254 |
255 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
256 | def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
257 | """Test request_defining_symbol for an imported class/function."""
258 | file_path = os.path.join("lib", "main.dart")
259 | # Line 20 references 'subtract' which was imported from helper.dart
260 | defining_symbol = language_server.request_defining_symbol(file_path, 19, 18)
261 |
262 | # Verify that we found the defining symbol - this should be the subtract function from helper.dart
263 | if defining_symbol is not None:
264 | assert defining_symbol.get("name") == "subtract"
265 | # Could be Function or Method depending on language server interpretation
266 | assert defining_symbol.get("kind") in [SymbolKind.Function.value, SymbolKind.Method.value]
267 |
268 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
269 | def test_request_defining_symbol_class_method(self, language_server: SolidLanguageServer) -> None:
270 | """Test request_defining_symbol for a static class method."""
271 | file_path = os.path.join("lib", "main.dart")
272 | # Line 50 references MathHelper.power - test position on 'power'
273 | defining_symbol = language_server.request_defining_symbol(file_path, 49, 30)
274 |
275 | # Verify that we found the defining symbol - should be the power method
276 | if defining_symbol is not None:
277 | assert defining_symbol.get("name") == "power"
278 | assert defining_symbol.get("kind") == SymbolKind.Method.value
279 |
280 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
281 | def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
282 | """Test getting document symbols from a Dart file."""
283 | file_path = os.path.join("lib", "main.dart")
284 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
285 |
286 | # Check that we have symbols
287 | assert len(symbols) > 0
288 |
289 | # Flatten the symbols if they're nested
290 | symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
291 |
292 | # Look for expected classes and methods
293 | symbol_names = [s.get("name") for s in symbol_list]
294 | assert "Calculator" in symbol_names
295 |
296 | # Check for nested symbols (methods inside classes) - optional
297 | calculator_symbol = next((s for s in symbol_list if s.get("name") == "Calculator"), None)
298 | if calculator_symbol and "children" in calculator_symbol and calculator_symbol["children"]:
299 | method_names = [child.get("name") for child in calculator_symbol["children"]]
300 | # If children are populated, we should find the add method
301 | assert "add" in method_names
302 | else:
303 | # Some language servers may not populate children in document symbols
304 | # This is acceptable behavior - the important thing is we found the class
305 | pass
306 |
307 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
308 | def test_request_referencing_symbols_comprehensive(self, language_server: SolidLanguageServer) -> None:
309 | """Test comprehensive referencing symbols functionality."""
310 | file_path = os.path.join("lib", "main.dart")
311 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
312 |
313 | # Handle nested symbol structure
314 | symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
315 |
316 | # Find Calculator class and test its references
317 | calculator_symbol = None
318 | for sym in symbol_list:
319 | if sym.get("name") == "Calculator":
320 | calculator_symbol = sym
321 | break
322 |
323 | if calculator_symbol and "selectionRange" in calculator_symbol:
324 | sel_start = calculator_symbol["selectionRange"]["start"]
325 | refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
326 |
327 | # Should find references to Calculator (constructor calls, etc.)
328 | if refs:
329 | # Verify the structure of referencing symbols
330 | for ref in refs:
331 | assert "uri" in ref or "relativePath" in ref
332 | if "range" in ref:
333 | assert "start" in ref["range"]
334 | assert "end" in ref["range"]
335 |
336 | @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
337 | def test_cross_file_symbol_resolution(self, language_server: SolidLanguageServer) -> None:
338 | """Test symbol resolution across multiple files."""
339 | helper_file_path = os.path.join("lib", "helper.dart")
340 |
341 | # Test finding references to subtract function from helper.dart in main.dart
342 | helper_symbols = language_server.request_document_symbols(helper_file_path).get_all_symbols_and_roots()
343 | symbol_list = helper_symbols[0] if helper_symbols and isinstance(helper_symbols[0], list) else helper_symbols
344 |
345 | subtract_symbol = next((s for s in symbol_list if s.get("name") == "subtract"), None)
346 |
347 | if subtract_symbol and "selectionRange" in subtract_symbol:
348 | sel_start = subtract_symbol["selectionRange"]["start"]
349 | refs = language_server.request_references(helper_file_path, sel_start["line"], sel_start["character"])
350 |
351 | # Should find references in main.dart
352 | main_dart_refs = [ref for ref in refs if "main.dart" in ref.get("uri", "") or "main.dart" in ref.get("relativePath", "")]
353 | # Note: This may not always work depending on language server capabilities
354 | # So we don't assert - just verify the structure if we get results
355 | if main_dart_refs:
356 | for ref in main_dart_refs:
357 | assert "range" in ref or "location" in ref
358 |
```
--------------------------------------------------------------------------------
/src/solidlsp/language_servers/omnisharp/initialize_params.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
3 | "processId": "os.getpid()",
4 | "clientInfo": {
5 | "name": "Visual Studio Code - Insiders",
6 | "version": "1.82.0-insider"
7 | },
8 | "locale": "en",
9 | "rootPath": "$rootPath",
10 | "rootUri": "$rootUri",
11 | "capabilities": {
12 | "workspace": {
13 | "applyEdit": true,
14 | "workspaceEdit": {
15 | "documentChanges": true,
16 | "resourceOperations": [
17 | "create",
18 | "rename",
19 | "delete"
20 | ],
21 | "failureHandling": "textOnlyTransactional",
22 | "normalizesLineEndings": true,
23 | "changeAnnotationSupport": {
24 | "groupsOnLabel": true
25 | }
26 | },
27 | "configuration": false,
28 | "didChangeWatchedFiles": {
29 | "dynamicRegistration": true,
30 | "relativePatternSupport": true
31 | },
32 | "symbol": {
33 | "dynamicRegistration": true,
34 | "symbolKind": {
35 | "valueSet": [
36 | 1,
37 | 2,
38 | 3,
39 | 4,
40 | 5,
41 | 6,
42 | 7,
43 | 8,
44 | 9,
45 | 10,
46 | 11,
47 | 12,
48 | 13,
49 | 14,
50 | 15,
51 | 16,
52 | 17,
53 | 18,
54 | 19,
55 | 20,
56 | 21,
57 | 22,
58 | 23,
59 | 24,
60 | 25,
61 | 26
62 | ]
63 | },
64 | "tagSupport": {
65 | "valueSet": [
66 | 1
67 | ]
68 | },
69 | "resolveSupport": {
70 | "properties": [
71 | "location.range"
72 | ]
73 | }
74 | },
75 | "codeLens": {
76 | "refreshSupport": true
77 | },
78 | "executeCommand": {
79 | "dynamicRegistration": true
80 | },
81 | "didChangeConfiguration": {
82 | "dynamicRegistration": true
83 | },
84 | "workspaceFolders": true,
85 | "semanticTokens": {
86 | "refreshSupport": true
87 | },
88 | "fileOperations": {
89 | "dynamicRegistration": true,
90 | "didCreate": true,
91 | "didRename": true,
92 | "didDelete": true,
93 | "willCreate": true,
94 | "willRename": true,
95 | "willDelete": true
96 | },
97 | "inlineValue": {
98 | "refreshSupport": true
99 | },
100 | "inlayHint": {
101 | "refreshSupport": true
102 | },
103 | "diagnostics": {
104 | "refreshSupport": true
105 | }
106 | },
107 | "textDocument": {
108 | "publishDiagnostics": {
109 | "relatedInformation": true,
110 | "versionSupport": false,
111 | "tagSupport": {
112 | "valueSet": [
113 | 1,
114 | 2
115 | ]
116 | },
117 | "codeDescriptionSupport": true,
118 | "dataSupport": true
119 | },
120 | "synchronization": {
121 | "dynamicRegistration": true,
122 | "willSave": true,
123 | "willSaveWaitUntil": true,
124 | "didSave": true
125 | },
126 | "completion": {
127 | "dynamicRegistration": true,
128 | "contextSupport": true,
129 | "completionItem": {
130 | "snippetSupport": true,
131 | "commitCharactersSupport": true,
132 | "documentationFormat": [
133 | "markdown",
134 | "plaintext"
135 | ],
136 | "deprecatedSupport": true,
137 | "preselectSupport": true,
138 | "tagSupport": {
139 | "valueSet": [
140 | 1
141 | ]
142 | },
143 | "insertReplaceSupport": true,
144 | "resolveSupport": {
145 | "properties": [
146 | "documentation",
147 | "detail",
148 | "additionalTextEdits"
149 | ]
150 | },
151 | "insertTextModeSupport": {
152 | "valueSet": [
153 | 1,
154 | 2
155 | ]
156 | },
157 | "labelDetailsSupport": true
158 | },
159 | "insertTextMode": 2,
160 | "completionItemKind": {
161 | "valueSet": [
162 | 1,
163 | 2,
164 | 3,
165 | 4,
166 | 5,
167 | 6,
168 | 7,
169 | 8,
170 | 9,
171 | 10,
172 | 11,
173 | 12,
174 | 13,
175 | 14,
176 | 16,
177 | 17,
178 | 18,
179 | 19,
180 | 20,
181 | 21,
182 | 22,
183 | 23,
184 | 24,
185 | 25
186 | ]
187 | },
188 | "completionList": {
189 | "itemDefaults": [
190 | "commitCharacters",
191 | "editRange",
192 | "insertTextFormat",
193 | "insertTextMode"
194 | ]
195 | }
196 | },
197 | "hover": {
198 | "dynamicRegistration": true,
199 | "contentFormat": [
200 | "markdown",
201 | "plaintext"
202 | ]
203 | },
204 | "signatureHelp": {
205 | "dynamicRegistration": true,
206 | "signatureInformation": {
207 | "documentationFormat": [
208 | "markdown",
209 | "plaintext"
210 | ],
211 | "parameterInformation": {
212 | "labelOffsetSupport": true
213 | },
214 | "activeParameterSupport": true
215 | },
216 | "contextSupport": true
217 | },
218 | "definition": {
219 | "dynamicRegistration": true,
220 | "linkSupport": true
221 | },
222 | "references": {
223 | "dynamicRegistration": true
224 | },
225 | "documentHighlight": {
226 | "dynamicRegistration": true
227 | },
228 | "documentSymbol": {
229 | "dynamicRegistration": true,
230 | "symbolKind": {
231 | "valueSet": [
232 | 1,
233 | 2,
234 | 3,
235 | 4,
236 | 5,
237 | 6,
238 | 7,
239 | 8,
240 | 9,
241 | 10,
242 | 11,
243 | 12,
244 | 13,
245 | 14,
246 | 15,
247 | 16,
248 | 17,
249 | 18,
250 | 19,
251 | 20,
252 | 21,
253 | 22,
254 | 23,
255 | 24,
256 | 25,
257 | 26
258 | ]
259 | },
260 | "hierarchicalDocumentSymbolSupport": true,
261 | "tagSupport": {
262 | "valueSet": [
263 | 1
264 | ]
265 | },
266 | "labelSupport": true
267 | },
268 | "codeAction": {
269 | "dynamicRegistration": true,
270 | "isPreferredSupport": true,
271 | "disabledSupport": true,
272 | "dataSupport": true,
273 | "resolveSupport": {
274 | "properties": [
275 | "edit"
276 | ]
277 | },
278 | "codeActionLiteralSupport": {
279 | "codeActionKind": {
280 | "valueSet": [
281 | "",
282 | "quickfix",
283 | "refactor",
284 | "refactor.extract",
285 | "refactor.inline",
286 | "refactor.rewrite",
287 | "source",
288 | "source.organizeImports"
289 | ]
290 | }
291 | },
292 | "honorsChangeAnnotations": false
293 | },
294 | "codeLens": {
295 | "dynamicRegistration": true
296 | },
297 | "formatting": {
298 | "dynamicRegistration": true
299 | },
300 | "rangeFormatting": {
301 | "dynamicRegistration": true
302 | },
303 | "onTypeFormatting": {
304 | "dynamicRegistration": true
305 | },
306 | "rename": {
307 | "dynamicRegistration": true,
308 | "prepareSupport": true,
309 | "prepareSupportDefaultBehavior": 1,
310 | "honorsChangeAnnotations": true
311 | },
312 | "documentLink": {
313 | "dynamicRegistration": true,
314 | "tooltipSupport": true
315 | },
316 | "typeDefinition": {
317 | "dynamicRegistration": true,
318 | "linkSupport": true
319 | },
320 | "implementation": {
321 | "dynamicRegistration": true,
322 | "linkSupport": true
323 | },
324 | "colorProvider": {
325 | "dynamicRegistration": true
326 | },
327 | "foldingRange": {
328 | "dynamicRegistration": true,
329 | "rangeLimit": 5000,
330 | "lineFoldingOnly": true,
331 | "foldingRangeKind": {
332 | "valueSet": [
333 | "comment",
334 | "imports",
335 | "region"
336 | ]
337 | },
338 | "foldingRange": {
339 | "collapsedText": false
340 | }
341 | },
342 | "declaration": {
343 | "dynamicRegistration": true,
344 | "linkSupport": true
345 | },
346 | "selectionRange": {
347 | "dynamicRegistration": true
348 | },
349 | "callHierarchy": {
350 | "dynamicRegistration": true
351 | },
352 | "semanticTokens": {
353 | "dynamicRegistration": true,
354 | "tokenTypes": [
355 | "namespace",
356 | "type",
357 | "class",
358 | "enum",
359 | "interface",
360 | "struct",
361 | "typeParameter",
362 | "parameter",
363 | "variable",
364 | "property",
365 | "enumMember",
366 | "event",
367 | "function",
368 | "method",
369 | "macro",
370 | "keyword",
371 | "modifier",
372 | "comment",
373 | "string",
374 | "number",
375 | "regexp",
376 | "operator",
377 | "decorator"
378 | ],
379 | "tokenModifiers": [
380 | "declaration",
381 | "definition",
382 | "readonly",
383 | "static",
384 | "deprecated",
385 | "abstract",
386 | "async",
387 | "modification",
388 | "documentation",
389 | "defaultLibrary"
390 | ],
391 | "formats": [
392 | "relative"
393 | ],
394 | "requests": {
395 | "range": true,
396 | "full": {
397 | "delta": true
398 | }
399 | },
400 | "multilineTokenSupport": false,
401 | "overlappingTokenSupport": false,
402 | "serverCancelSupport": true,
403 | "augmentsSyntaxTokens": false
404 | },
405 | "linkedEditingRange": {
406 | "dynamicRegistration": true
407 | },
408 | "typeHierarchy": {
409 | "dynamicRegistration": true
410 | },
411 | "inlineValue": {
412 | "dynamicRegistration": true
413 | },
414 | "inlayHint": {
415 | "dynamicRegistration": true,
416 | "resolveSupport": {
417 | "properties": [
418 | "tooltip",
419 | "textEdits",
420 | "label.tooltip",
421 | "label.location",
422 | "label.command"
423 | ]
424 | }
425 | },
426 | "diagnostic": {
427 | "dynamicRegistration": true,
428 | "relatedDocumentSupport": false
429 | }
430 | },
431 | "window": {
432 | "showMessage": {
433 | "messageActionItem": {
434 | "additionalPropertiesSupport": true
435 | }
436 | },
437 | "showDocument": {
438 | "support": true
439 | },
440 | "workDoneProgress": true
441 | },
442 | "general": {
443 | "staleRequestSupport": {
444 | "cancel": true,
445 | "retryOnContentModified": [
446 | "textDocument/semanticTokens/full",
447 | "textDocument/semanticTokens/range",
448 | "textDocument/semanticTokens/full/delta"
449 | ]
450 | },
451 | "regularExpressions": {
452 | "engine": "ECMAScript",
453 | "version": "ES2020"
454 | },
455 | "markdown": {
456 | "parser": "marked",
457 | "version": "1.1.0",
458 | "allowedTags": [
459 | "ul",
460 | "li",
461 | "p",
462 | "code",
463 | "blockquote",
464 | "ol",
465 | "h1",
466 | "h2",
467 | "h3",
468 | "h4",
469 | "h5",
470 | "h6",
471 | "hr",
472 | "em",
473 | "pre",
474 | "table",
475 | "thead",
476 | "tbody",
477 | "tr",
478 | "th",
479 | "td",
480 | "div",
481 | "del",
482 | "a",
483 | "strong",
484 | "br",
485 | "img",
486 | "span"
487 | ]
488 | },
489 | "positionEncodings": [
490 | "utf-16"
491 | ]
492 | },
493 | "notebookDocument": {
494 | "synchronization": {
495 | "dynamicRegistration": true,
496 | "executionSummarySupport": true
497 | }
498 | },
499 | "experimental": {
500 | "snippetTextEdit": true,
501 | "codeActionGroup": true,
502 | "hoverActions": true,
503 | "serverStatusNotification": true,
504 | "colorDiagnosticOutput": true,
505 | "openServerLogs": true,
506 | "commands": {
507 | "commands": [
508 | "editor.action.triggerParameterHints"
509 | ]
510 | }
511 | }
512 | },
513 | "initializationOptions": {
514 | "RoslynExtensionsOptions": {
515 | "EnableDecompilationSupport": false,
516 | "EnableAnalyzersSupport": true,
517 | "EnableImportCompletion": true,
518 | "EnableAsyncCompletion": false,
519 | "DocumentAnalysisTimeoutMs": 30000,
520 | "DiagnosticWorkersThreadCount": 18,
521 | "AnalyzeOpenDocumentsOnly": true,
522 | "InlayHintsOptions": {
523 | "EnableForParameters": false,
524 | "ForLiteralParameters": false,
525 | "ForIndexerParameters": false,
526 | "ForObjectCreationParameters": false,
527 | "ForOtherParameters": false,
528 | "SuppressForParametersThatDifferOnlyBySuffix": false,
529 | "SuppressForParametersThatMatchMethodIntent": false,
530 | "SuppressForParametersThatMatchArgumentName": false,
531 | "EnableForTypes": false,
532 | "ForImplicitVariableTypes": false,
533 | "ForLambdaParameterTypes": false,
534 | "ForImplicitObjectCreation": false
535 | },
536 | "LocationPaths": null
537 | },
538 | "FormattingOptions": {
539 | "OrganizeImports": false,
540 | "EnableEditorConfigSupport": true,
541 | "NewLine": "\n",
542 | "UseTabs": false,
543 | "TabSize": 4,
544 | "IndentationSize": 4,
545 | "SpacingAfterMethodDeclarationName": false,
546 | "SeparateImportDirectiveGroups": false,
547 | "SpaceWithinMethodDeclarationParenthesis": false,
548 | "SpaceBetweenEmptyMethodDeclarationParentheses": false,
549 | "SpaceAfterMethodCallName": false,
550 | "SpaceWithinMethodCallParentheses": false,
551 | "SpaceBetweenEmptyMethodCallParentheses": false,
552 | "SpaceAfterControlFlowStatementKeyword": true,
553 | "SpaceWithinExpressionParentheses": false,
554 | "SpaceWithinCastParentheses": false,
555 | "SpaceWithinOtherParentheses": false,
556 | "SpaceAfterCast": false,
557 | "SpaceBeforeOpenSquareBracket": false,
558 | "SpaceBetweenEmptySquareBrackets": false,
559 | "SpaceWithinSquareBrackets": false,
560 | "SpaceAfterColonInBaseTypeDeclaration": true,
561 | "SpaceAfterComma": true,
562 | "SpaceAfterDot": false,
563 | "SpaceAfterSemicolonsInForStatement": true,
564 | "SpaceBeforeColonInBaseTypeDeclaration": true,
565 | "SpaceBeforeComma": false,
566 | "SpaceBeforeDot": false,
567 | "SpaceBeforeSemicolonsInForStatement": false,
568 | "SpacingAroundBinaryOperator": "single",
569 | "IndentBraces": false,
570 | "IndentBlock": true,
571 | "IndentSwitchSection": true,
572 | "IndentSwitchCaseSection": true,
573 | "IndentSwitchCaseSectionWhenBlock": true,
574 | "LabelPositioning": "oneLess",
575 | "WrappingPreserveSingleLine": true,
576 | "WrappingKeepStatementsOnSingleLine": true,
577 | "NewLinesForBracesInTypes": true,
578 | "NewLinesForBracesInMethods": true,
579 | "NewLinesForBracesInProperties": true,
580 | "NewLinesForBracesInAccessors": true,
581 | "NewLinesForBracesInAnonymousMethods": true,
582 | "NewLinesForBracesInControlBlocks": true,
583 | "NewLinesForBracesInAnonymousTypes": true,
584 | "NewLinesForBracesInObjectCollectionArrayInitializers": true,
585 | "NewLinesForBracesInLambdaExpressionBody": true,
586 | "NewLineForElse": true,
587 | "NewLineForCatch": true,
588 | "NewLineForFinally": true,
589 | "NewLineForMembersInObjectInit": true,
590 | "NewLineForMembersInAnonymousTypes": true,
591 | "NewLineForClausesInQuery": true
592 | },
593 | "FileOptions": {
594 | "SystemExcludeSearchPatterns": [
595 | "**/node_modules/**/*",
596 | "**/bin/**/*",
597 | "**/obj/**/*",
598 | "**/.git/**/*",
599 | "**/.git",
600 | "**/.svn",
601 | "**/.hg",
602 | "**/CVS",
603 | "**/.DS_Store",
604 | "**/Thumbs.db"
605 | ],
606 | "ExcludeSearchPatterns": []
607 | },
608 | "RenameOptions": {
609 | "RenameOverloads": false,
610 | "RenameInStrings": false,
611 | "RenameInComments": false
612 | },
613 | "ImplementTypeOptions": {
614 | "InsertionBehavior": 0,
615 | "PropertyGenerationBehavior": 0
616 | },
617 | "DotNetCliOptions": {
618 | "LocationPaths": null
619 | },
620 | "Plugins": {
621 | "LocationPaths": null
622 | }
623 | },
624 | "trace": "verbose",
625 | "workspaceFolders": [
626 | {
627 | "uri": "$uri",
628 | "name": "$name"
629 | }
630 | ]
631 | }
```