This is page 6 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
--------------------------------------------------------------------------------
/src/serena/ls_manager.py:
--------------------------------------------------------------------------------
```python
1 | import logging
2 | import threading
3 | from collections.abc import Iterator
4 |
5 | from sensai.util.logging import LogTime
6 |
7 | from serena.config.serena_config import SerenaPaths
8 | from serena.constants import SERENA_MANAGED_DIR_NAME
9 | from solidlsp import SolidLanguageServer
10 | from solidlsp.ls_config import Language, LanguageServerConfig
11 | from solidlsp.settings import SolidLSPSettings
12 |
13 | log = logging.getLogger(__name__)
14 |
15 |
16 | class LanguageServerFactory:
17 | def __init__(
18 | self,
19 | project_root: str,
20 | encoding: str,
21 | ignored_patterns: list[str],
22 | ls_timeout: float | None = None,
23 | ls_specific_settings: dict | None = None,
24 | trace_lsp_communication: bool = False,
25 | ):
26 | self.project_root = project_root
27 | self.encoding = encoding
28 | self.ignored_patterns = ignored_patterns
29 | self.ls_timeout = ls_timeout
30 | self.ls_specific_settings = ls_specific_settings
31 | self.trace_lsp_communication = trace_lsp_communication
32 |
33 | def create_language_server(self, language: Language) -> SolidLanguageServer:
34 | ls_config = LanguageServerConfig(
35 | code_language=language,
36 | ignored_paths=self.ignored_patterns,
37 | trace_lsp_communication=self.trace_lsp_communication,
38 | encoding=self.encoding,
39 | )
40 |
41 | log.info(f"Creating language server instance for {self.project_root}, language={language}.")
42 | return SolidLanguageServer.create(
43 | ls_config,
44 | self.project_root,
45 | timeout=self.ls_timeout,
46 | solidlsp_settings=SolidLSPSettings(
47 | solidlsp_dir=SerenaPaths().serena_user_home_dir,
48 | project_data_relative_path=SERENA_MANAGED_DIR_NAME,
49 | ls_specific_settings=self.ls_specific_settings or {},
50 | ),
51 | )
52 |
53 |
54 | class LanguageServerManager:
55 | """
56 | Manages one or more language servers for a project.
57 | """
58 |
59 | def __init__(
60 | self,
61 | language_servers: dict[Language, SolidLanguageServer],
62 | language_server_factory: LanguageServerFactory | None = None,
63 | ) -> None:
64 | """
65 | :param language_servers: a mapping from language to language server; the servers are assumed to be already started.
66 | The first server in the iteration order is used as the default server.
67 | All servers are assumed to serve the same project root.
68 | :param language_server_factory: factory for language server creation; if None, dynamic (re)creation of language servers
69 | is not supported
70 | """
71 | self._language_servers = language_servers
72 | self._language_server_factory = language_server_factory
73 | self._default_language_server = next(iter(language_servers.values()))
74 | self._root_path = self._default_language_server.repository_root_path
75 |
76 | @staticmethod
77 | def from_languages(languages: list[Language], factory: LanguageServerFactory) -> "LanguageServerManager":
78 | """
79 | Creates a manager with language servers for the given languages using the given factory.
80 | The language servers are started in parallel threads.
81 |
82 | :param languages: the languages for which to spawn language servers
83 | :param factory: the factory for language server creation
84 | :return: the instance
85 | """
86 | language_servers: dict[Language, SolidLanguageServer] = {}
87 | threads = []
88 | exceptions = {}
89 | lock = threading.Lock()
90 |
91 | def start_language_server(language: Language) -> None:
92 | try:
93 | with LogTime(f"Language server startup (language={language.value})"):
94 | language_server = factory.create_language_server(language)
95 | language_server.start()
96 | if not language_server.is_running():
97 | raise RuntimeError(f"Failed to start the language server for language {language.value}")
98 | with lock:
99 | language_servers[language] = language_server
100 | except Exception as e:
101 | log.error(f"Error starting language server for language {language.value}: {e}", exc_info=e)
102 | with lock:
103 | exceptions[language] = e
104 |
105 | # start language servers in parallel threads
106 | for language in languages:
107 | thread = threading.Thread(target=start_language_server, args=(language,), name="StartLS:" + language.value)
108 | thread.start()
109 | threads.append(thread)
110 | for thread in threads:
111 | thread.join()
112 |
113 | # If any server failed to start up, raise an exception and stop all started language servers
114 | if exceptions:
115 | for ls in language_servers.values():
116 | ls.stop()
117 | failure_messages = "\n".join([f"{lang.value}: {e}" for lang, e in exceptions.items()])
118 | raise Exception(f"Failed to start language servers:\n{failure_messages}")
119 |
120 | return LanguageServerManager(language_servers, factory)
121 |
122 | def get_root_path(self) -> str:
123 | return self._root_path
124 |
125 | def _ensure_functional_ls(self, ls: SolidLanguageServer) -> SolidLanguageServer:
126 | if not ls.is_running():
127 | log.warning(f"Language server for language {ls.language} is not running; restarting ...")
128 | ls = self.restart_language_server(ls.language)
129 | return ls
130 |
131 | def get_language_server(self, relative_path: str) -> SolidLanguageServer:
132 | ls: SolidLanguageServer | None = None
133 | if len(self._language_servers) > 1:
134 | for candidate in self._language_servers.values():
135 | if not candidate.is_ignored_path(relative_path, ignore_unsupported_files=True):
136 | ls = candidate
137 | break
138 | if ls is None:
139 | ls = self._default_language_server
140 | return self._ensure_functional_ls(ls)
141 |
142 | def _create_and_start_language_server(self, language: Language) -> SolidLanguageServer:
143 | if self._language_server_factory is None:
144 | raise ValueError(f"No language server factory available to create language server for {language}")
145 | language_server = self._language_server_factory.create_language_server(language)
146 | language_server.start()
147 | self._language_servers[language] = language_server
148 | return language_server
149 |
150 | def restart_language_server(self, language: Language) -> SolidLanguageServer:
151 | """
152 | Forces recreation and restart of the language server for the given language.
153 | It is assumed that the language server for the given language is no longer running.
154 |
155 | :param language: the language
156 | :return: the newly created language server
157 | """
158 | if language not in self._language_servers:
159 | raise ValueError(f"No language server for language {language.value} present; cannot restart")
160 | return self._create_and_start_language_server(language)
161 |
162 | def add_language_server(self, language: Language) -> SolidLanguageServer:
163 | """
164 | Dynamically adds a new language server for the given language.
165 |
166 | :param language: the language
167 | :param factory: the factory to create the language server
168 | :return: the newly created language server
169 | """
170 | if language in self._language_servers:
171 | raise ValueError(f"Language server for language {language.value} already present")
172 | return self._create_and_start_language_server(language)
173 |
174 | def remove_language_server(self, language: Language, save_cache: bool = False) -> None:
175 | """
176 | Removes the language server for the given language, stopping it if it is running.
177 |
178 | :param language: the language
179 | """
180 | if language not in self._language_servers:
181 | raise ValueError(f"No language server for language {language.value} present; cannot remove")
182 | ls = self._language_servers.pop(language)
183 | self._stop_language_server(ls, save_cache=save_cache)
184 |
185 | def get_active_languages(self) -> list[Language]:
186 | """
187 | Returns the list of languages for which language servers are currently managed.
188 |
189 | :return: list of languages
190 | """
191 | return list(self._language_servers.keys())
192 |
193 | @staticmethod
194 | def _stop_language_server(ls: SolidLanguageServer, save_cache: bool = False, timeout: float = 2.0) -> None:
195 | if ls.is_running():
196 | if save_cache:
197 | ls.save_cache()
198 | log.info(f"Stopping language server for language {ls.language} ...")
199 | ls.stop(shutdown_timeout=timeout)
200 |
201 | def iter_language_servers(self) -> Iterator[SolidLanguageServer]:
202 | for ls in self._language_servers.values():
203 | yield self._ensure_functional_ls(ls)
204 |
205 | def stop_all(self, save_cache: bool = False, timeout: float = 2.0) -> None:
206 | """
207 | Stops all managed language servers.
208 |
209 | :param save_cache: whether to save the cache before stopping
210 | :param timeout: timeout for shutdown of each language server
211 | """
212 | for ls in self.iter_language_servers():
213 | self._stop_language_server(ls, save_cache=save_cache, timeout=timeout)
214 |
215 | def save_all_caches(self) -> None:
216 | """
217 | Saves the caches of all managed language servers.
218 | """
219 | for ls in self.iter_language_servers():
220 | if ls.is_running():
221 | ls.save_cache()
222 |
```
--------------------------------------------------------------------------------
/test/serena/test_mcp.py:
--------------------------------------------------------------------------------
```python
1 | """Tests for the mcp.py module in serena."""
2 |
3 | import pytest
4 | from mcp.server.fastmcp.tools.base import Tool as MCPTool
5 |
6 | from serena.agent import Tool, ToolRegistry
7 | from serena.config.context_mode import SerenaAgentContext
8 | from serena.mcp import SerenaMCPFactory
9 |
10 | make_tool = SerenaMCPFactory.make_mcp_tool
11 |
12 |
13 | # Create a mock agent for tool initialization
14 | class MockAgent:
15 | def __init__(self):
16 | self.project_config = None
17 | self.serena_config = None
18 |
19 | @staticmethod
20 | def get_context() -> SerenaAgentContext:
21 | return SerenaAgentContext.load_default()
22 |
23 |
24 | class BaseMockTool(Tool):
25 | """A mock Tool class for testing."""
26 |
27 | def __init__(self):
28 | super().__init__(MockAgent()) # type: ignore
29 |
30 |
31 | class BasicTool(BaseMockTool):
32 | """A mock Tool class for testing."""
33 |
34 | def apply(self, name: str, age: int = 0) -> str:
35 | """This is a test function.
36 |
37 | :param name: The person's name
38 | :param age: The person's age
39 | :return: A greeting message
40 | """
41 | return f"Hello {name}, you are {age} years old!"
42 |
43 | def apply_ex(
44 | self,
45 | log_call: bool = True,
46 | catch_exceptions: bool = True,
47 | **kwargs,
48 | ) -> str:
49 | """Mock implementation of apply_ex."""
50 | return self.apply(**kwargs)
51 |
52 |
53 | def test_make_tool_basic() -> None:
54 | """Test that make_tool correctly creates an MCP tool from a Tool object."""
55 | mock_tool = BasicTool()
56 |
57 | mcp_tool = make_tool(mock_tool)
58 |
59 | # Test that the MCP tool has the correct properties
60 | assert isinstance(mcp_tool, MCPTool)
61 | assert mcp_tool.name == "basic"
62 | assert "This is a test function. Returns A greeting message." in mcp_tool.description
63 |
64 | # Test that the parameters were correctly processed
65 | parameters = mcp_tool.parameters
66 | assert "properties" in parameters
67 | assert "name" in parameters["properties"]
68 | assert "age" in parameters["properties"]
69 | assert parameters["properties"]["name"]["description"] == "The person's name."
70 | assert parameters["properties"]["age"]["description"] == "The person's age."
71 |
72 |
73 | def test_make_tool_execution() -> None:
74 | """Test that the execution function created by make_tool works correctly."""
75 | mock_tool = BasicTool()
76 | mcp_tool = make_tool(mock_tool)
77 |
78 | # Execute the MCP tool function
79 | result = mcp_tool.fn(name="Alice", age=30)
80 |
81 | assert result == "Hello Alice, you are 30 years old!"
82 |
83 |
84 | def test_make_tool_no_params() -> None:
85 | """Test make_tool with a function that has no parameters."""
86 |
87 | class NoParamsTool(BaseMockTool):
88 | def apply(self) -> str:
89 | """This is a test function with no parameters.
90 |
91 | :return: A simple result
92 | """
93 | return "Simple result"
94 |
95 | def apply_ex(self, *args, **kwargs) -> str:
96 | return self.apply()
97 |
98 | tool = NoParamsTool()
99 | mcp_tool = make_tool(tool)
100 |
101 | assert mcp_tool.name == "no_params"
102 | assert "This is a test function with no parameters. Returns A simple result." in mcp_tool.description
103 | assert mcp_tool.parameters["properties"] == {}
104 |
105 |
106 | def test_make_tool_no_return_description() -> None:
107 | """Test make_tool with a function that has no return description."""
108 |
109 | class NoReturnTool(BaseMockTool):
110 | def apply(self, param: str) -> str:
111 | """This is a test function.
112 |
113 | :param param: The parameter
114 | """
115 | return f"Processed: {param}"
116 |
117 | def apply_ex(self, *args, **kwargs) -> str:
118 | return self.apply(**kwargs)
119 |
120 | tool = NoReturnTool()
121 | mcp_tool = make_tool(tool)
122 |
123 | assert mcp_tool.name == "no_return"
124 | assert mcp_tool.description == "This is a test function."
125 | assert mcp_tool.parameters["properties"]["param"]["description"] == "The parameter."
126 |
127 |
128 | def test_make_tool_parameter_not_in_docstring() -> None:
129 | """Test make_tool when a parameter in properties is not in the docstring."""
130 |
131 | class MissingParamTool(BaseMockTool):
132 | def apply(self, name: str, missing_param: str = "") -> str:
133 | """This is a test function.
134 |
135 | :param name: The person's name
136 | """
137 | return f"Hello {name}! Missing param: {missing_param}"
138 |
139 | def apply_ex(self, *args, **kwargs) -> str:
140 | return self.apply(**kwargs)
141 |
142 | tool = MissingParamTool()
143 | mcp_tool = make_tool(tool)
144 |
145 | assert "name" in mcp_tool.parameters["properties"]
146 | assert "missing_param" in mcp_tool.parameters["properties"]
147 | assert mcp_tool.parameters["properties"]["name"]["description"] == "The person's name."
148 | assert "description" not in mcp_tool.parameters["properties"]["missing_param"]
149 |
150 |
151 | def test_make_tool_multiline_docstring() -> None:
152 | """Test make_tool with a complex multi-line docstring."""
153 |
154 | class ComplexDocTool(BaseMockTool):
155 | def apply(self, project_file_path: str, host: str, port: int) -> str:
156 | """Create an MCP server.
157 |
158 | This function creates and configures a Model Context Protocol server
159 | with the specified settings.
160 |
161 | :param project_file_path: The path to the project file, or None
162 | :param host: The host to bind to
163 | :param port: The port to bind to
164 | :return: A configured FastMCP server instance
165 | """
166 | return f"Server config: {project_file_path}, {host}:{port}"
167 |
168 | def apply_ex(self, *args, **kwargs) -> str:
169 | return self.apply(**kwargs)
170 |
171 | tool = ComplexDocTool()
172 | mcp_tool = make_tool(tool)
173 |
174 | assert "Create an MCP server" in mcp_tool.description
175 | assert "Returns A configured FastMCP server instance" in mcp_tool.description
176 | assert mcp_tool.parameters["properties"]["project_file_path"]["description"] == "The path to the project file, or None."
177 | assert mcp_tool.parameters["properties"]["host"]["description"] == "The host to bind to."
178 | assert mcp_tool.parameters["properties"]["port"]["description"] == "The port to bind to."
179 |
180 |
181 | def test_make_tool_capitalization_and_periods() -> None:
182 | """Test that make_tool properly handles capitalization and periods in descriptions."""
183 |
184 | class FormatTool(BaseMockTool):
185 | def apply(self, param1: str, param2: str, param3: str) -> str:
186 | """Test function.
187 |
188 | :param param1: lowercase description
189 | :param param2: description with period.
190 | :param param3: description with Capitalized word.
191 | """
192 | return f"Formatted: {param1}, {param2}, {param3}"
193 |
194 | def apply_ex(self, *args, **kwargs) -> str:
195 | return self.apply(**kwargs)
196 |
197 | tool = FormatTool()
198 | mcp_tool = make_tool(tool)
199 |
200 | assert mcp_tool.parameters["properties"]["param1"]["description"] == "Lowercase description."
201 | assert mcp_tool.parameters["properties"]["param2"]["description"] == "Description with period."
202 | assert mcp_tool.parameters["properties"]["param3"]["description"] == "Description with Capitalized word."
203 |
204 |
205 | def test_make_tool_missing_apply() -> None:
206 | """Test make_tool with a tool that doesn't have an apply method."""
207 |
208 | class BadTool(BaseMockTool):
209 | pass
210 |
211 | tool = BadTool()
212 |
213 | with pytest.raises(AttributeError):
214 | make_tool(tool)
215 |
216 |
217 | @pytest.mark.parametrize(
218 | "docstring, expected_description",
219 | [
220 | (
221 | """This is a test function.
222 |
223 | :param param: The parameter
224 | :return: A result
225 | """,
226 | "This is a test function. Returns A result.",
227 | ),
228 | (
229 | """
230 | :param param: The parameter
231 | :return: A result
232 | """,
233 | "Returns A result.",
234 | ),
235 | (
236 | """
237 | :param param: The parameter
238 | """,
239 | "",
240 | ),
241 | ("Description without params.", "Description without params."),
242 | ],
243 | )
244 | def test_make_tool_descriptions(docstring, expected_description) -> None:
245 | """Test make_tool with various docstring formats."""
246 |
247 | class TestTool(BaseMockTool):
248 | def apply(self, param: str) -> str:
249 | return f"Result: {param}"
250 |
251 | def apply_ex(self, *args, **kwargs) -> str:
252 | return self.apply(**kwargs)
253 |
254 | # Dynamically set the docstring
255 | TestTool.apply.__doc__ = docstring
256 |
257 | tool = TestTool()
258 | mcp_tool = make_tool(tool)
259 |
260 | assert mcp_tool.name == "test"
261 | assert mcp_tool.description == expected_description
262 |
263 |
264 | def is_test_mock_class(tool_class: type) -> bool:
265 | """Check if a class is a test mock class."""
266 | # Check if the class is defined in a test module
267 | module_name = tool_class.__module__
268 | return (
269 | module_name.startswith(("test.", "tests."))
270 | or "test_" in module_name
271 | or tool_class.__name__
272 | in [
273 | "BaseMockTool",
274 | "BasicTool",
275 | "BadTool",
276 | "NoParamsTool",
277 | "NoReturnTool",
278 | "MissingParamTool",
279 | "ComplexDocTool",
280 | "FormatTool",
281 | "NoDescriptionTool",
282 | ]
283 | )
284 |
285 |
286 | @pytest.mark.parametrize("tool_class", ToolRegistry().get_all_tool_classes())
287 | def test_make_tool_all_tools(tool_class) -> None:
288 | """Test that make_tool works for all tools in the codebase."""
289 | # Create an instance of the tool
290 | tool_instance = tool_class(MockAgent())
291 |
292 | # Try to create an MCP tool from it
293 | mcp_tool = make_tool(tool_instance)
294 |
295 | # Basic validation
296 | assert isinstance(mcp_tool, MCPTool)
297 | assert mcp_tool.name == tool_class.get_name_from_cls()
298 |
299 | # The description should be a string (either from docstring or default)
300 | assert isinstance(mcp_tool.description, str)
301 |
```
--------------------------------------------------------------------------------
/src/solidlsp/language_servers/bash_language_server.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
3 | Contains various configurations and settings specific to Bash scripting.
4 | """
5 |
6 | import logging
7 | import os
8 | import pathlib
9 | import shutil
10 | import threading
11 |
12 | from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
13 | from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
14 | from solidlsp.ls_config import LanguageServerConfig
15 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
16 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
17 | from solidlsp.settings import SolidLSPSettings
18 |
19 | log = logging.getLogger(__name__)
20 |
21 |
22 | class BashLanguageServer(SolidLanguageServer):
23 | """
24 | Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
25 | Contains various configurations and settings specific to Bash scripting.
26 | """
27 |
28 | def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
29 | """
30 | Creates a BashLanguageServer instance. This class is not meant to be instantiated directly.
31 | Use LanguageServer.create() instead.
32 | """
33 | bash_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
34 | super().__init__(
35 | config,
36 | repository_root_path,
37 | ProcessLaunchInfo(cmd=bash_lsp_executable_path, cwd=repository_root_path),
38 | "bash",
39 | solidlsp_settings,
40 | )
41 | self.server_ready = threading.Event()
42 | self.initialize_searcher_command_available = threading.Event()
43 |
44 | @classmethod
45 | def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
46 | """
47 | Setup runtime dependencies for Bash Language Server and return the command to start the server.
48 | """
49 | # Verify both node and npm are installed
50 | is_node_installed = shutil.which("node") is not None
51 | assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
52 | is_npm_installed = shutil.which("npm") is not None
53 | assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
54 |
55 | deps = RuntimeDependencyCollection(
56 | [
57 | RuntimeDependency(
58 | id="bash-language-server",
59 | description="bash-language-server package",
60 | command="npm install --prefix ./ [email protected]",
61 | platform_id="any",
62 | ),
63 | ]
64 | )
65 |
66 | # Install bash-language-server if not already installed
67 | bash_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "bash-lsp")
68 | bash_executable_path = os.path.join(bash_ls_dir, "node_modules", ".bin", "bash-language-server")
69 |
70 | # Handle Windows executable extension
71 | if os.name == "nt":
72 | bash_executable_path += ".cmd"
73 |
74 | if not os.path.exists(bash_executable_path):
75 | log.info(f"Bash Language Server executable not found at {bash_executable_path}. Installing...")
76 | deps.install(bash_ls_dir)
77 | log.info("Bash language server dependencies installed successfully")
78 |
79 | if not os.path.exists(bash_executable_path):
80 | raise FileNotFoundError(
81 | f"bash-language-server executable not found at {bash_executable_path}, something went wrong with the installation."
82 | )
83 | return f"{bash_executable_path} start"
84 |
85 | @staticmethod
86 | def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
87 | """
88 | Returns the initialize params for the Bash Language Server.
89 | """
90 | root_uri = pathlib.Path(repository_absolute_path).as_uri()
91 | initialize_params = {
92 | "locale": "en",
93 | "capabilities": {
94 | "textDocument": {
95 | "synchronization": {"didSave": True, "dynamicRegistration": True},
96 | "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
97 | "definition": {"dynamicRegistration": True},
98 | "references": {"dynamicRegistration": True},
99 | "documentSymbol": {
100 | "dynamicRegistration": True,
101 | "hierarchicalDocumentSymbolSupport": True,
102 | "symbolKind": {"valueSet": list(range(1, 27))},
103 | },
104 | "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
105 | "signatureHelp": {"dynamicRegistration": True},
106 | "codeAction": {"dynamicRegistration": True},
107 | },
108 | "workspace": {
109 | "workspaceFolders": True,
110 | "didChangeConfiguration": {"dynamicRegistration": True},
111 | "symbol": {"dynamicRegistration": True},
112 | },
113 | },
114 | "processId": os.getpid(),
115 | "rootPath": repository_absolute_path,
116 | "rootUri": root_uri,
117 | "workspaceFolders": [
118 | {
119 | "uri": root_uri,
120 | "name": os.path.basename(repository_absolute_path),
121 | }
122 | ],
123 | }
124 | return initialize_params # type: ignore
125 |
126 | def _start_server(self) -> None:
127 | """
128 | Starts the Bash Language Server, waits for the server to be ready and yields the LanguageServer instance.
129 | """
130 |
131 | def register_capability_handler(params: dict) -> None:
132 | assert "registrations" in params
133 | for registration in params["registrations"]:
134 | if registration["method"] == "workspace/executeCommand":
135 | self.initialize_searcher_command_available.set()
136 | return
137 |
138 | def execute_client_command_handler(params: dict) -> list:
139 | return []
140 |
141 | def do_nothing(params: dict) -> None:
142 | return
143 |
144 | def window_log_message(msg: dict) -> None:
145 | log.info(f"LSP: window/logMessage: {msg}")
146 | # Check for bash-language-server ready signals
147 | message_text = msg.get("message", "")
148 | if "Analyzing" in message_text or "analysis complete" in message_text.lower():
149 | log.info("Bash language server analysis signals detected")
150 | self.server_ready.set()
151 | self.completions_available.set()
152 |
153 | self.server.on_request("client/registerCapability", register_capability_handler)
154 | self.server.on_notification("window/logMessage", window_log_message)
155 | self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
156 | self.server.on_notification("$/progress", do_nothing)
157 | self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
158 |
159 | log.info("Starting Bash server process")
160 | self.server.start()
161 | initialize_params = self._get_initialize_params(self.repository_root_path)
162 |
163 | log.info("Sending initialize request from LSP client to LSP server and awaiting response")
164 | init_response = self.server.send.initialize(initialize_params)
165 | log.debug(f"Received initialize response from bash server: {init_response}")
166 |
167 | # Enhanced capability checks for bash-language-server 5.6.0
168 | assert init_response["capabilities"]["textDocumentSync"] in [1, 2] # Full or Incremental
169 | assert "completionProvider" in init_response["capabilities"]
170 |
171 | # Verify document symbol support is available
172 | if "documentSymbolProvider" in init_response["capabilities"]:
173 | log.info("Bash server supports document symbols")
174 | else:
175 | log.warning("Warning: Bash server does not report document symbol support")
176 |
177 | self.server.notify.initialized({})
178 |
179 | # Wait for server readiness with timeout
180 | log.info("Waiting for Bash language server to be ready...")
181 | if not self.server_ready.wait(timeout=3.0):
182 | # Fallback: assume server is ready after timeout
183 | # This is common. bash-language-server doesn't always send explicit ready signals. Log as info
184 | log.info("Timeout waiting for bash server ready signal, proceeding anyway")
185 | self.server_ready.set()
186 | self.completions_available.set()
187 | else:
188 | log.info("Bash server initialization complete")
189 |
190 | def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
191 | # Uses the standard LSP documentSymbol request which provides reliable function detection
192 | # for all bash function syntaxes including:
193 | # - function name() { ... } (with function keyword)
194 | # - name() { ... } (traditional syntax)
195 | # - Functions with various indentation levels
196 | # - Functions with comments before/after/inside
197 |
198 | log.debug(f"Requesting document symbols via LSP for {relative_file_path}")
199 |
200 | # Use the standard LSP approach - bash-language-server handles all function syntaxes correctly
201 | document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)
202 |
203 | # Log detection results for debugging
204 | functions = [s for s in document_symbols.iter_symbols() if s.get("kind") == 12]
205 | log.info(f"LSP function detection for {relative_file_path}: Found {len(functions)} functions")
206 |
207 | return document_symbols
208 |
```
--------------------------------------------------------------------------------
/src/serena/task_executor.py:
--------------------------------------------------------------------------------
```python
1 | import concurrent.futures
2 | import threading
3 | import time
4 | from collections.abc import Callable
5 | from concurrent.futures import Future
6 | from dataclasses import dataclass
7 | from threading import Thread
8 | from typing import Generic, TypeVar
9 |
10 | from sensai.util import logging
11 | from sensai.util.logging import LogTime
12 | from sensai.util.string import ToStringMixin
13 |
14 | log = logging.getLogger(__name__)
15 | T = TypeVar("T")
16 |
17 |
18 | class TaskExecutor:
19 | def __init__(self, name: str):
20 | self._task_executor_lock = threading.Lock()
21 | self._task_executor_queue: list[TaskExecutor.Task] = []
22 | self._task_executor_thread = Thread(target=self._process_task_queue, name=name, daemon=True)
23 | self._task_executor_thread.start()
24 | self._task_executor_task_index = 1
25 | self._task_executor_current_task: TaskExecutor.Task | None = None
26 | self._task_executor_last_executed_task_info: TaskExecutor.TaskInfo | None = None
27 |
28 | class Task(ToStringMixin, Generic[T]):
29 | def __init__(self, function: Callable[[], T], name: str, logged: bool = True, timeout: float | None = None):
30 | """
31 | :param function: the function representing the task to execute
32 | :param name: the name of the task
33 | :param logged: whether to log management of the task; if False, only errors will be logged
34 | :param timeout: the maximum time to wait for task completion in seconds, or None to wait indefinitely
35 | """
36 | self.name = name
37 | self.future: concurrent.futures.Future = concurrent.futures.Future()
38 | self.logged = logged
39 | self.timeout = timeout
40 | self._function = function
41 |
42 | def _tostring_includes(self) -> list[str]:
43 | return ["name"]
44 |
45 | def start(self) -> None:
46 | """
47 | Executes the task in a separate thread, setting the result or exception on the future.
48 | """
49 |
50 | def run_task() -> None:
51 | try:
52 | if self.future.done():
53 | if self.logged:
54 | log.info(f"Task {self.name} was already completed/cancelled; skipping execution")
55 | return
56 | with LogTime(self.name, logger=log, enabled=self.logged):
57 | result = self._function()
58 | if not self.future.done():
59 | self.future.set_result(result)
60 | except Exception as e:
61 | if not self.future.done():
62 | log.error(f"Error during execution of {self.name}: {e}", exc_info=e)
63 | self.future.set_exception(e)
64 |
65 | thread = Thread(target=run_task, name=self.name)
66 | thread.start()
67 |
68 | def is_done(self) -> bool:
69 | """
70 | :return: whether the task has completed (either successfully, with failure, or via cancellation)
71 | """
72 | return self.future.done()
73 |
74 | def result(self, timeout: float | None = None) -> T:
75 | """
76 | Blocks until the task is done or the timeout is reached, and returns the result.
77 | If an exception occurred during task execution, it is raised here.
78 | If the timeout is reached, a TimeoutError is raised (but the task is not cancelled).
79 | If the task is cancelled, a CancelledError is raised.
80 |
81 | :param timeout: the maximum time to wait in seconds; if None, use the task's own timeout
82 | (which may be None to wait indefinitely)
83 | :return: True if the task is done, False if the timeout was reached
84 | """
85 | return self.future.result(timeout=timeout)
86 |
87 | def cancel(self) -> None:
88 | """
89 | Cancels the task. If it has not yet started, it will not be executed.
90 | If it has already started, its future will be marked as cancelled and will raise a CancelledError
91 | when its result is requested.
92 | """
93 | self.future.cancel()
94 |
95 | def wait_until_done(self, timeout: float | None = None) -> None:
96 | """
97 | Waits until the task is done or the timeout is reached.
98 | The task is done if it either completed successfully, failed with an exception, or was cancelled.
99 |
100 | :param timeout: the maximum time to wait in seconds; if None, use the task's own timeout
101 | (which may be None to wait indefinitely)
102 | """
103 | try:
104 | self.future.result(timeout=timeout)
105 | except:
106 | pass
107 |
108 | def _process_task_queue(self) -> None:
109 | while True:
110 | # obtain task from the queue
111 | task: TaskExecutor.Task | None = None
112 | with self._task_executor_lock:
113 | if len(self._task_executor_queue) > 0:
114 | task = self._task_executor_queue.pop(0)
115 | if task is None:
116 | time.sleep(0.1)
117 | continue
118 |
119 | # start task execution asynchronously
120 | with self._task_executor_lock:
121 | self._task_executor_current_task = task
122 | if task.logged:
123 | log.info("Starting execution of %s", task.name)
124 | task.start()
125 |
126 | # wait for task completion
127 | task.wait_until_done(timeout=task.timeout)
128 | with self._task_executor_lock:
129 | self._task_executor_current_task = None
130 | if task.logged:
131 | self._task_executor_last_executed_task_info = self.TaskInfo.from_task(task, is_running=False)
132 |
133 | @dataclass
134 | class TaskInfo:
135 | name: str
136 | is_running: bool
137 | future: Future
138 | """
139 | future for accessing the task's result
140 | """
141 | task_id: int
142 | """
143 | unique identifier of the task
144 | """
145 | logged: bool
146 |
147 | def finished_successfully(self) -> bool:
148 | return self.future.done() and not self.future.cancelled() and self.future.exception() is None
149 |
150 | @staticmethod
151 | def from_task(task: "TaskExecutor.Task", is_running: bool) -> "TaskExecutor.TaskInfo":
152 | return TaskExecutor.TaskInfo(name=task.name, is_running=is_running, future=task.future, task_id=id(task), logged=task.logged)
153 |
154 | def cancel(self) -> None:
155 | self.future.cancel()
156 |
157 | def get_current_tasks(self) -> list[TaskInfo]:
158 | """
159 | Gets the list of tasks currently running or queued for execution.
160 | The function returns a list of thread-safe TaskInfo objects (specifically created for the caller).
161 |
162 | :return: the list of tasks in the execution order (running task first)
163 | """
164 | tasks = []
165 | with self._task_executor_lock:
166 | if self._task_executor_current_task is not None:
167 | tasks.append(self.TaskInfo.from_task(self._task_executor_current_task, True))
168 | for task in self._task_executor_queue:
169 | if not task.is_done():
170 | tasks.append(self.TaskInfo.from_task(task, False))
171 | return tasks
172 |
173 | def issue_task(self, task: Callable[[], T], name: str | None = None, logged: bool = True, timeout: float | None = None) -> Task[T]:
174 | """
175 | Issue a task to the executor for asynchronous execution.
176 | It is ensured that tasks are executed in the order they are issued, one after another.
177 |
178 | :param task: the task to execute
179 | :param name: the name of the task for logging purposes; if None, use the task function's name
180 | :param logged: whether to log management of the task; if False, only errors will be logged
181 | :param timeout: the maximum time to wait for task completion in seconds, or None to wait indefinitely
182 | :return: the task object, through which the task's future result can be accessed
183 | """
184 | with self._task_executor_lock:
185 | if logged:
186 | task_prefix_name = f"Task-{self._task_executor_task_index}"
187 | self._task_executor_task_index += 1
188 | else:
189 | task_prefix_name = "BackgroundTask"
190 | task_name = f"{task_prefix_name}:{name or task.__name__}"
191 | if logged:
192 | log.info(f"Scheduling {task_name}")
193 | task_obj = self.Task(function=task, name=task_name, logged=logged, timeout=timeout)
194 | self._task_executor_queue.append(task_obj)
195 | return task_obj
196 |
197 | def execute_task(self, task: Callable[[], T], name: str | None = None, logged: bool = True, timeout: float | None = None) -> T:
198 | """
199 | Executes the given task synchronously via the agent's task executor.
200 | This is useful for tasks that need to be executed immediately and whose results are needed right away.
201 |
202 | :param task: the task to execute
203 | :param name: the name of the task for logging purposes; if None, use the task function's name
204 | :param logged: whether to log management of the task; if False, only errors will be logged
205 | :param timeout: the maximum time to wait for task completion in seconds, or None to wait indefinitely
206 | :return: the result of the task execution
207 | """
208 | task_obj = self.issue_task(task, name=name, logged=logged, timeout=timeout)
209 | return task_obj.result()
210 |
211 | def get_last_executed_task(self) -> TaskInfo | None:
212 | """
213 | Gets information about the last executed task.
214 |
215 | :return: TaskInfo of the last executed task, or None if no task has been executed yet.
216 | """
217 | with self._task_executor_lock:
218 | return self._task_executor_last_executed_task_info
219 |
```
--------------------------------------------------------------------------------
/src/solidlsp/language_servers/vts_language_server.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Language Server implementation for TypeScript/JavaScript using https://github.com/yioneko/vtsls,
3 | which provides TypeScript language server functionality via VSCode's TypeScript extension
4 | (contrary to typescript-language-server, which uses the TypeScript compiler directly).
5 | """
6 |
7 | import logging
8 | import os
9 | import pathlib
10 | import shutil
11 | import threading
12 | from typing import cast
13 |
14 | from overrides import override
15 |
16 | from solidlsp.ls import SolidLanguageServer
17 | from solidlsp.ls_config import LanguageServerConfig
18 | from solidlsp.ls_utils import PlatformId, PlatformUtils
19 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
20 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
21 | from solidlsp.settings import SolidLSPSettings
22 |
23 | from .common import RuntimeDependency, RuntimeDependencyCollection
24 |
25 | log = logging.getLogger(__name__)
26 |
27 |
28 | class VtsLanguageServer(SolidLanguageServer):
29 | """
30 | Provides TypeScript specific instantiation of the LanguageServer class using vtsls.
31 | Contains various configurations and settings specific to TypeScript via vtsls wrapper.
32 | """
33 |
34 | def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
35 | """
36 | Creates a VtsLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
37 | """
38 | vts_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
39 | super().__init__(
40 | config,
41 | repository_root_path,
42 | ProcessLaunchInfo(cmd=vts_lsp_executable_path, cwd=repository_root_path),
43 | "typescript",
44 | solidlsp_settings,
45 | )
46 | self.server_ready = threading.Event()
47 | self.initialize_searcher_command_available = threading.Event()
48 |
49 | @override
50 | def is_ignored_dirname(self, dirname: str) -> bool:
51 | return super().is_ignored_dirname(dirname) or dirname in [
52 | "node_modules",
53 | "dist",
54 | "build",
55 | "coverage",
56 | ]
57 |
58 | @classmethod
59 | def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
60 | """
61 | Setup runtime dependencies for VTS Language Server and return the command to start the server.
62 | """
63 | platform_id = PlatformUtils.get_platform_id()
64 |
65 | valid_platforms = [
66 | PlatformId.LINUX_x64,
67 | PlatformId.LINUX_arm64,
68 | PlatformId.OSX,
69 | PlatformId.OSX_x64,
70 | PlatformId.OSX_arm64,
71 | PlatformId.WIN_x64,
72 | PlatformId.WIN_arm64,
73 | ]
74 | assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for vtsls at the moment"
75 |
76 | deps = RuntimeDependencyCollection(
77 | [
78 | RuntimeDependency(
79 | id="vtsls",
80 | description="vtsls language server package",
81 | command="npm install --prefix ./ @vtsls/[email protected]",
82 | platform_id="any",
83 | ),
84 | ]
85 | )
86 | vts_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "vts-lsp")
87 | vts_executable_path = os.path.join(vts_ls_dir, "vtsls")
88 |
89 | # Verify both node and npm are installed
90 | is_node_installed = shutil.which("node") is not None
91 | assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
92 | is_npm_installed = shutil.which("npm") is not None
93 | assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
94 |
95 | # Install vtsls if not already installed
96 | if not os.path.exists(vts_ls_dir):
97 | os.makedirs(vts_ls_dir, exist_ok=True)
98 | deps.install(vts_ls_dir)
99 |
100 | vts_executable_path = os.path.join(vts_ls_dir, "node_modules", ".bin", "vtsls")
101 |
102 | assert os.path.exists(vts_executable_path), "vtsls executable not found. Please install @vtsls/language-server and try again."
103 | return f"{vts_executable_path} --stdio"
104 |
105 | @staticmethod
106 | def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
107 | """
108 | Returns the initialize params for the VTS Language Server.
109 | """
110 | root_uri = pathlib.Path(repository_absolute_path).as_uri()
111 | initialize_params = {
112 | "locale": "en",
113 | "capabilities": {
114 | "textDocument": {
115 | "synchronization": {"didSave": True, "dynamicRegistration": True},
116 | "definition": {"dynamicRegistration": True},
117 | "references": {"dynamicRegistration": True},
118 | "documentSymbol": {
119 | "dynamicRegistration": True,
120 | "hierarchicalDocumentSymbolSupport": True,
121 | "symbolKind": {"valueSet": list(range(1, 27))},
122 | },
123 | "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
124 | "signatureHelp": {"dynamicRegistration": True},
125 | "codeAction": {"dynamicRegistration": True},
126 | },
127 | "workspace": {
128 | "workspaceFolders": True,
129 | "didChangeConfiguration": {"dynamicRegistration": True},
130 | "symbol": {"dynamicRegistration": True},
131 | "configuration": True, # This might be needed for vtsls
132 | },
133 | },
134 | "processId": os.getpid(),
135 | "rootPath": repository_absolute_path,
136 | "rootUri": root_uri,
137 | "workspaceFolders": [
138 | {
139 | "uri": root_uri,
140 | "name": os.path.basename(repository_absolute_path),
141 | }
142 | ],
143 | }
144 | return cast(InitializeParams, initialize_params)
145 |
146 | def _start_server(self) -> None:
147 | """
148 | Starts the VTS Language Server, waits for the server to be ready and yields the LanguageServer instance.
149 |
150 | Usage:
151 | ```
152 | async with lsp.start_server():
153 | # LanguageServer has been initialized and ready to serve requests
154 | await lsp.request_definition(...)
155 | await lsp.request_references(...)
156 | # Shutdown the LanguageServer on exit from scope
157 | # LanguageServer has been shutdown
158 | """
159 |
160 | def register_capability_handler(params: dict) -> None:
161 | assert "registrations" in params
162 | for registration in params["registrations"]:
163 | if registration["method"] == "workspace/executeCommand":
164 | self.initialize_searcher_command_available.set()
165 | return
166 |
167 | def execute_client_command_handler(params: dict) -> list:
168 | return []
169 |
170 | def workspace_configuration_handler(params: dict) -> list[dict] | dict:
171 | # VTS may request workspace configuration
172 | # Return empty configuration for each requested item
173 | if "items" in params:
174 | return [{}] * len(params["items"])
175 | return {}
176 |
177 | def do_nothing(params: dict) -> None:
178 | return
179 |
180 | def window_log_message(msg: dict) -> None:
181 | log.info(f"LSP: window/logMessage: {msg}")
182 |
183 | def check_experimental_status(params: dict) -> None:
184 | """
185 | Also listen for experimental/serverStatus as a backup signal
186 | """
187 | if params.get("quiescent") is True:
188 | self.server_ready.set()
189 | self.completions_available.set()
190 |
191 | self.server.on_request("client/registerCapability", register_capability_handler)
192 | self.server.on_notification("window/logMessage", window_log_message)
193 | self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
194 | self.server.on_request("workspace/configuration", workspace_configuration_handler)
195 | self.server.on_notification("$/progress", do_nothing)
196 | self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
197 | self.server.on_notification("experimental/serverStatus", check_experimental_status)
198 |
199 | log.info("Starting VTS server process")
200 | self.server.start()
201 | initialize_params = self._get_initialize_params(self.repository_root_path)
202 |
203 | log.info("Sending initialize request from LSP client to LSP server and awaiting response")
204 | init_response = self.server.send.initialize(initialize_params)
205 |
206 | # VTS-specific capability checks
207 | # Be more flexible with capabilities since vtsls might have different structure
208 | log.debug(f"VTS init response capabilities: {init_response['capabilities']}")
209 |
210 | # Basic checks to ensure essential capabilities are present
211 | assert "textDocumentSync" in init_response["capabilities"]
212 | assert "completionProvider" in init_response["capabilities"]
213 |
214 | # Log the actual values for debugging
215 | log.debug(f"textDocumentSync: {init_response['capabilities']['textDocumentSync']}")
216 | log.debug(f"completionProvider: {init_response['capabilities']['completionProvider']}")
217 |
218 | self.server.notify.initialized({})
219 | if self.server_ready.wait(timeout=1.0):
220 | log.info("VTS server is ready")
221 | else:
222 | log.info("Timeout waiting for VTS server to become ready, proceeding anyway")
223 | # Fallback: assume server is ready after timeout
224 | self.server_ready.set()
225 | self.completions_available.set()
226 |
227 | @override
228 | def _get_wait_time_for_cross_file_referencing(self) -> float:
229 | return 1
230 |
```
--------------------------------------------------------------------------------
/test/solidlsp/powershell/test_powershell_basic.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Basic integration tests for the PowerShell language server functionality.
3 |
4 | These tests validate the functionality of the language server APIs
5 | like request_document_symbols using the PowerShell test repository.
6 | """
7 |
8 | import pytest
9 |
10 | from solidlsp import SolidLanguageServer
11 | from solidlsp.ls_config import Language
12 |
13 |
14 | @pytest.mark.powershell
15 | class TestPowerShellLanguageServerBasics:
16 | """Test basic functionality of the PowerShell language server."""
17 |
18 | @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True)
19 | def test_powershell_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
20 | """Test that PowerShell language server can be initialized successfully."""
21 | assert language_server is not None
22 | assert language_server.language == Language.POWERSHELL
23 |
24 | @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True)
25 | def test_powershell_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
26 | """Test request_document_symbols for PowerShell files."""
27 | # Test getting symbols from main.ps1
28 | all_symbols, _root_symbols = language_server.request_document_symbols("main.ps1").get_all_symbols_and_roots()
29 |
30 | # Extract function symbols (LSP Symbol Kind 12)
31 | function_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 12]
32 | function_names = [symbol["name"] for symbol in function_symbols]
33 |
34 | # PSES returns function names in format "function FuncName ()" - check for function name substring
35 | def has_function(name: str) -> bool:
36 | return any(name in fn for fn in function_names)
37 |
38 | # Should detect the main functions from main.ps1
39 | assert has_function("Greet-User"), f"Should find Greet-User function in {function_names}"
40 | assert has_function("Process-Items"), f"Should find Process-Items function in {function_names}"
41 | assert has_function("Main"), f"Should find Main function in {function_names}"
42 | assert len(function_symbols) >= 3, f"Should find at least 3 functions, found {len(function_symbols)}"
43 |
44 | @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True)
45 | def test_powershell_utils_functions(self, language_server: SolidLanguageServer) -> None:
46 | """Test function detection in utils.ps1 file."""
47 | # Test with utils.ps1
48 | utils_all_symbols, _utils_root_symbols = language_server.request_document_symbols("utils.ps1").get_all_symbols_and_roots()
49 |
50 | utils_function_symbols = [symbol for symbol in utils_all_symbols if symbol.get("kind") == 12]
51 | utils_function_names = [symbol["name"] for symbol in utils_function_symbols]
52 |
53 | # PSES returns function names in format "function FuncName ()" - check for function name substring
54 | def has_function(name: str) -> bool:
55 | return any(name in fn for fn in utils_function_names)
56 |
57 | # Should detect functions from utils.ps1
58 | expected_utils_functions = [
59 | "Convert-ToUpperCase",
60 | "Convert-ToLowerCase",
61 | "Remove-Whitespace",
62 | "Backup-File",
63 | "Test-ArrayContains",
64 | "Write-LogMessage",
65 | "Test-ValidEmail",
66 | "Test-IsNumber",
67 | ]
68 |
69 | for func_name in expected_utils_functions:
70 | assert has_function(func_name), f"Should find {func_name} function in utils.ps1, got {utils_function_names}"
71 |
72 | assert len(utils_function_symbols) >= 8, f"Should find at least 8 functions in utils.ps1, found {len(utils_function_symbols)}"
73 |
74 | @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True)
75 | def test_powershell_function_with_parameters(self, language_server: SolidLanguageServer) -> None:
76 | """Test that functions with CmdletBinding and parameters are detected correctly."""
77 | all_symbols, _root_symbols = language_server.request_document_symbols("main.ps1").get_all_symbols_and_roots()
78 |
79 | function_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 12]
80 |
81 | # Find Greet-User function which has parameters
82 | # PSES returns function names in format "function FuncName ()"
83 | greet_user_symbol = next((sym for sym in function_symbols if "Greet-User" in sym["name"]), None)
84 | assert greet_user_symbol is not None, f"Should find Greet-User function in {[s['name'] for s in function_symbols]}"
85 |
86 | # Find Process-Items function which has array parameter
87 | process_items_symbol = next((sym for sym in function_symbols if "Process-Items" in sym["name"]), None)
88 | assert process_items_symbol is not None, f"Should find Process-Items function in {[s['name'] for s in function_symbols]}"
89 |
90 | @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True)
91 | def test_powershell_all_function_detection(self, language_server: SolidLanguageServer) -> None:
92 | """Test that all expected functions are detected across both files."""
93 | # Get symbols from main.ps1
94 | main_all_symbols, _main_root_symbols = language_server.request_document_symbols("main.ps1").get_all_symbols_and_roots()
95 | main_functions = [symbol for symbol in main_all_symbols if symbol.get("kind") == 12]
96 | main_function_names = [func["name"] for func in main_functions]
97 |
98 | # Get symbols from utils.ps1
99 | utils_all_symbols, _utils_root_symbols = language_server.request_document_symbols("utils.ps1").get_all_symbols_and_roots()
100 | utils_functions = [symbol for symbol in utils_all_symbols if symbol.get("kind") == 12]
101 | utils_function_names = [func["name"] for func in utils_functions]
102 |
103 | # PSES returns function names in format "function FuncName ()" - check for function name substring
104 | def has_main_function(name: str) -> bool:
105 | return any(name in fn for fn in main_function_names)
106 |
107 | def has_utils_function(name: str) -> bool:
108 | return any(name in fn for fn in utils_function_names)
109 |
110 | # Verify main.ps1 functions
111 | expected_main = ["Greet-User", "Process-Items", "Main"]
112 | for expected_func in expected_main:
113 | assert has_main_function(expected_func), f"Should detect {expected_func} function in main.ps1, got {main_function_names}"
114 |
115 | # Verify utils.ps1 functions
116 | expected_utils = [
117 | "Convert-ToUpperCase",
118 | "Convert-ToLowerCase",
119 | "Remove-Whitespace",
120 | "Backup-File",
121 | "Test-ArrayContains",
122 | "Write-LogMessage",
123 | "Test-ValidEmail",
124 | "Test-IsNumber",
125 | ]
126 | for expected_func in expected_utils:
127 | assert has_utils_function(expected_func), f"Should detect {expected_func} function in utils.ps1, got {utils_function_names}"
128 |
129 | # Verify total counts
130 | assert len(main_functions) >= 3, f"Should find at least 3 functions in main.ps1, found {len(main_functions)}"
131 | assert len(utils_functions) >= 8, f"Should find at least 8 functions in utils.ps1, found {len(utils_functions)}"
132 |
133 | @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True)
134 | def test_powershell_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
135 | """Test finding references to a function within the same file."""
136 | main_path = "main.ps1"
137 |
138 | # Get symbols to find the Greet-User function which is called from Main
139 | all_symbols, _root_symbols = language_server.request_document_symbols(main_path).get_all_symbols_and_roots()
140 |
141 | # Find Greet-User function definition
142 | function_symbols = [s for s in all_symbols if s.get("kind") == 12]
143 | greet_user_symbol = next((s for s in function_symbols if "Greet-User" in s["name"]), None)
144 | assert greet_user_symbol is not None, f"Should find Greet-User function in {[s['name'] for s in function_symbols]}"
145 |
146 | # Find references to Greet-User (should be called from Main function at line 91)
147 | sel_start = greet_user_symbol["selectionRange"]["start"]
148 | refs = language_server.request_references(main_path, sel_start["line"], sel_start["character"])
149 |
150 | # Should find at least the call site in Main function
151 | assert refs is not None and len(refs) >= 1, f"Should find references to Greet-User, got {refs}"
152 | assert any(
153 | "main.ps1" in ref.get("uri", ref.get("relativePath", "")) for ref in refs
154 | ), f"Should find reference in main.ps1, got {refs}"
155 |
156 | @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True)
157 | def test_powershell_find_definition_across_files(self, language_server: SolidLanguageServer) -> None:
158 | """Test finding definition of functions across files (main.ps1 -> utils.ps1)."""
159 | # main.ps1 calls Convert-ToUpperCase from utils.ps1 at line 99 (0-indexed: 98)
160 | # The call is: $upperName = Convert-ToUpperCase -InputString $User
161 | # We'll request definition from the call site in main.ps1
162 | main_path = "main.ps1"
163 |
164 | # Find definition of Convert-ToUpperCase from its usage in main.ps1
165 | # Line 99 (1-indexed) = line 98 (0-indexed), character position ~16 for "Convert-ToUpperCase"
166 | definition_locations = language_server.request_definition(main_path, 98, 18)
167 |
168 | # Should find the definition in utils.ps1
169 | assert (
170 | definition_locations is not None and len(definition_locations) >= 1
171 | ), f"Should find definition of Convert-ToUpperCase, got {definition_locations}"
172 | assert any(
173 | "utils.ps1" in loc.get("uri", "") for loc in definition_locations
174 | ), f"Should find definition in utils.ps1, got {definition_locations}"
175 |
```
--------------------------------------------------------------------------------
/src/solidlsp/language_servers/clojure_lsp.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Provides Clojure specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Clojure.
3 | """
4 |
5 | import logging
6 | import os
7 | import pathlib
8 | import shutil
9 | import subprocess
10 | import threading
11 | from typing import cast
12 |
13 | from solidlsp.ls import SolidLanguageServer
14 | from solidlsp.ls_config import LanguageServerConfig
15 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
16 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
17 | from solidlsp.settings import SolidLSPSettings
18 |
19 | from .common import RuntimeDependency, RuntimeDependencyCollection
20 |
21 | log = logging.getLogger(__name__)
22 |
23 |
24 | def run_command(cmd: list, capture_output: bool = True) -> subprocess.CompletedProcess:
25 | return subprocess.run(
26 | cmd, stdout=subprocess.PIPE if capture_output else None, stderr=subprocess.STDOUT if capture_output else None, text=True, check=True
27 | )
28 |
29 |
30 | def verify_clojure_cli() -> None:
31 | install_msg = "Please install the official Clojure CLI from:\n https://clojure.org/guides/getting_started"
32 | if shutil.which("clojure") is None:
33 | raise FileNotFoundError("`clojure` not found.\n" + install_msg)
34 |
35 | help_proc = run_command(["clojure", "--help"])
36 | if "-Aaliases" not in help_proc.stdout:
37 | raise RuntimeError("Detected a Clojure executable, but it does not support '-Aaliases'.\n" + install_msg)
38 |
39 | spath_proc = run_command(["clojure", "-Spath"], capture_output=False)
40 | if spath_proc.returncode != 0:
41 | raise RuntimeError("`clojure -Spath` failed; please upgrade to Clojure CLI ≥ 1.10.")
42 |
43 |
44 | class ClojureLSP(SolidLanguageServer):
45 | """
46 | Provides a clojure-lsp specific instantiation of the LanguageServer class. Contains various configurations and settings specific to clojure.
47 | """
48 |
49 | clojure_lsp_releases = "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download"
50 | runtime_dependencies = RuntimeDependencyCollection(
51 | [
52 | RuntimeDependency(
53 | id="clojure-lsp",
54 | url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip",
55 | platform_id="osx-arm64",
56 | archive_type="zip",
57 | binary_name="clojure-lsp",
58 | ),
59 | RuntimeDependency(
60 | id="clojure-lsp",
61 | url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-amd64.zip",
62 | platform_id="osx-x64",
63 | archive_type="zip",
64 | binary_name="clojure-lsp",
65 | ),
66 | RuntimeDependency(
67 | id="clojure-lsp",
68 | url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip",
69 | platform_id="linux-arm64",
70 | archive_type="zip",
71 | binary_name="clojure-lsp",
72 | ),
73 | RuntimeDependency(
74 | id="clojure-lsp",
75 | url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip",
76 | platform_id="linux-x64",
77 | archive_type="zip",
78 | binary_name="clojure-lsp",
79 | ),
80 | RuntimeDependency(
81 | id="clojure-lsp",
82 | url=f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip",
83 | platform_id="win-x64",
84 | archive_type="zip",
85 | binary_name="clojure-lsp.exe",
86 | ),
87 | ]
88 | )
89 |
90 | def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
91 | """
92 | Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
93 | """
94 | clojure_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
95 | super().__init__(
96 | config,
97 | repository_root_path,
98 | ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path),
99 | "clojure",
100 | solidlsp_settings,
101 | )
102 | self.server_ready = threading.Event()
103 | self.initialize_searcher_command_available = threading.Event()
104 | self.resolve_main_method_available = threading.Event()
105 | self.service_ready_event = threading.Event()
106 |
107 | @classmethod
108 | def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
109 | """Setup runtime dependencies for clojure-lsp and return the command to start the server."""
110 | verify_clojure_cli()
111 | deps = ClojureLSP.runtime_dependencies
112 | dependency = deps.get_single_dep_for_current_platform()
113 |
114 | clojurelsp_ls_dir = cls.ls_resources_dir(solidlsp_settings)
115 | clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir)
116 | if not os.path.exists(clojurelsp_executable_path):
117 | log.info(
118 | f"Downloading and extracting clojure-lsp from {dependency.url} to {clojurelsp_ls_dir}",
119 | )
120 | deps.install(clojurelsp_ls_dir)
121 | if not os.path.exists(clojurelsp_executable_path):
122 | raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}")
123 | os.chmod(clojurelsp_executable_path, 0o755)
124 | return clojurelsp_executable_path
125 |
126 | @staticmethod
127 | def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
128 | """Returns the init params for clojure-lsp."""
129 | root_uri = pathlib.Path(repository_absolute_path).as_uri()
130 | result = { # type: ignore
131 | "processId": os.getpid(),
132 | "rootPath": repository_absolute_path,
133 | "rootUri": root_uri,
134 | "capabilities": {
135 | "workspace": {
136 | "applyEdit": True,
137 | "workspaceEdit": {"documentChanges": True},
138 | "symbol": {"symbolKind": {"valueSet": list(range(1, 27))}},
139 | "workspaceFolders": True,
140 | },
141 | "textDocument": {
142 | "synchronization": {"didSave": True},
143 | "publishDiagnostics": {"relatedInformation": True, "tagSupport": {"valueSet": [1, 2]}},
144 | "definition": {"linkSupport": True},
145 | "references": {},
146 | "hover": {"contentFormat": ["markdown", "plaintext"]},
147 | "documentSymbol": {
148 | "hierarchicalDocumentSymbolSupport": True,
149 | "symbolKind": {"valueSet": list(range(1, 27))}, #
150 | },
151 | },
152 | "general": {"positionEncodings": ["utf-16"]},
153 | },
154 | "initializationOptions": {"dependency-scheme": "jar", "text-document-sync-kind": "incremental"},
155 | "trace": "off",
156 | "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
157 | }
158 | return cast(InitializeParams, result)
159 |
160 | def _start_server(self) -> None:
161 | def register_capability_handler(params: dict) -> None:
162 | assert "registrations" in params
163 | for registration in params["registrations"]:
164 | if registration["method"] == "workspace/executeCommand":
165 | self.initialize_searcher_command_available.set()
166 | self.resolve_main_method_available.set()
167 | return
168 |
169 | def lang_status_handler(params: dict) -> None:
170 | # TODO: Should we wait for
171 | # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
172 | # Before proceeding?
173 | if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
174 | self.service_ready_event.set()
175 |
176 | def execute_client_command_handler(params: dict) -> list:
177 | return []
178 |
179 | def do_nothing(params: dict) -> None:
180 | return
181 |
182 | def check_experimental_status(params: dict) -> None:
183 | if params["quiescent"] is True:
184 | self.server_ready.set()
185 |
186 | def window_log_message(msg: dict) -> None:
187 | log.info(f"LSP: window/logMessage: {msg}")
188 |
189 | self.server.on_request("client/registerCapability", register_capability_handler)
190 | self.server.on_notification("language/status", lang_status_handler)
191 | self.server.on_notification("window/logMessage", window_log_message)
192 | self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
193 | self.server.on_notification("$/progress", do_nothing)
194 | self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
195 | self.server.on_notification("language/actionableNotification", do_nothing)
196 | self.server.on_notification("experimental/serverStatus", check_experimental_status)
197 |
198 | log.info("Starting clojure-lsp server process")
199 | self.server.start()
200 |
201 | initialize_params = self._get_initialize_params(self.repository_root_path)
202 |
203 | log.info("Sending initialize request from LSP client to LSP server and awaiting response")
204 | init_response = self.server.send.initialize(initialize_params)
205 | assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore
206 | assert "completionProvider" in init_response["capabilities"]
207 | # Clojure-lsp completion provider capabilities are more flexible than other servers'
208 | completion_provider = init_response["capabilities"]["completionProvider"]
209 | assert completion_provider["resolveProvider"] is True
210 | assert "triggerCharacters" in completion_provider
211 | self.server.notify.initialized({})
212 | # after initialize, Clojure-lsp is ready to serve
213 | self.server_ready.set()
214 | self.completions_available.set()
215 |
```
--------------------------------------------------------------------------------
/src/solidlsp/language_servers/clangd_language_server.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++.
3 | """
4 |
5 | import logging
6 | import os
7 | import pathlib
8 | import threading
9 | from typing import Any, cast
10 |
11 | from solidlsp.ls import SolidLanguageServer
12 | from solidlsp.ls_config import LanguageServerConfig
13 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
14 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
15 | from solidlsp.settings import SolidLSPSettings
16 |
17 | from .common import RuntimeDependency, RuntimeDependencyCollection
18 |
19 | log = logging.getLogger(__name__)
20 |
21 |
22 | class ClangdLanguageServer(SolidLanguageServer):
23 | """
24 | Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++.
25 | As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly.
26 | Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example.
27 | """
28 |
29 | def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
30 | """
31 | Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
32 | """
33 | clangd_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
34 | super().__init__(
35 | config, repository_root_path, ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), "cpp", solidlsp_settings
36 | )
37 | self.server_ready = threading.Event()
38 | self.service_ready_event = threading.Event()
39 | self.initialize_searcher_command_available = threading.Event()
40 | self.resolve_main_method_available = threading.Event()
41 |
42 | @classmethod
43 | def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
44 | """
45 | Setup runtime dependencies for ClangdLanguageServer and return the command to start the server.
46 | """
47 | import shutil
48 |
49 | deps = RuntimeDependencyCollection(
50 | [
51 | RuntimeDependency(
52 | id="Clangd",
53 | description="Clangd for Linux (x64)",
54 | url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip",
55 | platform_id="linux-x64",
56 | archive_type="zip",
57 | binary_name="clangd_19.1.2/bin/clangd",
58 | ),
59 | RuntimeDependency(
60 | id="Clangd",
61 | description="Clangd for Windows (x64)",
62 | url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip",
63 | platform_id="win-x64",
64 | archive_type="zip",
65 | binary_name="clangd_19.1.2/bin/clangd.exe",
66 | ),
67 | RuntimeDependency(
68 | id="Clangd",
69 | description="Clangd for macOS (x64)",
70 | url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
71 | platform_id="osx-x64",
72 | archive_type="zip",
73 | binary_name="clangd_19.1.2/bin/clangd",
74 | ),
75 | RuntimeDependency(
76 | id="Clangd",
77 | description="Clangd for macOS (Arm64)",
78 | url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip",
79 | platform_id="osx-arm64",
80 | archive_type="zip",
81 | binary_name="clangd_19.1.2/bin/clangd",
82 | ),
83 | ]
84 | )
85 |
86 | clangd_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "clangd")
87 |
88 | try:
89 | dep = deps.get_single_dep_for_current_platform()
90 | except RuntimeError:
91 | dep = None
92 |
93 | if dep is None:
94 | # No prebuilt binary available, look for system-installed clangd
95 | clangd_executable_path = shutil.which("clangd")
96 | if not clangd_executable_path:
97 | raise FileNotFoundError(
98 | "Clangd is not installed on your system.\n"
99 | + "Please install clangd using your system package manager:\n"
100 | + " Ubuntu/Debian: sudo apt-get install clangd\n"
101 | + " Fedora/RHEL: sudo dnf install clang-tools-extra\n"
102 | + " Arch Linux: sudo pacman -S clang\n"
103 | + "See https://clangd.llvm.org/installation for more details."
104 | )
105 | log.info(f"Using system-installed clangd at {clangd_executable_path}")
106 | else:
107 | # Standard download and install for platforms with prebuilt binaries
108 | clangd_executable_path = deps.binary_path(clangd_ls_dir)
109 | if not os.path.exists(clangd_executable_path):
110 | log.info(f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}")
111 | _ = deps.install(clangd_ls_dir)
112 | if not os.path.exists(clangd_executable_path):
113 | raise FileNotFoundError(
114 | f"Clangd executable not found at {clangd_executable_path}.\n"
115 | + "Make sure you have installed clangd. See https://clangd.llvm.org/installation"
116 | )
117 | os.chmod(clangd_executable_path, 0o755)
118 | return clangd_executable_path
119 |
120 | @staticmethod
121 | def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
122 | """
123 | Returns the initialize params for the clangd Language Server.
124 | """
125 | root_uri = pathlib.Path(repository_absolute_path).as_uri()
126 | initialize_params = {
127 | "locale": "en",
128 | "capabilities": {
129 | "textDocument": {
130 | "synchronization": {"didSave": True, "dynamicRegistration": True},
131 | "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
132 | "definition": {"dynamicRegistration": True},
133 | },
134 | "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
135 | },
136 | "processId": os.getpid(),
137 | "rootPath": repository_absolute_path,
138 | "rootUri": root_uri,
139 | "workspaceFolders": [
140 | {
141 | "uri": root_uri,
142 | "name": "$name",
143 | }
144 | ],
145 | }
146 |
147 | return cast(InitializeParams, initialize_params)
148 |
149 | def _start_server(self) -> None:
150 | """
151 | Starts the Clangd Language Server, waits for the server to be ready and yields the LanguageServer instance.
152 |
153 | Usage:
154 | ```
155 | async with lsp.start_server():
156 | # LanguageServer has been initialized and ready to serve requests
157 | await lsp.request_definition(...)
158 | await lsp.request_references(...)
159 | # Shutdown the LanguageServer on exit from scope
160 | # LanguageServer has been shutdown
161 | """
162 |
163 | def register_capability_handler(params: Any) -> None:
164 | assert "registrations" in params
165 | for registration in params["registrations"]:
166 | if registration["method"] == "workspace/executeCommand":
167 | self.initialize_searcher_command_available.set()
168 | self.resolve_main_method_available.set()
169 | return
170 |
171 | def lang_status_handler(params: Any) -> None:
172 | # TODO: Should we wait for
173 | # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
174 | # Before proceeding?
175 | if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
176 | self.service_ready_event.set()
177 |
178 | def execute_client_command_handler(params: Any) -> list:
179 | return []
180 |
181 | def do_nothing(params: Any) -> None:
182 | return
183 |
184 | def check_experimental_status(params: Any) -> None:
185 | if params["quiescent"] == True:
186 | self.server_ready.set()
187 |
188 | def window_log_message(msg: dict) -> None:
189 | log.info(f"LSP: window/logMessage: {msg}")
190 |
191 | self.server.on_request("client/registerCapability", register_capability_handler)
192 | self.server.on_notification("language/status", lang_status_handler)
193 | self.server.on_notification("window/logMessage", window_log_message)
194 | self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
195 | self.server.on_notification("$/progress", do_nothing)
196 | self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
197 | self.server.on_notification("language/actionableNotification", do_nothing)
198 | self.server.on_notification("experimental/serverStatus", check_experimental_status)
199 |
200 | log.info("Starting Clangd server process")
201 | self.server.start()
202 | initialize_params = self._get_initialize_params(self.repository_root_path)
203 |
204 | log.info("Sending initialize request from LSP client to LSP server and awaiting response")
205 | init_response = self.server.send.initialize(initialize_params)
206 | assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore
207 | assert "completionProvider" in init_response["capabilities"]
208 | assert init_response["capabilities"]["completionProvider"] == {
209 | "triggerCharacters": [".", "<", ">", ":", '"', "/", "*"],
210 | "resolveProvider": False,
211 | }
212 |
213 | self.server.notify.initialized({})
214 |
215 | self.completions_available.set()
216 | # set ready flag
217 | self.server_ready.set()
218 | self.server_ready.wait()
219 |
```
--------------------------------------------------------------------------------
/test/solidlsp/pascal/test_pascal_basic.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Basic integration tests for the Pascal language server functionality.
3 |
4 | These tests validate the functionality of the language server APIs
5 | like request_document_symbols using the Pascal test repository.
6 |
7 | Uses genericptr/pascal-language-server which returns SymbolInformation[] format:
8 | - Returns classes, structs, enums, typedefs, functions/procedures
9 | - Uses correct SymbolKind values: Class=5, Function=12, Method=6, Struct=23
10 | - Method names don't include parent class prefix; uses containerName instead
11 | """
12 |
13 | import pytest
14 |
15 | from solidlsp import SolidLanguageServer
16 | from solidlsp.ls_config import Language
17 | from solidlsp.ls_types import SymbolKind
18 | from test.conftest import language_tests_enabled
19 |
20 | pytestmark = [
21 | pytest.mark.pascal,
22 | pytest.mark.skipif(not language_tests_enabled(Language.PASCAL), reason="Pascal tests are disabled (pasls/fpc not available)"),
23 | ]
24 |
25 |
26 | @pytest.mark.pascal
27 | class TestPascalLanguageServerBasics:
28 | """Test basic functionality of the Pascal language server."""
29 |
30 | @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
31 | def test_pascal_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
32 | """Test that Pascal language server can be initialized successfully."""
33 | assert language_server is not None
34 | assert language_server.language == Language.PASCAL
35 |
36 | @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
37 | def test_pascal_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
38 | """Test request_document_symbols for Pascal files.
39 |
40 | genericptr pasls returns proper SymbolKind values:
41 | - Standalone functions: kind=12 (Function)
42 | - Classes: kind=5 (Class)
43 | """
44 | # Test getting symbols from main.pas
45 | all_symbols, _root_symbols = language_server.request_document_symbols("main.pas").get_all_symbols_and_roots()
46 |
47 | # Should have symbols
48 | assert len(all_symbols) > 0, "Should have symbols in main.pas"
49 |
50 | # Should detect standalone functions (SymbolKind.Function = 12)
51 | function_symbols = [s for s in all_symbols if s.get("kind") == SymbolKind.Function]
52 | function_names = [s["name"] for s in function_symbols]
53 |
54 | assert "CalculateSum" in function_names, "Should find CalculateSum function"
55 | assert "PrintMessage" in function_names, "Should find PrintMessage procedure"
56 |
57 | # Should detect classes (SymbolKind.Class = 5)
58 | class_symbols = [s for s in all_symbols if s.get("kind") == SymbolKind.Class]
59 | class_names = [s["name"] for s in class_symbols]
60 |
61 | assert "TUser" in class_names, "Should find TUser class"
62 | assert "TUserManager" in class_names, "Should find TUserManager class"
63 |
64 | @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
65 | def test_pascal_class_methods(self, language_server: SolidLanguageServer) -> None:
66 | """Test detection of class methods in Pascal files.
67 |
68 | pasls returns class methods with SymbolKind.Method (kind 6), not Function (kind 12).
69 | """
70 | all_symbols, _root_symbols = language_server.request_document_symbols("main.pas").get_all_symbols_and_roots()
71 |
72 | # Get all method symbols (pasls returns class methods as SymbolKind.Method = 6)
73 | method_symbols = [s for s in all_symbols if s.get("kind") == SymbolKind.Method]
74 | method_names = [s["name"] for s in method_symbols]
75 |
76 | # Should detect TUser methods
77 | expected_tuser_methods = ["Create", "Destroy", "GetInfo", "UpdateAge"]
78 | for method in expected_tuser_methods:
79 | found = method in method_names
80 | assert found, f"Should find method '{method}'"
81 |
82 | # Should detect TUserManager methods
83 | expected_manager_methods = ["Create", "Destroy", "AddUser", "GetUserCount", "FindUserByName"]
84 | for method in expected_manager_methods:
85 | found = method in method_names
86 | assert found, f"Should find method '{method}'"
87 |
88 | @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
89 | def test_pascal_helper_unit_symbols(self, language_server: SolidLanguageServer) -> None:
90 | """Test function detection in Helper unit."""
91 | # Test with lib/helper.pas
92 | helper_all_symbols, _helper_root_symbols = language_server.request_document_symbols("lib/helper.pas").get_all_symbols_and_roots()
93 |
94 | # Should have symbols
95 | assert len(helper_all_symbols) > 0, "Helper unit should have symbols"
96 |
97 | # Extract function symbols
98 | function_symbols = [s for s in helper_all_symbols if s.get("kind") == SymbolKind.Function]
99 | function_names = [s["name"] for s in function_symbols]
100 |
101 | # Should detect standalone functions
102 | expected_functions = ["GetHelperMessage", "MultiplyNumbers", "LogMessage"]
103 | for func_name in expected_functions:
104 | assert func_name in function_names, f"Should find {func_name} function in Helper unit"
105 |
106 | # Should also detect THelper class methods (returned as SymbolKind.Method = 6)
107 | method_symbols = [s for s in helper_all_symbols if s.get("kind") == SymbolKind.Method]
108 | method_names = [s["name"] for s in method_symbols]
109 | assert "FormatString" in method_names, "Should find FormatString method"
110 | assert "IsEven" in method_names, "Should find IsEven method"
111 |
112 | @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
113 | def test_pascal_cross_file_references(self, language_server: SolidLanguageServer) -> None:
114 | """Test that Pascal LSP can handle cross-file references."""
115 | # main.pas uses Helper unit
116 | main_symbols, _main_roots = language_server.request_document_symbols("main.pas").get_all_symbols_and_roots()
117 | helper_symbols, _helper_roots = language_server.request_document_symbols("lib/helper.pas").get_all_symbols_and_roots()
118 |
119 | # Verify both files have symbols
120 | assert len(main_symbols) > 0, "main.pas should have symbols"
121 | assert len(helper_symbols) > 0, "helper.pas should have symbols"
122 |
123 | # Verify GetHelperMessage is in Helper unit
124 | helper_function_names = [s["name"] for s in helper_symbols if s.get("kind") == SymbolKind.Function]
125 | assert "GetHelperMessage" in helper_function_names, "Helper unit should export GetHelperMessage"
126 |
127 | @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
128 | def test_pascal_symbol_locations(self, language_server: SolidLanguageServer) -> None:
129 | """Test that symbols have correct location information.
130 |
131 | Note: genericptr pasls returns the interface declaration location (line ~41),
132 | not the implementation location (line ~115).
133 | """
134 | all_symbols, _root_symbols = language_server.request_document_symbols("main.pas").get_all_symbols_and_roots()
135 |
136 | # Find CalculateSum function
137 | calc_symbols = [s for s in all_symbols if s.get("name") == "CalculateSum"]
138 | assert len(calc_symbols) > 0, "Should find CalculateSum"
139 |
140 | calc_symbol = calc_symbols[0]
141 |
142 | # Verify it has location information (SymbolInformation format uses location.range)
143 | if "location" in calc_symbol:
144 | location = calc_symbol["location"]
145 | assert "range" in location, "Location should have range"
146 | assert "start" in location["range"], "Range should have start"
147 | assert "line" in location["range"]["start"], "Start should have line"
148 | line = location["range"]["start"]["line"]
149 | else:
150 | # DocumentSymbol format uses range directly
151 | assert "range" in calc_symbol, "Symbol should have range"
152 | assert "start" in calc_symbol["range"], "Range should have start"
153 | line = calc_symbol["range"]["start"]["line"]
154 |
155 | # CalculateSum is declared at line 41 in main.pas (0-indexed would be 40)
156 | # genericptr pasls returns interface declaration location
157 | assert 35 <= line <= 45, f"CalculateSum should be around line 41 (interface), got {line}"
158 |
159 | @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
160 | def test_pascal_namespace_symbol(self, language_server: SolidLanguageServer) -> None:
161 | """Test that genericptr pasls returns Interface namespace symbol."""
162 | all_symbols, _root_symbols = language_server.request_document_symbols("main.pas").get_all_symbols_and_roots()
163 |
164 | # genericptr pasls adds an "Interface" namespace symbol
165 | symbol_names = [s["name"] for s in all_symbols]
166 |
167 | # The Interface section should be represented
168 | # Note: This depends on pasls configuration
169 | assert len(all_symbols) > 0, "Should have symbols"
170 | # Interface namespace may or may not be present depending on pasls configuration
171 | _ = symbol_names # used for potential future assertions
172 |
173 | @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True)
174 | def test_pascal_hover_with_doc_comments(self, language_server: SolidLanguageServer) -> None:
175 | """Test that hover returns documentation comments.
176 |
177 | CalculateSum has /// style doc comments that should appear in hover.
178 | """
179 | # CalculateSum is declared at line 46 (1-indexed), so line 45 (0-indexed)
180 | hover = language_server.request_hover("main.pas", 45, 12)
181 |
182 | assert hover is not None, "Hover should return a result"
183 |
184 | # Extract hover content - handle both dict and object formats
185 | if isinstance(hover, dict):
186 | contents = hover.get("contents", {})
187 | value = contents.get("value", "") if isinstance(contents, dict) else str(contents)
188 | else:
189 | value = hover.contents.value if hasattr(hover.contents, "value") else str(hover.contents)
190 |
191 | # Should contain the function signature
192 | assert "CalculateSum" in value, f"Hover should show function name. Got: {value[:500]}"
193 |
194 | # Should contain the doc comment
195 | assert "Calculates the sum" in value, f"Hover should include doc comment. Got: {value[:500]}"
196 |
```
--------------------------------------------------------------------------------
/src/solidlsp/language_servers/terraform_ls.py:
--------------------------------------------------------------------------------
```python
1 | import logging
2 | import os
3 | import shutil
4 | import threading
5 | from typing import cast
6 |
7 | from overrides import override
8 |
9 | from solidlsp.ls import SolidLanguageServer
10 | from solidlsp.ls_config import LanguageServerConfig
11 | from solidlsp.ls_utils import PathUtils, PlatformUtils
12 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
13 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
14 | from solidlsp.settings import SolidLSPSettings
15 |
16 | from .common import RuntimeDependency, RuntimeDependencyCollection
17 |
18 | log = logging.getLogger(__name__)
19 |
20 |
21 | class TerraformLS(SolidLanguageServer):
22 | """
23 | Provides Terraform specific instantiation of the LanguageServer class using terraform-ls.
24 | """
25 |
26 | @override
27 | def is_ignored_dirname(self, dirname: str) -> bool:
28 | return super().is_ignored_dirname(dirname) or dirname in [".terraform", "terraform.tfstate.d"]
29 |
30 | @staticmethod
31 | def _determine_log_level(line: str) -> int:
32 | """Classify terraform-ls stderr output to avoid false-positive errors."""
33 | line_lower = line.lower()
34 |
35 | # File discovery messages that are not actual errors
36 | if any(
37 | [
38 | "discover.go:" in line_lower,
39 | "walker.go:" in line_lower,
40 | "walking of {file://" in line_lower,
41 | "bus: -> discover" in line_lower,
42 | ]
43 | ):
44 | return logging.DEBUG
45 |
46 | # Known informational messages from terraform-ls that contain "error" but aren't errors
47 | # Note: pattern match is flexible to handle file paths between keywords
48 | if any(
49 | [
50 | "loading module metadata returned error:" in line_lower and "state not changed" in line_lower,
51 | "incoming notification for" in line_lower,
52 | ]
53 | ):
54 | return logging.DEBUG
55 |
56 | return SolidLanguageServer._determine_log_level(line)
57 |
58 | @staticmethod
59 | def _ensure_tf_command_available() -> None:
60 | log.debug("Starting terraform version detection...")
61 |
62 | # 1. Try to find terraform using shutil.which
63 | terraform_cmd = shutil.which("terraform")
64 | if terraform_cmd is not None:
65 | log.debug(f"Found terraform via shutil.which: {terraform_cmd}")
66 | return
67 |
68 | # TODO: is this needed?
69 | # 2. Fallback to TERRAFORM_CLI_PATH (set by hashicorp/setup-terraform action)
70 | if not terraform_cmd:
71 | terraform_cli_path = os.environ.get("TERRAFORM_CLI_PATH")
72 | if terraform_cli_path:
73 | log.debug(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}")
74 | # TODO: use binary name from runtime dependencies if we keep this code
75 | if os.name == "nt":
76 | terraform_binary = os.path.join(terraform_cli_path, "terraform.exe")
77 | else:
78 | terraform_binary = os.path.join(terraform_cli_path, "terraform")
79 | if os.path.exists(terraform_binary):
80 | terraform_cmd = terraform_binary
81 | log.debug(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}")
82 | return
83 |
84 | raise RuntimeError(
85 | "Terraform executable not found, please ensure Terraform is installed."
86 | "See https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli for instructions."
87 | )
88 |
89 | @classmethod
90 | def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str:
91 | """
92 | Setup runtime dependencies for terraform-ls.
93 | Downloads and installs terraform-ls if not already present.
94 | """
95 | cls._ensure_tf_command_available()
96 | platform_id = PlatformUtils.get_platform_id()
97 | deps = RuntimeDependencyCollection(
98 | [
99 | RuntimeDependency(
100 | id="TerraformLS",
101 | description="terraform-ls for macOS (ARM64)",
102 | url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip",
103 | platform_id="osx-arm64",
104 | archive_type="zip",
105 | binary_name="terraform-ls",
106 | ),
107 | RuntimeDependency(
108 | id="TerraformLS",
109 | description="terraform-ls for macOS (x64)",
110 | url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip",
111 | platform_id="osx-x64",
112 | archive_type="zip",
113 | binary_name="terraform-ls",
114 | ),
115 | RuntimeDependency(
116 | id="TerraformLS",
117 | description="terraform-ls for Linux (ARM64)",
118 | url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_arm64.zip",
119 | platform_id="linux-arm64",
120 | archive_type="zip",
121 | binary_name="terraform-ls",
122 | ),
123 | RuntimeDependency(
124 | id="TerraformLS",
125 | description="terraform-ls for Linux (x64)",
126 | url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip",
127 | platform_id="linux-x64",
128 | archive_type="zip",
129 | binary_name="terraform-ls",
130 | ),
131 | RuntimeDependency(
132 | id="TerraformLS",
133 | description="terraform-ls for Windows (x64)",
134 | url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip",
135 | platform_id="win-x64",
136 | archive_type="zip",
137 | binary_name="terraform-ls.exe",
138 | ),
139 | ]
140 | )
141 | dependency = deps.get_single_dep_for_current_platform()
142 |
143 | terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir(solidlsp_settings))
144 | if not os.path.exists(terraform_ls_executable_path):
145 | log.info(f"Downloading terraform-ls from {dependency.url}")
146 | deps.install(cls.ls_resources_dir(solidlsp_settings))
147 |
148 | assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}"
149 |
150 | # Make the executable file executable on Unix-like systems
151 | if platform_id.value != "win-x64":
152 | os.chmod(terraform_ls_executable_path, 0o755)
153 |
154 | return terraform_ls_executable_path
155 |
156 | def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
157 | """
158 | Creates a TerraformLS instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
159 | """
160 | terraform_ls_executable_path = self._setup_runtime_dependencies(solidlsp_settings)
161 |
162 | super().__init__(
163 | config,
164 | repository_root_path,
165 | ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path),
166 | "terraform",
167 | solidlsp_settings,
168 | )
169 | self.server_ready = threading.Event()
170 | self.request_id = 0
171 |
172 | @staticmethod
173 | def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
174 | """
175 | Returns the initialize params for the Terraform Language Server.
176 | """
177 | root_uri = PathUtils.path_to_uri(repository_absolute_path)
178 | result = {
179 | "processId": os.getpid(),
180 | "locale": "en",
181 | "rootPath": repository_absolute_path,
182 | "rootUri": root_uri,
183 | "capabilities": {
184 | "textDocument": {
185 | "synchronization": {"didSave": True, "dynamicRegistration": True},
186 | "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
187 | "definition": {"dynamicRegistration": True},
188 | "documentSymbol": {
189 | "dynamicRegistration": True,
190 | "hierarchicalDocumentSymbolSupport": True,
191 | "symbolKind": {"valueSet": list(range(1, 27))},
192 | },
193 | },
194 | "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
195 | },
196 | "workspaceFolders": [
197 | {
198 | "name": os.path.basename(repository_absolute_path),
199 | "uri": root_uri,
200 | }
201 | ],
202 | }
203 | return cast(InitializeParams, result)
204 |
205 | def _start_server(self) -> None:
206 | """Start terraform-ls server process"""
207 |
208 | def register_capability_handler(params: dict) -> None:
209 | return
210 |
211 | def window_log_message(msg: dict) -> None:
212 | log.info(f"LSP: window/logMessage: {msg}")
213 |
214 | def do_nothing(params: dict) -> None:
215 | return
216 |
217 | self.server.on_request("client/registerCapability", register_capability_handler)
218 | self.server.on_notification("window/logMessage", window_log_message)
219 | self.server.on_notification("$/progress", do_nothing)
220 | self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
221 |
222 | log.info("Starting terraform-ls server process")
223 | self.server.start()
224 | initialize_params = self._get_initialize_params(self.repository_root_path)
225 |
226 | log.info("Sending initialize request from LSP client to LSP server and awaiting response")
227 | init_response = self.server.send.initialize(initialize_params)
228 |
229 | # Verify server capabilities
230 | assert "textDocumentSync" in init_response["capabilities"]
231 | assert "completionProvider" in init_response["capabilities"]
232 | assert "definitionProvider" in init_response["capabilities"]
233 |
234 | self.server.notify.initialized({})
235 | self.completions_available.set()
236 |
237 | # terraform-ls server is typically ready immediately after initialization
238 | self.server_ready.set()
239 | self.server_ready.wait()
240 |
```
--------------------------------------------------------------------------------
/test/solidlsp/toml/test_toml_symbol_retrieval.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Tests for TOML language server symbol retrieval functionality.
3 |
4 | These tests focus on advanced symbol operations:
5 | - request_containing_symbol
6 | - request_document_overview
7 | - request_full_symbol_tree
8 | - request_dir_overview
9 | """
10 |
11 | from pathlib import Path
12 |
13 | import pytest
14 |
15 | from solidlsp import SolidLanguageServer
16 | from solidlsp.ls_config import Language
17 |
18 | pytestmark = pytest.mark.toml
19 |
20 |
21 | class TestTomlSymbolRetrieval:
22 | """Test advanced symbol retrieval functionality for TOML files."""
23 |
24 | @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
25 | @pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
26 | def test_request_containing_symbol_behavior(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
27 | """Test request_containing_symbol behavior for TOML files.
28 |
29 | Note: Taplo LSP doesn't support definition/containing symbol lookups for TOML files
30 | since TOML is a configuration format, not code. This test verifies the behavior.
31 | """
32 | # Line 2 (0-indexed: 1) is inside the [package] table
33 | containing_symbol = language_server.request_containing_symbol("Cargo.toml", 1, 5)
34 |
35 | # Taplo doesn't support containing symbol lookup - returns None
36 | # This is expected behavior for a configuration file format
37 | assert containing_symbol is None, "TOML LSP doesn't support containing symbol lookup"
38 |
39 | @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
40 | @pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
41 | def test_request_document_overview_cargo(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
42 | """Test request_document_overview for Cargo.toml."""
43 | overview = language_server.request_document_overview("Cargo.toml")
44 |
45 | assert overview is not None
46 | assert len(overview) > 0
47 |
48 | # Get symbol names from overview
49 | symbol_names = {symbol.get("name") for symbol in overview if "name" in symbol}
50 |
51 | # Verify expected top-level tables appear
52 | expected_tables = {"package", "dependencies", "dev-dependencies", "features", "workspace"}
53 | assert expected_tables.issubset(symbol_names), f"Missing expected tables in overview: {expected_tables - symbol_names}"
54 |
55 | @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
56 | @pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
57 | def test_request_document_overview_pyproject(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
58 | """Test request_document_overview for pyproject.toml."""
59 | overview = language_server.request_document_overview("pyproject.toml")
60 |
61 | assert overview is not None
62 | assert len(overview) > 0
63 |
64 | # Get symbol names from overview
65 | symbol_names = {symbol.get("name") for symbol in overview if "name" in symbol}
66 |
67 | # Verify expected top-level tables appear
68 | assert "project" in symbol_names, "Should detect 'project' table"
69 | assert "build-system" in symbol_names, "Should detect 'build-system' table"
70 |
71 | @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
72 | @pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
73 | def test_request_full_symbol_tree(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
74 | """Test request_full_symbol_tree returns TOML files."""
75 | symbol_tree = language_server.request_full_symbol_tree()
76 |
77 | assert symbol_tree is not None
78 | assert len(symbol_tree) > 0
79 |
80 | # The root should be test_repo
81 | root = symbol_tree[0]
82 | assert root["name"] == "test_repo"
83 | assert "children" in root
84 |
85 | # Children should include TOML files
86 | child_names = {child["name"] for child in root.get("children", [])}
87 | # Note: File names are stripped of extension in some cases
88 | assert (
89 | "Cargo" in child_names or "Cargo.toml" in child_names or any("cargo" in name.lower() for name in child_names)
90 | ), f"Should find Cargo.toml in tree, got: {child_names}"
91 |
92 | @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
93 | @pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
94 | def test_request_dir_overview(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
95 | """Test request_dir_overview returns symbols for TOML files."""
96 | overview = language_server.request_dir_overview(".")
97 |
98 | assert overview is not None
99 | assert len(overview) > 0
100 |
101 | # Should have entries for both Cargo.toml and pyproject.toml
102 | file_paths = list(overview.keys())
103 | assert any("Cargo.toml" in path for path in file_paths), f"Should find Cargo.toml in overview, got: {file_paths}"
104 | assert any("pyproject.toml" in path for path in file_paths), f"Should find pyproject.toml in overview, got: {file_paths}"
105 |
106 | @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
107 | @pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
108 | def test_symbol_hierarchy_in_cargo(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
109 | """Test that symbol hierarchy is properly preserved in Cargo.toml."""
110 | all_symbols, root_symbols = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
111 |
112 | # Find the 'package' table
113 | package_symbol = next((s for s in root_symbols if s.get("name") == "package"), None)
114 | assert package_symbol is not None, "Should find 'package' as root symbol"
115 |
116 | # Verify it has children (nested keys)
117 | assert "children" in package_symbol, "'package' should have children"
118 | child_names = {child.get("name") for child in package_symbol.get("children", [])}
119 |
120 | # Package should have name, version, edition at minimum
121 | assert "name" in child_names, "'package' should have 'name' child"
122 | assert "version" in child_names, "'package' should have 'version' child"
123 | assert "edition" in child_names, "'package' should have 'edition' child"
124 |
125 | @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
126 | @pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
127 | def test_symbol_hierarchy_in_pyproject(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
128 | """Test that symbol hierarchy is properly preserved in pyproject.toml."""
129 | all_symbols, root_symbols = language_server.request_document_symbols("pyproject.toml").get_all_symbols_and_roots()
130 |
131 | # Find the 'project' table
132 | project_symbol = next((s for s in root_symbols if s.get("name") == "project"), None)
133 | assert project_symbol is not None, "Should find 'project' as root symbol"
134 |
135 | # Verify it has children
136 | assert "children" in project_symbol, "'project' should have children"
137 | child_names = {child.get("name") for child in project_symbol.get("children", [])}
138 |
139 | # Project should have name, version, dependencies at minimum
140 | assert "name" in child_names, "'project' should have 'name' child"
141 | assert "version" in child_names, "'project' should have 'version' child"
142 |
143 | @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
144 | @pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
145 | def test_tool_section_hierarchy(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
146 | """Test that tool sections in pyproject.toml are properly structured."""
147 | all_symbols, root_symbols = language_server.request_document_symbols("pyproject.toml").get_all_symbols_and_roots()
148 |
149 | # Get all symbol names
150 | all_names = [s.get("name") for s in all_symbols]
151 |
152 | # Should detect tool.ruff, tool.mypy, or tool.pytest
153 | has_ruff = any("ruff" in name.lower() for name in all_names if name)
154 | has_mypy = any("mypy" in name.lower() for name in all_names if name)
155 | has_pytest = any("pytest" in name.lower() for name in all_names if name)
156 |
157 | assert has_ruff or has_mypy or has_pytest, f"Should detect tool sections, got names: {all_names}"
158 |
159 | @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
160 | @pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
161 | def test_array_of_tables_symbol(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
162 | """Test that [[bin]] array of tables is detected."""
163 | all_symbols, root_symbols = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
164 |
165 | # Get all symbol names
166 | all_names = [s.get("name") for s in all_symbols]
167 |
168 | # Should detect bin array of tables
169 | has_bin = "bin" in all_names
170 | assert has_bin, f"Should detect [[bin]] array of tables, got names: {all_names}"
171 |
172 | # Find the bin symbol and verify its structure
173 | bin_symbol = next((s for s in all_symbols if s.get("name") == "bin"), None)
174 | assert bin_symbol is not None, "Should find bin symbol"
175 |
176 | # Array of tables should be kind 18 (array)
177 | assert bin_symbol.get("kind") == 18, "[[bin]] should have kind 18 (array)"
178 |
179 | # Children of array of tables are indexed by position ('0', '1', etc.)
180 | if "children" in bin_symbol:
181 | bin_children = bin_symbol.get("children", [])
182 | assert len(bin_children) > 0, "[[bin]] should have at least one child element"
183 | # First child is index '0'
184 | first_child = bin_children[0]
185 | assert first_child.get("name") == "0", f"First array element should be named '0', got: {first_child.get('name')}"
186 |
187 | # The '0' element should contain name and path as grandchildren
188 | if "children" in first_child:
189 | grandchild_names = {gc.get("name") for gc in first_child.get("children", [])}
190 | assert "name" in grandchild_names, f"[[bin]] element should have 'name' field, got: {grandchild_names}"
191 | assert "path" in grandchild_names, f"[[bin]] element should have 'path' field, got: {grandchild_names}"
192 |
```
--------------------------------------------------------------------------------
/src/solidlsp/language_servers/zls.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Provides Zig specific instantiation of the LanguageServer class using ZLS (Zig Language Server).
3 | """
4 |
5 | import logging
6 | import os
7 | import pathlib
8 | import platform
9 | import shutil
10 | import subprocess
11 | import threading
12 |
13 | from overrides import override
14 |
15 | from solidlsp.ls import SolidLanguageServer
16 | from solidlsp.ls_config import LanguageServerConfig
17 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
18 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
19 | from solidlsp.settings import SolidLSPSettings
20 |
21 | log = logging.getLogger(__name__)
22 |
23 |
24 | class ZigLanguageServer(SolidLanguageServer):
25 | """
26 | Provides Zig specific instantiation of the LanguageServer class using ZLS.
27 | """
28 |
29 | @override
30 | def is_ignored_dirname(self, dirname: str) -> bool:
31 | # For Zig projects, we should ignore:
32 | # - zig-cache: build cache directory
33 | # - zig-out: default build output directory
34 | # - .zig-cache: alternative cache location
35 | # - node_modules: if the project has JavaScript components
36 | return super().is_ignored_dirname(dirname) or dirname in ["zig-cache", "zig-out", ".zig-cache", "node_modules", "build", "dist"]
37 |
38 | @staticmethod
39 | def _get_zig_version() -> str | None:
40 | """Get the installed Zig version or None if not found."""
41 | try:
42 | result = subprocess.run(["zig", "version"], capture_output=True, text=True, check=False)
43 | if result.returncode == 0:
44 | return result.stdout.strip()
45 | except FileNotFoundError:
46 | return None
47 | return None
48 |
49 | @staticmethod
50 | def _get_zls_version() -> str | None:
51 | """Get the installed ZLS version or None if not found."""
52 | try:
53 | result = subprocess.run(["zls", "--version"], capture_output=True, text=True, check=False)
54 | if result.returncode == 0:
55 | return result.stdout.strip()
56 | except FileNotFoundError:
57 | return None
58 | return None
59 |
60 | @staticmethod
61 | def _check_zls_installed() -> bool:
62 | """Check if ZLS is installed in the system."""
63 | return shutil.which("zls") is not None
64 |
65 | @staticmethod
66 | def _setup_runtime_dependency() -> bool:
67 | """
68 | Check if required Zig runtime dependencies are available.
69 | Raises RuntimeError with helpful message if dependencies are missing.
70 | """
71 | # Check for Windows and provide error message
72 | if platform.system() == "Windows":
73 | raise RuntimeError(
74 | "Windows is not supported by ZLS in this integration. "
75 | "Cross-file references don't work reliably on Windows. Reason unknown."
76 | )
77 |
78 | zig_version = ZigLanguageServer._get_zig_version()
79 | if not zig_version:
80 | raise RuntimeError(
81 | "Zig is not installed. Please install Zig from https://ziglang.org/download/ and make sure it is added to your PATH."
82 | )
83 |
84 | if not ZigLanguageServer._check_zls_installed():
85 | zls_version = ZigLanguageServer._get_zls_version()
86 | if not zls_version:
87 | raise RuntimeError(
88 | "Found Zig but ZLS (Zig Language Server) is not installed.\n"
89 | "Please install ZLS from https://github.com/zigtools/zls\n"
90 | "You can install it via:\n"
91 | " - Package managers (brew install zls, scoop install zls, etc.)\n"
92 | " - Download pre-built binaries from GitHub releases\n"
93 | " - Build from source with: zig build -Doptimize=ReleaseSafe\n\n"
94 | "After installation, make sure 'zls' is added to your PATH."
95 | )
96 |
97 | return True
98 |
99 | def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
100 | self._setup_runtime_dependency()
101 |
102 | super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd="zls", cwd=repository_root_path), "zig", solidlsp_settings)
103 | self.server_ready = threading.Event()
104 | self.request_id = 0
105 |
106 | @staticmethod
107 | def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
108 | """
109 | Returns the initialize params for the Zig Language Server.
110 | """
111 | root_uri = pathlib.Path(repository_absolute_path).as_uri()
112 | initialize_params = {
113 | "locale": "en",
114 | "capabilities": {
115 | "textDocument": {
116 | "synchronization": {"didSave": True, "dynamicRegistration": True},
117 | "definition": {"dynamicRegistration": True},
118 | "references": {"dynamicRegistration": True},
119 | "documentSymbol": {
120 | "dynamicRegistration": True,
121 | "hierarchicalDocumentSymbolSupport": True,
122 | "symbolKind": {"valueSet": list(range(1, 27))},
123 | },
124 | "completion": {
125 | "dynamicRegistration": True,
126 | "completionItem": {
127 | "snippetSupport": True,
128 | "commitCharactersSupport": True,
129 | "documentationFormat": ["markdown", "plaintext"],
130 | "deprecatedSupport": True,
131 | "preselectSupport": True,
132 | },
133 | },
134 | "hover": {
135 | "dynamicRegistration": True,
136 | "contentFormat": ["markdown", "plaintext"],
137 | },
138 | },
139 | "workspace": {
140 | "workspaceFolders": True,
141 | "didChangeConfiguration": {"dynamicRegistration": True},
142 | "configuration": True,
143 | },
144 | },
145 | "processId": os.getpid(),
146 | "rootPath": repository_absolute_path,
147 | "rootUri": root_uri,
148 | "workspaceFolders": [
149 | {
150 | "uri": root_uri,
151 | "name": os.path.basename(repository_absolute_path),
152 | }
153 | ],
154 | "initializationOptions": {
155 | # ZLS specific options based on schema.json
156 | # Critical paths for ZLS to understand the project
157 | "zig_exe_path": shutil.which("zig"), # Path to zig executable
158 | "zig_lib_path": None, # Let ZLS auto-detect
159 | "build_runner_path": None, # Let ZLS use its built-in runner
160 | "global_cache_path": None, # Let ZLS use default cache
161 | # Build configuration
162 | "enable_build_on_save": True, # Enable to analyze project structure
163 | "build_on_save_args": ["build"],
164 | # Features
165 | "enable_snippets": True,
166 | "enable_argument_placeholders": True,
167 | "semantic_tokens": "full",
168 | "warn_style": False,
169 | "highlight_global_var_declarations": False,
170 | "skip_std_references": False,
171 | "prefer_ast_check_as_child_process": True,
172 | "completion_label_details": True,
173 | # Inlay hints configuration
174 | "inlay_hints_show_variable_type_hints": True,
175 | "inlay_hints_show_struct_literal_field_type": True,
176 | "inlay_hints_show_parameter_name": True,
177 | "inlay_hints_show_builtin": True,
178 | "inlay_hints_exclude_single_argument": True,
179 | "inlay_hints_hide_redundant_param_names": False,
180 | "inlay_hints_hide_redundant_param_names_last_token": False,
181 | },
182 | }
183 | return initialize_params # type: ignore[return-value]
184 |
185 | def _start_server(self) -> None:
186 | """Start ZLS server process"""
187 |
188 | def register_capability_handler(params: dict) -> None:
189 | return
190 |
191 | def window_log_message(msg: dict) -> None:
192 | log.info(f"LSP: window/logMessage: {msg}")
193 |
194 | def do_nothing(params: dict) -> None:
195 | return
196 |
197 | self.server.on_request("client/registerCapability", register_capability_handler)
198 | self.server.on_notification("window/logMessage", window_log_message)
199 | self.server.on_notification("$/progress", do_nothing)
200 | self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
201 |
202 | log.info("Starting ZLS server process")
203 | self.server.start()
204 | initialize_params = self._get_initialize_params(self.repository_root_path)
205 |
206 | log.info("Sending initialize request from LSP client to LSP server and awaiting response")
207 | init_response = self.server.send.initialize(initialize_params)
208 |
209 | # Verify server capabilities
210 | assert "textDocumentSync" in init_response["capabilities"]
211 | assert "definitionProvider" in init_response["capabilities"]
212 | assert "documentSymbolProvider" in init_response["capabilities"]
213 | assert "referencesProvider" in init_response["capabilities"]
214 |
215 | self.server.notify.initialized({})
216 | self.completions_available.set()
217 |
218 | # ZLS server is ready after initialization
219 | self.server_ready.set()
220 | self.server_ready.wait()
221 |
222 | # Open build.zig if it exists to help ZLS understand project structure
223 | build_zig_path = os.path.join(self.repository_root_path, "build.zig")
224 | if os.path.exists(build_zig_path):
225 | try:
226 | with open(build_zig_path, encoding="utf-8") as f:
227 | content = f.read()
228 | uri = pathlib.Path(build_zig_path).as_uri()
229 | self.server.notify.did_open_text_document(
230 | {
231 | "textDocument": {
232 | "uri": uri,
233 | "languageId": "zig",
234 | "version": 1,
235 | "text": content,
236 | }
237 | }
238 | )
239 | log.info("Opened build.zig to provide project context to ZLS")
240 | except Exception as e:
241 | log.warning(f"Failed to open build.zig: {e}")
242 |
```
--------------------------------------------------------------------------------
/test/solidlsp/yaml_ls/test_yaml_basic.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Basic integration tests for the YAML language server functionality.
3 |
4 | These tests validate the functionality of the language server APIs
5 | like request_document_symbols using the YAML test repository.
6 | """
7 |
8 | from pathlib import Path
9 |
10 | import pytest
11 |
12 | from solidlsp import SolidLanguageServer
13 | from solidlsp.ls_config import Language
14 |
15 |
16 | @pytest.mark.yaml
17 | class TestYAMLLanguageServerBasics:
18 | """Test basic functionality of the YAML language server."""
19 |
20 | @pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
21 | @pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
22 | def test_yaml_language_server_initialization(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
23 | """Test that YAML language server can be initialized successfully."""
24 | assert language_server is not None
25 | assert language_server.language == Language.YAML
26 | assert language_server.is_running()
27 | assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
28 |
29 | @pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
30 | @pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
31 | def test_yaml_config_file_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
32 | """Test document symbols detection in config.yaml with specific symbol verification."""
33 | all_symbols, root_symbols = language_server.request_document_symbols("config.yaml").get_all_symbols_and_roots()
34 |
35 | assert all_symbols is not None, "Should return symbols for config.yaml"
36 | assert len(all_symbols) > 0, f"Should find symbols in config.yaml, found {len(all_symbols)}"
37 |
38 | # Verify specific top-level keys are detected
39 | symbol_names = [sym.get("name") for sym in all_symbols]
40 | assert "app" in symbol_names, "Should detect 'app' key in config.yaml"
41 | assert "database" in symbol_names, "Should detect 'database' key in config.yaml"
42 | assert "logging" in symbol_names, "Should detect 'logging' key in config.yaml"
43 | assert "features" in symbol_names, "Should detect 'features' key in config.yaml"
44 |
45 | # Verify nested symbols exist (child keys under 'app')
46 | assert "name" in symbol_names, "Should detect nested 'name' key"
47 | assert "port" in symbol_names, "Should detect nested 'port' key"
48 | assert "debug" in symbol_names, "Should detect nested 'debug' key"
49 |
50 | # Check symbol kinds are appropriate (LSP kinds: 2=module/namespace, 15=string, 16=number, 17=boolean)
51 | app_symbol = next((s for s in all_symbols if s.get("name") == "app"), None)
52 | assert app_symbol is not None, "Should find 'app' symbol"
53 | assert app_symbol.get("kind") == 2, "Top-level object should have kind 2 (module/namespace)"
54 |
55 | port_symbol = next((s for s in all_symbols if s.get("name") == "port"), None)
56 | assert port_symbol is not None, "Should find 'port' symbol"
57 | assert port_symbol.get("kind") == 16, "'port' should have kind 16 (number)"
58 |
59 | debug_symbol = next((s for s in all_symbols if s.get("name") == "debug"), None)
60 | assert debug_symbol is not None, "Should find 'debug' symbol"
61 | assert debug_symbol.get("kind") == 17, "'debug' should have kind 17 (boolean)"
62 |
63 | @pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
64 | @pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
65 | def test_yaml_services_file_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
66 | """Test symbol detection in services.yml Docker Compose file."""
67 | all_symbols, root_symbols = language_server.request_document_symbols("services.yml").get_all_symbols_and_roots()
68 |
69 | assert all_symbols is not None, "Should return symbols for services.yml"
70 | assert len(all_symbols) > 0, f"Should find symbols in services.yml, found {len(all_symbols)}"
71 |
72 | # Verify specific top-level keys from Docker Compose file
73 | symbol_names = [sym.get("name") for sym in all_symbols]
74 | assert "version" in symbol_names, "Should detect 'version' key"
75 | assert "services" in symbol_names, "Should detect 'services' key"
76 | assert "networks" in symbol_names, "Should detect 'networks' key"
77 | assert "volumes" in symbol_names, "Should detect 'volumes' key"
78 |
79 | # Verify service names
80 | assert "web" in symbol_names, "Should detect 'web' service"
81 | assert "api" in symbol_names, "Should detect 'api' service"
82 | assert "database" in symbol_names, "Should detect 'database' service"
83 |
84 | # Check that arrays are properly detected
85 | ports_symbols = [s for s in all_symbols if s.get("name") == "ports"]
86 | assert len(ports_symbols) > 0, "Should find 'ports' arrays in services"
87 | # Arrays should have kind 18
88 | for ports_sym in ports_symbols:
89 | assert ports_sym.get("kind") == 18, "'ports' should have kind 18 (array)"
90 |
91 | @pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
92 | @pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
93 | def test_yaml_data_file_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
94 | """Test symbol detection in data.yaml file with array structures."""
95 | all_symbols, root_symbols = language_server.request_document_symbols("data.yaml").get_all_symbols_and_roots()
96 |
97 | assert all_symbols is not None, "Should return symbols for data.yaml"
98 | assert len(all_symbols) > 0, f"Should find symbols in data.yaml, found {len(all_symbols)}"
99 |
100 | # Verify top-level keys
101 | symbol_names = [sym.get("name") for sym in all_symbols]
102 | assert "users" in symbol_names, "Should detect 'users' array"
103 | assert "projects" in symbol_names, "Should detect 'projects' array"
104 |
105 | # Verify array elements (indexed by position)
106 | # data.yaml has user entries and project entries
107 | assert "id" in symbol_names, "Should detect 'id' fields in array elements"
108 | assert "name" in symbol_names, "Should detect 'name' fields"
109 | assert "email" in symbol_names, "Should detect 'email' fields"
110 | assert "roles" in symbol_names, "Should detect 'roles' arrays"
111 |
112 | @pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
113 | @pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
114 | def test_yaml_symbols_with_body(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
115 | """Test request_document_symbols with body extraction."""
116 | all_symbols, root_symbols = language_server.request_document_symbols("config.yaml").get_all_symbols_and_roots()
117 |
118 | assert all_symbols is not None, "Should return symbols for config.yaml"
119 | assert len(all_symbols) > 0, "Should have symbols"
120 |
121 | # Find the 'app' symbol and verify its body
122 | app_symbol = next((s for s in all_symbols if s.get("name") == "app"), None)
123 | assert app_symbol is not None, "Should find 'app' symbol"
124 |
125 | # Check that body exists and contains expected content
126 | assert "body" in app_symbol, "'app' symbol should have body"
127 | app_body = app_symbol["body"]
128 | assert "app:" in app_body, "Body should start with 'app:'"
129 | assert "name: test-application" in app_body, "Body should contain 'name' field"
130 | assert "version: 1.0.0" in app_body, "Body should contain 'version' field"
131 | assert "port: 8080" in app_body, "Body should contain 'port' field"
132 | assert "debug: true" in app_body, "Body should contain 'debug' field"
133 |
134 | # Find a simple string value symbol and verify its body
135 | name_symbols = [s for s in all_symbols if s.get("name") == "name" and "body" in s]
136 | assert len(name_symbols) > 0, "Should find 'name' symbols with bodies"
137 | # At least one should contain "test-application"
138 | assert any("test-application" in s["body"] for s in name_symbols), "Should find name with test-application"
139 |
140 | # Find the database symbol and check its body
141 | database_symbol = next((s for s in all_symbols if s.get("name") == "database"), None)
142 | assert database_symbol is not None, "Should find 'database' symbol"
143 | assert "body" in database_symbol, "'database' symbol should have body"
144 | db_body = database_symbol["body"]
145 | assert "database:" in db_body, "Body should start with 'database:'"
146 | assert "host: localhost" in db_body, "Body should contain host configuration"
147 | assert "port: 5432" in db_body, "Body should contain port configuration"
148 |
149 | @pytest.mark.parametrize("language_server", [Language.YAML], indirect=True)
150 | @pytest.mark.parametrize("repo_path", [Language.YAML], indirect=True)
151 | def test_yaml_symbol_ranges(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
152 | """Test that symbols have proper range information."""
153 | all_symbols, root_symbols = language_server.request_document_symbols("config.yaml").get_all_symbols_and_roots()
154 |
155 | assert all_symbols is not None
156 | assert len(all_symbols) > 0
157 |
158 | # Check the 'app' symbol range
159 | app_symbol = next((s for s in all_symbols if s.get("name") == "app"), None)
160 | assert app_symbol is not None, "Should find 'app' symbol"
161 | assert "range" in app_symbol, "'app' symbol should have range"
162 |
163 | app_range = app_symbol["range"]
164 | assert "start" in app_range, "Range should have start"
165 | assert "end" in app_range, "Range should have end"
166 | assert app_range["start"]["line"] == 1, "'app' should start at line 1 (0-indexed, actual line 2)"
167 | # The app block spans from line 2 to line 7 in the file (1-indexed)
168 | # In 0-indexed LSP coordinates: line 1 (start) to line 6 (end)
169 | assert app_range["end"]["line"] == 6, "'app' should end at line 6 (0-indexed)"
170 |
171 | # Check a nested symbol range
172 | port_symbols = [s for s in all_symbols if s.get("name") == "port"]
173 | assert len(port_symbols) > 0, "Should find 'port' symbols"
174 | # Find the one under 'app' (should be at line 4 in 0-indexed, actual line 5)
175 | app_port = next((s for s in port_symbols if s["range"]["start"]["line"] == 4), None)
176 | assert app_port is not None, "Should find 'port' under 'app'"
177 | assert app_port["range"]["start"]["character"] == 2, "'port' should be indented 2 spaces"
178 |
```
--------------------------------------------------------------------------------
/test/solidlsp/haskell/test_haskell_basic.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Rigorous tests for Haskell Language Server integration with Serena.
3 |
4 | Tests prove that Serena's symbol tools can:
5 | 1. Discover all expected symbols with precise matching
6 | 2. Track cross-file references accurately
7 | 3. Identify data type structures and record fields
8 | 4. Navigate between definitions and usages
9 |
10 | Test Repository Structure:
11 | - src/Calculator.hs: Calculator data type, arithmetic functions (add, subtract, multiply, divide, calculate)
12 | - src/Helper.hs: Helper functions (validateNumber, isPositive, isNegative, absolute)
13 | - app/Main.hs: Main entry point using Calculator and Helper modules
14 | """
15 |
16 | import sys
17 |
18 | import pytest
19 |
20 | from solidlsp.ls import SolidLanguageServer
21 | from solidlsp.ls_config import Language
22 |
23 |
24 | @pytest.mark.haskell
25 | @pytest.mark.skipif(sys.platform == "win32", reason="HLS not installed on Windows CI")
26 | class TestHaskellLanguageServer:
27 | @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
28 | def test_calculator_module_symbols(self, language_server: SolidLanguageServer):
29 | """
30 | Test precise symbol discovery in Calculator.hs.
31 |
32 | Verifies that Serena can identify:
33 | - Data type definition (Calculator with record fields)
34 | - All exported functions with correct names
35 | - Module structure
36 | """
37 | all_symbols, _ = language_server.request_document_symbols("src/Calculator.hs").get_all_symbols_and_roots()
38 | symbol_names = {s["name"] for s in all_symbols}
39 |
40 | # Verify exact set of expected top-level symbols
41 | expected_symbols = {
42 | "Calculator", # Data type
43 | "add", # Function: Int -> Int -> Int
44 | "subtract", # Function: Int -> Int -> Int
45 | "multiply", # Function: Int -> Int -> Int
46 | "divide", # Function: Int -> Int -> Maybe Int
47 | "calculate", # Function: Calculator -> String -> Int -> Int -> Maybe Int
48 | }
49 |
50 | # Verify all expected symbols are present
51 | missing = expected_symbols - symbol_names
52 | assert not missing, f"Missing expected symbols in Calculator.hs: {missing}"
53 |
54 | # Verify Calculator data type exists
55 | calculator_symbol = next((s for s in all_symbols if s["name"] == "Calculator"), None)
56 | assert calculator_symbol is not None, "Calculator data type not found"
57 |
58 | # The Calculator should be identified as a data type
59 | # HLS may use different SymbolKind values (1=File, 5=Class, 23=Struct)
60 | assert calculator_symbol["kind"] in [
61 | 1,
62 | 5,
63 | 23,
64 | ], f"Calculator should be a data type (kind 1, 5, or 23), got kind {calculator_symbol['kind']}"
65 |
66 | @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
67 | def test_helper_module_symbols(self, language_server: SolidLanguageServer):
68 | """
69 | Test precise symbol discovery in Helper.hs.
70 |
71 | Verifies Serena identifies all helper functions that are imported
72 | and used by Calculator module.
73 | """
74 | all_symbols, _ = language_server.request_document_symbols("src/Helper.hs").get_all_symbols_and_roots()
75 | symbol_names = {s["name"] for s in all_symbols}
76 |
77 | # Verify expected helper functions (module name may also appear)
78 | expected_symbols = {
79 | "validateNumber", # Function used by Calculator.add and Calculator.subtract
80 | "isPositive", # Predicate function
81 | "isNegative", # Predicate function used by absolute
82 | "absolute", # Function that uses isNegative
83 | }
84 |
85 | # All expected symbols should be present (module name is optional)
86 | missing = expected_symbols - symbol_names
87 | assert not missing, f"Missing expected symbols in Helper.hs: {missing}"
88 |
89 | # Verify no unexpected symbols beyond the module name
90 | extra = symbol_names - expected_symbols - {"Helper"}
91 | assert not extra, f"Unexpected symbols in Helper.hs: {extra}"
92 |
93 | @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
94 | def test_main_module_imports(self, language_server: SolidLanguageServer):
95 | """
96 | Test that Main.hs properly references both Calculator and Helper modules.
97 |
98 | Verifies Serena can identify cross-module dependencies.
99 | """
100 | all_symbols, _ = language_server.request_document_symbols("app/Main.hs").get_all_symbols_and_roots()
101 | symbol_names = {s["name"] for s in all_symbols}
102 |
103 | # Main.hs should have the main function
104 | assert "main" in symbol_names, "Main.hs should contain 'main' function"
105 |
106 | @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
107 | def test_cross_file_references_validateNumber(self, language_server: SolidLanguageServer):
108 | """
109 | Test cross-file reference tracking for validateNumber function.
110 |
111 | validateNumber is defined in Helper.hs:9 and used in:
112 | - Calculator.hs:21 (in add function)
113 | - Calculator.hs:25 (in subtract function)
114 |
115 | This proves Serena can track function usage across module boundaries.
116 | """
117 | # Get references to validateNumber (defined at line 9, 0-indexed = line 8)
118 | references = language_server.request_references("src/Helper.hs", line=8, column=0)
119 |
120 | # Should find at least: definition in Helper.hs + 2 usages in Calculator.hs
121 | assert len(references) >= 2, f"Expected at least 2 references to validateNumber (used in add and subtract), got {len(references)}"
122 |
123 | # Verify we have references in Calculator.hs
124 | reference_paths = [ref["relativePath"] for ref in references]
125 | calculator_refs = [path for path in reference_paths if "Calculator.hs" in path]
126 |
127 | assert len(calculator_refs) >= 2, (
128 | f"Expected at least 2 references in Calculator.hs (add and subtract functions), "
129 | f"got {len(calculator_refs)} references in Calculator.hs"
130 | )
131 |
132 | @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
133 | def test_within_file_references_isNegative(self, language_server: SolidLanguageServer):
134 | """
135 | Test within-file reference tracking for isNegative function.
136 |
137 | isNegative is defined in Helper.hs:17 and used in Helper.hs:22 (absolute function).
138 | This proves Serena can track intra-module function calls.
139 | """
140 | # isNegative defined at line 17 (0-indexed = line 16)
141 | references = language_server.request_references("src/Helper.hs", line=16, column=0)
142 |
143 | # Should find: definition + usage in absolute function
144 | assert len(references) >= 1, f"Expected at least 1 reference to isNegative (used in absolute), got {len(references)}"
145 |
146 | # All references should be in Helper.hs
147 | reference_paths = [ref["relativePath"] for ref in references]
148 | assert all(
149 | "Helper.hs" in path for path in reference_paths
150 | ), f"All isNegative references should be in Helper.hs, got: {reference_paths}"
151 |
152 | @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
153 | def test_function_references_from_main(self, language_server: SolidLanguageServer):
154 | """
155 | Test that functions used in Main.hs can be traced back to their definitions.
156 |
157 | Main.hs:12 calls 'add' from Calculator module.
158 | Main.hs:25 calls 'isPositive' from Helper module.
159 | Main.hs:26 calls 'absolute' from Helper module.
160 |
161 | This proves Serena can track cross-module function calls from executable code.
162 | """
163 | # Test 'add' function references (defined in Calculator.hs:20, 0-indexed = line 19)
164 | add_refs = language_server.request_references("src/Calculator.hs", line=19, column=0)
165 |
166 | # Should find references in Main.hs and possibly Calculator.hs (calculate function uses it)
167 | assert len(add_refs) >= 1, f"Expected at least 1 reference to 'add', got {len(add_refs)}"
168 |
169 | add_ref_paths = [ref["relativePath"] for ref in add_refs]
170 | # Should have at least one reference in Main.hs or Calculator.hs
171 | assert any(
172 | "Main.hs" in path or "Calculator.hs" in path for path in add_ref_paths
173 | ), f"Expected 'add' to be referenced in Main.hs or Calculator.hs, got: {add_ref_paths}"
174 |
175 | @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
176 | def test_multiply_function_usage_in_calculate(self, language_server: SolidLanguageServer):
177 | """
178 | Test that multiply function usage is tracked within Calculator module.
179 |
180 | multiply is defined in Calculator.hs:28 and used in:
181 | - Calculator.hs:41 (in calculate function via pattern matching)
182 | - Main.hs:20 (via calculate call with "multiply" operator)
183 |
184 | This proves Serena can track function references even when called indirectly.
185 | """
186 | # multiply defined at line 28 (0-indexed = line 27)
187 | multiply_refs = language_server.request_references("src/Calculator.hs", line=27, column=0)
188 |
189 | # Should find at least the usage in calculate function
190 | assert len(multiply_refs) >= 1, f"Expected at least 1 reference to 'multiply', got {len(multiply_refs)}"
191 |
192 | # Should have reference in Calculator.hs (calculate function)
193 | multiply_ref_paths = [ref["relativePath"] for ref in multiply_refs]
194 | assert any(
195 | "Calculator.hs" in path for path in multiply_ref_paths
196 | ), f"Expected 'multiply' to be referenced in Calculator.hs, got: {multiply_ref_paths}"
197 |
198 | @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
199 | def test_data_type_constructor_references(self, language_server: SolidLanguageServer):
200 | """
201 | Test that Calculator data type constructor usage is tracked.
202 |
203 | Calculator is defined in Calculator.hs:14 and used in:
204 | - Main.hs:8 (constructor call: Calculator "TestCalc" 1)
205 | - Calculator.hs:37 (type signature for calculate function)
206 |
207 | This proves Serena can track data type constructor references.
208 | """
209 | # Calculator data type defined at line 14 (0-indexed = line 13)
210 | calculator_refs = language_server.request_references("src/Calculator.hs", line=13, column=5)
211 |
212 | # Should find usage in Main.hs
213 | assert len(calculator_refs) >= 1, f"Expected at least 1 reference to Calculator constructor, got {len(calculator_refs)}"
214 |
215 | # Should have at least one reference in Main.hs or Calculator.hs
216 | calc_ref_paths = [ref["relativePath"] for ref in calculator_refs]
217 | assert any(
218 | "Main.hs" in path or "Calculator.hs" in path for path in calc_ref_paths
219 | ), f"Expected Calculator to be referenced in Main.hs or Calculator.hs, got: {calc_ref_paths}"
220 |
```
--------------------------------------------------------------------------------
/test/solidlsp/erlang/test_erlang_ignored_dirs.py:
--------------------------------------------------------------------------------
```python
1 | from collections.abc import Generator
2 | from pathlib import Path
3 |
4 | import pytest
5 |
6 | from solidlsp import SolidLanguageServer
7 | from solidlsp.ls_config import Language
8 | from test.conftest import start_ls_context
9 |
10 | from . import ERLANG_LS_UNAVAILABLE, ERLANG_LS_UNAVAILABLE_REASON
11 |
12 | # These marks will be applied to all tests in this module
13 | pytestmark = [
14 | pytest.mark.erlang,
15 | pytest.mark.skipif(ERLANG_LS_UNAVAILABLE, reason=f"Erlang LS not available: {ERLANG_LS_UNAVAILABLE_REASON}"),
16 | ]
17 |
18 |
19 | @pytest.fixture(scope="module")
20 | def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]:
21 | """Fixture to set up an LS for the erlang test repo with the 'ignored_dir' directory ignored."""
22 | ignored_paths = ["_build", "ignored_dir"]
23 | with start_ls_context(language=Language.ERLANG, ignored_paths=ignored_paths) as ls:
24 | yield ls
25 |
26 |
27 | @pytest.mark.timeout(60) # Add 60 second timeout
28 | @pytest.mark.xfail(reason="Known timeout issue on Ubuntu CI with Erlang LS server startup", strict=False)
29 | @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ERLANG], indirect=True)
30 | def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
31 | """Tests that request_full_symbol_tree ignores the configured directory."""
32 | root = ls_with_ignored_dirs.request_full_symbol_tree()[0]
33 | root_children = root["children"]
34 | children_names = {child["name"] for child in root_children}
35 |
36 | # Should have src, include, and test directories, but not _build or ignored_dir
37 | expected_dirs = {"src", "include", "test"}
38 | found_expected = expected_dirs.intersection(children_names)
39 | assert len(found_expected) > 0, f"Expected some dirs from {expected_dirs} to be in {children_names}"
40 | assert "_build" not in children_names, f"_build should not be in {children_names}"
41 | assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names}"
42 |
43 |
44 | @pytest.mark.timeout(60) # Add 60 second timeout
45 | @pytest.mark.xfail(reason="Known timeout issue on Ubuntu CI with Erlang LS server startup", strict=False)
46 | @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ERLANG], indirect=True)
47 | def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
48 | """Tests that find_references ignores the configured directory."""
49 | # Location of user record, which might be referenced in ignored_dir
50 | definition_file = "include/records.hrl"
51 |
52 | # Find the user record definition
53 | symbols = ls_with_ignored_dirs.request_document_symbols(definition_file).get_all_symbols_and_roots()
54 | user_symbol = None
55 | for symbol_group in symbols:
56 | user_symbol = next((s for s in symbol_group if "user" in s.get("name", "").lower()), None)
57 | if user_symbol:
58 | break
59 |
60 | if not user_symbol or "selectionRange" not in user_symbol:
61 | pytest.skip("User record symbol not found for reference testing")
62 |
63 | sel_start = user_symbol["selectionRange"]["start"]
64 | references = ls_with_ignored_dirs.request_references(definition_file, sel_start["line"], sel_start["character"])
65 |
66 | # Assert that _build and ignored_dir do not appear in the references
67 | assert not any("_build" in ref["relativePath"] for ref in references), "_build should be ignored"
68 | assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored"
69 |
70 |
71 | @pytest.mark.timeout(60) # Add 60 second timeout
72 | @pytest.mark.xfail(reason="Known timeout issue on Ubuntu CI with Erlang LS server startup", strict=False)
73 | @pytest.mark.parametrize("repo_path", [Language.ERLANG], indirect=True)
74 | def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
75 | """Tests that refs and symbols with glob patterns are ignored."""
76 | ignored_paths = ["_build*", "ignored_*", "*.tmp"]
77 | with start_ls_context(language=Language.ERLANG, repo_path=str(repo_path), ignored_paths=ignored_paths) as ls:
78 | # Same as in the above tests
79 | root = ls.request_full_symbol_tree()[0]
80 | root_children = root["children"]
81 | children_names = {child["name"] for child in root_children}
82 |
83 | # Should have src, include, and test directories, but not _build or ignored_dir
84 | expected_dirs = {"src", "include", "test"}
85 | found_expected = expected_dirs.intersection(children_names)
86 | assert len(found_expected) > 0, f"Expected some dirs from {expected_dirs} to be in {children_names}"
87 | assert "_build" not in children_names, f"_build should not be in {children_names} (glob pattern)"
88 | assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names} (glob pattern)"
89 |
90 | # Test that the refs and symbols with glob patterns are ignored
91 | definition_file = "include/records.hrl"
92 |
93 | # Find the user record definition
94 | symbols = ls.request_document_symbols(definition_file).get_all_symbols_and_roots()
95 | user_symbol = None
96 | for symbol_group in symbols:
97 | user_symbol = next((s for s in symbol_group if "user" in s.get("name", "").lower()), None)
98 | if user_symbol:
99 | break
100 |
101 | if user_symbol and "selectionRange" in user_symbol:
102 | sel_start = user_symbol["selectionRange"]["start"]
103 | references = ls.request_references(definition_file, sel_start["line"], sel_start["character"])
104 |
105 | # Assert that _build and ignored_dir do not appear in references
106 | assert not any("_build" in ref["relativePath"] for ref in references), "_build should be ignored (glob)"
107 | assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored (glob)"
108 |
109 |
110 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
111 | def test_default_ignored_directories(language_server: SolidLanguageServer):
112 | """Test that default Erlang directories are ignored."""
113 | # Test that Erlang-specific directories are ignored by default
114 | assert language_server.is_ignored_dirname("_build"), "_build should be ignored"
115 | assert language_server.is_ignored_dirname("ebin"), "ebin should be ignored"
116 | assert language_server.is_ignored_dirname("deps"), "deps should be ignored"
117 | assert language_server.is_ignored_dirname(".rebar3"), ".rebar3 should be ignored"
118 | assert language_server.is_ignored_dirname("_checkouts"), "_checkouts should be ignored"
119 | assert language_server.is_ignored_dirname("node_modules"), "node_modules should be ignored"
120 |
121 | # Test that important directories are not ignored
122 | assert not language_server.is_ignored_dirname("src"), "src should not be ignored"
123 | assert not language_server.is_ignored_dirname("include"), "include should not be ignored"
124 | assert not language_server.is_ignored_dirname("test"), "test should not be ignored"
125 | assert not language_server.is_ignored_dirname("priv"), "priv should not be ignored"
126 |
127 |
128 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
129 | def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer):
130 | """Test that symbol tree excludes build and dependency directories."""
131 | symbol_tree = language_server.request_full_symbol_tree()
132 |
133 | if symbol_tree:
134 | root = symbol_tree[0]
135 | children_names = {child["name"] for child in root.get("children", [])}
136 |
137 | # Build and dependency directories should not appear
138 | ignored_dirs = {"_build", "ebin", "deps", ".rebar3", "_checkouts", "node_modules"}
139 | found_ignored = ignored_dirs.intersection(children_names)
140 | assert len(found_ignored) == 0, f"Found ignored directories in symbol tree: {found_ignored}"
141 |
142 | # Important directories should appear
143 | important_dirs = {"src", "include", "test"}
144 | found_important = important_dirs.intersection(children_names)
145 | assert len(found_important) > 0, f"Expected to find important directories: {important_dirs}, got: {children_names}"
146 |
147 |
148 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
149 | def test_ignore_compiled_files(language_server: SolidLanguageServer):
150 | """Test that compiled Erlang files are ignored."""
151 | # Test that beam files are ignored
152 | assert language_server.is_ignored_filename("module.beam"), "BEAM files should be ignored"
153 | assert language_server.is_ignored_filename("app.beam"), "BEAM files should be ignored"
154 |
155 | # Test that source files are not ignored
156 | assert not language_server.is_ignored_filename("module.erl"), "Erlang source files should not be ignored"
157 | assert not language_server.is_ignored_filename("records.hrl"), "Header files should not be ignored"
158 |
159 |
160 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
161 | def test_rebar_directories_ignored(language_server: SolidLanguageServer):
162 | """Test that rebar-specific directories are ignored."""
163 | # Test rebar3-specific directories
164 | assert language_server.is_ignored_dirname("_build"), "rebar3 _build should be ignored"
165 | assert language_server.is_ignored_dirname("_checkouts"), "rebar3 _checkouts should be ignored"
166 | assert language_server.is_ignored_dirname(".rebar3"), "rebar3 cache should be ignored"
167 |
168 | # Test that rebar.lock and rebar.config are not ignored (they are configuration files)
169 | assert not language_server.is_ignored_filename("rebar.config"), "rebar.config should not be ignored"
170 | assert not language_server.is_ignored_filename("rebar.lock"), "rebar.lock should not be ignored"
171 |
172 |
173 | @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ERLANG], indirect=True)
174 | def test_document_symbols_ignores_dirs(ls_with_ignored_dirs: SolidLanguageServer):
175 | """Test that document symbols from ignored directories are not included."""
176 | # Try to get symbols from a file in ignored directory (should not find it)
177 | try:
178 | ignored_file = "ignored_dir/ignored_module.erl"
179 | symbols = ls_with_ignored_dirs.request_document_symbols(ignored_file).get_all_symbols_and_roots()
180 | # If we get here, the file was found - symbols should be empty or None
181 | if symbols:
182 | assert len(symbols) == 0, "Should not find symbols in ignored directory"
183 | except Exception:
184 | # This is expected - the file should not be accessible
185 | pass
186 |
187 |
188 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
189 | def test_erlang_specific_ignore_patterns(language_server: SolidLanguageServer):
190 | """Test Erlang-specific ignore patterns work correctly."""
191 | erlang_ignored_dirs = ["_build", "ebin", ".rebar3", "_checkouts", "cover"]
192 |
193 | # These should be ignored
194 | for dirname in erlang_ignored_dirs:
195 | assert language_server.is_ignored_dirname(dirname), f"{dirname} should be ignored"
196 |
197 | # These should not be ignored
198 | erlang_important_dirs = ["src", "include", "test", "priv"]
199 | for dirname in erlang_important_dirs:
200 | assert not language_server.is_ignored_dirname(dirname), f"{dirname} should not be ignored"
201 |
```