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

# Directory Structure

```
├── .devcontainer
│   └── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── config.yml
│   │   ├── feature_request.md
│   │   └── issue--bug--performance-problem--question-.md
│   └── workflows
│       ├── codespell.yml
│       ├── docker.yml
│       ├── docs.yaml
│       ├── junie.yml
│       ├── publish.yml
│       └── pytest.yml
├── .gitignore
├── .serena
│   ├── .gitignore
│   ├── memories
│   │   ├── adding_new_language_support_guide.md
│   │   ├── serena_core_concepts_and_architecture.md
│   │   ├── serena_repository_structure.md
│   │   └── suggested_commands.md
│   └── project.yml
├── .vscode
│   └── settings.json
├── CHANGELOG.md
├── CLAUDE.md
├── compose.yaml
├── CONTRIBUTING.md
├── docker_build_and_run.sh
├── DOCKER.md
├── Dockerfile
├── docs
│   ├── _config.yml
│   ├── _static
│   │   └── images
│   │       └── jetbrains-marketplace-button.png
│   ├── .gitignore
│   ├── 01-about
│   │   ├── 000_intro.md
│   │   ├── 010_llm-integration.md
│   │   ├── 020_programming-languages.md
│   │   ├── 030_serena-in-action.md
│   │   ├── 035_tools.md
│   │   ├── 040_comparison-to-other-agents.md
│   │   └── 050_acknowledgements.md
│   ├── 02-usage
│   │   ├── 000_intro.md
│   │   ├── 010_prerequisites.md
│   │   ├── 020_running.md
│   │   ├── 025_jetbrains_plugin.md
│   │   ├── 030_clients.md
│   │   ├── 040_workflow.md
│   │   ├── 050_configuration.md
│   │   ├── 060_dashboard.md
│   │   ├── 070_security.md
│   │   └── 999_additional-usage.md
│   ├── 03-special-guides
│   │   ├── 000_intro.md
│   │   ├── custom_agent.md
│   │   ├── groovy_setup_guide_for_serena.md
│   │   ├── scala_setup_guide_for_serena.md
│   │   └── serena_on_chatgpt.md
│   ├── autogen_rst.py
│   ├── create_toc.py
│   └── index.md
├── flake.lock
├── flake.nix
├── lessons_learned.md
├── LICENSE
├── llms-install.md
├── pyproject.toml
├── README.md
├── repo_dir_sync.py
├── resources
│   ├── jetbrains-marketplace-button.cdr
│   ├── serena-icons.cdr
│   ├── serena-logo-dark-mode.svg
│   ├── serena-logo.cdr
│   ├── serena-logo.svg
│   └── vscode_sponsor_logo.png
├── roadmap.md
├── scripts
│   ├── agno_agent.py
│   ├── demo_run_tools.py
│   ├── gen_prompt_factory.py
│   ├── mcp_server.py
│   ├── print_mode_context_options.py
│   ├── print_tool_overview.py
│   └── profile_tool_call.py
├── src
│   ├── interprompt
│   │   ├── __init__.py
│   │   ├── .syncCommitId.remote
│   │   ├── .syncCommitId.this
│   │   ├── jinja_template.py
│   │   ├── multilang_prompt.py
│   │   ├── prompt_factory.py
│   │   └── util
│   │       ├── __init__.py
│   │       └── class_decorators.py
│   ├── README.md
│   ├── serena
│   │   ├── __init__.py
│   │   ├── agent.py
│   │   ├── agno.py
│   │   ├── analytics.py
│   │   ├── cli.py
│   │   ├── code_editor.py
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   ├── context_mode.py
│   │   │   └── serena_config.py
│   │   ├── constants.py
│   │   ├── dashboard.py
│   │   ├── generated
│   │   │   └── generated_prompt_factory.py
│   │   ├── gui_log_viewer.py
│   │   ├── ls_manager.py
│   │   ├── mcp.py
│   │   ├── project.py
│   │   ├── prompt_factory.py
│   │   ├── resources
│   │   │   ├── config
│   │   │   │   ├── contexts
│   │   │   │   │   ├── agent.yml
│   │   │   │   │   ├── chatgpt.yml
│   │   │   │   │   ├── claude-code.yml
│   │   │   │   │   ├── codex.yml
│   │   │   │   │   ├── context.template.yml
│   │   │   │   │   ├── desktop-app.yml
│   │   │   │   │   ├── ide.yml
│   │   │   │   │   └── oaicompat-agent.yml
│   │   │   │   ├── internal_modes
│   │   │   │   │   └── jetbrains.yml
│   │   │   │   ├── modes
│   │   │   │   │   ├── editing.yml
│   │   │   │   │   ├── interactive.yml
│   │   │   │   │   ├── mode.template.yml
│   │   │   │   │   ├── no-memories.yml
│   │   │   │   │   ├── no-onboarding.yml
│   │   │   │   │   ├── onboarding.yml
│   │   │   │   │   ├── one-shot.yml
│   │   │   │   │   └── planning.yml
│   │   │   │   └── prompt_templates
│   │   │   │       ├── simple_tool_outputs.yml
│   │   │   │       └── system_prompt.yml
│   │   │   ├── dashboard
│   │   │   │   ├── dashboard.css
│   │   │   │   ├── dashboard.js
│   │   │   │   ├── index.html
│   │   │   │   ├── jquery.min.js
│   │   │   │   ├── serena-icon-16.png
│   │   │   │   ├── serena-icon-32.png
│   │   │   │   ├── serena-icon-48.png
│   │   │   │   ├── serena-logo-dark-mode.svg
│   │   │   │   ├── serena-logo.svg
│   │   │   │   ├── serena-logs-dark-mode.png
│   │   │   │   └── serena-logs.png
│   │   │   ├── project.template.yml
│   │   │   └── serena_config.template.yml
│   │   ├── symbol.py
│   │   ├── task_executor.py
│   │   ├── text_utils.py
│   │   ├── tools
│   │   │   ├── __init__.py
│   │   │   ├── cmd_tools.py
│   │   │   ├── config_tools.py
│   │   │   ├── file_tools.py
│   │   │   ├── jetbrains_plugin_client.py
│   │   │   ├── jetbrains_tools.py
│   │   │   ├── memory_tools.py
│   │   │   ├── symbol_tools.py
│   │   │   ├── tools_base.py
│   │   │   └── workflow_tools.py
│   │   └── util
│   │       ├── class_decorators.py
│   │       ├── cli_util.py
│   │       ├── exception.py
│   │       ├── file_system.py
│   │       ├── general.py
│   │       ├── git.py
│   │       ├── gui.py
│   │       ├── inspection.py
│   │       ├── logging.py
│   │       ├── shell.py
│   │       └── thread.py
│   └── solidlsp
│       ├── __init__.py
│       ├── .gitignore
│       ├── language_servers
│       │   ├── al_language_server.py
│       │   ├── bash_language_server.py
│       │   ├── clangd_language_server.py
│       │   ├── clojure_lsp.py
│       │   ├── common.py
│       │   ├── csharp_language_server.py
│       │   ├── dart_language_server.py
│       │   ├── eclipse_jdtls.py
│       │   ├── elixir_tools
│       │   │   ├── __init__.py
│       │   │   ├── elixir_tools.py
│       │   │   └── README.md
│       │   ├── elm_language_server.py
│       │   ├── erlang_language_server.py
│       │   ├── fortran_language_server.py
│       │   ├── fsharp_language_server.py
│       │   ├── gopls.py
│       │   ├── groovy_language_server.py
│       │   ├── haskell_language_server.py
│       │   ├── intelephense.py
│       │   ├── jedi_server.py
│       │   ├── julia_server.py
│       │   ├── kotlin_language_server.py
│       │   ├── lua_ls.py
│       │   ├── marksman.py
│       │   ├── matlab_language_server.py
│       │   ├── nixd_ls.py
│       │   ├── omnisharp
│       │   │   ├── initialize_params.json
│       │   │   ├── runtime_dependencies.json
│       │   │   └── workspace_did_change_configuration.json
│       │   ├── omnisharp.py
│       │   ├── pascal_server.py
│       │   ├── perl_language_server.py
│       │   ├── powershell_language_server.py
│       │   ├── pyright_server.py
│       │   ├── r_language_server.py
│       │   ├── regal_server.py
│       │   ├── ruby_lsp.py
│       │   ├── rust_analyzer.py
│       │   ├── scala_language_server.py
│       │   ├── solargraph.py
│       │   ├── sourcekit_lsp.py
│       │   ├── taplo_server.py
│       │   ├── terraform_ls.py
│       │   ├── typescript_language_server.py
│       │   ├── vts_language_server.py
│       │   ├── vue_language_server.py
│       │   ├── yaml_language_server.py
│       │   └── zls.py
│       ├── ls_config.py
│       ├── ls_exceptions.py
│       ├── ls_handler.py
│       ├── ls_request.py
│       ├── ls_types.py
│       ├── ls_utils.py
│       ├── ls.py
│       ├── lsp_protocol_handler
│       │   ├── lsp_constants.py
│       │   ├── lsp_requests.py
│       │   ├── lsp_types.py
│       │   └── server.py
│       ├── settings.py
│       └── util
│           ├── cache.py
│           ├── subprocess_util.py
│           └── zip.py
├── sync.py
├── test
│   ├── __init__.py
│   ├── conftest.py
│   ├── resources
│   │   └── repos
│   │       ├── al
│   │       │   └── test_repo
│   │       │       ├── app.json
│   │       │       └── src
│   │       │           ├── Codeunits
│   │       │           │   ├── CustomerMgt.Codeunit.al
│   │       │           │   └── PaymentProcessorImpl.Codeunit.al
│   │       │           ├── Enums
│   │       │           │   └── CustomerType.Enum.al
│   │       │           ├── Interfaces
│   │       │           │   └── IPaymentProcessor.Interface.al
│   │       │           ├── Pages
│   │       │           │   ├── CustomerCard.Page.al
│   │       │           │   └── CustomerList.Page.al
│   │       │           ├── TableExtensions
│   │       │           │   └── Item.TableExt.al
│   │       │           └── Tables
│   │       │               └── Customer.Table.al
│   │       ├── bash
│   │       │   └── test_repo
│   │       │       ├── config.sh
│   │       │       ├── main.sh
│   │       │       └── utils.sh
│   │       ├── clojure
│   │       │   └── test_repo
│   │       │       ├── deps.edn
│   │       │       └── src
│   │       │           └── test_app
│   │       │               ├── core.clj
│   │       │               └── utils.clj
│   │       ├── csharp
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── Models
│   │       │       │   └── Person.cs
│   │       │       ├── Program.cs
│   │       │       ├── serena.sln
│   │       │       └── TestProject.csproj
│   │       ├── dart
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── helper.dart
│   │       │       │   ├── main.dart
│   │       │       │   └── models.dart
│   │       │       └── pubspec.yaml
│   │       ├── elixir
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── examples.ex
│   │       │       │   ├── ignored_dir
│   │       │       │   │   └── ignored_module.ex
│   │       │       │   ├── models.ex
│   │       │       │   ├── services.ex
│   │       │       │   ├── test_repo.ex
│   │       │       │   └── utils.ex
│   │       │       ├── mix.exs
│   │       │       ├── mix.lock
│   │       │       ├── scripts
│   │       │       │   └── build_script.ex
│   │       │       └── test
│   │       │           ├── models_test.exs
│   │       │           └── test_repo_test.exs
│   │       ├── elm
│   │       │   └── test_repo
│   │       │       ├── elm.json
│   │       │       ├── Main.elm
│   │       │       └── Utils.elm
│   │       ├── erlang
│   │       │   └── test_repo
│   │       │       ├── hello.erl
│   │       │       ├── ignored_dir
│   │       │       │   └── ignored_module.erl
│   │       │       ├── include
│   │       │       │   ├── records.hrl
│   │       │       │   └── types.hrl
│   │       │       ├── math_utils.erl
│   │       │       ├── rebar.config
│   │       │       ├── src
│   │       │       │   ├── app.erl
│   │       │       │   ├── models.erl
│   │       │       │   ├── services.erl
│   │       │       │   └── utils.erl
│   │       │       └── test
│   │       │           ├── models_tests.erl
│   │       │           └── utils_tests.erl
│   │       ├── fortran
│   │       │   └── test_repo
│   │       │       ├── main.f90
│   │       │       └── modules
│   │       │           ├── geometry.f90
│   │       │           └── math_utils.f90
│   │       ├── fsharp
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── Calculator.fs
│   │       │       ├── Models
│   │       │       │   └── Person.fs
│   │       │       ├── Program.fs
│   │       │       ├── README.md
│   │       │       └── TestProject.fsproj
│   │       ├── go
│   │       │   └── test_repo
│   │       │       └── main.go
│   │       ├── groovy
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── build.gradle
│   │       │       └── src
│   │       │           └── main
│   │       │               └── groovy
│   │       │                   └── com
│   │       │                       └── example
│   │       │                           ├── Main.groovy
│   │       │                           ├── Model.groovy
│   │       │                           ├── ModelUser.groovy
│   │       │                           └── Utils.groovy
│   │       ├── haskell
│   │       │   └── test_repo
│   │       │       ├── app
│   │       │       │   └── Main.hs
│   │       │       ├── haskell-test-repo.cabal
│   │       │       ├── package.yaml
│   │       │       ├── src
│   │       │       │   ├── Calculator.hs
│   │       │       │   └── Helper.hs
│   │       │       └── stack.yaml
│   │       ├── java
│   │       │   └── test_repo
│   │       │       ├── pom.xml
│   │       │       └── src
│   │       │           └── main
│   │       │               └── java
│   │       │                   └── test_repo
│   │       │                       ├── Main.java
│   │       │                       ├── Model.java
│   │       │                       ├── ModelUser.java
│   │       │                       └── Utils.java
│   │       ├── julia
│   │       │   └── test_repo
│   │       │       ├── lib
│   │       │       │   └── helper.jl
│   │       │       └── main.jl
│   │       ├── kotlin
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── build.gradle.kts
│   │       │       └── src
│   │       │           └── main
│   │       │               └── kotlin
│   │       │                   └── test_repo
│   │       │                       ├── Main.kt
│   │       │                       ├── Model.kt
│   │       │                       ├── ModelUser.kt
│   │       │                       └── Utils.kt
│   │       ├── lua
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── main.lua
│   │       │       ├── src
│   │       │       │   ├── calculator.lua
│   │       │       │   └── utils.lua
│   │       │       └── tests
│   │       │           └── test_calculator.lua
│   │       ├── markdown
│   │       │   └── test_repo
│   │       │       ├── api.md
│   │       │       ├── CONTRIBUTING.md
│   │       │       ├── guide.md
│   │       │       └── README.md
│   │       ├── matlab
│   │       │   └── test_repo
│   │       │       ├── Calculator.m
│   │       │       └── main.m
│   │       ├── nix
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── default.nix
│   │       │       ├── flake.nix
│   │       │       ├── lib
│   │       │       │   └── utils.nix
│   │       │       ├── modules
│   │       │       │   └── example.nix
│   │       │       └── scripts
│   │       │           └── hello.sh
│   │       ├── pascal
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   └── helper.pas
│   │       │       └── main.pas
│   │       ├── perl
│   │       │   └── test_repo
│   │       │       ├── helper.pl
│   │       │       └── main.pl
│   │       ├── php
│   │       │   └── test_repo
│   │       │       ├── helper.php
│   │       │       ├── index.php
│   │       │       └── simple_var.php
│   │       ├── powershell
│   │       │   └── test_repo
│   │       │       ├── main.ps1
│   │       │       ├── PowerShellEditorServices.json
│   │       │       └── utils.ps1
│   │       ├── python
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── custom_test
│   │       │       │   ├── __init__.py
│   │       │       │   └── advanced_features.py
│   │       │       ├── examples
│   │       │       │   ├── __init__.py
│   │       │       │   └── user_management.py
│   │       │       ├── ignore_this_dir_with_postfix
│   │       │       │   └── ignored_module.py
│   │       │       ├── scripts
│   │       │       │   ├── __init__.py
│   │       │       │   └── run_app.py
│   │       │       └── test_repo
│   │       │           ├── __init__.py
│   │       │           ├── complex_types.py
│   │       │           ├── models.py
│   │       │           ├── name_collisions.py
│   │       │           ├── nested_base.py
│   │       │           ├── nested.py
│   │       │           ├── overloaded.py
│   │       │           ├── services.py
│   │       │           ├── utils.py
│   │       │           └── variables.py
│   │       ├── r
│   │       │   └── test_repo
│   │       │       ├── .Rbuildignore
│   │       │       ├── DESCRIPTION
│   │       │       ├── examples
│   │       │       │   └── analysis.R
│   │       │       ├── NAMESPACE
│   │       │       └── R
│   │       │           ├── models.R
│   │       │           └── utils.R
│   │       ├── rego
│   │       │   └── test_repo
│   │       │       ├── policies
│   │       │       │   ├── authz.rego
│   │       │       │   └── validation.rego
│   │       │       └── utils
│   │       │           └── helpers.rego
│   │       ├── ruby
│   │       │   └── test_repo
│   │       │       ├── .solargraph.yml
│   │       │       ├── examples
│   │       │       │   └── user_management.rb
│   │       │       ├── lib.rb
│   │       │       ├── main.rb
│   │       │       ├── models.rb
│   │       │       ├── nested.rb
│   │       │       ├── services.rb
│   │       │       └── variables.rb
│   │       ├── rust
│   │       │   ├── test_repo
│   │       │   │   ├── Cargo.lock
│   │       │   │   ├── Cargo.toml
│   │       │   │   └── src
│   │       │   │       ├── lib.rs
│   │       │   │       └── main.rs
│   │       │   └── test_repo_2024
│   │       │       ├── Cargo.lock
│   │       │       ├── Cargo.toml
│   │       │       └── src
│   │       │           ├── lib.rs
│   │       │           └── main.rs
│   │       ├── scala
│   │       │   ├── build.sbt
│   │       │   ├── project
│   │       │   │   ├── build.properties
│   │       │   │   ├── metals.sbt
│   │       │   │   └── plugins.sbt
│   │       │   └── src
│   │       │       └── main
│   │       │           └── scala
│   │       │               └── com
│   │       │                   └── example
│   │       │                       ├── Main.scala
│   │       │                       └── Utils.scala
│   │       ├── swift
│   │       │   └── test_repo
│   │       │       ├── Package.swift
│   │       │       └── src
│   │       │           ├── main.swift
│   │       │           └── utils.swift
│   │       ├── terraform
│   │       │   └── test_repo
│   │       │       ├── data.tf
│   │       │       ├── main.tf
│   │       │       ├── outputs.tf
│   │       │       └── variables.tf
│   │       ├── toml
│   │       │   └── test_repo
│   │       │       ├── Cargo.toml
│   │       │       ├── config.toml
│   │       │       └── pyproject.toml
│   │       ├── typescript
│   │       │   └── test_repo
│   │       │       ├── .serena
│   │       │       │   └── project.yml
│   │       │       ├── index.ts
│   │       │       ├── tsconfig.json
│   │       │       ├── use_helper.ts
│   │       │       └── ws_manager.js
│   │       ├── vue
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── index.html
│   │       │       ├── package.json
│   │       │       ├── src
│   │       │       │   ├── App.vue
│   │       │       │   ├── components
│   │       │       │   │   ├── CalculatorButton.vue
│   │       │       │   │   ├── CalculatorDisplay.vue
│   │       │       │   │   └── CalculatorInput.vue
│   │       │       │   ├── composables
│   │       │       │   │   ├── useFormatter.ts
│   │       │       │   │   └── useTheme.ts
│   │       │       │   ├── main.ts
│   │       │       │   ├── stores
│   │       │       │   │   └── calculator.ts
│   │       │       │   └── types
│   │       │       │       └── index.ts
│   │       │       ├── tsconfig.json
│   │       │       ├── tsconfig.node.json
│   │       │       └── vite.config.ts
│   │       ├── yaml
│   │       │   └── test_repo
│   │       │       ├── config.yaml
│   │       │       ├── data.yaml
│   │       │       └── services.yml
│   │       └── zig
│   │           └── test_repo
│   │               ├── .gitignore
│   │               ├── build.zig
│   │               ├── src
│   │               │   ├── calculator.zig
│   │               │   ├── main.zig
│   │               │   └── math_utils.zig
│   │               └── zls.json
│   ├── serena
│   │   ├── __init__.py
│   │   ├── __snapshots__
│   │   │   └── test_symbol_editing.ambr
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   └── test_serena_config.py
│   │   ├── test_cli_project_commands.py
│   │   ├── test_edit_marker.py
│   │   ├── test_mcp.py
│   │   ├── test_serena_agent.py
│   │   ├── test_symbol_editing.py
│   │   ├── test_symbol.py
│   │   ├── test_task_executor.py
│   │   ├── test_text_utils.py
│   │   ├── test_tool_parameter_types.py
│   │   └── util
│   │       ├── test_exception.py
│   │       └── test_file_system.py
│   └── solidlsp
│       ├── al
│       │   └── test_al_basic.py
│       ├── bash
│       │   ├── __init__.py
│       │   └── test_bash_basic.py
│       ├── clojure
│       │   ├── __init__.py
│       │   └── test_clojure_basic.py
│       ├── csharp
│       │   └── test_csharp_basic.py
│       ├── dart
│       │   ├── __init__.py
│       │   └── test_dart_basic.py
│       ├── elixir
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_elixir_basic.py
│       │   ├── test_elixir_ignored_dirs.py
│       │   ├── test_elixir_integration.py
│       │   └── test_elixir_symbol_retrieval.py
│       ├── elm
│       │   └── test_elm_basic.py
│       ├── erlang
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_erlang_basic.py
│       │   ├── test_erlang_ignored_dirs.py
│       │   └── test_erlang_symbol_retrieval.py
│       ├── fortran
│       │   ├── __init__.py
│       │   └── test_fortran_basic.py
│       ├── fsharp
│       │   └── test_fsharp_basic.py
│       ├── go
│       │   └── test_go_basic.py
│       ├── groovy
│       │   └── test_groovy_basic.py
│       ├── haskell
│       │   ├── __init__.py
│       │   └── test_haskell_basic.py
│       ├── java
│       │   └── test_java_basic.py
│       ├── julia
│       │   └── test_julia_basic.py
│       ├── kotlin
│       │   └── test_kotlin_basic.py
│       ├── lua
│       │   └── test_lua_basic.py
│       ├── markdown
│       │   ├── __init__.py
│       │   └── test_markdown_basic.py
│       ├── matlab
│       │   ├── __init__.py
│       │   └── test_matlab_basic.py
│       ├── nix
│       │   └── test_nix_basic.py
│       ├── pascal
│       │   ├── __init__.py
│       │   └── test_pascal_basic.py
│       ├── perl
│       │   └── test_perl_basic.py
│       ├── php
│       │   └── test_php_basic.py
│       ├── powershell
│       │   ├── __init__.py
│       │   └── test_powershell_basic.py
│       ├── python
│       │   ├── test_python_basic.py
│       │   ├── test_retrieval_with_ignored_dirs.py
│       │   └── test_symbol_retrieval.py
│       ├── r
│       │   ├── __init__.py
│       │   └── test_r_basic.py
│       ├── rego
│       │   └── test_rego_basic.py
│       ├── ruby
│       │   ├── test_ruby_basic.py
│       │   └── test_ruby_symbol_retrieval.py
│       ├── rust
│       │   ├── test_rust_2024_edition.py
│       │   ├── test_rust_analyzer_detection.py
│       │   └── test_rust_basic.py
│       ├── scala
│       │   └── test_scala_language_server.py
│       ├── swift
│       │   └── test_swift_basic.py
│       ├── terraform
│       │   └── test_terraform_basic.py
│       ├── test_lsp_protocol_handler_server.py
│       ├── toml
│       │   ├── __init__.py
│       │   ├── test_toml_basic.py
│       │   ├── test_toml_edge_cases.py
│       │   ├── test_toml_ignored_dirs.py
│       │   └── test_toml_symbol_retrieval.py
│       ├── typescript
│       │   └── test_typescript_basic.py
│       ├── util
│       │   └── test_zip.py
│       ├── vue
│       │   ├── __init__.py
│       │   ├── test_vue_basic.py
│       │   ├── test_vue_error_cases.py
│       │   ├── test_vue_rename.py
│       │   └── test_vue_symbol_retrieval.py
│       ├── yaml_ls
│       │   ├── __init__.py
│       │   └── test_yaml_basic.py
│       └── zig
│           └── test_zig_basic.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/test/solidlsp/php/test_php_basic.py:
--------------------------------------------------------------------------------

```python
  1 | from pathlib import Path
  2 | 
  3 | import pytest
  4 | 
  5 | from solidlsp import SolidLanguageServer
  6 | from solidlsp.ls_config import Language
  7 | 
  8 | 
  9 | @pytest.mark.php
 10 | class TestPhpLanguageServer:
 11 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
 12 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
 13 |     def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 14 |         """Test that the language server starts and stops successfully."""
 15 |         # The fixture already handles start and stop
 16 |         assert language_server.is_running()
 17 |         assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
 18 | 
 19 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
 20 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
 21 |     def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 22 | 
 23 |         # In index.php:
 24 |         # Line 9 (1-indexed): $greeting = greet($userName);
 25 |         # Line 11 (1-indexed): echo $greeting;
 26 |         # We want to find the definition of $greeting (defined on line 9)
 27 |         # from its usage in echo $greeting; on line 11.
 28 |         # LSP is 0-indexed: definition on line 8, usage on line 10.
 29 |         # $greeting in echo $greeting; is at char 5 on line 11 (0-indexed: line 10, char 5)
 30 |         # e c h o   $ g r e e t i n g
 31 |         #           ^ char 5
 32 |         definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 10, 6)  # cursor on 'g' in $greeting
 33 | 
 34 |         assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
 35 |         assert len(definition_location_list) == 1
 36 |         definition_location = definition_location_list[0]
 37 |         assert definition_location["uri"].endswith("index.php")
 38 |         # Definition of $greeting is on line 10 (1-indexed) / line 9 (0-indexed), char 0
 39 |         assert definition_location["range"]["start"]["line"] == 9
 40 |         assert definition_location["range"]["start"]["character"] == 0
 41 | 
 42 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
 43 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
 44 |     def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 45 |         definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 12, 5)  # helperFunction
 46 | 
 47 |         assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
 48 |         assert len(definition_location_list) == 1
 49 |         definition_location = definition_location_list[0]
 50 |         assert definition_location["uri"].endswith("helper.php")
 51 |         assert definition_location["range"]["start"]["line"] == 2
 52 |         assert definition_location["range"]["start"]["character"] == 0
 53 | 
 54 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
 55 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
 56 |     def test_find_definition_simple_variable(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 57 |         file_path = str(repo_path / "simple_var.php")
 58 | 
 59 |         # In simple_var.php:
 60 |         # Line 2 (1-indexed): $localVar = "test";
 61 |         # Line 3 (1-indexed): echo $localVar;
 62 |         # LSP is 0-indexed: definition on line 1, usage on line 2
 63 |         # Find definition of $localVar (char 5 on line 3 / 0-indexed: line 2, char 5)
 64 |         # $localVar in echo $localVar;  (e c h o   $ l o c a l V a r)
 65 |         #                           ^ char 5
 66 |         definition_location_list = language_server.request_definition(file_path, 2, 6)  # cursor on 'l' in $localVar
 67 | 
 68 |         assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
 69 |         assert len(definition_location_list) == 1
 70 |         definition_location = definition_location_list[0]
 71 |         assert definition_location["uri"].endswith("simple_var.php")
 72 |         assert definition_location["range"]["start"]["line"] == 1  # Definition of $localVar (0-indexed)
 73 |         assert definition_location["range"]["start"]["character"] == 0  # $localVar (0-indexed)
 74 | 
 75 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
 76 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
 77 |     def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 78 |         index_php_path = str(repo_path / "index.php")
 79 | 
 80 |         # In index.php (0-indexed lines):
 81 |         # Line 9: $greeting = greet($userName); // Definition of $greeting
 82 |         # Line 11: echo $greeting;            // Usage of $greeting
 83 |         # Find references for $greeting from its usage in "echo $greeting;" (line 11, char 6 for 'g')
 84 |         references = language_server.request_references(index_php_path, 11, 6)
 85 | 
 86 |         assert references
 87 |         # Intelephense, when asked for references from usage, seems to only return the usage itself.
 88 |         assert len(references) == 1, "Expected to find 1 reference for $greeting (the usage itself)"
 89 | 
 90 |         expected_locations = [{"uri_suffix": "index.php", "line": 11, "character": 5}]  # Usage: echo $greeting (points to $)
 91 | 
 92 |         # Convert actual references to a comparable format and sort
 93 |         actual_locations = sorted(
 94 |             [
 95 |                 {
 96 |                     "uri_suffix": loc["uri"].split("/")[-1],
 97 |                     "line": loc["range"]["start"]["line"],
 98 |                     "character": loc["range"]["start"]["character"],
 99 |                 }
100 |                 for loc in references
101 |             ],
102 |             key=lambda x: (x["uri_suffix"], x["line"], x["character"]),
103 |         )
104 | 
105 |         expected_locations = sorted(expected_locations, key=lambda x: (x["uri_suffix"], x["line"], x["character"]))
106 | 
107 |         assert actual_locations == expected_locations
108 | 
109 |     @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
110 |     @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
111 |     def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
112 |         helper_php_path = str(repo_path / "helper.php")
113 |         # In index.php (0-indexed lines):
114 |         # Line 13: helperFunction(); // Usage of helperFunction
115 |         # Find references for helperFunction from its definition
116 |         references = language_server.request_references(helper_php_path, 2, len("function "))
117 | 
118 |         assert references, f"Expected non-empty references for helperFunction but got {references=}"
119 |         # Intelephense might return 1 (usage) or 2 (usage + definition) references.
120 |         # Let's check for at least the usage in index.php
121 |         # Definition is in helper.php, line 2, char 0 (based on previous findings)
122 |         # Usage is in index.php, line 13, char 0
123 | 
124 |         actual_locations_comparable = []
125 |         for loc in references:
126 |             actual_locations_comparable.append(
127 |                 {
128 |                     "uri_suffix": loc["uri"].split("/")[-1],
129 |                     "line": loc["range"]["start"]["line"],
130 |                     "character": loc["range"]["start"]["character"],
131 |                 }
132 |             )
133 | 
134 |         usage_in_index_php = {"uri_suffix": "index.php", "line": 13, "character": 0}
135 |         assert usage_in_index_php in actual_locations_comparable, "Usage of helperFunction in index.php not found"
136 | 
```

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

```python
  1 | """
  2 | Provides Markdown specific instantiation of the LanguageServer class using marksman.
  3 | Contains various configurations and settings specific to Markdown.
  4 | """
  5 | 
  6 | import logging
  7 | import os
  8 | import pathlib
  9 | import threading
 10 | 
 11 | from overrides import override
 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 | class Marksman(SolidLanguageServer):
 25 |     """
 26 |     Provides Markdown specific instantiation of the LanguageServer class using marksman.
 27 |     """
 28 | 
 29 |     marksman_releases = "https://github.com/artempyanykh/marksman/releases/download/2024-12-18"
 30 |     runtime_dependencies = RuntimeDependencyCollection(
 31 |         [
 32 |             RuntimeDependency(
 33 |                 id="marksman",
 34 |                 url=f"{marksman_releases}/marksman-linux-x64",
 35 |                 platform_id="linux-x64",
 36 |                 archive_type="binary",
 37 |                 binary_name="marksman",
 38 |             ),
 39 |             RuntimeDependency(
 40 |                 id="marksman",
 41 |                 url=f"{marksman_releases}/marksman-linux-arm64",
 42 |                 platform_id="linux-arm64",
 43 |                 archive_type="binary",
 44 |                 binary_name="marksman",
 45 |             ),
 46 |             RuntimeDependency(
 47 |                 id="marksman",
 48 |                 url=f"{marksman_releases}/marksman-macos",
 49 |                 platform_id="osx-x64",
 50 |                 archive_type="binary",
 51 |                 binary_name="marksman",
 52 |             ),
 53 |             RuntimeDependency(
 54 |                 id="marksman",
 55 |                 url=f"{marksman_releases}/marksman-macos",
 56 |                 platform_id="osx-arm64",
 57 |                 archive_type="binary",
 58 |                 binary_name="marksman",
 59 |             ),
 60 |             RuntimeDependency(
 61 |                 id="marksman",
 62 |                 url=f"{marksman_releases}/marksman.exe",
 63 |                 platform_id="win-x64",
 64 |                 archive_type="binary",
 65 |                 binary_name="marksman.exe",
 66 |             ),
 67 |         ]
 68 |     )
 69 | 
 70 |     @classmethod
 71 |     def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
 72 |         """Setup runtime dependencies for marksman and return the command to start the server."""
 73 |         deps = cls.runtime_dependencies
 74 |         dependency = deps.get_single_dep_for_current_platform()
 75 | 
 76 |         marksman_ls_dir = cls.ls_resources_dir(solidlsp_settings)
 77 |         marksman_executable_path = deps.binary_path(marksman_ls_dir)
 78 |         if not os.path.exists(marksman_executable_path):
 79 |             log.info(
 80 |                 f"Downloading marksman from {dependency.url} to {marksman_ls_dir}",
 81 |             )
 82 |             deps.install(marksman_ls_dir)
 83 |         if not os.path.exists(marksman_executable_path):
 84 |             raise FileNotFoundError(f"Download failed? Could not find marksman executable at {marksman_executable_path}")
 85 |         os.chmod(marksman_executable_path, 0o755)
 86 |         return marksman_executable_path
 87 | 
 88 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 89 |         """
 90 |         Creates a Marksman instance. This class is not meant to be instantiated directly.
 91 |         Use LanguageServer.create() instead.
 92 |         """
 93 |         marksman_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
 94 | 
 95 |         super().__init__(
 96 |             config,
 97 |             repository_root_path,
 98 |             ProcessLaunchInfo(cmd=f"{marksman_executable_path} server", cwd=repository_root_path),
 99 |             "markdown",
100 |             solidlsp_settings,
101 |         )
102 |         self.server_ready = threading.Event()
103 | 
104 |     @override
105 |     def is_ignored_dirname(self, dirname: str) -> bool:
106 |         return super().is_ignored_dirname(dirname) or dirname in ["node_modules", ".obsidian", ".vitepress", ".vuepress"]
107 | 
108 |     @staticmethod
109 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
110 |         """
111 |         Returns the initialize params for the Marksman Language Server.
112 |         """
113 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
114 |         initialize_params: InitializeParams = {  # type: ignore
115 |             "processId": os.getpid(),
116 |             "locale": "en",
117 |             "rootPath": repository_absolute_path,
118 |             "rootUri": root_uri,
119 |             "capabilities": {
120 |                 "textDocument": {
121 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
122 |                     "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
123 |                     "definition": {"dynamicRegistration": True},
124 |                     "references": {"dynamicRegistration": True},
125 |                     "documentSymbol": {
126 |                         "dynamicRegistration": True,
127 |                         "hierarchicalDocumentSymbolSupport": True,
128 |                         "symbolKind": {"valueSet": list(range(1, 27))},  # type: ignore[arg-type]
129 |                     },
130 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},  # type: ignore[list-item]
131 |                     "codeAction": {"dynamicRegistration": True},
132 |                 },
133 |                 "workspace": {
134 |                     "workspaceFolders": True,
135 |                     "didChangeConfiguration": {"dynamicRegistration": True},
136 |                     "symbol": {"dynamicRegistration": True},
137 |                 },
138 |             },
139 |             "workspaceFolders": [
140 |                 {
141 |                     "uri": root_uri,
142 |                     "name": os.path.basename(repository_absolute_path),
143 |                 }
144 |             ],
145 |         }
146 |         return initialize_params
147 | 
148 |     def _start_server(self) -> None:
149 |         """
150 |         Starts the Marksman Language Server and waits for it to be ready.
151 |         """
152 | 
153 |         def register_capability_handler(_params: dict) -> None:
154 |             return
155 | 
156 |         def window_log_message(msg: dict) -> None:
157 |             log.info(f"LSP: window/logMessage: {msg}")
158 | 
159 |         def do_nothing(_params: dict) -> None:
160 |             return
161 | 
162 |         self.server.on_request("client/registerCapability", register_capability_handler)
163 |         self.server.on_notification("window/logMessage", window_log_message)
164 |         self.server.on_notification("$/progress", do_nothing)
165 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
166 | 
167 |         log.info("Starting marksman server process")
168 |         self.server.start()
169 |         initialize_params = self._get_initialize_params(self.repository_root_path)
170 | 
171 |         log.info("Sending initialize request from LSP client to marksman server and awaiting response")
172 |         init_response = self.server.send.initialize(initialize_params)
173 |         log.debug(f"Received initialize response from marksman server: {init_response}")
174 | 
175 |         # Verify server capabilities
176 |         assert "textDocumentSync" in init_response["capabilities"]
177 |         assert "completionProvider" in init_response["capabilities"]
178 |         assert "definitionProvider" in init_response["capabilities"]
179 | 
180 |         self.server.notify.initialized({})
181 | 
182 |         # marksman is typically ready immediately after initialization
183 |         log.info("Marksman server initialization complete")
184 |         self.server_ready.set()
185 |         self.completions_available.set()
186 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/elixir/test_elixir_integration.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Integration tests for Elixir language server with test repository.
  3 | 
  4 | These tests verify that the language server works correctly with a real Elixir project
  5 | and can perform advanced operations like cross-file symbol resolution.
  6 | """
  7 | 
  8 | import os
  9 | from pathlib import Path
 10 | 
 11 | import pytest
 12 | 
 13 | from serena.project import Project
 14 | from solidlsp import SolidLanguageServer
 15 | from solidlsp.ls_config import Language
 16 | 
 17 | from . import EXPERT_UNAVAILABLE, EXPERT_UNAVAILABLE_REASON
 18 | 
 19 | # These marks will be applied to all tests in this module
 20 | pytestmark = [pytest.mark.elixir, pytest.mark.skipif(EXPERT_UNAVAILABLE, reason=f"Next LS not available: {EXPERT_UNAVAILABLE_REASON}")]
 21 | 
 22 | 
 23 | class TestElixirIntegration:
 24 |     """Integration tests for Elixir language server with test repository."""
 25 | 
 26 |     @pytest.fixture
 27 |     def elixir_test_repo_path(self):
 28 |         """Get the path to the Elixir test repository."""
 29 |         test_dir = Path(__file__).parent.parent.parent
 30 |         return str(test_dir / "resources" / "repos" / "elixir" / "test_repo")
 31 | 
 32 |     def test_elixir_repo_structure(self, elixir_test_repo_path):
 33 |         """Test that the Elixir test repository has the expected structure."""
 34 |         repo_path = Path(elixir_test_repo_path)
 35 | 
 36 |         # Check that key files exist
 37 |         assert (repo_path / "mix.exs").exists(), "mix.exs should exist"
 38 |         assert (repo_path / "lib" / "test_repo.ex").exists(), "main module should exist"
 39 |         assert (repo_path / "lib" / "utils.ex").exists(), "utils module should exist"
 40 |         assert (repo_path / "lib" / "models.ex").exists(), "models module should exist"
 41 |         assert (repo_path / "lib" / "services.ex").exists(), "services module should exist"
 42 |         assert (repo_path / "lib" / "examples.ex").exists(), "examples module should exist"
 43 |         assert (repo_path / "test" / "test_repo_test.exs").exists(), "test file should exist"
 44 |         assert (repo_path / "test" / "models_test.exs").exists(), "models test should exist"
 45 | 
 46 |     @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
 47 |     def test_cross_file_symbol_resolution(self, language_server: SolidLanguageServer):
 48 |         """Test that symbols can be resolved across different files."""
 49 |         # Test that User struct from models.ex can be found when referenced in services.ex
 50 |         services_file = os.path.join("lib", "services.ex")
 51 | 
 52 |         # Find where User is referenced in services.ex
 53 |         content = language_server.retrieve_full_file_content(services_file)
 54 |         lines = content.split("\n")
 55 |         user_reference_line = None
 56 |         for i, line in enumerate(lines):
 57 |             if "alias TestRepo.Models.{User" in line:
 58 |                 user_reference_line = i
 59 |                 break
 60 | 
 61 |         if user_reference_line is None:
 62 |             pytest.skip("Could not find User reference in services.ex")
 63 | 
 64 |         # Try to find the definition
 65 |         defining_symbol = language_server.request_defining_symbol(services_file, user_reference_line, 30)
 66 | 
 67 |         if defining_symbol and "location" in defining_symbol:
 68 |             # Should point to models.ex
 69 |             assert "models.ex" in defining_symbol["location"]["uri"]
 70 | 
 71 |     @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
 72 |     def test_module_hierarchy_understanding(self, language_server: SolidLanguageServer):
 73 |         """Test that the language server understands Elixir module hierarchy."""
 74 |         models_file = os.path.join("lib", "models.ex")
 75 |         symbols = language_server.request_document_symbols(models_file).get_all_symbols_and_roots()
 76 | 
 77 |         if symbols:
 78 |             # Flatten symbol structure
 79 |             all_symbols = []
 80 |             for symbol_group in symbols:
 81 |                 if isinstance(symbol_group, list):
 82 |                     all_symbols.extend(symbol_group)
 83 |                 else:
 84 |                     all_symbols.append(symbol_group)
 85 | 
 86 |             symbol_names = [s.get("name", "") for s in all_symbols]
 87 | 
 88 |             # Should understand nested module structure
 89 |             expected_modules = ["TestRepo.Models", "User", "Item", "Order"]
 90 |             found_modules = [name for name in expected_modules if any(name in symbol_name for symbol_name in symbol_names)]
 91 |             assert len(found_modules) > 0, f"Expected modules {expected_modules}, found symbols {symbol_names}"
 92 | 
 93 |     def test_file_extension_matching(self):
 94 |         """Test that the Elixir language recognizes the correct file extensions."""
 95 |         language = Language.ELIXIR
 96 |         matcher = language.get_source_fn_matcher()
 97 | 
 98 |         # Test Elixir file extensions
 99 |         assert matcher.is_relevant_filename("lib/test_repo.ex")
100 |         assert matcher.is_relevant_filename("test/test_repo_test.exs")
101 |         assert matcher.is_relevant_filename("config/config.exs")
102 |         assert matcher.is_relevant_filename("mix.exs")
103 |         assert matcher.is_relevant_filename("lib/models.ex")
104 |         assert matcher.is_relevant_filename("lib/services.ex")
105 | 
106 |         # Test non-Elixir files
107 |         assert not matcher.is_relevant_filename("README.md")
108 |         assert not matcher.is_relevant_filename("lib/test_repo.py")
109 |         assert not matcher.is_relevant_filename("package.json")
110 |         assert not matcher.is_relevant_filename("Cargo.toml")
111 | 
112 | 
113 | class TestElixirProject:
114 |     @pytest.mark.parametrize("project", [Language.ELIXIR], indirect=True)
115 |     def test_comprehensive_symbol_search(self, project: Project):
116 |         """Test comprehensive symbol search across the entire project."""
117 |         # Search for all function definitions
118 |         function_pattern = r"def\s+\w+\s*[\(\s]"
119 |         function_matches = project.search_source_files_for_pattern(function_pattern)
120 | 
121 |         # Should find functions across multiple files
122 |         if function_matches:
123 |             files_with_functions = set()
124 |             for match in function_matches:
125 |                 if match.source_file_path:
126 |                     files_with_functions.add(os.path.basename(match.source_file_path))
127 | 
128 |             # Should find functions in multiple files
129 |             expected_files = {"models.ex", "services.ex", "examples.ex", "utils.ex", "test_repo.ex"}
130 |             found_files = expected_files.intersection(files_with_functions)
131 |             assert len(found_files) > 0, f"Expected functions in {expected_files}, found in {files_with_functions}"
132 | 
133 |         # Search for struct definitions
134 |         struct_pattern = r"defstruct\s+\["
135 |         struct_matches = project.search_source_files_for_pattern(struct_pattern)
136 | 
137 |         if struct_matches:
138 |             # Should find structs primarily in models.ex
139 |             models_structs = [m for m in struct_matches if m.source_file_path and "models.ex" in m.source_file_path]
140 |             assert len(models_structs) > 0, "Should find struct definitions in models.ex"
141 | 
142 |     @pytest.mark.parametrize("project", [Language.ELIXIR], indirect=True)
143 |     def test_protocol_and_implementation_understanding(self, project: Project):
144 |         """Test that the language server understands Elixir protocols and implementations."""
145 |         # Search for protocol definitions
146 |         protocol_pattern = r"defprotocol\s+\w+"
147 |         protocol_matches = project.search_source_files_for_pattern(protocol_pattern, paths_include_glob="**/models.ex")
148 | 
149 |         if protocol_matches:
150 |             # Should find the Serializable protocol
151 |             serializable_matches = [m for m in protocol_matches if "Serializable" in str(m)]
152 |             assert len(serializable_matches) > 0, "Should find Serializable protocol definition"
153 | 
154 |         # Search for protocol implementations
155 |         impl_pattern = r"defimpl\s+\w+"
156 |         impl_matches = project.search_source_files_for_pattern(impl_pattern, paths_include_glob="**/models.ex")
157 | 
158 |         if impl_matches:
159 |             # Should find multiple implementations
160 |             assert len(impl_matches) >= 3, f"Should find at least 3 protocol implementations, found {len(impl_matches)}"
161 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/elixir/test_elixir_ignored_dirs.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | from collections.abc import Generator
  3 | from pathlib import Path
  4 | 
  5 | import pytest
  6 | 
  7 | from solidlsp import SolidLanguageServer
  8 | from solidlsp.ls_config import Language
  9 | from test.conftest import start_ls_context
 10 | 
 11 | from . import EXPERT_UNAVAILABLE, EXPERT_UNAVAILABLE_REASON
 12 | 
 13 | # These marks will be applied to all tests in this module
 14 | pytestmark = [pytest.mark.elixir, pytest.mark.skipif(EXPERT_UNAVAILABLE, reason=f"Expert not available: {EXPERT_UNAVAILABLE_REASON}")]
 15 | 
 16 | # Skip slow tests in CI - they require multiple Expert instances which is too slow
 17 | IN_CI = bool(os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS"))
 18 | SKIP_SLOW_IN_CI = pytest.mark.skipif(
 19 |     IN_CI,
 20 |     reason="Slow tests skipped in CI - require multiple Expert instances (~60-90s each)",
 21 | )
 22 | 
 23 | 
 24 | @pytest.fixture(scope="session")
 25 | def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]:
 26 |     """Fixture to set up an LS for the elixir test repo with the 'scripts' directory ignored.
 27 | 
 28 |     Uses session scope to avoid restarting Expert for each test.
 29 |     """
 30 |     ignored_paths = ["scripts", "ignored_dir"]
 31 |     with start_ls_context(language=Language.ELIXIR, ignored_paths=ignored_paths) as ls:
 32 |         yield ls
 33 | 
 34 | 
 35 | @pytest.mark.slow
 36 | @SKIP_SLOW_IN_CI
 37 | def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
 38 |     """Tests that request_full_symbol_tree ignores the configured directory.
 39 | 
 40 |     Note: This test uses a separate Expert instance with custom ignored paths,
 41 |     which adds ~60-90s startup time.
 42 |     """
 43 |     root = ls_with_ignored_dirs.request_full_symbol_tree()[0]
 44 |     root_children = root["children"]
 45 |     children_names = {child["name"] for child in root_children}
 46 | 
 47 |     # Should have lib and test directories, but not scripts or ignored_dir
 48 |     expected_dirs = {"lib", "test"}
 49 |     assert expected_dirs.issubset(children_names), f"Expected {expected_dirs} to be in {children_names}"
 50 |     assert "scripts" not in children_names, f"scripts should not be in {children_names}"
 51 |     assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names}"
 52 | 
 53 | 
 54 | @pytest.mark.slow
 55 | @SKIP_SLOW_IN_CI
 56 | def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
 57 |     """Tests that find_references ignores the configured directory.
 58 | 
 59 |     Note: This test uses a separate Expert instance with custom ignored paths,
 60 |     which adds ~60-90s startup time.
 61 |     """
 62 |     # Location of User struct, which is referenced in scripts and ignored_dir
 63 |     definition_file = "lib/models.ex"
 64 | 
 65 |     # Find the User struct definition
 66 |     symbols = ls_with_ignored_dirs.request_document_symbols(definition_file).get_all_symbols_and_roots()
 67 |     user_symbol = None
 68 |     for symbol_group in symbols:
 69 |         user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None)
 70 |         if user_symbol:
 71 |             break
 72 | 
 73 |     if not user_symbol or "selectionRange" not in user_symbol:
 74 |         pytest.skip("User symbol not found for reference testing")
 75 | 
 76 |     sel_start = user_symbol["selectionRange"]["start"]
 77 |     references = ls_with_ignored_dirs.request_references(definition_file, sel_start["line"], sel_start["character"])
 78 | 
 79 |     # Assert that scripts and ignored_dir do not appear in the references
 80 |     assert not any("scripts" in ref["relativePath"] for ref in references), "scripts should be ignored"
 81 |     assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored"
 82 | 
 83 | 
 84 | @pytest.mark.slow
 85 | @SKIP_SLOW_IN_CI
 86 | @pytest.mark.parametrize("repo_path", [Language.ELIXIR], indirect=True)
 87 | def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
 88 |     """Tests that refs and symbols with glob patterns are ignored.
 89 | 
 90 |     Note: This test uses a separate Expert instance with custom ignored paths,
 91 |     which adds ~60-90s startup time.
 92 |     """
 93 |     ignored_paths = ["*cripts", "ignored_*"]  # codespell:ignore cripts
 94 |     with start_ls_context(language=Language.ELIXIR, repo_path=str(repo_path), ignored_paths=ignored_paths) as ls:
 95 | 
 96 |         # Same as in the above tests
 97 |         root = ls.request_full_symbol_tree()[0]
 98 |         root_children = root["children"]
 99 |         children_names = {child["name"] for child in root_children}
100 | 
101 |         # Should have lib and test directories, but not scripts or ignored_dir
102 |         expected_dirs = {"lib", "test"}
103 |         assert expected_dirs.issubset(children_names), f"Expected {expected_dirs} to be in {children_names}"
104 |         assert "scripts" not in children_names, f"scripts should not be in {children_names} (glob pattern)"
105 |         assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names} (glob pattern)"
106 | 
107 |         # Test that the refs and symbols with glob patterns are ignored
108 |         definition_file = "lib/models.ex"
109 | 
110 |         # Find the User struct definition
111 |         symbols = ls.request_document_symbols(definition_file).get_all_symbols_and_roots()
112 |         user_symbol = None
113 |         for symbol_group in symbols:
114 |             user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None)
115 |             if user_symbol:
116 |                 break
117 | 
118 |         if user_symbol and "selectionRange" in user_symbol:
119 |             sel_start = user_symbol["selectionRange"]["start"]
120 |             references = ls.request_references(definition_file, sel_start["line"], sel_start["character"])
121 | 
122 |             # Assert that scripts and ignored_dir do not appear in references
123 |             assert not any("scripts" in ref["relativePath"] for ref in references), "scripts should be ignored (glob)"
124 |             assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored (glob)"
125 | 
126 | 
127 | @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
128 | def test_default_ignored_directories(language_server: SolidLanguageServer):
129 |     """Test that default Elixir directories are ignored."""
130 |     # Test that Elixir-specific directories are ignored by default
131 |     assert language_server.is_ignored_dirname("_build"), "_build should be ignored"
132 |     assert language_server.is_ignored_dirname("deps"), "deps should be ignored"
133 |     assert language_server.is_ignored_dirname(".elixir_ls"), ".elixir_ls should be ignored"
134 |     assert language_server.is_ignored_dirname("cover"), "cover should be ignored"
135 |     assert language_server.is_ignored_dirname("node_modules"), "node_modules should be ignored"
136 | 
137 |     # Test that important directories are not ignored
138 |     assert not language_server.is_ignored_dirname("lib"), "lib should not be ignored"
139 |     assert not language_server.is_ignored_dirname("test"), "test should not be ignored"
140 |     assert not language_server.is_ignored_dirname("config"), "config should not be ignored"
141 |     assert not language_server.is_ignored_dirname("priv"), "priv should not be ignored"
142 | 
143 | 
144 | @pytest.mark.xfail(
145 |     reason="Expert 0.1.0 bug: document_symbols may return nil for some files (flaky)",
146 |     raises=Exception,
147 | )
148 | @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
149 | def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer):
150 |     """Test that symbol tree excludes build and dependency directories."""
151 |     symbol_tree = language_server.request_full_symbol_tree()
152 | 
153 |     if symbol_tree:
154 |         root = symbol_tree[0]
155 |         children_names = {child["name"] for child in root.get("children", [])}
156 | 
157 |         # Build and dependency directories should not appear
158 |         ignored_dirs = {"_build", "deps", ".elixir_ls", "cover", "node_modules"}
159 |         found_ignored = ignored_dirs.intersection(children_names)
160 |         assert len(found_ignored) == 0, f"Found ignored directories in symbol tree: {found_ignored}"
161 | 
162 |         # Important directories should appear
163 |         important_dirs = {"lib", "test"}
164 |         found_important = important_dirs.intersection(children_names)
165 |         assert len(found_important) > 0, f"Expected to find important directories: {important_dirs}, got: {children_names}"
166 | 
```

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

```python
  1 | """
  2 | Provides YAML specific instantiation of the LanguageServer class using yaml-language-server.
  3 | Contains various configurations and settings specific to YAML files.
  4 | """
  5 | 
  6 | import logging
  7 | import os
  8 | import pathlib
  9 | import shutil
 10 | import threading
 11 | from typing import Any
 12 | 
 13 | from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
 14 | from solidlsp.ls import SolidLanguageServer
 15 | from solidlsp.ls_config import LanguageServerConfig
 16 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 17 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 18 | from solidlsp.settings import SolidLSPSettings
 19 | 
 20 | log = logging.getLogger(__name__)
 21 | 
 22 | 
 23 | class YamlLanguageServer(SolidLanguageServer):
 24 |     """
 25 |     Provides YAML specific instantiation of the LanguageServer class using yaml-language-server.
 26 |     Contains various configurations and settings specific to YAML files.
 27 |     """
 28 | 
 29 |     @staticmethod
 30 |     def _determine_log_level(line: str) -> int:
 31 |         """Classify yaml-language-server stderr output to avoid false-positive errors."""
 32 |         line_lower = line.lower()
 33 | 
 34 |         # Known informational messages from yaml-language-server that aren't critical errors
 35 |         if any(
 36 |             [
 37 |                 "cannot find module" in line_lower and "package.json" in line_lower,  # Schema resolution - not critical
 38 |                 "no parser" in line_lower,  # Parser messages - informational
 39 |             ]
 40 |         ):
 41 |             return logging.DEBUG
 42 | 
 43 |         return SolidLanguageServer._determine_log_level(line)
 44 | 
 45 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 46 |         """
 47 |         Creates a YamlLanguageServer instance. This class is not meant to be instantiated directly.
 48 |         Use LanguageServer.create() instead.
 49 |         """
 50 |         yaml_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
 51 |         super().__init__(
 52 |             config,
 53 |             repository_root_path,
 54 |             ProcessLaunchInfo(cmd=yaml_lsp_executable_path, cwd=repository_root_path),
 55 |             "yaml",
 56 |             solidlsp_settings,
 57 |         )
 58 |         self.server_ready = threading.Event()
 59 | 
 60 |     @classmethod
 61 |     def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
 62 |         """
 63 |         Setup runtime dependencies for YAML Language Server and return the command to start the server.
 64 |         """
 65 |         # Verify both node and npm are installed
 66 |         is_node_installed = shutil.which("node") is not None
 67 |         assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
 68 |         is_npm_installed = shutil.which("npm") is not None
 69 |         assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
 70 | 
 71 |         deps = RuntimeDependencyCollection(
 72 |             [
 73 |                 RuntimeDependency(
 74 |                     id="yaml-language-server",
 75 |                     description="yaml-language-server package (Red Hat)",
 76 |                     command="npm install --prefix ./ [email protected]",
 77 |                     platform_id="any",
 78 |                 ),
 79 |             ]
 80 |         )
 81 | 
 82 |         # Install yaml-language-server if not already installed
 83 |         yaml_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "yaml-lsp")
 84 |         yaml_executable_path = os.path.join(yaml_ls_dir, "node_modules", ".bin", "yaml-language-server")
 85 | 
 86 |         # Handle Windows executable extension
 87 |         if os.name == "nt":
 88 |             yaml_executable_path += ".cmd"
 89 | 
 90 |         if not os.path.exists(yaml_executable_path):
 91 |             log.info(f"YAML Language Server executable not found at {yaml_executable_path}. Installing...")
 92 |             deps.install(yaml_ls_dir)
 93 |             log.info("YAML language server dependencies installed successfully")
 94 | 
 95 |         if not os.path.exists(yaml_executable_path):
 96 |             raise FileNotFoundError(
 97 |                 f"yaml-language-server executable not found at {yaml_executable_path}, something went wrong with the installation."
 98 |             )
 99 |         return f"{yaml_executable_path} --stdio"
100 | 
101 |     @staticmethod
102 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
103 |         """
104 |         Returns the initialize params for the YAML Language Server.
105 |         """
106 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
107 |         initialize_params = {
108 |             "locale": "en",
109 |             "capabilities": {
110 |                 "textDocument": {
111 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
112 |                     "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
113 |                     "definition": {"dynamicRegistration": True},
114 |                     "references": {"dynamicRegistration": True},
115 |                     "documentSymbol": {
116 |                         "dynamicRegistration": True,
117 |                         "hierarchicalDocumentSymbolSupport": True,
118 |                         "symbolKind": {"valueSet": list(range(1, 27))},
119 |                     },
120 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
121 |                     "codeAction": {"dynamicRegistration": True},
122 |                 },
123 |                 "workspace": {
124 |                     "workspaceFolders": True,
125 |                     "didChangeConfiguration": {"dynamicRegistration": True},
126 |                     "symbol": {"dynamicRegistration": True},
127 |                 },
128 |             },
129 |             "processId": os.getpid(),
130 |             "rootPath": repository_absolute_path,
131 |             "rootUri": root_uri,
132 |             "workspaceFolders": [
133 |                 {
134 |                     "uri": root_uri,
135 |                     "name": os.path.basename(repository_absolute_path),
136 |                 }
137 |             ],
138 |             "initializationOptions": {
139 |                 "yaml": {
140 |                     "schemaStore": {"enable": True, "url": "https://www.schemastore.org/api/json/catalog.json"},
141 |                     "format": {"enable": True},
142 |                     "validate": True,
143 |                     "hover": True,
144 |                     "completion": True,
145 |                 }
146 |             },
147 |         }
148 |         return initialize_params  # type: ignore
149 | 
150 |     def _start_server(self) -> None:
151 |         """
152 |         Starts the YAML Language Server, waits for the server to be ready and yields the LanguageServer instance.
153 |         """
154 | 
155 |         def register_capability_handler(params: Any) -> None:
156 |             return
157 | 
158 |         def do_nothing(params: Any) -> None:
159 |             return
160 | 
161 |         def window_log_message(msg: dict) -> None:
162 |             log.info(f"LSP: window/logMessage: {msg}")
163 | 
164 |         self.server.on_request("client/registerCapability", register_capability_handler)
165 |         self.server.on_notification("window/logMessage", window_log_message)
166 |         self.server.on_notification("$/progress", do_nothing)
167 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
168 | 
169 |         log.info("Starting YAML server process")
170 |         self.server.start()
171 |         initialize_params = self._get_initialize_params(self.repository_root_path)
172 | 
173 |         log.info("Sending initialize request from LSP client to LSP server and awaiting response")
174 |         init_response = self.server.send.initialize(initialize_params)
175 |         log.debug(f"Received initialize response from YAML server: {init_response}")
176 | 
177 |         # Verify document symbol support is available
178 |         if "documentSymbolProvider" in init_response["capabilities"]:
179 |             log.info("YAML server supports document symbols")
180 |         else:
181 |             log.warning("Warning: YAML server does not report document symbol support")
182 | 
183 |         self.server.notify.initialized({})
184 | 
185 |         # YAML language server is ready immediately after initialization
186 |         log.info("YAML server initialization complete")
187 |         self.server_ready.set()
188 |         self.completions_available.set()
189 | 
```

--------------------------------------------------------------------------------
/src/serena/tools/jetbrains_tools.py:
--------------------------------------------------------------------------------

```python
  1 | from collections import defaultdict
  2 | from typing import Any
  3 | 
  4 | from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicRead
  5 | from serena.tools.jetbrains_plugin_client import JetBrainsPluginClient
  6 | 
  7 | 
  8 | class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
  9 |     """
 10 |     Performs a global (or local) search for symbols using the JetBrains backend
 11 |     """
 12 | 
 13 |     def apply(
 14 |         self,
 15 |         name_path_pattern: str,
 16 |         depth: int = 0,
 17 |         relative_path: str | None = None,
 18 |         include_body: bool = False,
 19 |         search_deps: bool = False,
 20 |         max_answer_chars: int = -1,
 21 |     ) -> str:
 22 |         """
 23 |         Retrieves information on all symbols/code entities (classes, methods, etc.) based on the given name path pattern.
 24 |         The returned symbol information can be used for edits or further queries.
 25 |         Specify `depth > 0` to retrieve children (e.g., methods of a class).
 26 | 
 27 |         A name path is a path in the symbol tree *within a source file*.
 28 |         For example, the method `my_method` defined in class `MyClass` would have the name path `MyClass/my_method`.
 29 |         If a symbol is overloaded (e.g., in Java), a 0-based index is appended (e.g. "MyClass/my_method[0]") to
 30 |         uniquely identify it.
 31 | 
 32 |         To search for a symbol, you provide a name path pattern that is used to match against name paths.
 33 |         It can be
 34 |          * a simple name (e.g. "method"), which will match any symbol with that name
 35 |          * a relative path like "class/method", which will match any symbol with that name path suffix
 36 |          * an absolute name path "/class/method" (absolute name path), which requires an exact match of the full name path within the source file.
 37 |         Append an index `[i]` to match a specific overload only, e.g. "MyClass/my_method[1]".
 38 | 
 39 |         :param name_path_pattern: the name path matching pattern (see above)
 40 |         :param depth: depth up to which descendants shall be retrieved (e.g. use 1 to also retrieve immediate children;
 41 |             for the case where the symbol is a class, this will return its methods).
 42 |             Default 0.
 43 |         :param relative_path: Optional. Restrict search to this file or directory. If None, searches entire codebase.
 44 |             If a directory is passed, the search will be restricted to the files in that directory.
 45 |             If a file is passed, the search will be restricted to that file.
 46 |             If you have some knowledge about the codebase, you should use this parameter, as it will significantly
 47 |             speed up the search as well as reduce the number of results.
 48 |         :param include_body: If True, include the symbol's source code. Use judiciously.
 49 |         :param search_deps: If True, also search in project dependencies (e.g., libraries).
 50 |         :param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned.
 51 |             -1 means the default value from the config will be used.
 52 |         :return: JSON string: a list of symbols (with locations) matching the name.
 53 |         """
 54 |         if relative_path == ".":
 55 |             relative_path = None
 56 |         with JetBrainsPluginClient.from_project(self.project) as client:
 57 |             response_dict = client.find_symbol(
 58 |                 name_path=name_path_pattern,
 59 |                 relative_path=relative_path,
 60 |                 depth=depth,
 61 |                 include_body=include_body,
 62 |                 search_deps=search_deps,
 63 |             )
 64 |             result = self._to_json(response_dict)
 65 |         return self._limit_length(result, max_answer_chars)
 66 | 
 67 | 
 68 | class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
 69 |     """
 70 |     Finds symbols that reference the given symbol using the JetBrains backend
 71 |     """
 72 | 
 73 |     def apply(
 74 |         self,
 75 |         name_path: str,
 76 |         relative_path: str,
 77 |         max_answer_chars: int = -1,
 78 |     ) -> str:
 79 |         """
 80 |         Finds symbols that reference the symbol at the given `name_path`.
 81 |         The result will contain metadata about the referencing symbols.
 82 | 
 83 |         :param name_path: name path of the symbol for which to find references; matching logic as described in find symbol tool.
 84 |         :param relative_path: the relative path to the file containing the symbol for which to find references.
 85 |             Note that here you can't pass a directory but must pass a file.
 86 |         :param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned. -1 means the
 87 |             default value from the config will be used.
 88 |         :return: a list of JSON objects with the symbols referencing the requested symbol
 89 |         """
 90 |         with JetBrainsPluginClient.from_project(self.project) as client:
 91 |             response_dict = client.find_references(
 92 |                 name_path=name_path,
 93 |                 relative_path=relative_path,
 94 |             )
 95 |             result = self._to_json(response_dict)
 96 |         return self._limit_length(result, max_answer_chars)
 97 | 
 98 | 
 99 | class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
100 |     """
101 |     Retrieves an overview of the top-level symbols within a specified file using the JetBrains backend
102 |     """
103 | 
104 |     USE_COMPACT_FORMAT = True
105 | 
106 |     @staticmethod
107 |     def _transform_symbols_to_compact_format(symbols: list[dict[str, Any]]) -> dict[str, list]:
108 |         """
109 |         Transform symbol overview from verbose format to compact grouped format.
110 | 
111 |         Groups symbols by kind and uses names instead of full symbol objects.
112 |         For symbols with children, creates nested dictionaries.
113 | 
114 |         The name_path can be inferred from the hierarchical structure:
115 |         - Top-level symbols: name_path = name
116 |         - Nested symbols: name_path = parent_name + "/" + name
117 |         For example, "convert" under class "ProjectType" has name_path "ProjectType/convert".
118 |         """
119 |         result = defaultdict(list)
120 | 
121 |         for symbol in symbols:
122 |             kind = symbol.get("type", "Unknown")
123 |             name_path = symbol["name_path"]
124 |             name = name_path.split("/")[-1]
125 |             children = symbol.get("children", [])
126 | 
127 |             if children:
128 |                 # Symbol has children: create nested dict {name: children_dict}
129 |                 children_dict = JetBrainsGetSymbolsOverviewTool._transform_symbols_to_compact_format(children)
130 |                 result[kind].append({name: children_dict})
131 |             else:
132 |                 # Symbol has no children: just add the name
133 |                 result[kind].append(name)
134 | 
135 |         return result
136 | 
137 |     def apply(
138 |         self,
139 |         relative_path: str,
140 |         depth: int = 0,
141 |         max_answer_chars: int = -1,
142 |     ) -> str:
143 |         """
144 |         Gets an overview of the top-level symbols in the given file.
145 |         Calling this is often a good idea before more targeted reading, searching or editing operations on the code symbols.
146 |         Before requesting a symbol overview, it is usually a good idea to narrow down the scope of the overview
147 |         by first understanding the basic directory structure of the repository that you can get from memories
148 |         or by using the `list_dir` and `find_file` tools (or similar).
149 | 
150 |         :param relative_path: the relative path to the file to get the overview of
151 |         :param depth: depth up to which descendants shall be retrieved (e.g., use 1 to also retrieve immediate children).
152 |         :param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned.
153 |             -1 means the default value from the config will be used.
154 |         :return: a JSON object containing the symbols grouped by kind in a compact format.
155 |         """
156 |         with JetBrainsPluginClient.from_project(self.project) as client:
157 |             response_dict = client.get_symbols_overview(relative_path=relative_path, depth=depth)
158 |         symbols = response_dict["symbols"]
159 |         if self.USE_COMPACT_FORMAT:
160 |             compact_symbols = self._transform_symbols_to_compact_format(symbols)
161 |             result = self._to_json(compact_symbols)
162 |         else:
163 |             result = self._to_json(symbols)
164 |         return self._limit_length(result, max_answer_chars)
165 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/al/test_al_basic.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | 
  3 | import pytest
  4 | 
  5 | from solidlsp import SolidLanguageServer
  6 | from solidlsp.ls_config import Language
  7 | from solidlsp.ls_utils import SymbolUtils
  8 | from test.conftest import language_tests_enabled
  9 | 
 10 | pytestmark = [pytest.mark.al, pytest.mark.skipif(not language_tests_enabled(Language.AL), reason="AL tests are disabled")]
 11 | 
 12 | 
 13 | @pytest.mark.al
 14 | class TestALLanguageServer:
 15 |     @pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
 16 |     def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
 17 |         """Test that AL Language Server can find symbols in the test repository."""
 18 |         symbols = language_server.request_full_symbol_tree()
 19 | 
 20 |         # Check for table symbols - AL returns full object names like 'Table 50000 "TEST Customer"'
 21 |         assert SymbolUtils.symbol_tree_contains_name(symbols, 'Table 50000 "TEST Customer"'), "TEST Customer table not found in symbol tree"
 22 | 
 23 |         # Check for page symbols
 24 |         assert SymbolUtils.symbol_tree_contains_name(
 25 |             symbols, 'Page 50001 "TEST Customer Card"'
 26 |         ), "TEST Customer Card page not found in symbol tree"
 27 |         assert SymbolUtils.symbol_tree_contains_name(
 28 |             symbols, 'Page 50002 "TEST Customer List"'
 29 |         ), "TEST Customer List page not found in symbol tree"
 30 | 
 31 |         # Check for codeunit symbols
 32 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Codeunit 50000 CustomerMgt"), "CustomerMgt codeunit not found in symbol tree"
 33 |         assert SymbolUtils.symbol_tree_contains_name(
 34 |             symbols, "Codeunit 50001 PaymentProcessorImpl"
 35 |         ), "PaymentProcessorImpl codeunit not found in symbol tree"
 36 | 
 37 |         # Check for enum symbol
 38 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Enum 50000 CustomerType"), "CustomerType enum not found in symbol tree"
 39 | 
 40 |         # Check for interface symbol
 41 |         assert SymbolUtils.symbol_tree_contains_name(
 42 |             symbols, "Interface IPaymentProcessor"
 43 |         ), "IPaymentProcessor interface not found in symbol tree"
 44 | 
 45 |     @pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
 46 |     def test_find_table_fields(self, language_server: SolidLanguageServer) -> None:
 47 |         """Test that AL Language Server can find fields within a table."""
 48 |         file_path = os.path.join("src", "Tables", "Customer.Table.al")
 49 |         symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
 50 | 
 51 |         # AL tables should have their fields as child symbols
 52 |         customer_table = None
 53 |         _all_symbols, root_symbols = symbols
 54 |         for sym in root_symbols:
 55 |             if "TEST Customer" in sym.get("name", ""):
 56 |                 customer_table = sym
 57 |                 break
 58 | 
 59 |         assert customer_table is not None, "Could not find TEST Customer table symbol"
 60 | 
 61 |         # Check for field symbols (AL nests fields under a "fields" group)
 62 |         if "children" in customer_table:
 63 |             # Find the fields group
 64 |             fields_group = None
 65 |             for child in customer_table.get("children", []):
 66 |                 if child.get("name") == "fields":
 67 |                     fields_group = child
 68 |                     break
 69 | 
 70 |             assert fields_group is not None, "Fields group not found in Customer table"
 71 | 
 72 |             # Check actual field names
 73 |             if "children" in fields_group:
 74 |                 field_names = [child.get("name", "") for child in fields_group.get("children", [])]
 75 |                 assert any("Name" in name for name in field_names), f"Name field not found. Fields: {field_names}"
 76 |                 assert any("Balance" in name for name in field_names), f"Balance field not found. Fields: {field_names}"
 77 | 
 78 |     @pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
 79 |     def test_find_procedures(self, language_server: SolidLanguageServer) -> None:
 80 |         """Test that AL Language Server can find procedures in codeunits."""
 81 |         file_path = os.path.join("src", "Codeunits", "CustomerMgt.Codeunit.al")
 82 |         symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
 83 | 
 84 |         # Find the codeunit symbol - AL returns 'Codeunit 50000 CustomerMgt'
 85 |         codeunit_symbol = None
 86 |         _all_symbols, root_symbols = symbols
 87 |         for sym in root_symbols:
 88 |             if "CustomerMgt" in sym.get("name", ""):
 89 |                 codeunit_symbol = sym
 90 |                 break
 91 | 
 92 |         assert codeunit_symbol is not None, "Could not find CustomerMgt codeunit symbol"
 93 | 
 94 |         # Check for procedure symbols (if hierarchical)
 95 |         if "children" in codeunit_symbol:
 96 |             procedure_names = [child.get("name", "") for child in codeunit_symbol.get("children", [])]
 97 |             assert any("CreateCustomer" in name for name in procedure_names), "CreateCustomer procedure not found"
 98 |             # Note: UpdateCustomerBalance doesn't exist in our test repo, check for actual procedures
 99 |             assert any("TestNoSeries" in name for name in procedure_names), "TestNoSeries procedure not found"
100 | 
101 |     @pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
102 |     def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
103 |         """Test that AL Language Server can find references to symbols."""
104 |         # Find references to the Customer table from the CustomerMgt codeunit
105 |         table_file = os.path.join("src", "Tables", "Customer.Table.al")
106 |         symbols = language_server.request_document_symbols(table_file).get_all_symbols_and_roots()
107 | 
108 |         # Find the Customer table symbol
109 |         customer_symbol = None
110 |         _all_symbols, root_symbols = symbols
111 |         for sym in root_symbols:
112 |             if "TEST Customer" in sym.get("name", ""):
113 |                 customer_symbol = sym
114 |                 break
115 | 
116 |         if customer_symbol and "selectionRange" in customer_symbol:
117 |             sel_start = customer_symbol["selectionRange"]["start"]
118 |             refs = language_server.request_references(table_file, sel_start["line"], sel_start["character"])
119 | 
120 |             # The Customer table should be referenced in CustomerMgt.Codeunit.al
121 |             assert any(
122 |                 "CustomerMgt.Codeunit.al" in ref.get("relativePath", "") for ref in refs
123 |             ), "Customer table should be referenced in CustomerMgt.Codeunit.al"
124 | 
125 |             # It should also be referenced in CustomerCard.Page.al
126 |             assert any(
127 |                 "CustomerCard.Page.al" in ref.get("relativePath", "") for ref in refs
128 |             ), "Customer table should be referenced in CustomerCard.Page.al"
129 | 
130 |     @pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
131 |     def test_cross_file_symbols(self, language_server: SolidLanguageServer) -> None:
132 |         """Test that AL Language Server can handle cross-file symbol relationships."""
133 |         # Get all symbols to verify cross-file visibility
134 |         symbols = language_server.request_full_symbol_tree()
135 | 
136 |         # Count how many AL-specific symbols we found
137 |         al_symbols = []
138 | 
139 |         def collect_symbols(syms):
140 |             for sym in syms:
141 |                 if isinstance(sym, dict):
142 |                     name = sym.get("name", "")
143 |                     # Look for AL object names (Table, Page, Codeunit, etc.)
144 |                     if any(keyword in name for keyword in ["Table", "Page", "Codeunit", "Enum", "Interface"]):
145 |                         al_symbols.append(name)
146 |                     if "children" in sym:
147 |                         collect_symbols(sym["children"])
148 | 
149 |         collect_symbols(symbols)
150 | 
151 |         # We should find symbols from multiple files
152 |         assert len(al_symbols) >= 5, f"Expected at least 5 AL object symbols, found {len(al_symbols)}: {al_symbols}"
153 | 
154 |         # Verify we have symbols from different AL object types
155 |         has_table = any("Table" in s for s in al_symbols)
156 |         has_page = any("Page" in s for s in al_symbols)
157 |         has_codeunit = any("Codeunit" in s for s in al_symbols)
158 | 
159 |         assert has_table, f"No Table symbols found in: {al_symbols}"
160 |         assert has_page, f"No Page symbols found in: {al_symbols}"
161 |         assert has_codeunit, f"No Codeunit symbols found in: {al_symbols}"
162 | 
```

--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------

```yaml
  1 | # Book settings
  2 | # Learn more at https://jupyterbook.org/customize/config.html
  3 | 
  4 | #######################################################################################
  5 | # A default configuration that will be loaded for all jupyter books
  6 | # Users are expected to override these values in their own `_config.yml` file.
  7 | # This is also the "master list" of all allowed keys and values.
  8 | 
  9 | #######################################################################################
 10 | # Book settings
 11 | title                       : Serena Documentation  # The title of the book. Will be placed in the left navbar.
 12 | author                      : Oraios AI & Oraios Software  # The author of the book
 13 | copyright                   : "2025 by Serena contributors"  # Copyright year to be placed in the footer
 14 | # Patterns to skip when building the book. Can be glob-style (e.g. "*skip.ipynb")
 15 | exclude_patterns            : ['**.ipynb_checkpoints', '.DS_Store', 'Thumbs.db', '_build', 'jupyter_execute', '.jupyter_cache', '.pytest_cache', 'docs/autogen_rst.py', 'docs/create_toc.py']
 16 | # Auto-exclude files not in the toc
 17 | only_build_toc_files        : true
 18 | 
 19 | #######################################################################################
 20 | # Execution settings
 21 | execute:
 22 |   # NOTE: Notebooks are not executed, because test_notebooks.py executes them and stores them with outputs in the docs/ folder
 23 |   # NOTE: If changed, repeat below in `nb_execution_mode`.
 24 |   execute_notebooks         : "off"  # Whether to execute notebooks at build time. Must be one of ("auto", "force", "cache", "off")
 25 |   cache                     : ""    # A path to the jupyter cache that will be used to store execution artifacts. Defaults to `_build/.jupyter_cache/`
 26 |   exclude_patterns          : []    # A list of patterns to *skip* in execution (e.g. a notebook that takes a really long time)
 27 |   timeout                   : 1000    # The maximum time (in seconds) each notebook cell is allowed to run.
 28 |   run_in_temp               : false # If `True`, then a temporary directory will be created and used as the command working directory (cwd),
 29 |                                     # otherwise the notebook's parent directory will be the cwd.
 30 |   allow_errors              : true # If `False`, when a code cell raises an error the execution is stopped, otherwise all cells are always run.
 31 |   stderr_output             : show  # One of 'show', 'remove', 'remove-warn', 'warn', 'error', 'severe'
 32 | 
 33 | #######################################################################################
 34 | # Parse and render settings
 35 | parse:
 36 |   myst_enable_extensions: # default extensions to enable in the myst parser. See https://myst-parser.readthedocs.io/en/latest/using/syntax-optional.html
 37 |     - amsmath
 38 |     - colon_fence
 39 |     # - deflist
 40 |     - dollarmath
 41 |     # - html_admonition
 42 |     # - html_image
 43 |     - linkify
 44 |     # - replacements
 45 |     # - smartquotes
 46 |     - substitution
 47 |     - tasklist
 48 |     - html_admonition
 49 |     - html_image
 50 |   myst_url_schemes: [ mailto, http, https ] # URI schemes that will be recognised as external URLs in Markdown links
 51 |   myst_dmath_double_inline: true  # Allow display math ($$) within an inline context
 52 | 
 53 | #######################################################################################
 54 | # HTML-specific settings
 55 | html:
 56 |   favicon                   : "../src/serena/resources/dashboard/serena-icon-32.png"
 57 |   use_edit_page_button      : false  # Whether to add an "edit this page" button to pages. If `true`, repository information in repository: must be filled in
 58 |   use_repository_button     : false  # Whether to add a link to your repository button
 59 |   use_issues_button         : false  # Whether to add an "open an issue" button
 60 |   use_multitoc_numbering    : true   # Continuous numbering across parts/chapters
 61 |   use_darkmode_button       : false
 62 |   extra_footer              : ""
 63 |   home_page_in_navbar       : true  # Whether to include your home page in the left Navigation Bar
 64 |   baseurl                   : "https://oraios.github.io/serena/"
 65 |   comments:
 66 |     hypothesis              : false
 67 |     utterances              : false
 68 |   announcement              : "" # A banner announcement at the top of the site.
 69 | 
 70 | #######################################################################################
 71 | # LaTeX-specific settings
 72 | latex:
 73 |   latex_engine              : pdflatex  # one of 'pdflatex', 'xelatex' (recommended for unicode), 'luatex', 'platex', 'uplatex'
 74 |   use_jupyterbook_latex     : true # use sphinx-jupyterbook-latex for pdf builds as default
 75 |   targetname                : book.tex
 76 | # Add a bibtex file so that we can create citations
 77 | #bibtex_bibfiles:
 78 | #  - refs.bib
 79 | 
 80 | #######################################################################################
 81 | # Launch button settings
 82 | launch_buttons:
 83 |   notebook_interface        : classic  # The interface interactive links will activate ["classic", "jupyterlab"]
 84 |   binderhub_url             : ""  # The URL of the BinderHub (e.g., https://mybinder.org)
 85 |   jupyterhub_url            : ""  # The URL of the JupyterHub (e.g., https://datahub.berkeley.edu)
 86 |   thebe                     : false  # Add a thebe button to pages (requires the repository to run on Binder)
 87 |   colab_url                 : "https://colab.research.google.com"
 88 | 
 89 | repository:
 90 |   url                       : https://github.com/oraios/serena  # The URL to your book's repository
 91 |   path_to_book              : docs  # A path to your book's folder, relative to the repository root.
 92 |   branch                    : main  # Which branch of the repository should be used when creating links
 93 | 
 94 | #######################################################################################
 95 | # Advanced and power-user settings
 96 | sphinx:
 97 |   extra_extensions          :
 98 |     - sphinx.ext.autodoc
 99 |     - sphinx.ext.viewcode
100 |     - sphinx_toolbox.more_autodoc.sourcelink
101 |     #- sphinxcontrib.spelling
102 |   local_extensions          :   # A list of local extensions to load by sphinx specified by "name: path" items
103 |   recursive_update          : false # A boolean indicating whether to overwrite the Sphinx config (true) or recursively update (false)
104 |   config                    :   # key-value pairs to directly over-ride the Sphinx configuration
105 |     master_doc: "01-about/000_intro.md"
106 |     html_theme_options:
107 |       logo:
108 |         image_light: ../resources/serena-logo.svg
109 |         image_dark: ../resources/serena-logo-dark-mode.svg
110 |     autodoc_typehints_format: "short"
111 |     autodoc_member_order: "bysource"
112 |     autoclass_content: "both"
113 |     autodoc_default_options:
114 |       show-inheritance: True
115 |     autodoc_show_sourcelink: True
116 |     add_module_names: False
117 |     github_username: oraios
118 |     github_repository: serena
119 |     nb_execution_mode: "off"
120 |     nb_merge_streams: True  # This is important for cell outputs to appear as single blocks rather than one block per line
121 |     python_use_unqualified_type_names: True
122 |     nb_mime_priority_overrides: [
123 |       [ 'html', 'application/vnd.jupyter.widget-view+json', 10 ],
124 |       [ 'html', 'application/javascript', 20 ],
125 |       [ 'html', 'text/html', 30 ],
126 |       [ 'html', 'text/latex', 40 ],
127 |       [ 'html', 'image/svg+xml', 50 ],
128 |       [ 'html', 'image/png', 60 ],
129 |       [ 'html', 'image/jpeg', 70 ],
130 |       [ 'html', 'text/markdown', 80 ],
131 |       [ 'html', 'text/plain', 90 ],
132 |       [ 'spelling', 'application/vnd.jupyter.widget-view+json', 10 ],
133 |       [ 'spelling', 'application/javascript', 20 ],
134 |       [ 'spelling', 'text/html', 30 ],
135 |       [ 'spelling', 'text/latex', 40 ],
136 |       [ 'spelling', 'image/svg+xml', 50 ],
137 |       [ 'spelling', 'image/png', 60 ],
138 |       [ 'spelling', 'image/jpeg', 70 ],
139 |       [ 'spelling', 'text/markdown', 80 ],
140 |       [ 'spelling', 'text/plain', 90 ],
141 |     ]
142 |     mathjax_path: https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
143 |     mathjax3_config:
144 |       loader: { load: [ '[tex]/configmacros' ] }
145 |       tex:
146 |         packages: { '[+]': [ 'configmacros' ] }
147 |         macros:
148 |           vect: ["{\\mathbf{\\boldsymbol{#1}} }", 1]
149 |           E: "{\\mathbb{E}}"
150 |           P: "{\\mathbb{P}}"
151 |           R: "{\\mathbb{R}}"
152 |           abs: ["{\\left| #1 \\right|}", 1]
153 |           simpl: ["{\\Delta^{#1} }", 1]
154 |           amax: "{\\text{argmax}}"
155 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
  1 | [build-system]
  2 | build-backend = "hatchling.build"
  3 | requires = ["hatchling"]
  4 | 
  5 | [project]
  6 | name = "serena-agent"
  7 | version = "0.1.4"
  8 | description = ""
  9 | authors = [{ name = "Oraios AI", email = "[email protected]" }]
 10 | readme = "README.md"
 11 | requires-python = ">=3.11, <3.12"
 12 | classifiers = [
 13 |   "License :: OSI Approved :: MIT License",
 14 |   "Programming Language :: Python :: 3.11",
 15 | ]
 16 | dependencies = [
 17 |   "requests>=2.32.3,<3",
 18 |   "pyright>=1.1.396,<2",
 19 |   "fortls>=3.2.2",
 20 |   "overrides>=7.7.0,<8",
 21 |   "python-dotenv>=1.0.0, <2",
 22 |   "mcp==1.23.0",
 23 |   "flask>=3.0.0",
 24 |   "sensai-utils>=1.5.0",
 25 |   "pydantic>=2.10.6",
 26 |   "types-pyyaml>=6.0.12.20241230",
 27 |   "pyyaml>=6.0.2",
 28 |   "ruamel.yaml==0.18.14",
 29 |   "jinja2>=3.1.6",
 30 |   "dotenv>=0.9.9",
 31 |   "pathspec>=0.12.1",
 32 |   "psutil>=7.0.0",
 33 |   "docstring_parser>=0.16",
 34 |   "joblib>=1.5.1",
 35 |   "tqdm>=4.67.1",
 36 |   "tiktoken>=0.9.0",
 37 |   "anthropic>=0.54.0",
 38 | ]
 39 | 
 40 | [[tool.uv.index]]
 41 | name = "testpypi"
 42 | url = "https://test.pypi.org/simple/"
 43 | publish-url = "https://test.pypi.org/legacy/"
 44 | explicit = true
 45 | 
 46 | [project.scripts]
 47 | serena = "serena.cli:top_level"
 48 | serena-mcp-server = "serena.cli:start_mcp_server"
 49 | index-project = "serena.cli:index_project"        # deprecated
 50 | 
 51 | [project.license]
 52 | text = "MIT"
 53 | 
 54 | [project.optional-dependencies]
 55 | dev = [
 56 |   "black[jupyter]>=23.7.0",
 57 |   "jinja2",
 58 |   # In version 1.0.4 we get a NoneType error related to some config conversion (yml_analytics is None and should be a list)
 59 |   "mypy>=1.16.1",
 60 |   "poethepoet>=0.20.0",
 61 |   "pytest>=8.0.2",
 62 |   "pytest-xdist>=3.5.0",
 63 |   "ruff>=0.0.285",
 64 |   "toml-sort>=0.24.2",
 65 |   "types-pyyaml>=6.0.12.20241230",
 66 |   "syrupy>=4.9.1",
 67 |   "types-requests>=2.32.4.20241230",
 68 |   # docs
 69 |   "sphinx>=7,<8",
 70 |   "sphinx_rtd_theme>=0.5.1",
 71 |   "sphinx-toolbox==3.7.0",
 72 |   "jupyter-book>=1,<2",
 73 |   "nbsphinx",
 74 |   "pyinstrument",
 75 |   "pytest-timeout>=2.4.0",
 76 | ]
 77 | agno = ["agno>=2.2.1", "sqlalchemy>=2.0.40"]
 78 | google = ["google-genai>=1.8.0"]
 79 | 
 80 | [project.urls]
 81 | Homepage = "https://github.com/oraios/serena"
 82 | 
 83 | [tool.hatch.build.targets.wheel]
 84 | packages = ["src/serena", "src/interprompt", "src/solidlsp"]
 85 | 
 86 | [tool.black]
 87 | line-length = 140
 88 | target-version = ["py311"]
 89 | exclude = '''
 90 | /(
 91 |     src/solidlsp/language_servers/.*/static|src/multilspy
 92 | )/
 93 | '''
 94 | 
 95 | [tool.doc8]
 96 | max-line-length = 1000
 97 | 
 98 | [tool.mypy]
 99 | allow_redefinition = true
100 | check_untyped_defs = true
101 | disallow_incomplete_defs = true
102 | disallow_untyped_defs = true
103 | ignore_missing_imports = true
104 | no_implicit_optional = true
105 | pretty = true
106 | show_error_codes = true
107 | show_error_context = true
108 | show_traceback = true
109 | strict_equality = true
110 | strict_optional = true
111 | warn_no_return = true
112 | warn_redundant_casts = true
113 | warn_unreachable = true
114 | warn_unused_configs = true
115 | warn_unused_ignores = false
116 | exclude = "^build/|^docs/"
117 | 
118 | [tool.poe.env]
119 | PYDEVD_DISABLE_FILE_VALIDATION = "1"
120 | 
121 | [tool.poe.tasks]
122 | # Uses PYTEST_MARKERS env var for default markers
123 | # For custom markers, one can either adjust the env var or just use -m option in the command line,
124 | # as the second -m option will override the first one.
125 | test = "pytest test -vv"
126 | _black_check = "black --check src scripts test"
127 | _ruff_check = "ruff check src scripts test"
128 | _black_format = "black src scripts test"
129 | _ruff_format = "ruff check --fix src scripts test"
130 | lint = ["_black_check", "_ruff_check"]
131 | format = ["_ruff_format", "_black_format"]
132 | _mypy = "mypy src/serena src/solidlsp"
133 | type-check = ["_mypy"]
134 | # docs
135 | _autogen_rst = "python docs/autogen_rst.py"
136 | _sphinx_build = "sphinx-build -b html docs docs/_build -W --keep-going"
137 | _jb_generate_toc = "python docs/create_toc.py"
138 | _jb_generate_config = "jupyter-book config sphinx docs/"
139 | doc-clean = "rm -rf docs/_build docs/03_api"
140 | doc-generate-files = ["_autogen_rst", "_jb_generate_toc", "_jb_generate_config"]
141 | doc-build = ["doc-clean", "doc-generate-files", "_sphinx_build"]
142 | 
143 | [tool.ruff]
144 | target-version = "py311"
145 | line-length = 140
146 | exclude = ["src/solidlsp/language_servers/**/static", "src/multilspy"]
147 | 
148 | [tool.ruff.format]
149 | quote-style = "double"
150 | indent-style = "space"
151 | line-ending = "auto"
152 | skip-magic-trailing-comma = false
153 | docstring-code-format = true
154 | 
155 | [tool.ruff.lint]
156 | select = [
157 |   "ASYNC",
158 |   "B",
159 |   "C4",
160 |   "C90",
161 |   "COM",
162 |   "D",
163 |   "DTZ",
164 |   "E",
165 |   "F",
166 |   "FLY",
167 |   "G",
168 |   "I",
169 |   "ISC",
170 |   "PIE",
171 |   "PLC",
172 |   "PLE",
173 |   "PLW",
174 |   "RET",
175 |   "RUF",
176 |   "RSE",
177 |   "SIM",
178 |   "TID",
179 |   "UP",
180 |   "W",
181 |   "YTT",
182 | ]
183 | ignore = [
184 |   "PLC0415",
185 |   "RUF002",
186 |   "RUF005",
187 |   "RUF059",
188 |   "SIM118",
189 |   "SIM108",
190 |   "E501",
191 |   "E741",
192 |   "B008",
193 |   "B011",
194 |   "B028",
195 |   "D100",
196 |   "D101",
197 |   "D102",
198 |   "D103",
199 |   "D104",
200 |   "D105",
201 |   "D107",
202 |   "D200",
203 |   "D203",
204 |   "D213",
205 |   "D401",
206 |   "D402",
207 |   "DTZ005",
208 |   "E402",
209 |   "E501",
210 |   "E701",
211 |   "E731",
212 |   "C408",
213 |   "E203",
214 |   "G004",
215 |   "RET505",
216 |   "D106",
217 |   "D205",
218 |   "D212",
219 |   "PLW2901",
220 |   "B027",
221 |   "D404",
222 |   "D407",
223 |   "D408",
224 |   "D409",
225 |   "D400",
226 |   "D415",
227 |   "COM812",
228 |   "RET503",
229 |   "RET504",
230 |   "F403",
231 |   "F405",
232 |   "C401",
233 |   "C901",
234 |   "ASYNC230",
235 |   "ISC003",
236 |   "B024",
237 |   "B007",
238 |   "SIM102",
239 |   "W291",
240 |   "W293",
241 |   "B009",
242 |   "SIM103",   # forbids multiple returns
243 |   "SIM110",   # requires use of any(...) instead of for-loop
244 |   "G001",     # forbids str.format in log statements
245 |   "E722",     # forbids unspecific except clause
246 |   "SIM105",   # forbids empty/general except clause
247 |   "SIM113",   # wants to enforce use of enumerate
248 |   "E712",     # forbids equality comparison with True/False
249 |   "UP007",    # forbids some uses of Union
250 |   "TID252",   # forbids relative imports
251 |   "B904",     # forces use of raise from other_exception
252 |   "RUF012",   # forbids mutable attributes as ClassVar
253 |   "SIM117",   # forbids nested with statements
254 |   "C400",     # wants to unnecessarily force use of list comprehension
255 |   "UP037",    # can incorrectly (!) convert quoted type to unquoted type, causing an error
256 |   "UP045",    # imposes T | None instead of Optional[T]
257 |   "UP031",    # forbids % operator to format strings
258 | ]
259 | unfixable = ["F841", "F601", "F602", "B018"]
260 | extend-fixable = ["F401", "B905", "W291"]
261 | 
262 | [tool.ruff.lint.mccabe]
263 | max-complexity = 20
264 | 
265 | [tool.ruff.lint.per-file-ignores]
266 | "tests/**" = ["D103"]
267 | "scripts/**" = ["D103"]
268 | 
269 | [tool.pytest.ini_options]
270 | addopts = "--snapshot-patch-pycharm-diff"
271 | markers = [
272 |   "clojure: language server running for Clojure",
273 |   "python: language server running for Python",
274 |   "go: language server running for Go",
275 |   "java: language server running for Java",
276 |   "kotlin: language server running for kotlin",
277 |   "groovy: language server running for Groovy",
278 |   "rust: language server running for Rust",
279 |   "typescript: language server running for TypeScript",
280 |   "vue: language server running for Vue (uses TypeScript LSP)",
281 |   "php: language server running for PHP",
282 |   "perl: language server running for Perl",
283 |   "csharp: language server running for C#",
284 |   "elixir: language server running for Elixir",
285 |   "elm: language server running for Elm",
286 |   "terraform: language server running for Terraform",
287 |   "swift: language server running for Swift",
288 |   "bash: language server running for Bash",
289 |   "r: language server running for R",
290 |   "snapshot: snapshot tests for symbolic editing operations",
291 |   "ruby: language server running for Ruby (uses ruby-lsp)",
292 |   "zig: language server running for Zig",
293 |   "lua: language server running for Lua",
294 |   "nix: language server running for Nix",
295 |   "dart: language server running for Dart",
296 |   "erlang: language server running for Erlang",
297 |   "scala: language server running for Scala",
298 |   "al: language server running for AL (Microsoft Dynamics 365 Business Central)",
299 |   "fsharp: language server running for F#",
300 |   "rego: language server running for Rego",
301 |   "markdown: language server running for Markdown",
302 |   "julia: Julia language server tests",
303 |   "fortran: language server running for Fortran",
304 |   "haskell: Haskell language server tests",
305 |   "yaml: language server running for YAML",
306 |   "powershell: language server running for PowerShell",
307 |   "pascal: language server running for Pascal (Free Pascal/Lazarus)",
308 |   "slow: tests that require additional Expert instances and have long startup times (~60-90s each)",
309 |   "toml: language server running for TOML",
310 |   "matlab: language server running for MATLAB (requires MATLAB R2021b+)",
311 | ]
312 | 
313 | [tool.codespell]
314 | # Ref: https://github.com/codespell-project/codespell#using-a-config-file
315 | skip = '.git*,*.svg,*.lock,*.min.*'
316 | check-hidden = true
317 | # ignore-regex = ''
318 | ignore-words-list = 'paket'
```

--------------------------------------------------------------------------------
/docs/02-usage/050_configuration.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Configuration
  2 | 
  3 | Serena is very flexible in terms of configuration. While for most users, the default configurations will work,
  4 | you can fully adjust it to your needs by editing a few yaml files. You can disable tools, change Serena's instructions
  5 | (what we denote as the `system_prompt`), adjust the output of tools that just provide a prompt, and even adjust tool descriptions.
  6 | 
  7 | Serena is configured in four places:
  8 | 
  9 | 1. The `serena_config.yml` for general settings that apply to all clients and projects.
 10 |    It is located in your user directory under `.serena/serena_config.yml`.
 11 |    The file will be auto-generated when you first run Serena.
 12 |    You can edit it directly or use
 13 | 
 14 |    ```shell
 15 |    <serena> config edit
 16 |    ```
 17 | 
 18 |    where `<serena>` is [your way of running Serena](020_running).  
 19 |    The configuration file can also be accessed through [Serena's dashboard](060_dashboard).
 20 | 2. In the arguments passed to the `start-mcp-server` in your client's config (see below),
 21 |    which will apply to all sessions started by the respective client. In particular, the [context](contexts) parameter
 22 |    should be set appropriately for Serena to be best adjusted to existing tools and capabilities of your client.
 23 |    See for a detailed explanation. You can override all entries from the `serena_config.yml` through command line arguments.
 24 | 3. In the `.serena/project.yml` file within your project. This will hold project-level configuration that is used whenever
 25 |    that project is activated. This file will be autogenerated when you first use Serena on that project, but you can also
 26 |    create it explicitly with `serena project create [options]` (see the [](project-creation-indexing) 
 27 |    for details on available options).
 28 | 4. Through the context and modes (see below).
 29 | 
 30 | After the initial setup, continue with one of the sections below, depending on how you
 31 | want to use Serena.
 32 | 
 33 | ## Modes and Contexts
 34 | 
 35 | Serena's behavior and toolset can be adjusted using contexts and modes.
 36 | These allow for a high degree of customization to best suit your workflow and the environment Serena is operating in.
 37 | 
 38 | (contexts)=
 39 | ### Contexts
 40 | 
 41 | A **context** defines the general environment in which Serena is operating.
 42 | It influences the initial system prompt and the set of available tools.
 43 | A context is set at startup when launching Serena (e.g., via CLI options for an MCP server or in the agent script) and cannot be changed during an active session.
 44 | 
 45 | Serena comes with pre-defined contexts:
 46 | 
 47 | * `desktop-app`: Tailored for use with desktop applications like Claude Desktop. This is the default.
 48 |   The full set of Serena's tools is provided, as the application is assumed to have no prior coding-specific capabilities.
 49 | * `claude-code`: Optimized for use with Claude Code, it disables tools that would duplicate Claude Code's built-in capabilities.
 50 | * `codex`: Optimized for use with OpenAI Codex.
 51 | * `ide`: Generic context for IDE assistants/coding agents, e.g. VSCode, Cursor, or Cline, focusing on augmenting existing capabilities.
 52 |   Basic file operations and shell execution are assumed to be handled by the assistant's own capabilities.
 53 | * `agent`: Designed for scenarios where Serena acts as a more autonomous agent, for example, when used with Agno.
 54 | 
 55 | Choose the context that best matches the type of integration you are using.
 56 | 
 57 | Find the concrete definitions of the above contexts [here](https://github.com/oraios/serena/tree/main/src/serena/resources/config/contexts).
 58 | 
 59 | Note that the contexts `ide` and `claude-code` are **single-project contexts** (defining `single_project: true`).
 60 | For such contexts, if a project is provided at startup, the set of tools is limited to those required by the project's
 61 | concrete configuration, and other tools are excluded completely, allowing the set of tools to be minimal.
 62 | Tools explicitly disabled by the project will not be available at all. Since changing the active project
 63 | ceases to be a relevant operation in this case, the project activation tool is disabled.
 64 | 
 65 | When launching Serena, specify the context using `--context <context-name>`.
 66 | Note that for cases where parameter lists are specified (e.g. Claude Desktop), you must add two parameters to the list.
 67 | 
 68 | If you are using a local server (such as Llama.cpp) which requires you to use OpenAI-compatible tool descriptions, use context `oaicompat-agent` instead of `agent`.
 69 | 
 70 | You can manage contexts using the `context` command,
 71 | 
 72 |     <serena> context --help
 73 |     <serena> context list
 74 |     <serena> context create <context-name>
 75 |     <serena> context edit <context-name>
 76 |     <serena> context delete <context-name>
 77 | 
 78 | where `<serena>` is [your way of running Serena](020_running).
 79 | 
 80 | (modes)=
 81 | ### Modes
 82 | 
 83 | Modes further refine Serena's behavior for specific types of tasks or interaction styles. Multiple modes can be active simultaneously, allowing you to combine their effects. Modes influence the system prompt and can also alter the set of available tools by excluding certain ones.
 84 | 
 85 | Examples of built-in modes include:
 86 | 
 87 | * `planning`: Focuses Serena on planning and analysis tasks.
 88 | * `editing`: Optimizes Serena for direct code modification tasks.
 89 | * `interactive`: Suitable for a conversational, back-and-forth interaction style.
 90 | * `one-shot`: Configures Serena for tasks that should be completed in a single response, often used with `planning` for generating reports or initial plans.
 91 | * `no-onboarding`: Skips the initial onboarding process if it's not needed for a particular session but retains the memory tools (assuming initial memories were created externally).
 92 | * `onboarding`: Focuses on the project onboarding process.
 93 | * `no-memories`: Disables all memory tools (and tools building on memories such as onboarding tools)  
 94 | 
 95 | Find the concrete definitions of these modes [here](https://github.com/oraios/serena/tree/main/src/serena/resources/config/modes).
 96 | 
 97 | :::{important}
 98 | By default, Serena activates the two modes `interactive` and `editing`.  
 99 | 
100 | As soon as you start to specify modes, only the modes you explicitly specify will be active, however.
101 | Therefore, if you want to keep the default modes, you must specify them as well.  
102 | For example, to add mode `no-memories` to the default behaviour, specify
103 | ```shell
104 | --mode interactive --mode editing --mode no-memories
105 | ```
106 | :::
107 | 
108 | Modes can be set at startup (similar to contexts) but can also be _switched dynamically_ during a session. 
109 | You can instruct the LLM to use the `switch_modes` tool to activate a different set of modes (e.g., "Switch to planning and one-shot modes").
110 | 
111 | When launching Serena, specify modes using `--mode <mode-name>`; multiple modes can be specified, e.g. `--mode planning --mode no-onboarding`.
112 | 
113 | :::{note}
114 | **Mode Compatibility**: While you can combine modes, some may be semantically incompatible (e.g., `interactive` and `one-shot`). 
115 | Serena currently does not prevent incompatible combinations; it is up to the user to choose sensible mode configurations.
116 | :::
117 | 
118 | You can manage modes using the `mode` command,
119 | 
120 |     <serena> mode --help
121 |     <serena> mode list
122 |     <serena> mode create <mode-name>
123 |     <serena> mode edit <mode-name>
124 |     <serena> mode delete <mode-name>
125 | 
126 | where `<serena>` is [your way of running Serena](020_running).
127 | 
128 | ## Advanced Configuration
129 | 
130 | For advanced users, Serena's configuration can be further customized.
131 | 
132 | ### Serena Data Directory
133 | 
134 | The Serena user data directory (where configuration, language server files, logs, etc. are stored) defaults to `~/.serena`.
135 | You can change this location by setting the `SERENA_HOME` environment variable to your desired path.
136 | 
137 | ### Custom Prompts
138 | 
139 | All of Serena's prompts can be fully customized.
140 | We define prompt as jinja templates in yaml files, and you can inspect our default prompts [here](https://github.com/oraios/serena/tree/main/src/serena/resources/config/prompt_templates).
141 | 
142 | To override a prompt, simply add a .yml file to the `prompt_templates` folder in your Serena data directory
143 | which defines the prompt with the same name as the default prompt you want to override.
144 | For example, to override the `system_prompt`, you could create a file `~/.serena/prompt_templates/system_prompt.yml` (assuming default Serena data folder location) 
145 | with content like:
146 | 
147 | ```yaml
148 | prompts:
149 |   system_prompt: |
150 |     Whatever you want ...
151 | ```
152 | 
153 | It is advisable to use the default prompt as a starting point and modify it to suit your needs.
154 | 
```

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

```python
  1 | """
  2 | Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import re
  9 | import threading
 10 | from typing import cast
 11 | 
 12 | from overrides import override
 13 | 
 14 | from solidlsp.ls import SolidLanguageServer
 15 | from solidlsp.ls_config import LanguageServerConfig
 16 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 17 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 18 | from solidlsp.settings import SolidLSPSettings
 19 | 
 20 | log = logging.getLogger(__name__)
 21 | 
 22 | 
 23 | class PyrightServer(SolidLanguageServer):
 24 |     """
 25 |     Provides Python specific instantiation of the LanguageServer class using Pyright.
 26 |     Contains various configurations and settings specific to Python.
 27 |     """
 28 | 
 29 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 30 |         """
 31 |         Creates a PyrightServer instance. This class is not meant to be instantiated directly.
 32 |         Use LanguageServer.create() instead.
 33 |         """
 34 |         super().__init__(
 35 |             config,
 36 |             repository_root_path,
 37 |             ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path),
 38 |             "python",
 39 |             solidlsp_settings,
 40 |         )
 41 | 
 42 |         # Event to signal when initial workspace analysis is complete
 43 |         self.analysis_complete = threading.Event()
 44 |         self.found_source_files = False
 45 | 
 46 |     @override
 47 |     def is_ignored_dirname(self, dirname: str) -> bool:
 48 |         return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
 49 | 
 50 |     @staticmethod
 51 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
 52 |         """
 53 |         Returns the initialize params for the Pyright Language Server.
 54 |         """
 55 |         # Create basic initialization parameters
 56 |         initialize_params = {  # type: ignore
 57 |             "processId": os.getpid(),
 58 |             "rootPath": repository_absolute_path,
 59 |             "rootUri": pathlib.Path(repository_absolute_path).as_uri(),
 60 |             "initializationOptions": {
 61 |                 "exclude": [
 62 |                     "**/__pycache__",
 63 |                     "**/.venv",
 64 |                     "**/.env",
 65 |                     "**/build",
 66 |                     "**/dist",
 67 |                     "**/.pixi",
 68 |                 ],
 69 |                 "reportMissingImports": "error",
 70 |             },
 71 |             "capabilities": {
 72 |                 "workspace": {
 73 |                     "workspaceEdit": {"documentChanges": True},
 74 |                     "didChangeConfiguration": {"dynamicRegistration": True},
 75 |                     "didChangeWatchedFiles": {"dynamicRegistration": True},
 76 |                     "symbol": {
 77 |                         "dynamicRegistration": True,
 78 |                         "symbolKind": {"valueSet": list(range(1, 27))},
 79 |                     },
 80 |                     "executeCommand": {"dynamicRegistration": True},
 81 |                 },
 82 |                 "textDocument": {
 83 |                     "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
 84 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
 85 |                     "signatureHelp": {
 86 |                         "dynamicRegistration": True,
 87 |                         "signatureInformation": {
 88 |                             "documentationFormat": ["markdown", "plaintext"],
 89 |                             "parameterInformation": {"labelOffsetSupport": True},
 90 |                         },
 91 |                     },
 92 |                     "definition": {"dynamicRegistration": True},
 93 |                     "references": {"dynamicRegistration": True},
 94 |                     "documentSymbol": {
 95 |                         "dynamicRegistration": True,
 96 |                         "symbolKind": {"valueSet": list(range(1, 27))},
 97 |                         "hierarchicalDocumentSymbolSupport": True,
 98 |                     },
 99 |                     "publishDiagnostics": {"relatedInformation": True},
100 |                 },
101 |             },
102 |             "workspaceFolders": [
103 |                 {"uri": pathlib.Path(repository_absolute_path).as_uri(), "name": os.path.basename(repository_absolute_path)}
104 |             ],
105 |         }
106 | 
107 |         return cast(InitializeParams, initialize_params)
108 | 
109 |     def _start_server(self) -> None:
110 |         """
111 |         Starts the Pyright Language Server and waits for initial workspace analysis to complete.
112 | 
113 |         This prevents zombie processes by ensuring Pyright has finished its initial background
114 |         tasks before we consider the server ready.
115 | 
116 |         Usage:
117 |         ```
118 |         async with lsp.start_server():
119 |             # LanguageServer has been initialized and workspace analysis is complete
120 |             await lsp.request_definition(...)
121 |             await lsp.request_references(...)
122 |             # Shutdown the LanguageServer on exit from scope
123 |         # LanguageServer has been shutdown cleanly
124 |         ```
125 |         """
126 | 
127 |         def execute_client_command_handler(params: dict) -> list:
128 |             return []
129 | 
130 |         def do_nothing(params: dict) -> None:
131 |             return
132 | 
133 |         def window_log_message(msg: dict) -> None:
134 |             """
135 |             Monitor Pyright's log messages to detect when initial analysis is complete.
136 |             Pyright logs "Found X source files" when it finishes scanning the workspace.
137 |             """
138 |             message_text = msg.get("message", "")
139 |             log.info(f"LSP: window/logMessage: {message_text}")
140 | 
141 |             # Look for "Found X source files" which indicates workspace scanning is complete
142 |             # Unfortunately, pyright is unreliable and there seems to be no better way
143 |             if re.search(r"Found \d+ source files?", message_text):
144 |                 log.info("Pyright workspace scanning complete")
145 |                 self.found_source_files = True
146 |                 self.analysis_complete.set()
147 |                 self.completions_available.set()
148 | 
149 |         def check_experimental_status(params: dict) -> None:
150 |             """
151 |             Also listen for experimental/serverStatus as a backup signal
152 |             """
153 |             if params.get("quiescent") == True:
154 |                 log.info("Received experimental/serverStatus with quiescent=true")
155 |                 if not self.found_source_files:
156 |                     self.analysis_complete.set()
157 |                     self.completions_available.set()
158 | 
159 |         # Set up notification handlers
160 |         self.server.on_request("client/registerCapability", do_nothing)
161 |         self.server.on_notification("language/status", do_nothing)
162 |         self.server.on_notification("window/logMessage", window_log_message)
163 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
164 |         self.server.on_notification("$/progress", do_nothing)
165 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
166 |         self.server.on_notification("language/actionableNotification", do_nothing)
167 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
168 | 
169 |         log.info("Starting pyright-langserver server process")
170 |         self.server.start()
171 | 
172 |         # Send proper initialization parameters
173 |         initialize_params = self._get_initialize_params(self.repository_root_path)
174 | 
175 |         log.info("Sending initialize request from LSP client to pyright server and awaiting response")
176 |         init_response = self.server.send.initialize(initialize_params)
177 |         log.info(f"Received initialize response from pyright server: {init_response}")
178 | 
179 |         # Verify that the server supports our required features
180 |         assert "textDocumentSync" in init_response["capabilities"]
181 |         assert "completionProvider" in init_response["capabilities"]
182 |         assert "definitionProvider" in init_response["capabilities"]
183 | 
184 |         # Complete the initialization handshake
185 |         self.server.notify.initialized({})
186 | 
187 |         # Wait for Pyright to complete its initial workspace analysis
188 |         # This prevents zombie processes by ensuring background tasks finish
189 |         log.info("Waiting for Pyright to complete initial workspace analysis...")
190 |         if self.analysis_complete.wait(timeout=5.0):
191 |             log.info("Pyright initial analysis complete, server ready")
192 |         else:
193 |             log.warning("Timeout waiting for Pyright analysis completion, proceeding anyway")
194 |             # Fallback: assume analysis is complete after timeout
195 |             self.analysis_complete.set()
196 |             self.completions_available.set()
197 | 
```

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

```python
  1 | """
  2 | Provides PHP specific instantiation of the LanguageServer class using Intelephense.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import shutil
  9 | from time import sleep
 10 | 
 11 | from overrides import override
 12 | 
 13 | from solidlsp.ls import SolidLanguageServer
 14 | from solidlsp.ls_config import LanguageServerConfig
 15 | from solidlsp.ls_utils import PlatformId, PlatformUtils
 16 | from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, InitializeParams, LocationLink
 17 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 18 | from solidlsp.settings import SolidLSPSettings
 19 | 
 20 | from ..lsp_protocol_handler import lsp_types
 21 | from .common import RuntimeDependency, RuntimeDependencyCollection
 22 | 
 23 | log = logging.getLogger(__name__)
 24 | 
 25 | 
 26 | class Intelephense(SolidLanguageServer):
 27 |     """
 28 |     Provides PHP specific instantiation of the LanguageServer class using Intelephense.
 29 | 
 30 |     You can pass the following entries in ls_specific_settings["php"]:
 31 |         - maxMemory: sets intelephense.maxMemory
 32 |         - maxFileSize: sets intelephense.files.maxSize
 33 |         - ignore_vendor: whether or ignore directories named "vendor" (default: true)
 34 |     """
 35 | 
 36 |     @override
 37 |     def is_ignored_dirname(self, dirname: str) -> bool:
 38 |         return super().is_ignored_dirname(dirname) or dirname in self._ignored_dirnames
 39 | 
 40 |     @classmethod
 41 |     def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> list[str]:
 42 |         """
 43 |         Setup runtime dependencies for Intelephense and return the command to start the server.
 44 |         """
 45 |         platform_id = PlatformUtils.get_platform_id()
 46 | 
 47 |         valid_platforms = [
 48 |             PlatformId.LINUX_x64,
 49 |             PlatformId.LINUX_arm64,
 50 |             PlatformId.OSX,
 51 |             PlatformId.OSX_x64,
 52 |             PlatformId.OSX_arm64,
 53 |             PlatformId.WIN_x64,
 54 |             PlatformId.WIN_arm64,
 55 |         ]
 56 |         assert platform_id in valid_platforms, f"Platform {platform_id} is not supported by {cls.__name__} at the moment"
 57 | 
 58 |         # Verify both node and npm are installed
 59 |         is_node_installed = shutil.which("node") is not None
 60 |         assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
 61 |         is_npm_installed = shutil.which("npm") is not None
 62 |         assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
 63 | 
 64 |         # Install intelephense if not already installed
 65 |         intelephense_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "php-lsp")
 66 |         os.makedirs(intelephense_ls_dir, exist_ok=True)
 67 |         intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense")
 68 |         if not os.path.exists(intelephense_executable_path):
 69 |             deps = RuntimeDependencyCollection(
 70 |                 [
 71 |                     RuntimeDependency(
 72 |                         id="intelephense",
 73 |                         command="npm install --prefix ./ [email protected]",
 74 |                         platform_id="any",
 75 |                     )
 76 |                 ]
 77 |             )
 78 |             deps.install(intelephense_ls_dir)
 79 | 
 80 |         assert os.path.exists(
 81 |             intelephense_executable_path
 82 |         ), f"intelephense executable not found at {intelephense_executable_path}, something went wrong."
 83 | 
 84 |         return [intelephense_executable_path, "--stdio"]
 85 | 
 86 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 87 |         # Setup runtime dependencies before initializing
 88 |         intelephense_cmd = self._setup_runtime_dependencies(solidlsp_settings)
 89 | 
 90 |         super().__init__(
 91 |             config, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php", solidlsp_settings
 92 |         )
 93 |         self.request_id = 0
 94 | 
 95 |         # For PHP projects, we should ignore:
 96 |         # - node_modules: if the project has JavaScript components
 97 |         # - cache: commonly used for caching
 98 |         # - (configurable) vendor: third-party dependencies <managed by Composer
 99 |         self._ignored_dirnames = {"node_modules", "cache"}
100 |         if self._custom_settings.get("ignore_vendor", True):
101 |             self._ignored_dirnames.add("vendor")
102 |         log.info(f"Ignoring the following directories for PHP projects: {', '.join(sorted(self._ignored_dirnames))}")
103 | 
104 |     def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
105 |         """
106 |         Returns the initialization params for the Intelephense Language Server.
107 |         """
108 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
109 |         initialize_params = {
110 |             "locale": "en",
111 |             "capabilities": {
112 |                 "textDocument": {
113 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
114 |                     "definition": {"dynamicRegistration": True},
115 |                 },
116 |                 "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
117 |             },
118 |             "processId": os.getpid(),
119 |             "rootPath": repository_absolute_path,
120 |             "rootUri": root_uri,
121 |             "workspaceFolders": [
122 |                 {
123 |                     "uri": root_uri,
124 |                     "name": os.path.basename(repository_absolute_path),
125 |                 }
126 |             ],
127 |         }
128 |         initialization_options = {}
129 |         # Add license key if provided via environment variable
130 |         license_key = os.environ.get("INTELEPHENSE_LICENSE_KEY")
131 |         if license_key:
132 |             initialization_options["licenceKey"] = license_key
133 | 
134 |         max_memory = self._custom_settings.get("maxMemory")
135 |         max_file_size = self._custom_settings.get("maxFileSize")
136 |         if max_memory is not None:
137 |             initialization_options["intelephense.maxMemory"] = max_memory
138 |         if max_file_size is not None:
139 |             initialization_options["intelephense.files.maxSize"] = max_file_size
140 | 
141 |         initialize_params["initializationOptions"] = initialization_options
142 |         return initialize_params  # type: ignore
143 | 
144 |     def _start_server(self) -> None:
145 |         """Start Intelephense server process"""
146 | 
147 |         def register_capability_handler(params: dict) -> None:
148 |             return
149 | 
150 |         def window_log_message(msg: dict) -> None:
151 |             log.info(f"LSP: window/logMessage: {msg}")
152 | 
153 |         def do_nothing(params: dict) -> None:
154 |             return
155 | 
156 |         self.server.on_request("client/registerCapability", register_capability_handler)
157 |         self.server.on_notification("window/logMessage", window_log_message)
158 |         self.server.on_notification("$/progress", do_nothing)
159 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
160 | 
161 |         log.info("Starting Intelephense server process")
162 |         self.server.start()
163 |         initialize_params = self._get_initialize_params(self.repository_root_path)
164 | 
165 |         log.info("Sending initialize request from LSP client to LSP server and awaiting response")
166 |         init_response = self.server.send.initialize(initialize_params)
167 |         log.info("After sent initialize params")
168 | 
169 |         # Verify server capabilities
170 |         assert "textDocumentSync" in init_response["capabilities"]
171 |         assert "completionProvider" in init_response["capabilities"]
172 |         assert "definitionProvider" in init_response["capabilities"]
173 | 
174 |         self.server.notify.initialized({})
175 |         self.completions_available.set()
176 | 
177 |         # Intelephense server is typically ready immediately after initialization
178 |         # TODO: This is probably incorrect; the server does send an initialized notification, which we could wait for!
179 | 
180 |     @override
181 |     # For some reason, the LS may need longer to process this, so we just retry
182 |     def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
183 |         # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is
184 |         #   despite the LS having processed requests already. I don't know what causes this, but sleeping
185 |         #   one second helps. It may be that sleeping only once is enough but that's hard to reliably test.
186 |         # May be related to the time it takes to read the files or something like that.
187 |         # The sleeping doesn't seem to be needed on all systems
188 |         sleep(1)
189 |         return super()._send_references_request(relative_file_path, line, column)
190 | 
191 |     @override
192 |     def _send_definition_request(self, definition_params: DefinitionParams) -> Definition | list[LocationLink] | None:
193 |         # TODO: same as above, also only a problem if the definition is in another file
194 |         sleep(1)
195 |         return super()._send_definition_request(definition_params)
196 | 
```

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

```python
  1 | """
  2 | Provides Perl specific instantiation of the LanguageServer class using Perl::LanguageServer.
  3 | 
  4 | Note: Windows is not supported as Nix itself doesn't support Windows natively.
  5 | """
  6 | 
  7 | import logging
  8 | import os
  9 | import pathlib
 10 | import subprocess
 11 | import time
 12 | from typing import Any
 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 DidChangeConfigurationParams, InitializeParams
 20 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 21 | from solidlsp.settings import SolidLSPSettings
 22 | 
 23 | log = logging.getLogger(__name__)
 24 | 
 25 | 
 26 | class PerlLanguageServer(SolidLanguageServer):
 27 |     """
 28 |     Provides Perl specific instantiation of the LanguageServer class using Perl::LanguageServer.
 29 |     """
 30 | 
 31 |     @staticmethod
 32 |     def _get_perl_version() -> str | None:
 33 |         """Get the installed Perl version or None if not found."""
 34 |         try:
 35 |             result = subprocess.run(["perl", "-v"], capture_output=True, text=True, check=False)
 36 |             if result.returncode == 0:
 37 |                 return result.stdout.strip()
 38 |         except FileNotFoundError:
 39 |             return None
 40 |         return None
 41 | 
 42 |     @staticmethod
 43 |     def _get_perl_language_server_version() -> str | None:
 44 |         """Get the installed Perl::LanguageServer version or None if not found."""
 45 |         try:
 46 |             result = subprocess.run(
 47 |                 ["perl", "-MPerl::LanguageServer", "-e", "print $Perl::LanguageServer::VERSION"],
 48 |                 capture_output=True,
 49 |                 text=True,
 50 |                 check=False,
 51 |             )
 52 |             if result.returncode == 0:
 53 |                 return result.stdout.strip()
 54 |         except FileNotFoundError:
 55 |             return None
 56 |         return None
 57 | 
 58 |     @override
 59 |     def is_ignored_dirname(self, dirname: str) -> bool:
 60 |         # For Perl projects, we should ignore:
 61 |         # - blib: build library directory
 62 |         # - local: local Perl module installation
 63 |         # - .carton: Carton dependency manager cache
 64 |         # - vendor: vendored dependencies
 65 |         # - _build: Module::Build output
 66 |         return super().is_ignored_dirname(dirname) or dirname in ["blib", "local", ".carton", "vendor", "_build", "cover_db"]
 67 | 
 68 |     @classmethod
 69 |     def _setup_runtime_dependencies(cls) -> str:
 70 |         """
 71 |         Check if required Perl runtime dependencies are available.
 72 |         Raises RuntimeError with helpful message if dependencies are missing.
 73 |         """
 74 |         platform_id = PlatformUtils.get_platform_id()
 75 | 
 76 |         valid_platforms = [
 77 |             PlatformId.LINUX_x64,
 78 |             PlatformId.LINUX_arm64,
 79 |             PlatformId.OSX,
 80 |             PlatformId.OSX_x64,
 81 |             PlatformId.OSX_arm64,
 82 |         ]
 83 |         if platform_id not in valid_platforms:
 84 |             raise RuntimeError(f"Platform {platform_id} is not supported for Perl at the moment")
 85 | 
 86 |         perl_version = cls._get_perl_version()
 87 |         if not perl_version:
 88 |             raise RuntimeError(
 89 |                 "Perl is not installed. Please install Perl from https://www.perl.org/get.html and make sure it is added to your PATH."
 90 |             )
 91 | 
 92 |         perl_ls_version = cls._get_perl_language_server_version()
 93 |         if not perl_ls_version:
 94 |             raise RuntimeError(
 95 |                 "Found a Perl version but Perl::LanguageServer is not installed.\n"
 96 |                 "Please install Perl::LanguageServer: cpanm Perl::LanguageServer\n"
 97 |                 "See: https://metacpan.org/pod/Perl::LanguageServer"
 98 |             )
 99 | 
100 |         return "perl -MPerl::LanguageServer -e 'Perl::LanguageServer::run'"
101 | 
102 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
103 |         # Setup runtime dependencies before initializing
104 |         perl_ls_cmd = self._setup_runtime_dependencies()
105 | 
106 |         super().__init__(
107 |             config, repository_root_path, ProcessLaunchInfo(cmd=perl_ls_cmd, cwd=repository_root_path), "perl", solidlsp_settings
108 |         )
109 |         self.request_id = 0
110 | 
111 |     @staticmethod
112 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
113 |         """
114 |         Returns the initialize params for Perl::LanguageServer.
115 |         Based on the expected structure from Perl::LanguageServer::Methods::_rpcreq_initialize.
116 |         """
117 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
118 |         initialize_params = {
119 |             "processId": os.getpid(),
120 |             "rootPath": repository_absolute_path,
121 |             "rootUri": root_uri,
122 |             "capabilities": {
123 |                 "textDocument": {
124 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
125 |                     "definition": {"dynamicRegistration": True},
126 |                     "references": {"dynamicRegistration": True},
127 |                     "documentSymbol": {"dynamicRegistration": True},
128 |                     "hover": {"dynamicRegistration": True},
129 |                 },
130 |                 "workspace": {
131 |                     "workspaceFolders": True,
132 |                     "didChangeConfiguration": {"dynamicRegistration": True},
133 |                     "symbol": {"dynamicRegistration": True},
134 |                 },
135 |             },
136 |             "initializationOptions": {},
137 |             "workspaceFolders": [
138 |                 {
139 |                     "uri": root_uri,
140 |                     "name": os.path.basename(repository_absolute_path),
141 |                 }
142 |             ],
143 |         }
144 | 
145 |         return initialize_params  # type: ignore
146 | 
147 |     def _start_server(self) -> None:
148 |         """Start Perl::LanguageServer process"""
149 | 
150 |         def register_capability_handler(params: Any) -> None:
151 |             return
152 | 
153 |         def window_log_message(msg: dict) -> None:
154 |             log.info(f"LSP: window/logMessage: {msg}")
155 | 
156 |         def do_nothing(params: Any) -> None:
157 |             return
158 | 
159 |         def workspace_configuration_handler(params: Any) -> Any:
160 |             """Handle workspace/configuration request from Perl::LanguageServer."""
161 |             log.info(f"Received workspace/configuration request: {params}")
162 | 
163 |             perl_config = {
164 |                 "perlInc": [self.repository_root_path, "."],
165 |                 "fileFilter": [".pm", ".pl"],
166 |                 "ignoreDirs": [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"],
167 |             }
168 | 
169 |             return [perl_config]
170 | 
171 |         self.server.on_request("client/registerCapability", register_capability_handler)
172 |         self.server.on_request("workspace/configuration", workspace_configuration_handler)
173 |         self.server.on_notification("window/logMessage", window_log_message)
174 |         self.server.on_notification("$/progress", do_nothing)
175 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
176 | 
177 |         log.info("Starting Perl::LanguageServer process")
178 |         self.server.start()
179 |         initialize_params = self._get_initialize_params(self.repository_root_path)
180 | 
181 |         log.info("Sending initialize request from LSP client to LSP server and awaiting response")
182 |         init_response = self.server.send.initialize(initialize_params)
183 |         log.info(
184 |             "After sent initialize params",
185 |         )
186 | 
187 |         # Verify server capabilities
188 |         assert "textDocumentSync" in init_response["capabilities"]
189 |         assert "definitionProvider" in init_response["capabilities"]
190 |         assert "referencesProvider" in init_response["capabilities"]
191 | 
192 |         self.server.notify.initialized({})
193 | 
194 |         # Send workspace configuration to Perl::LanguageServer
195 |         # Perl::LanguageServer requires didChangeConfiguration to set perlInc, fileFilter, and ignoreDirs
196 |         # See: Perl::LanguageServer::Methods::workspace::_rpcnot_didChangeConfiguration
197 |         perl_config: DidChangeConfigurationParams = {
198 |             "settings": {
199 |                 "perl": {
200 |                     "perlInc": [self.repository_root_path, "."],
201 |                     "fileFilter": [".pm", ".pl"],
202 |                     "ignoreDirs": [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"],
203 |                 }
204 |             }
205 |         }
206 |         log.info(f"Sending workspace/didChangeConfiguration notification with config: {perl_config}")
207 |         self.server.notify.workspace_did_change_configuration(perl_config)
208 | 
209 |         self.completions_available.set()
210 | 
211 |         # Perl::LanguageServer needs time to index files and resolve cross-file references
212 |         # Without this delay, requests for definitions/references may return empty results
213 |         settling_time = 0.5
214 |         log.info(f"Allowing {settling_time} seconds for Perl::LanguageServer to index files...")
215 |         time.sleep(settling_time)
216 |         log.info("Perl::LanguageServer settling period complete")
217 | 
```

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

```python
  1 | """
  2 | Provides Elm specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Elm.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import shutil
  9 | import threading
 10 | 
 11 | from overrides import override
 12 | from sensai.util.logging import LogTime
 13 | 
 14 | from solidlsp.ls import SolidLanguageServer
 15 | from solidlsp.ls_config import LanguageServerConfig
 16 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 17 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 18 | from solidlsp.settings import SolidLSPSettings
 19 | 
 20 | from .common import RuntimeDependency, RuntimeDependencyCollection
 21 | 
 22 | log = logging.getLogger(__name__)
 23 | 
 24 | 
 25 | class ElmLanguageServer(SolidLanguageServer):
 26 |     """
 27 |     Provides Elm specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Elm.
 28 |     """
 29 | 
 30 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 31 |         """
 32 |         Creates an ElmLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 33 |         """
 34 |         elm_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
 35 | 
 36 |         # Resolve ELM_HOME to absolute path if it's set to a relative path
 37 |         env = {}
 38 |         elm_home = os.environ.get("ELM_HOME")
 39 |         if elm_home:
 40 |             if not os.path.isabs(elm_home):
 41 |                 # Convert relative ELM_HOME to absolute based on repository root
 42 |                 elm_home = os.path.abspath(os.path.join(repository_root_path, elm_home))
 43 |             env["ELM_HOME"] = elm_home
 44 |             log.info(f"Using ELM_HOME: {elm_home}")
 45 | 
 46 |         super().__init__(
 47 |             config,
 48 |             repository_root_path,
 49 |             ProcessLaunchInfo(cmd=elm_lsp_executable_path, cwd=repository_root_path, env=env),
 50 |             "elm",
 51 |             solidlsp_settings,
 52 |         )
 53 |         self.server_ready = threading.Event()
 54 | 
 55 |     @override
 56 |     def is_ignored_dirname(self, dirname: str) -> bool:
 57 |         return super().is_ignored_dirname(dirname) or dirname in [
 58 |             "elm-stuff",
 59 |             "node_modules",
 60 |             "dist",
 61 |             "build",
 62 |         ]
 63 | 
 64 |     @classmethod
 65 |     def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]:
 66 |         """
 67 |         Setup runtime dependencies for Elm Language Server and return the command to start the server.
 68 |         """
 69 |         # Check if elm-language-server is already installed globally
 70 |         system_elm_ls = shutil.which("elm-language-server")
 71 |         if system_elm_ls:
 72 |             log.info(f"Found system-installed elm-language-server at {system_elm_ls}")
 73 |             return [system_elm_ls, "--stdio"]
 74 | 
 75 |         # Verify node and npm are installed
 76 |         is_node_installed = shutil.which("node") is not None
 77 |         assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
 78 |         is_npm_installed = shutil.which("npm") is not None
 79 |         assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
 80 | 
 81 |         deps = RuntimeDependencyCollection(
 82 |             [
 83 |                 RuntimeDependency(
 84 |                     id="elm-language-server",
 85 |                     description="@elm-tooling/elm-language-server package",
 86 |                     command=["npm", "install", "--prefix", "./", "@elm-tooling/[email protected]"],
 87 |                     platform_id="any",
 88 |                 ),
 89 |             ]
 90 |         )
 91 | 
 92 |         # Install elm-language-server if not already installed
 93 |         elm_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "elm-lsp")
 94 |         elm_ls_executable_path = os.path.join(elm_ls_dir, "node_modules", ".bin", "elm-language-server")
 95 |         if not os.path.exists(elm_ls_executable_path):
 96 |             log.info(f"Elm Language Server executable not found at {elm_ls_executable_path}. Installing...")
 97 |             with LogTime("Installation of Elm language server dependencies", logger=log):
 98 |                 deps.install(elm_ls_dir)
 99 | 
100 |         if not os.path.exists(elm_ls_executable_path):
101 |             raise FileNotFoundError(
102 |                 f"elm-language-server executable not found at {elm_ls_executable_path}, something went wrong with the installation."
103 |             )
104 |         return [elm_ls_executable_path, "--stdio"]
105 | 
106 |     @staticmethod
107 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
108 |         """
109 |         Returns the initialize params for the Elm Language Server.
110 |         """
111 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
112 | 
113 |         initialize_params = {
114 |             "locale": "en",
115 |             "capabilities": {
116 |                 "textDocument": {
117 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
118 |                     "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
119 |                     "definition": {"dynamicRegistration": True},
120 |                     "references": {"dynamicRegistration": True},
121 |                     "documentSymbol": {
122 |                         "dynamicRegistration": True,
123 |                         "hierarchicalDocumentSymbolSupport": True,
124 |                         "symbolKind": {"valueSet": list(range(1, 27))},
125 |                     },
126 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
127 |                     "codeAction": {"dynamicRegistration": True},
128 |                     "rename": {"dynamicRegistration": True},
129 |                 },
130 |                 "workspace": {
131 |                     "workspaceFolders": True,
132 |                     "didChangeConfiguration": {"dynamicRegistration": True},
133 |                     "symbol": {"dynamicRegistration": True},
134 |                 },
135 |             },
136 |             "initializationOptions": {
137 |                 "elmPath": shutil.which("elm") or "elm",
138 |                 "elmFormatPath": shutil.which("elm-format") or "elm-format",
139 |                 "elmTestPath": shutil.which("elm-test") or "elm-test",
140 |                 "skipInstallPackageConfirmation": True,
141 |                 "onlyUpdateDiagnosticsOnSave": False,
142 |             },
143 |             "processId": os.getpid(),
144 |             "rootPath": repository_absolute_path,
145 |             "rootUri": root_uri,
146 |             "workspaceFolders": [
147 |                 {
148 |                     "uri": root_uri,
149 |                     "name": os.path.basename(repository_absolute_path),
150 |                 }
151 |             ],
152 |         }
153 |         return initialize_params  # type: ignore[return-value]
154 | 
155 |     def _start_server(self) -> None:
156 |         """
157 |         Starts the Elm Language Server, waits for the server to be ready and yields the LanguageServer instance.
158 |         """
159 |         workspace_ready = threading.Event()
160 | 
161 |         def do_nothing(params: dict) -> None:
162 |             return
163 | 
164 |         def window_log_message(msg: dict) -> None:
165 |             log.info(f"LSP: window/logMessage: {msg}")
166 | 
167 |         def on_diagnostics(params: dict) -> None:
168 |             # Receiving diagnostics indicates the workspace has been scanned
169 |             log.info("LSP: Received diagnostics notification, workspace is ready")
170 |             workspace_ready.set()
171 | 
172 |         self.server.on_notification("window/logMessage", window_log_message)
173 |         self.server.on_notification("$/progress", do_nothing)
174 |         self.server.on_notification("textDocument/publishDiagnostics", on_diagnostics)
175 | 
176 |         log.info("Starting Elm server process")
177 |         self.server.start()
178 |         initialize_params = self._get_initialize_params(self.repository_root_path)
179 | 
180 |         log.info("Sending initialize request from LSP client to LSP server and awaiting response")
181 |         init_response = self.server.send.initialize(initialize_params)
182 | 
183 |         # Elm-specific capability checks
184 |         assert "textDocumentSync" in init_response["capabilities"]
185 |         assert "completionProvider" in init_response["capabilities"]
186 |         assert "definitionProvider" in init_response["capabilities"]
187 |         assert "referencesProvider" in init_response["capabilities"]
188 |         assert "documentSymbolProvider" in init_response["capabilities"]
189 | 
190 |         self.server.notify.initialized({})
191 |         log.info("Elm server initialized, waiting for workspace scan...")
192 | 
193 |         # Wait for workspace to be scanned (indicated by receiving diagnostics)
194 |         if workspace_ready.wait(timeout=30.0):
195 |             log.info("Elm server workspace scan completed")
196 |         else:
197 |             log.warning("Timeout waiting for Elm workspace scan, proceeding anyway")
198 | 
199 |         self.server_ready.set()
200 |         self.completions_available.set()
201 |         log.info("Elm server ready")
202 | 
203 |     @override
204 |     def _get_wait_time_for_cross_file_referencing(self) -> float:
205 |         return 1.0
206 | 
```

--------------------------------------------------------------------------------
/docs/02-usage/020_running.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Running Serena
  2 | 
  3 | Serena is a command-line tool with a variety of sub-commands.
  4 | This section describes
  5 |  * various ways of running Serena
  6 |  * how to run and configure the most important command, i.e. starting the MCP server
  7 |  * other useful commands.
  8 | 
  9 | ## Ways of Running Serena
 10 | 
 11 | In the following, we will refer to the command used to run Serena as `<serena>`,
 12 | which you should replace with the appropriate command based on your chosen method,
 13 | as detailed below.
 14 | 
 15 | In general, to get help, append `--help` to the command, i.e.
 16 | 
 17 |     <serena> --help
 18 |     <serena> <command> --help
 19 | 
 20 | ### Using uvx
 21 | 
 22 | `uvx` is part of `uv`. It can be used to run the latest version of Serena directly from the repository, without an explicit local installation.
 23 | 
 24 |     uvx --from git+https://github.com/oraios/serena serena 
 25 | 
 26 | Explore the CLI to see some of the customization options that serena provides (more info on them below).
 27 | 
 28 | ### Local Installation
 29 | 
 30 | 1. Clone the repository and change into it.
 31 | 
 32 |    ```shell
 33 |    git clone https://github.com/oraios/serena
 34 |    cd serena
 35 |    ```
 36 | 
 37 | 2. Run Serena via
 38 | 
 39 |    ```shell
 40 |    uv run serena 
 41 |    ```
 42 | 
 43 |    when within the serena installation directory.   
 44 |    From other directories, run it with the `--directory` option, i.e.
 45 | 
 46 |    ```shell
 47 |     uv run --directory /abs/path/to/serena serena
 48 |     ```
 49 | 
 50 | :::{note}
 51 | Adding the `--directory` option results in the working directory being set to the Serena directory.
 52 | As a consequence, you will need to specify paths when using CLI commands that would otherwise operate on the current directory.
 53 | :::
 54 | 
 55 | (docker)=
 56 | ### Using Docker 
 57 | 
 58 | The Docker approach offers several advantages:
 59 | 
 60 | * better security isolation for shell command execution
 61 | * no need to install language servers and dependencies locally
 62 | * consistent environment across different systems
 63 | 
 64 | You can run the Serena MCP server directly via Docker as follows,
 65 | assuming that the projects you want to work on are all located in `/path/to/your/projects`:
 66 | 
 67 | ```shell
 68 | docker run --rm -i --network host -v /path/to/your/projects:/workspaces/projects ghcr.io/oraios/serena:latest serena 
 69 | ```
 70 | 
 71 | This command mounts your projects into the container under `/workspaces/projects`, so when working with projects, 
 72 | you need to refer to them using the respective path (e.g. `/workspaces/projects/my-project`).
 73 | 
 74 | Alternatively, you may use Docker compose with the `compose.yml` file provided in the repository.
 75 | See our [advanced Docker usage](https://github.com/oraios/serena/blob/main/DOCKER.md) documentation for more detailed instructions, configuration options, and limitations.
 76 | 
 77 | :::{note}
 78 | Docker usage is subject to limitations; see the [advanced Docker usage](https://github.com/oraios/serena/blob/main/DOCKER.md) documentation for details.
 79 | :::
 80 | 
 81 | ### Using Nix
 82 | 
 83 | If you are using Nix and [have enabled the `nix-command` and `flakes` features](https://nixos.wiki/wiki/flakes), you can run Serena using the following command:
 84 | 
 85 | ```bash
 86 | nix run github:oraios/serena -- <command> [options]
 87 | ```
 88 | 
 89 | You can also install Serena by referencing this repo (`github:oraios/serena`) and using it in your Nix flake. The package is exported as `serena`.
 90 | 
 91 | (start-mcp-server)=
 92 | ## Running the MCP Server
 93 | 
 94 | Given your preferred method of running Serena, you can start the MCP server using the `start-mcp-server` command:
 95 | 
 96 |     <serena> start-mcp-server [options]  
 97 | 
 98 | Note that no matter how you run the MCP server, Serena will, by default, start a web-based dashboard on localhost that will allow you to inspect
 99 | the server's operations, logs, and configuration.
100 | 
101 | :::{tip}
102 | By default, Serena will use language servers for code understanding and analysis.    
103 | With the [Serena JetBrains Plugin](025_jetbrains_plugin), we recently introduced a powerful alternative,
104 | which has several advantages over the language server-based approach.
105 | :::
106 | 
107 | ### Standard I/O Mode
108 | 
109 | The typical usage involves the client (e.g. Claude Code, Codex or Cursor) running
110 | the MCP server as a subprocess and using the process' stdin/stdout streams to communicate with it.
111 | In order to launch the server, the client thus needs to be provided with the command to run the MCP server.
112 | 
113 | :::{note}
114 | MCP servers which use stdio as a protocol are somewhat unusual as far as client/server architectures go, as the server
115 | necessarily has to be started by the client in order for communication to take place via the server's standard input/output streams.
116 | In other words, you do not need to start the server yourself. The client application (e.g. Claude Desktop) takes care of this and
117 | therefore needs to be configured with a launch command.
118 | :::
119 | 
120 | Communication over stdio is the default for the Serena MCP server, so in the simplest
121 | case, you can simply run the `start-mcp-server` command without any additional options.
122 |  
123 |     <serena> start-mcp-server
124 | 
125 | For example, to run the server in stdio mode via `uvx`, you would run:
126 | 
127 |     uvx --from git+https://github.com/oraios/serena serena start-mcp-server 
128 |  
129 | See the section ["Configuring Your MCP Client"](030_clients) for specific information on how to configure your MCP client (e.g. Claude Code, Codex, Cursor, etc.)
130 | to use such a launch command.
131 | 
132 | (streamable-http)=
133 | ### Streamable HTTP Mode
134 | 
135 | When using instead the *Streamable HTTP* mode, you control the server lifecycle yourself,
136 | i.e. you start the server and provide the client with the URL to connect to it.
137 | 
138 | Simply provide `start-mcp-server` with the `--transport streamable-http` option and optionally provide the desired port
139 | via the `--port` option.
140 | 
141 |     <serena> start-mcp-server --transport streamable-http --port <port>
142 | 
143 | For example, to run the Serena MCP server in streamable HTTP mode on port 9121 using uvx,
144 | you would run
145 | 
146 |     uvx --from git+https://github.com/oraios/serena serena start-mcp-server --transport streamable-http --port 9121
147 | 
148 | and then configure your client to connect to `http://localhost:9121/mcp`.
149 | 
150 | Note that while the legacy SSE transport is also supported (via `--transport sse` with corresponding /sse endpoint), its use is discouraged.
151 | 
152 | (mcp-args)=
153 | ### MCP Server Command-Line Arguments
154 | 
155 | The Serena MCP server supports a wide range of additional command-line options.
156 | Use the command
157 | 
158 |     <serena> start-mcp-server --help
159 | 
160 | to get a list of all available options.
161 | 
162 | Some useful options include:
163 | 
164 |   * `--project <path|name>`: specify the project to work on by name or path.
165 |   * `--project-from-cwd`: auto-detect the project from current working directory   
166 |     (looking for a directory containing `.serena/project.yml` or `.git` in parent directories, activating the current directory if none is found);  
167 |     This option is intended for CLI-based agents like Claude Code, Gemini and Codex, which are typically started from within the project directory
168 |     and which do not change directories during their operation.
169 |   * `--language-backend JetBrains`: use the Serena JetBrains Plugin as the language backend (overriding the default backend configured in the central configuration)
170 |   * `--context <context>`: specify the operation [context](contexts) in which Serena shall operate
171 |   * `--mode <mode>`: specify one or more [modes](modes) to enable (can be passed several times)
172 |   * `--enable-web-dashboard <true|false>`: enable or disable the web dashboard (enabled by default)
173 | 
174 | ## Other Commands
175 | 
176 | Serena provides several other commands in addition to `start-mcp-server`, 
177 | most of which are related to project setup and configuration.
178 | 
179 | To get a list of available commands, run:
180 | 
181 |     <serena> --help
182 | 
183 | To get help on a specific command, run:
184 | 
185 |     <serena> <command> --help
186 | 
187 | In general, add `--help` to any command or sub-command to get information about its usage and available options.
188 | 
189 | Here are some examples of commands you might find useful:
190 | 
191 | ```bash
192 | # get help about a sub-command
193 | <serena> tools list --help
194 | 
195 | # list all available tools
196 | <serena> tools list --all
197 | 
198 | # get detailed description of a specific tool
199 | <serena> tools description find_symbol
200 | 
201 | # creating a new Serena project in the current directory 
202 | <serena> project create
203 | 
204 | # creating and immediately indexing a project
205 | <serena> project create --index
206 | 
207 | # indexing the project in the current directory (auto-creates if needed)
208 | <serena> project index
209 | 
210 | # run a health check on the project in the current directory
211 | <serena> project health-check
212 | 
213 | # check if a path is ignored by the project
214 | <serena> project is_ignored_path path/to/check
215 | 
216 | # edit Serena's configuration file
217 | <serena> config edit
218 | 
219 | # list available contexts
220 | <serena> context list
221 | 
222 | # create a new context
223 | <serena> context create my-custom-context
224 | 
225 | # edit a custom context
226 | <serena> context edit my-custom-context
227 | 
228 | # list available modes
229 | <serena> mode list
230 | 
231 | # create a new mode
232 | <serena> mode create my-custom-mode
233 | 
234 | # edit a custom mode
235 | <serena> mode edit my-custom-mode
236 | 
237 | # list available prompt definitions
238 | <serena> prompts list
239 | 
240 | # create an override for internal prompts
241 | <serena> prompts create-override prompt-name
242 | 
243 | # edit a prompt override
244 | <serena> prompts edit-override prompt-name
245 | ```
246 | 
247 | Explore the full set of commands and options using the CLI itself!
248 | 
```

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

```python
  1 | """
  2 | Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | from typing import cast
  9 | 
 10 | from overrides import override
 11 | 
 12 | from solidlsp.ls import SolidLanguageServer
 13 | from solidlsp.ls_config import LanguageServerConfig
 14 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 15 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 16 | from solidlsp.settings import SolidLSPSettings
 17 | 
 18 | log = logging.getLogger(__name__)
 19 | 
 20 | 
 21 | class JediServer(SolidLanguageServer):
 22 |     """
 23 |     Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
 24 |     """
 25 | 
 26 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 27 |         """
 28 |         Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 29 |         """
 30 |         super().__init__(
 31 |             config,
 32 |             repository_root_path,
 33 |             ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path),
 34 |             "python",
 35 |             solidlsp_settings,
 36 |         )
 37 | 
 38 |     @override
 39 |     def is_ignored_dirname(self, dirname: str) -> bool:
 40 |         return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
 41 | 
 42 |     @staticmethod
 43 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
 44 |         """
 45 |         Returns the initialize params for the Jedi Language Server.
 46 |         """
 47 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
 48 |         initialize_params = {
 49 |             "processId": os.getpid(),
 50 |             "clientInfo": {"name": "Serena", "version": "0.1.0"},
 51 |             "locale": "en",
 52 |             "rootPath": repository_absolute_path,
 53 |             "rootUri": root_uri,
 54 |             # Note: this is not necessarily the minimal set of capabilities...
 55 |             "capabilities": {
 56 |                 "workspace": {
 57 |                     "applyEdit": True,
 58 |                     "workspaceEdit": {
 59 |                         "documentChanges": True,
 60 |                         "resourceOperations": ["create", "rename", "delete"],
 61 |                         "failureHandling": "textOnlyTransactional",
 62 |                         "normalizesLineEndings": True,
 63 |                         "changeAnnotationSupport": {"groupsOnLabel": True},
 64 |                     },
 65 |                     "configuration": True,
 66 |                     "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True},
 67 |                     "symbol": {
 68 |                         "dynamicRegistration": True,
 69 |                         "symbolKind": {"valueSet": list(range(1, 27))},
 70 |                         "tagSupport": {"valueSet": [1]},
 71 |                         "resolveSupport": {"properties": ["location.range"]},
 72 |                     },
 73 |                     "workspaceFolders": True,
 74 |                     "fileOperations": {
 75 |                         "dynamicRegistration": True,
 76 |                         "didCreate": True,
 77 |                         "didRename": True,
 78 |                         "didDelete": True,
 79 |                         "willCreate": True,
 80 |                         "willRename": True,
 81 |                         "willDelete": True,
 82 |                     },
 83 |                     "inlineValue": {"refreshSupport": True},
 84 |                     "inlayHint": {"refreshSupport": True},
 85 |                     "diagnostics": {"refreshSupport": True},
 86 |                 },
 87 |                 "textDocument": {
 88 |                     "publishDiagnostics": {
 89 |                         "relatedInformation": True,
 90 |                         "versionSupport": False,
 91 |                         "tagSupport": {"valueSet": [1, 2]},
 92 |                         "codeDescriptionSupport": True,
 93 |                         "dataSupport": True,
 94 |                     },
 95 |                     "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
 96 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
 97 |                     "signatureHelp": {
 98 |                         "dynamicRegistration": True,
 99 |                         "signatureInformation": {
100 |                             "documentationFormat": ["markdown", "plaintext"],
101 |                             "parameterInformation": {"labelOffsetSupport": True},
102 |                             "activeParameterSupport": True,
103 |                         },
104 |                         "contextSupport": True,
105 |                     },
106 |                     "definition": {"dynamicRegistration": True, "linkSupport": True},
107 |                     "references": {"dynamicRegistration": True},
108 |                     "documentHighlight": {"dynamicRegistration": True},
109 |                     "documentSymbol": {
110 |                         "dynamicRegistration": True,
111 |                         "symbolKind": {"valueSet": list(range(1, 27))},
112 |                         "hierarchicalDocumentSymbolSupport": True,
113 |                         "tagSupport": {"valueSet": [1]},
114 |                         "labelSupport": True,
115 |                     },
116 |                     "documentLink": {"dynamicRegistration": True, "tooltipSupport": True},
117 |                     "typeDefinition": {"dynamicRegistration": True, "linkSupport": True},
118 |                     "implementation": {"dynamicRegistration": True, "linkSupport": True},
119 |                     "declaration": {"dynamicRegistration": True, "linkSupport": True},
120 |                     "selectionRange": {"dynamicRegistration": True},
121 |                     "callHierarchy": {"dynamicRegistration": True},
122 |                     "linkedEditingRange": {"dynamicRegistration": True},
123 |                     "typeHierarchy": {"dynamicRegistration": True},
124 |                     "inlineValue": {"dynamicRegistration": True},
125 |                     "inlayHint": {
126 |                         "dynamicRegistration": True,
127 |                         "resolveSupport": {"properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"]},
128 |                     },
129 |                     "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False},
130 |                 },
131 |                 "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}},
132 |                 "experimental": {
133 |                     "serverStatusNotification": True,
134 |                     "openServerLogs": True,
135 |                 },
136 |             },
137 |             # See https://github.com/pappasam/jedi-language-server?tab=readme-ov-file
138 |             # We use the default options except for maxSymbols, where 0 means no limit
139 |             "initializationOptions": {
140 |                 "workspace": {
141 |                     "symbols": {"ignoreFolders": [".nox", ".tox", ".venv", "__pycache__", "venv"], "maxSymbols": 0},
142 |                 },
143 |             },
144 |             "trace": "verbose",
145 |             "workspaceFolders": [
146 |                 {
147 |                     "uri": root_uri,
148 |                     "name": os.path.basename(repository_absolute_path),
149 |                 }
150 |             ],
151 |         }
152 |         return cast(InitializeParams, initialize_params)
153 | 
154 |     def _start_server(self) -> None:
155 |         """
156 |         Starts the JEDI Language Server
157 |         """
158 | 
159 |         def execute_client_command_handler(params: dict) -> list:
160 |             return []
161 | 
162 |         def do_nothing(params: dict) -> None:
163 |             return
164 | 
165 |         def check_experimental_status(params: dict) -> None:
166 |             if params["quiescent"] == True:
167 |                 self.completions_available.set()
168 | 
169 |         def window_log_message(msg: dict) -> None:
170 |             log.info(f"LSP: window/logMessage: {msg}")
171 | 
172 |         self.server.on_request("client/registerCapability", do_nothing)
173 |         self.server.on_notification("language/status", do_nothing)
174 |         self.server.on_notification("window/logMessage", window_log_message)
175 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
176 |         self.server.on_notification("$/progress", do_nothing)
177 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
178 |         self.server.on_notification("language/actionableNotification", do_nothing)
179 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
180 | 
181 |         log.info("Starting jedi-language-server server process")
182 |         self.server.start()
183 |         initialize_params = self._get_initialize_params(self.repository_root_path)
184 | 
185 |         log.info("Sending initialize request from LSP client to LSP server and awaiting response")
186 |         init_response = self.server.send.initialize(initialize_params)
187 |         assert init_response["capabilities"]["textDocumentSync"]["change"] == 2  # type: ignore
188 |         assert "completionProvider" in init_response["capabilities"]
189 |         assert init_response["capabilities"]["completionProvider"] == {
190 |             "triggerCharacters": [".", "'", '"'],
191 |             "resolveProvider": True,
192 |         }
193 | 
194 |         self.server.notify.initialized({})
195 | 
```

--------------------------------------------------------------------------------
/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 |     @staticmethod
186 |     def _stop_language_server(ls: SolidLanguageServer, save_cache: bool = False, timeout: float = 2.0) -> None:
187 |         if ls.is_running():
188 |             if save_cache:
189 |                 ls.save_cache()
190 |             log.info(f"Stopping language server for language {ls.language} ...")
191 |             ls.stop(shutdown_timeout=timeout)
192 | 
193 |     def iter_language_servers(self) -> Iterator[SolidLanguageServer]:
194 |         for ls in self._language_servers.values():
195 |             yield self._ensure_functional_ls(ls)
196 | 
197 |     def stop_all(self, save_cache: bool = False, timeout: float = 2.0) -> None:
198 |         """
199 |         Stops all managed language servers.
200 | 
201 |         :param save_cache: whether to save the cache before stopping
202 |         :param timeout: timeout for shutdown of each language server
203 |         """
204 |         for ls in self.iter_language_servers():
205 |             self._stop_language_server(ls, save_cache=save_cache, timeout=timeout)
206 | 
207 |     def save_all_caches(self) -> None:
208 |         """
209 |         Saves the caches of all managed language servers.
210 |         """
211 |         for ls in self.iter_language_servers():
212 |             if ls.is_running():
213 |                 ls.save_cache()
214 | 
```

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

```python
  1 | """Erlang Language Server implementation using Erlang LS."""
  2 | 
  3 | import logging
  4 | import os
  5 | import shutil
  6 | import subprocess
  7 | import threading
  8 | import time
  9 | 
 10 | from overrides import override
 11 | 
 12 | from solidlsp.ls import SolidLanguageServer
 13 | from solidlsp.ls_config import LanguageServerConfig
 14 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 15 | from solidlsp.settings import SolidLSPSettings
 16 | 
 17 | log = logging.getLogger(__name__)
 18 | 
 19 | 
 20 | class ErlangLanguageServer(SolidLanguageServer):
 21 |     """Language server for Erlang using Erlang LS."""
 22 | 
 23 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 24 |         """
 25 |         Creates an ErlangLanguageServer instance. This class is not meant to be instantiated directly.
 26 |         Use LanguageServer.create() instead.
 27 |         """
 28 |         self.erlang_ls_path = shutil.which("erlang_ls")
 29 |         if not self.erlang_ls_path:
 30 |             raise RuntimeError("Erlang LS not found. Install from: https://github.com/erlang-ls/erlang_ls")
 31 | 
 32 |         if not self._check_erlang_installation():
 33 |             raise RuntimeError("Erlang/OTP not found. Install from: https://www.erlang.org/downloads")
 34 | 
 35 |         super().__init__(
 36 |             config,
 37 |             repository_root_path,
 38 |             ProcessLaunchInfo(cmd=[self.erlang_ls_path, "--transport", "stdio"], cwd=repository_root_path),
 39 |             "erlang",
 40 |             solidlsp_settings,
 41 |         )
 42 | 
 43 |         # Add server readiness tracking like Elixir
 44 |         self.server_ready = threading.Event()
 45 | 
 46 |         # Set generous timeout for Erlang LS initialization
 47 |         self.set_request_timeout(120.0)
 48 | 
 49 |     def _check_erlang_installation(self) -> bool:
 50 |         """Check if Erlang/OTP is available."""
 51 |         try:
 52 |             result = subprocess.run(["erl", "-version"], check=False, capture_output=True, text=True, timeout=10)
 53 |             return result.returncode == 0
 54 |         except (subprocess.SubprocessError, FileNotFoundError):
 55 |             return False
 56 | 
 57 |     @classmethod
 58 |     def _get_erlang_version(cls) -> str | None:
 59 |         """Get the installed Erlang/OTP version or None if not found."""
 60 |         try:
 61 |             result = subprocess.run(["erl", "-version"], check=False, capture_output=True, text=True, timeout=10)
 62 |             if result.returncode == 0:
 63 |                 return result.stderr.strip()  # erl -version outputs to stderr
 64 |         except (subprocess.SubprocessError, FileNotFoundError):
 65 |             return None
 66 |         return None
 67 | 
 68 |     @classmethod
 69 |     def _check_rebar3_available(cls) -> bool:
 70 |         """Check if rebar3 build tool is available."""
 71 |         try:
 72 |             result = subprocess.run(["rebar3", "version"], check=False, capture_output=True, text=True, timeout=10)
 73 |             return result.returncode == 0
 74 |         except (subprocess.SubprocessError, FileNotFoundError):
 75 |             return False
 76 | 
 77 |     def _start_server(self) -> None:
 78 |         """Start Erlang LS server process with proper initialization waiting."""
 79 | 
 80 |         def register_capability_handler(params: dict) -> None:
 81 |             return
 82 | 
 83 |         def window_log_message(msg: dict) -> None:
 84 |             """Handle window/logMessage notifications from Erlang LS"""
 85 |             message_text = msg.get("message", "")
 86 |             log.info(f"LSP: window/logMessage: {message_text}")
 87 | 
 88 |             # Look for Erlang LS readiness signals
 89 |             # Common patterns: "Started Erlang LS", "initialized", "ready"
 90 |             readiness_signals = [
 91 |                 "Started Erlang LS",
 92 |                 "server started",
 93 |                 "initialized",
 94 |                 "ready to serve requests",
 95 |                 "compilation finished",
 96 |                 "indexing complete",
 97 |             ]
 98 | 
 99 |             message_lower = message_text.lower()
100 |             for signal in readiness_signals:
101 |                 if signal.lower() in message_lower:
102 |                     log.info(f"Erlang LS readiness signal detected: {message_text}")
103 |                     self.server_ready.set()
104 |                     break
105 | 
106 |         def do_nothing(params: dict) -> None:
107 |             return
108 | 
109 |         def check_server_ready(params: dict) -> None:
110 |             """Handle $/progress notifications from Erlang LS as fallback."""
111 |             value = params.get("value", {})
112 | 
113 |             # Check for initialization completion progress
114 |             if value.get("kind") == "end":
115 |                 message = value.get("message", "")
116 |                 if any(word in message.lower() for word in ["initialized", "ready", "complete"]):
117 |                     log.info("Erlang LS initialization progress completed")
118 |                     # Set as fallback if no window/logMessage was received
119 |                     if not self.server_ready.is_set():
120 |                         self.server_ready.set()
121 | 
122 |         # Set up notification handlers
123 |         self.server.on_request("client/registerCapability", register_capability_handler)
124 |         self.server.on_notification("window/logMessage", window_log_message)
125 |         self.server.on_notification("$/progress", check_server_ready)
126 |         self.server.on_notification("window/workDoneProgress/create", do_nothing)
127 |         self.server.on_notification("$/workDoneProgress", do_nothing)
128 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
129 | 
130 |         log.info("Starting Erlang LS server process")
131 |         self.server.start()
132 | 
133 |         # Send initialize request
134 |         initialize_params = {
135 |             "processId": None,
136 |             "rootPath": self.repository_root_path,
137 |             "rootUri": f"file://{self.repository_root_path}",
138 |             "capabilities": {
139 |                 "textDocument": {
140 |                     "synchronization": {"didSave": True},
141 |                     "completion": {"dynamicRegistration": True},
142 |                     "definition": {"dynamicRegistration": True},
143 |                     "references": {"dynamicRegistration": True},
144 |                     "documentSymbol": {"dynamicRegistration": True},
145 |                     "hover": {"dynamicRegistration": True},
146 |                 }
147 |             },
148 |         }
149 | 
150 |         log.info("Sending initialize request to Erlang LS")
151 |         init_response = self.server.send.initialize(initialize_params)  # type: ignore[arg-type]
152 | 
153 |         # Verify server capabilities
154 |         if "capabilities" in init_response:
155 |             log.info(f"Erlang LS capabilities: {list(init_response['capabilities'].keys())}")
156 | 
157 |         self.server.notify.initialized({})
158 |         self.completions_available.set()
159 | 
160 |         # Wait for Erlang LS to be ready - adjust timeout based on environment
161 |         is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true"
162 |         is_macos = os.uname().sysname == "Darwin" if hasattr(os, "uname") else False
163 | 
164 |         # macOS in CI can be particularly slow for language server startup
165 |         if is_ci and is_macos:
166 |             ready_timeout = 240.0  # 4 minutes for macOS CI
167 |             env_desc = "macOS CI"
168 |         elif is_ci:
169 |             ready_timeout = 180.0  # 3 minutes for other CI
170 |             env_desc = "CI"
171 |         else:
172 |             ready_timeout = 60.0  # 1 minute for local
173 |             env_desc = "local"
174 | 
175 |         log.info(f"Waiting up to {ready_timeout} seconds for Erlang LS readiness ({env_desc} environment)...")
176 | 
177 |         if self.server_ready.wait(timeout=ready_timeout):
178 |             log.info("Erlang LS is ready and available for requests")
179 | 
180 |             # Add settling period for indexing - adjust based on environment
181 |             settling_time = 15.0 if is_ci else 5.0
182 |             log.info(f"Allowing {settling_time} seconds for Erlang LS indexing to complete...")
183 |             time.sleep(settling_time)
184 |             log.info("Erlang LS settling period complete")
185 |         else:
186 |             # Set ready anyway and continue - Erlang LS might not send explicit ready messages
187 |             log.warning(f"Erlang LS readiness timeout reached after {ready_timeout}s, proceeding anyway (common in CI)")
188 |             self.server_ready.set()
189 | 
190 |             # Still give some time for basic initialization even without explicit readiness signal
191 |             basic_settling_time = 20.0 if is_ci else 10.0
192 |             log.info(f"Allowing {basic_settling_time} seconds for basic Erlang LS initialization...")
193 |             time.sleep(basic_settling_time)
194 |             log.info("Basic Erlang LS initialization period complete")
195 | 
196 |     @override
197 |     def is_ignored_dirname(self, dirname: str) -> bool:
198 |         # For Erlang projects, we should ignore:
199 |         # - _build: rebar3 build artifacts
200 |         # - deps: dependencies
201 |         # - ebin: compiled beam files
202 |         # - .rebar3: rebar3 cache
203 |         # - logs: log files
204 |         # - node_modules: if the project has JavaScript components
205 |         return super().is_ignored_dirname(dirname) or dirname in [
206 |             "_build",
207 |             "deps",
208 |             "ebin",
209 |             ".rebar3",
210 |             "logs",
211 |             "node_modules",
212 |             "_checkouts",
213 |             "cover",
214 |         ]
215 | 
216 |     def is_ignored_filename(self, filename: str) -> bool:
217 |         """Check if a filename should be ignored."""
218 |         # Ignore compiled BEAM files
219 |         if filename.endswith(".beam"):
220 |             return True
221 |         # Don't ignore Erlang source files, header files, or configuration files
222 |         return False
223 | 
```
Page 5/21FirstPrevNextLast