#
tokens: 49598/50000 22/410 files (page 4/21)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 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

--------------------------------------------------------------------------------
/docs/01-about/020_programming-languages.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Language Support
  2 | 
  3 | Serena provides a set of versatile code querying and editing functionalities
  4 | based on symbolic understanding of the code.
  5 | Equipped with these capabilities, Serena discovers and edits code just like a seasoned developer
  6 | making use of an IDE's capabilities would.
  7 | Serena can efficiently find the right context and do the right thing even in very large and
  8 | complex projects!
  9 | 
 10 | There are two alternative technologies powering these capabilities:
 11 | 
 12 | * **Language servers** implementing the language server Protocol (LSP) — the free/open-source alternative.
 13 | * **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing
 14 |   capabilities of your JetBrains IDE.
 15 | 
 16 | You can choose either of these backends depending on your preferences and requirements.
 17 | 
 18 | ## Language Servers
 19 | 
 20 | Serena incorporates a powerful abstraction layer for the integration of language servers 
 21 | that implement the language server protocol (LSP).
 22 | It even supports multiple language servers in parallel to support polyglot projects.
 23 | 
 24 | The language servers themselves are typically open-source projects (like Serena)
 25 | or at least freely available for use.
 26 | 
 27 | We currently provide direct, out-of-the-box support for the programming languages listed below.
 28 | Some languages require additional installations or setup steps, as noted.
 29 | 
 30 | * **AL**
 31 | * **Bash**
 32 | * **C#**
 33 | * **C/C++**  
 34 |   (you may experience issues with finding references, we are working on it)
 35 | * **Clojure**
 36 | * **Dart**
 37 | * **Elixir**  
 38 |   (requires Elixir installation; Expert language server is downloaded automatically)
 39 | * **Elm**  
 40 |   (requires Elm compiler)
 41 | * **Erlang**  
 42 |   (requires installation of beam and [erlang_ls](https://github.com/erlang-ls/erlang_ls); experimental, might be slow or hang)
 43 | * **F#**  
 44 |   (requires .NET SDK 8.0+; uses FsAutoComplete/Ionide, which is auto-installed; for Homebrew .NET on macOS, set DOTNET_ROOT in your environment)
 45 | * **Fortran**   
 46 |   (requires installation of fortls: `pip install fortls`)
 47 | * **Go**  
 48 |   (requires installation of `gopls`)
 49 | * **Groovy**  
 50 |   (requires local groovy-language-server.jar setup via GROOVY_LS_JAR_PATH or configuration)
 51 | * **Haskell**  
 52 |   (automatically locates HLS via ghcup, stack, or system PATH; supports Stack and Cabal projects)
 53 | * **Java**  
 54 | * **JavaScript**
 55 | * **Julia**
 56 | * **Kotlin**  
 57 |   (uses the pre-alpha [official kotlin LS](https://github.com/Kotlin/kotlin-lsp), some issues may appear)
 58 | * **Lua**
 59 | * **Markdown**  
 60 |   (must be explicitly specified via `--language markdown` when generating project config, primarily useful for documentation-heavy projects)
 61 | * **Nix**  
 62 |   (requires nixd installation)
 63 | * **Pascal**
 64 |   (Free Pascal/Lazarus; automatically downloads pasls binary; set PP and FPCDIR environment variables for source navigation)
 65 | * **Perl**
 66 |   (requires installation of Perl::LanguageServer)
 67 | * **PHP**  
 68 |   (uses Intelephense LSP; set `INTELEPHENSE_LICENSE_KEY` environment variable for premium features)
 69 | * **Python**
 70 | * **R**  
 71 |   (requires installation of the `languageserver` R package)
 72 | * **Ruby**  
 73 |   (by default, uses [ruby-lsp](https://github.com/Shopify/ruby-lsp), specify ruby_solargraph as your language to use the previous solargraph based implementation)
 74 | * **Rust**  
 75 |   (requires [rustup](https://rustup.rs/) - uses rust-analyzer from your toolchain)
 76 | * **Scala**  
 77 |   (requires some [manual setup](../03-special-guides/scala_setup_guide_for_serena); uses Metals LSP)
 78 | * **Swift**
 79 | * **TypeScript**
 80 | * **Vue**    
 81 |   (3.x with TypeScript; requires Node.js v18+ and npm; supports .vue Single File Components with monorepo detection)
 82 | * **YAML**
 83 | * **Zig**  
 84 |   (requires installation of ZLS - Zig Language Server)
 85 | 
 86 | Support for further languages can easily be added by providing a shallow adapter for a new language server implementation,
 87 | see Serena's [memory on that](https://github.com/oraios/serena/blob/main/.serena/memories/adding_new_language_support_guide.md).
 88 | 
 89 | ## The Serena JetBrains Plugin
 90 | 
 91 | As an alternative to language servers, the [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/)
 92 | leverages the powerful code analysis capabilities of JetBrains IDEs. 
 93 | The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs, 
 94 | including IntelliJ IDEA, PyCharm, Android Studio, AppCode, WebStorm, PhpStorm, RubyMine, GoLand, AppCode, CLion, Rider, and others.
 95 | 
 96 | When using the plugin, Serena connects to an instance of your JetBrains IDE via the plugin. For users who already
 97 | work in a JetBrains IDE, this means Serena seamlessly integrates with the IDE instance you typically have open anyway,
 98 | requiring no additional setup or configuration beyond the plugin itself. This approach offers several key advantages:
 99 | 
100 | * **External library indexing**: Dependencies and libraries are fully indexed and accessible to Serena
101 | * **No additional setup**: No need to download or configure separate language servers
102 | * **Enhanced performance**: Faster tool execution thanks to optimized IDE integration
103 | * **Multi-language excellence**: First-class support for polyglot projects with multiple languages and frameworks
104 | 
105 | Even if you prefer to work in a different code editor, you can still benefit from the JetBrains plugin by running 
106 | a JetBrains IDE instance (most have free community editions) alongside your preferred editor with your project 
107 | opened and indexed. Serena will connect to the IDE for code analysis while you continue working in your editor 
108 | of choice.
109 | 
110 | ```{raw} html
111 | <p>
112 | <a href="https://plugins.jetbrains.com/plugin/28946-serena/">
113 | <img style="background-color:transparent;" src="../_static/images/jetbrains-marketplace-button.png">
114 | </a>
115 | </p>
116 | ```
117 | 
118 | See the [JetBrains Plugin documentation](../02-usage/025_jetbrains_plugin) for usage details.
```

--------------------------------------------------------------------------------
/src/serena/resources/project.template.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # list of languages for which language servers are started; choose from:
 2 | #   al               bash             clojure          cpp              csharp           csharp_omnisharp
 3 | #   dart             elixir           elm              erlang           fortran          fsharp
 4 | #   go               groovy           haskell          java             julia            kotlin
 5 | #   lua              markdown         nix              pascal           perl             php
 6 | #   powershell       python           python_jedi      r                rego             ruby
 7 | #   ruby_solargraph  rust             scala            swift            terraform        toml
 8 | #   typescript       typescript_vts   yaml             zig
 9 | # Note:
10 | #   - For C, use cpp
11 | #   - For JavaScript, use typescript
12 | #   - For Free Pascal / Lazarus, use pascal
13 | # Special requirements:
14 | #   - csharp: Requires the presence of a .sln file in the project folder.
15 | #   - pascal: Requires Free Pascal Compiler (fpc) and optionally Lazarus.
16 | # When using multiple languages, the first language server that supports a given file will be used for that file.
17 | # The first language is the default language and the respective language server will be used as a fallback.
18 | # Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
19 | languages: ["python"]
20 | 
21 | # the encoding used by text files in the project
22 | # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
23 | encoding: "utf-8"
24 | 
25 | # whether to use the project's gitignore file to ignore files
26 | # Added on 2025-04-07
27 | ignore_all_files_in_gitignore: true
28 | 
29 | # list of additional paths to ignore
30 | # same syntax as gitignore, so you can use * and **
31 | # Was previously called `ignored_dirs`, please update your config if you are using that.
32 | # Added (renamed) on 2025-04-07
33 | ignored_paths: []
34 | 
35 | # whether the project is in read-only mode
36 | # If set to true, all editing tools will be disabled and attempts to use them will result in an error
37 | # Added on 2025-04-18
38 | read_only: false
39 | 
40 | # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
41 | # Below is the complete list of tools for convenience.
42 | # To make sure you have the latest list of tools, and to view their descriptions, 
43 | # execute `uv run scripts/print_tool_overview.py`.
44 | #
45 | #  * `activate_project`: Activates a project by name.
46 | #  * `check_onboarding_performed`: Checks whether project onboarding was already performed.
47 | #  * `create_text_file`: Creates/overwrites a file in the project directory.
48 | #  * `delete_lines`: Deletes a range of lines within a file.
49 | #  * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
50 | #  * `execute_shell_command`: Executes a shell command.
51 | #  * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
52 | #  * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
53 | #  * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
54 | #  * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
55 | #  * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
56 | #  * `initial_instructions`: Gets the initial instructions for the current project.
57 | #     Should only be used in settings where the system prompt cannot be set,
58 | #     e.g. in clients you have no control over, like Claude Desktop.
59 | #  * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
60 | #  * `insert_at_line`: Inserts content at a given line in a file.
61 | #  * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
62 | #  * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
63 | #  * `list_memories`: Lists memories in Serena's project-specific memory store.
64 | #  * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
65 | #  * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
66 | #  * `read_file`: Reads a file within the project directory.
67 | #  * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
68 | #  * `remove_project`: Removes a project from the Serena configuration.
69 | #  * `replace_lines`: Replaces a range of lines within a file with new content.
70 | #  * `replace_symbol_body`: Replaces the full definition of a symbol.
71 | #  * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
72 | #  * `search_for_pattern`: Performs a search for a pattern in the project.
73 | #  * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
74 | #  * `switch_modes`: Activates modes by providing a list of their names
75 | #  * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
76 | #  * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
77 | #  * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
78 | #  * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
79 | excluded_tools: []
80 | 
81 | # initial prompt for the project. It will always be given to the LLM upon activating the project
82 | # (contrary to the memories, which are loaded on demand).
83 | initial_prompt: ""
84 | 
85 | project_name: "project_name"
86 | 
```

--------------------------------------------------------------------------------
/DOCKER.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Docker Setup for Serena (Experimental)
  2 | 
  3 | ⚠️ **EXPERIMENTAL FEATURE**: The Docker setup for Serena is still experimental and has some limitations. Please read this entire document before using Docker with Serena.
  4 | 
  5 | ## Overview
  6 | 
  7 | Docker support allows you to run Serena in an isolated container environment, which provides better security isolation for the shell tool and consistent dependencies across different systems.
  8 | 
  9 | ## Benefits
 10 | 
 11 | - **Safer shell tool execution**: Commands run in an isolated container environment
 12 | - **Consistent dependencies**: No need to manage language servers and dependencies on your host system
 13 | - **Cross-platform support**: Works consistently across Windows, macOS, and Linux
 14 | 
 15 | ## Important Usage Pointers
 16 | 
 17 | ### Configuration
 18 | 
 19 | Serena's configuration and log files are stored in the container in `/workspaces/serena/config/`.
 20 | Any local configuration you may have for Serena will not apply; the container uses its own separate configuration.
 21 | 
 22 | You can mount a local configuration/data directory to persist settings across container restarts
 23 | (which will also contain session log files).
 24 | Simply mount your local directory to `/workspaces/serena/config` in the container.
 25 | Initially, be sure to add a `serena_config.yml` file to the mounted directory which applies the following
 26 | special settings for Docker usage:
 27 | ```
 28 | # Disable the GUI log window since it's not supported in Docker
 29 | gui_log_window: False
 30 | # Listen on all interfaces for the web dashboard to be accessible from outside the container
 31 | web_dashboard_listen_address: 0.0.0.0
 32 | # Disable opening the web dashboard on launch (not possible within the container)
 33 | web_dashboard_open_on_launch: False
 34 | ```
 35 | Set other configuration options as needed.
 36 | 
 37 | ### Project Activation Limitations
 38 | 
 39 | - **Only mounted directories work**: Projects must be mounted as volumes to be accessible
 40 | - Projects outside the mounted directories cannot be activated or accessed
 41 | - Since projects are not remembered across container restarts (unless you mount a local configuration as described above), 
 42 |   activate them using the full path (e.g. `/workspaces/projects/my-project`) when using dynamic project activation
 43 | 
 44 | ### Language Support Limitations
 45 | 
 46 | The default Docker image does not include dependencies for languages that
 47 | require explicit system-level installations.
 48 | Only languages that install their requirements on the fly will work out of the box.
 49 | 
 50 | ### Dashboard Port Configuration
 51 | 
 52 | The web dashboard runs on port 24282 (0x5EDA) by default. You can configure this using environment variables:
 53 | 
 54 | ```bash
 55 | # Use default ports
 56 | docker-compose up serena
 57 | 
 58 | # Use custom ports
 59 | SERENA_DASHBOARD_PORT=8080 docker-compose up serena
 60 | ```
 61 | 
 62 | ⚠️ **Note**: If the local port is occupied, you'll need to specify a different port using the environment variable.
 63 | 
 64 | ### Line Ending Issues on Windows
 65 | 
 66 | ⚠️ **Windows Users**: Be aware of potential line ending inconsistencies:
 67 | - Files edited within the Docker container may use Unix line endings (LF)
 68 | - Your Windows system may expect Windows line endings (CRLF)
 69 | - This can cause issues with version control and text editors
 70 | - Configure your Git settings appropriately: `git config core.autocrlf true`
 71 | 
 72 | ## Quick Start
 73 | 
 74 | ### Using Docker Compose (Recommended)
 75 | 
 76 | 1. **Production mode** (for using Serena as MCP server):
 77 |    ```bash
 78 |    docker-compose up serena
 79 |    ```
 80 | 
 81 | 2. **Development mode** (with source code mounted):
 82 |    ```bash
 83 |    docker-compose up serena-dev
 84 |    ```
 85 | 
 86 | Note: Edit the `compose.yaml` file to customize volume mounts for your projects.
 87 | 
 88 | ### Building the Docker Image Manually
 89 | 
 90 | ```bash
 91 | # Build the image
 92 | docker build -t serena .
 93 | 
 94 | # Run with current directory mounted
 95 | docker run -it --rm \
 96 |   -v "$(pwd)":/workspace \
 97 |   -p 9121:9121 \
 98 |   -p 24282:24282 \
 99 |   -e SERENA_DOCKER=1 \
100 |   serena
101 | ```
102 | 
103 | ### Using Docker Compose with Merge Compose files
104 | 
105 | To use Docker Compose with merge files, you can create a `compose.override.yml` file to customize the configuration:
106 | 
107 | ```yaml
108 | services:
109 |   serena:
110 |     # To work with projects, you must mount them as volumes:
111 |     volumes:
112 |       - ./my-project:/workspace/my-project
113 |       - /path/to/another/project:/workspace/another-project
114 |     # Add the context for the IDE assistant option:
115 |     command:
116 |       - "uv run --directory . serena-mcp-server --transport sse --port 9121 --host 0.0.0.0 --context claude-code"
117 | ```
118 | 
119 | See the [Docker Merge Compose files documentation](https://docs.docker.com/compose/how-tos/multiple-compose-files/merge/) for more details on using merge files.
120 | 
121 | ## Accessing the Dashboard
122 | 
123 | Once running, access the web dashboard at:
124 | - Default: http://localhost:24282/dashboard
125 | - Custom port: http://localhost:${SERENA_DASHBOARD_PORT}/dashboard
126 | 
127 | ## Volume Mounting
128 | 
129 | To work with projects, you must mount them as volumes:
130 | 
131 | ```yaml
132 | # In compose.yaml
133 | volumes:
134 |   - ./my-project:/workspace/my-project
135 |   - /path/to/another/project:/workspace/another-project
136 | ```
137 | 
138 | ## Environment Variables
139 | 
140 | - `SERENA_DOCKER=1`: Set automatically to indicate Docker environment
141 | - `SERENA_PORT`: MCP server port (default: 9121)
142 | - `SERENA_DASHBOARD_PORT`: Web dashboard port (default: 24282)
143 | - `INTELEPHENSE_LICENSE_KEY`: License key for Intelephense PHP LSP premium features (optional)
144 | 
145 | ## Troubleshooting
146 | 
147 | ### Port Already in Use
148 | 
149 | If you see "port already in use" errors:
150 | ```bash
151 | # Check what's using the port
152 | lsof -i :24282  # macOS/Linux
153 | netstat -ano | findstr :24282  # Windows
154 | 
155 | # Use a different port
156 | SERENA_DASHBOARD_PORT=8080 docker-compose up serena
157 | ```
158 | 
159 | ### Configuration Issues
160 | 
161 | If you need to reset Docker configuration:
162 | ```bash
163 | # Remove Docker-specific config
164 | rm serena_config.docker.yml
165 | 
166 | # Serena will auto-generate a new one on next run
167 | ```
168 | 
169 | ### Project Access Issues
170 | 
171 | Ensure projects are properly mounted:
172 | - Check volume mounts in `docker-compose.yaml`
173 | - Use absolute paths for external projects
174 | - Verify permissions on mounted directories
175 | 
```

--------------------------------------------------------------------------------
/src/serena/agno.py:
--------------------------------------------------------------------------------

```python
  1 | import argparse
  2 | import logging
  3 | import os
  4 | import threading
  5 | from pathlib import Path
  6 | from typing import Any
  7 | 
  8 | from agno.agent import Agent
  9 | from agno.db.sqlite import SqliteDb
 10 | from agno.memory import MemoryManager
 11 | from agno.models.base import Model
 12 | from agno.tools.function import Function
 13 | from agno.tools.toolkit import Toolkit
 14 | from dotenv import load_dotenv
 15 | from sensai.util.logging import LogTime
 16 | 
 17 | from serena.agent import SerenaAgent, Tool
 18 | from serena.config.context_mode import SerenaAgentContext
 19 | from serena.constants import REPO_ROOT
 20 | from serena.util.exception import show_fatal_exception_safe
 21 | 
 22 | log = logging.getLogger(__name__)
 23 | 
 24 | 
 25 | class SerenaAgnoToolkit(Toolkit):
 26 |     def __init__(self, serena_agent: SerenaAgent):
 27 |         super().__init__("Serena")
 28 |         for tool in serena_agent.get_exposed_tool_instances():
 29 |             self.functions[tool.get_name_from_cls()] = self._create_agno_function(tool)
 30 |         log.info("Agno agent functions: %s", list(self.functions.keys()))
 31 | 
 32 |     @staticmethod
 33 |     def _create_agno_function(tool: Tool) -> Function:
 34 |         def entrypoint(**kwargs: Any) -> str:
 35 |             if "kwargs" in kwargs:
 36 |                 # Agno sometimes passes a kwargs argument explicitly, so we merge it
 37 |                 kwargs.update(kwargs["kwargs"])
 38 |                 del kwargs["kwargs"]
 39 |             log.info(f"Calling tool {tool}")
 40 |             return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs)
 41 | 
 42 |         function = Function.from_callable(tool.get_apply_fn())
 43 |         function.name = tool.get_name_from_cls()
 44 |         function.entrypoint = entrypoint
 45 |         function.skip_entrypoint_processing = True
 46 |         return function
 47 | 
 48 | 
 49 | class SerenaAgnoAgentProvider:
 50 |     _agent: Agent | None = None
 51 |     _lock = threading.Lock()
 52 | 
 53 |     @classmethod
 54 |     def get_agent(cls, model: Model) -> Agent:
 55 |         """
 56 |         Returns the singleton instance of the Serena agent or creates it with the given parameters if it doesn't exist.
 57 | 
 58 |         NOTE: This is very ugly with poor separation of concerns, but the way in which the Agno UI works (reloading the
 59 |             module that defines the `app` variable) essentially forces us to do something like this.
 60 | 
 61 |         :param model: the large language model to use for the agent
 62 |         :return: the agent instance
 63 |         """
 64 |         with cls._lock:
 65 |             if cls._agent is not None:
 66 |                 return cls._agent
 67 | 
 68 |             # change to Serena root
 69 |             os.chdir(REPO_ROOT)
 70 | 
 71 |             load_dotenv()
 72 | 
 73 |             parser = argparse.ArgumentParser(description="Serena coding assistant")
 74 | 
 75 |             # Create a mutually exclusive group
 76 |             group = parser.add_mutually_exclusive_group()
 77 | 
 78 |             # Add arguments to the group, both pointing to the same destination
 79 |             group.add_argument(
 80 |                 "--project-file",
 81 |                 required=False,
 82 |                 help="Path to the project (or project.yml file).",
 83 |             )
 84 |             group.add_argument(
 85 |                 "--project",
 86 |                 required=False,
 87 |                 help="Path to the project (or project.yml file).",
 88 |             )
 89 |             args = parser.parse_args()
 90 | 
 91 |             args_project_file = args.project or args.project_file
 92 | 
 93 |             if args_project_file:
 94 |                 project_file = Path(args_project_file).resolve()
 95 |                 # If project file path is relative, make it absolute by joining with project root
 96 |                 if not project_file.is_absolute():
 97 |                     # Get the project root directory (parent of scripts directory)
 98 |                     project_root = Path(REPO_ROOT)
 99 |                     project_file = project_root / args_project_file
100 | 
101 |                 # Ensure the path is normalized and absolute
102 |                 project_file = str(project_file.resolve())
103 |             else:
104 |                 project_file = None
105 | 
106 |             with LogTime("Loading Serena agent"):
107 |                 try:
108 |                     serena_agent = SerenaAgent(project_file, context=SerenaAgentContext.load("agent"))
109 |                 except Exception as e:
110 |                     show_fatal_exception_safe(e)
111 |                     raise
112 | 
113 |             # Even though we don't want to keep history between sessions,
114 |             # for agno-ui to work as a conversation, we use a persistent database on disk.
115 |             # This database should be deleted between sessions.
116 |             # Note that this might collide with custom options for the agent, like adding vector-search based tools.
117 |             sql_db_path = (Path("temp") / "agno_agent_storage.db").absolute()
118 |             sql_db_path.parent.mkdir(exist_ok=True)
119 |             # delete the db file if it exists
120 |             log.info(f"Deleting DB from PID {os.getpid()}")
121 |             if sql_db_path.exists():
122 |                 sql_db_path.unlink()
123 | 
124 |             agno_agent = Agent(
125 |                 name="Serena",
126 |                 model=model,
127 |                 # See explanation above on why database is needed
128 |                 db=SqliteDb(db_file=str(sql_db_path)),
129 |                 description="A fully-featured coding assistant",
130 |                 tools=[SerenaAgnoToolkit(serena_agent)],
131 |                 # Tool calls will be shown in the UI since that's configurable per tool
132 |                 # To see detailed logs, you should use the serena logger (configure it in the project file path)
133 |                 markdown=True,
134 |                 system_message=serena_agent.create_system_prompt(),
135 |                 telemetry=False,
136 |                 memory_manager=MemoryManager(),
137 |                 add_history_to_context=True,
138 |                 num_history_runs=100,  # you might want to adjust this (expense vs. history awareness)
139 |             )
140 |             cls._agent = agno_agent
141 |             log.info(f"Agent instantiated: {agno_agent}")
142 | 
143 |         return agno_agent
144 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/rust/test_rust_2024_edition.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | from collections.abc import Iterator
  3 | from pathlib import Path
  4 | 
  5 | import pytest
  6 | 
  7 | from solidlsp import SolidLanguageServer
  8 | from solidlsp.ls_config import Language
  9 | from solidlsp.ls_utils import SymbolUtils
 10 | from test.conftest import start_ls_context
 11 | 
 12 | 
 13 | @pytest.fixture(scope="class")
 14 | def rust_language_server() -> Iterator[SolidLanguageServer]:
 15 |     """Set up the test class with the Rust 2024 edition test repository."""
 16 |     test_repo_2024_path = TestRust2024EditionLanguageServer.test_repo_2024_path
 17 | 
 18 |     if not test_repo_2024_path.exists():
 19 |         pytest.skip("Rust 2024 edition test repository not found")
 20 | 
 21 |     # Create and start the language server for the 2024 edition repo
 22 |     with start_ls_context(Language.RUST, str(test_repo_2024_path)) as ls:
 23 |         yield ls
 24 | 
 25 | 
 26 | @pytest.mark.rust
 27 | class TestRust2024EditionLanguageServer:
 28 |     test_repo_2024_path = Path(__file__).parent.parent.parent / "resources" / "repos" / "rust" / "test_repo_2024"
 29 | 
 30 |     def test_find_references_raw(self, rust_language_server) -> None:
 31 |         # Test finding references to the 'add' function defined in main.rs
 32 |         file_path = os.path.join("src", "main.rs")
 33 |         symbols = rust_language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
 34 |         add_symbol = None
 35 |         for sym in symbols[0]:
 36 |             if sym.get("name") == "add":
 37 |                 add_symbol = sym
 38 |                 break
 39 |         assert add_symbol is not None, "Could not find 'add' function symbol in main.rs"
 40 |         sel_start = add_symbol["selectionRange"]["start"]
 41 |         refs = rust_language_server.request_references(file_path, sel_start["line"], sel_start["character"])
 42 |         # The add function should be referenced within main.rs itself (in the main function)
 43 |         assert any("main.rs" in ref.get("relativePath", "") for ref in refs), "main.rs should reference add function"
 44 | 
 45 |     def test_find_symbol(self, rust_language_server) -> None:
 46 |         symbols = rust_language_server.request_full_symbol_tree()
 47 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree"
 48 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add function not found in symbol tree"
 49 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "multiply"), "multiply function not found in symbol tree"
 50 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator struct not found in symbol tree"
 51 | 
 52 |     def test_find_referencing_symbols_multiply(self, rust_language_server) -> None:
 53 |         # Find references to 'multiply' function defined in lib.rs
 54 |         file_path = os.path.join("src", "lib.rs")
 55 |         symbols = rust_language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
 56 |         multiply_symbol = None
 57 |         for sym in symbols[0]:
 58 |             if sym.get("name") == "multiply":
 59 |                 multiply_symbol = sym
 60 |                 break
 61 |         assert multiply_symbol is not None, "Could not find 'multiply' function symbol in lib.rs"
 62 |         sel_start = multiply_symbol["selectionRange"]["start"]
 63 |         refs = rust_language_server.request_references(file_path, sel_start["line"], sel_start["character"])
 64 |         # The multiply function exists but may not be referenced anywhere, which is fine
 65 |         # This test just verifies we can find the symbol and request references without error
 66 |         assert isinstance(refs, list), "Should return a list of references (even if empty)"
 67 | 
 68 |     def test_find_calculator_struct_and_impl(self, rust_language_server) -> None:
 69 |         # Test finding the Calculator struct and its impl block
 70 |         file_path = os.path.join("src", "lib.rs")
 71 |         symbols = rust_language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
 72 | 
 73 |         # Find the Calculator struct
 74 |         calculator_struct = None
 75 |         calculator_impl = None
 76 |         for sym in symbols[0]:
 77 |             if sym.get("name") == "Calculator" and sym.get("kind") == 23:  # Struct kind
 78 |                 calculator_struct = sym
 79 |             elif sym.get("name") == "Calculator" and sym.get("kind") == 11:  # Interface/Impl kind
 80 |                 calculator_impl = sym
 81 | 
 82 |         assert calculator_struct is not None, "Could not find 'Calculator' struct symbol in lib.rs"
 83 | 
 84 |         # The struct should have the 'result' field
 85 |         struct_children = calculator_struct.get("children", [])
 86 |         field_names = [child.get("name") for child in struct_children]
 87 |         assert "result" in field_names, "Calculator struct should have 'result' field"
 88 | 
 89 |         # Find the impl block and check its methods
 90 |         if calculator_impl is not None:
 91 |             impl_children = calculator_impl.get("children", [])
 92 |             method_names = [child.get("name") for child in impl_children]
 93 |             assert "new" in method_names, "Calculator impl should have 'new' method"
 94 |             assert "add" in method_names, "Calculator impl should have 'add' method"
 95 |             assert "get_result" in method_names, "Calculator impl should have 'get_result' method"
 96 | 
 97 |     def test_overview_methods(self, rust_language_server) -> None:
 98 |         symbols = rust_language_server.request_full_symbol_tree()
 99 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main missing from overview"
100 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add missing from overview"
101 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "multiply"), "multiply missing from overview"
102 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator missing from overview"
103 | 
104 |     def test_rust_2024_edition_specific(self) -> None:
105 |         # Verify we're actually working with the 2024 edition repository
106 |         cargo_toml_path = self.test_repo_2024_path / "Cargo.toml"
107 |         assert cargo_toml_path.exists(), "Cargo.toml should exist in test repository"
108 | 
109 |         with open(cargo_toml_path) as f:
110 |             content = f.read()
111 |             assert 'edition = "2024"' in content, "Should be using Rust 2024 edition"
112 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/bash/test_bash_basic.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Basic integration tests for the bash language server functionality.
  3 | 
  4 | These tests validate the functionality of the language server APIs
  5 | like request_document_symbols using the bash test repository.
  6 | """
  7 | 
  8 | import pytest
  9 | 
 10 | from solidlsp import SolidLanguageServer
 11 | from solidlsp.ls_config import Language
 12 | 
 13 | 
 14 | @pytest.mark.bash
 15 | class TestBashLanguageServerBasics:
 16 |     """Test basic functionality of the bash language server."""
 17 | 
 18 |     @pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
 19 |     def test_bash_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
 20 |         """Test that bash language server can be initialized successfully."""
 21 |         assert language_server is not None
 22 |         assert language_server.language == Language.BASH
 23 | 
 24 |     @pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
 25 |     def test_bash_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
 26 |         """Test request_document_symbols for bash files."""
 27 |         # Test getting symbols from main.sh
 28 |         all_symbols, _root_symbols = language_server.request_document_symbols("main.sh").get_all_symbols_and_roots()
 29 | 
 30 |         # Extract function symbols (LSP Symbol Kind 12)
 31 |         function_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 12]
 32 |         function_names = [symbol["name"] for symbol in function_symbols]
 33 | 
 34 |         # Should detect all 3 functions from main.sh
 35 |         assert "greet_user" in function_names, "Should find greet_user function"
 36 |         assert "process_items" in function_names, "Should find process_items function"
 37 |         assert "main" in function_names, "Should find main function"
 38 |         assert len(function_symbols) >= 3, f"Should find at least 3 functions, found {len(function_symbols)}"
 39 | 
 40 |     @pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
 41 |     def test_bash_request_document_symbols_with_body(self, language_server: SolidLanguageServer) -> None:
 42 |         """Test request_document_symbols with body extraction."""
 43 |         # Test with include_body=True
 44 |         all_symbols, _root_symbols = language_server.request_document_symbols("main.sh").get_all_symbols_and_roots()
 45 | 
 46 |         function_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 12]
 47 | 
 48 |         # Find greet_user function and check it has body
 49 |         greet_user_symbol = next((sym for sym in function_symbols if sym["name"] == "greet_user"), None)
 50 |         assert greet_user_symbol is not None, "Should find greet_user function"
 51 | 
 52 |         if "body" in greet_user_symbol:
 53 |             body = greet_user_symbol["body"]
 54 |             assert "function greet_user()" in body, "Function body should contain function definition"
 55 |             assert "case" in body.lower(), "Function body should contain case statement"
 56 | 
 57 |     @pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
 58 |     def test_bash_utils_functions(self, language_server: SolidLanguageServer) -> None:
 59 |         """Test function detection in utils.sh file."""
 60 |         # Test with utils.sh as well
 61 |         utils_all_symbols, _utils_root_symbols = language_server.request_document_symbols("utils.sh").get_all_symbols_and_roots()
 62 | 
 63 |         utils_function_symbols = [symbol for symbol in utils_all_symbols if symbol.get("kind") == 12]
 64 |         utils_function_names = [symbol["name"] for symbol in utils_function_symbols]
 65 | 
 66 |         # Should detect functions from utils.sh
 67 |         expected_utils_functions = [
 68 |             "to_uppercase",
 69 |             "to_lowercase",
 70 |             "trim_whitespace",
 71 |             "backup_file",
 72 |             "contains_element",
 73 |             "log_message",
 74 |             "is_valid_email",
 75 |             "is_number",
 76 |         ]
 77 | 
 78 |         for func_name in expected_utils_functions:
 79 |             assert func_name in utils_function_names, f"Should find {func_name} function in utils.sh"
 80 | 
 81 |         assert len(utils_function_symbols) >= 8, f"Should find at least 8 functions in utils.sh, found {len(utils_function_symbols)}"
 82 | 
 83 |     @pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
 84 |     def test_bash_function_syntax_patterns(self, language_server: SolidLanguageServer) -> None:
 85 |         """Test that LSP detects different bash function syntax patterns correctly."""
 86 |         # Test main.sh (has both 'function' keyword and traditional syntax)
 87 |         main_all_symbols, _main_root_symbols = language_server.request_document_symbols("main.sh").get_all_symbols_and_roots()
 88 |         main_functions = [symbol for symbol in main_all_symbols if symbol.get("kind") == 12]
 89 |         main_function_names = [func["name"] for func in main_functions]
 90 | 
 91 |         # Test utils.sh (all use 'function' keyword)
 92 |         utils_all_symbols, _utils_root_symbols = language_server.request_document_symbols("utils.sh").get_all_symbols_and_roots()
 93 |         utils_functions = [symbol for symbol in utils_all_symbols if symbol.get("kind") == 12]
 94 |         utils_function_names = [func["name"] for func in utils_functions]
 95 | 
 96 |         # Verify LSP detects both syntax patterns
 97 |         # main() uses traditional syntax: main() {
 98 |         assert "main" in main_function_names, "LSP should detect traditional function syntax"
 99 | 
100 |         # Functions with 'function' keyword: function name() {
101 |         assert "greet_user" in main_function_names, "LSP should detect function keyword syntax"
102 |         assert "process_items" in main_function_names, "LSP should detect function keyword syntax"
103 | 
104 |         # Verify all expected utils functions are detected by LSP
105 |         expected_utils = [
106 |             "to_uppercase",
107 |             "to_lowercase",
108 |             "trim_whitespace",
109 |             "backup_file",
110 |             "contains_element",
111 |             "log_message",
112 |             "is_valid_email",
113 |             "is_number",
114 |         ]
115 | 
116 |         for expected_func in expected_utils:
117 |             assert expected_func in utils_function_names, f"LSP should detect {expected_func} function"
118 | 
119 |         # Verify total counts match expectations
120 |         assert len(main_functions) >= 3, f"Should find at least 3 functions in main.sh, found {len(main_functions)}"
121 |         assert len(utils_functions) >= 8, f"Should find at least 8 functions in utils.sh, found {len(utils_functions)}"
122 | 
```

--------------------------------------------------------------------------------
/src/serena/analytics.py:
--------------------------------------------------------------------------------

```python
  1 | from __future__ import annotations
  2 | 
  3 | import logging
  4 | import threading
  5 | from abc import ABC, abstractmethod
  6 | from collections import defaultdict
  7 | from copy import copy
  8 | from dataclasses import asdict, dataclass
  9 | from enum import Enum
 10 | 
 11 | from anthropic.types import MessageParam, MessageTokensCount
 12 | from dotenv import load_dotenv
 13 | 
 14 | log = logging.getLogger(__name__)
 15 | 
 16 | 
 17 | class TokenCountEstimator(ABC):
 18 |     @abstractmethod
 19 |     def estimate_token_count(self, text: str) -> int:
 20 |         """
 21 |         Estimate the number of tokens in the given text.
 22 |         This is an abstract method that should be implemented by subclasses.
 23 |         """
 24 | 
 25 | 
 26 | class TiktokenCountEstimator(TokenCountEstimator):
 27 |     """
 28 |     Approximate token count using tiktoken.
 29 |     """
 30 | 
 31 |     def __init__(self, model_name: str = "gpt-4o"):
 32 |         """
 33 |         The tokenizer will be downloaded on the first initialization, which may take some time.
 34 | 
 35 |         :param model_name: see `tiktoken.model` to see available models.
 36 |         """
 37 |         import tiktoken
 38 | 
 39 |         log.info(f"Loading tiktoken encoding for model {model_name}, this may take a while on the first run.")
 40 |         self._encoding = tiktoken.encoding_for_model(model_name)
 41 | 
 42 |     def estimate_token_count(self, text: str) -> int:
 43 |         return len(self._encoding.encode(text))
 44 | 
 45 | 
 46 | class AnthropicTokenCount(TokenCountEstimator):
 47 |     """
 48 |     The exact count using the Anthropic API.
 49 |     Counting is free, but has a rate limit and will require an API key,
 50 |     (typically, set through an env variable).
 51 |     See https://docs.anthropic.com/en/docs/build-with-claude/token-counting
 52 |     """
 53 | 
 54 |     def __init__(self, model_name: str = "claude-sonnet-4-20250514", api_key: str | None = None):
 55 |         import anthropic
 56 | 
 57 |         self._model_name = model_name
 58 |         if api_key is None:
 59 |             load_dotenv()
 60 |         self._anthropic_client = anthropic.Anthropic(api_key=api_key)
 61 | 
 62 |     def _send_count_tokens_request(self, text: str) -> MessageTokensCount:
 63 |         return self._anthropic_client.messages.count_tokens(
 64 |             model=self._model_name,
 65 |             messages=[MessageParam(role="user", content=text)],
 66 |         )
 67 | 
 68 |     def estimate_token_count(self, text: str) -> int:
 69 |         return self._send_count_tokens_request(text).input_tokens
 70 | 
 71 | 
 72 | class CharCountEstimator(TokenCountEstimator):
 73 |     """
 74 |     A naive character count estimator that estimates tokens based on character count.
 75 |     """
 76 | 
 77 |     def __init__(self, avg_chars_per_token: int = 4):
 78 |         self._avg_chars_per_token = avg_chars_per_token
 79 | 
 80 |     def estimate_token_count(self, text: str) -> int:
 81 |         # Assuming an average of 4 characters per token
 82 |         return len(text) // self._avg_chars_per_token
 83 | 
 84 | 
 85 | _registered_token_estimator_instances_cache: dict[RegisteredTokenCountEstimator, TokenCountEstimator] = {}
 86 | 
 87 | 
 88 | class RegisteredTokenCountEstimator(Enum):
 89 |     TIKTOKEN_GPT4O = "TIKTOKEN_GPT4O"
 90 |     ANTHROPIC_CLAUDE_SONNET_4 = "ANTHROPIC_CLAUDE_SONNET_4"
 91 |     CHAR_COUNT = "CHAR_COUNT"
 92 | 
 93 |     @classmethod
 94 |     def get_valid_names(cls) -> list[str]:
 95 |         """
 96 |         Get a list of all registered token count estimator names.
 97 |         """
 98 |         return [estimator.name for estimator in cls]
 99 | 
100 |     def _create_estimator(self) -> TokenCountEstimator:
101 |         match self:
102 |             case RegisteredTokenCountEstimator.TIKTOKEN_GPT4O:
103 |                 return TiktokenCountEstimator(model_name="gpt-4o")
104 |             case RegisteredTokenCountEstimator.ANTHROPIC_CLAUDE_SONNET_4:
105 |                 return AnthropicTokenCount(model_name="claude-sonnet-4-20250514")
106 |             case RegisteredTokenCountEstimator.CHAR_COUNT:
107 |                 return CharCountEstimator(avg_chars_per_token=4)
108 |             case _:
109 |                 raise ValueError(f"Unknown token count estimator: {self}")
110 | 
111 |     def load_estimator(self) -> TokenCountEstimator:
112 |         estimator_instance = _registered_token_estimator_instances_cache.get(self)
113 |         if estimator_instance is None:
114 |             estimator_instance = self._create_estimator()
115 |             _registered_token_estimator_instances_cache[self] = estimator_instance
116 |         return estimator_instance
117 | 
118 | 
119 | class ToolUsageStats:
120 |     """
121 |     A class to record and manage tool usage statistics.
122 |     """
123 | 
124 |     def __init__(self, token_count_estimator: RegisteredTokenCountEstimator = RegisteredTokenCountEstimator.TIKTOKEN_GPT4O):
125 |         self._token_count_estimator = token_count_estimator.load_estimator()
126 |         self._token_estimator_name = token_count_estimator.value
127 |         self._tool_stats: dict[str, ToolUsageStats.Entry] = defaultdict(ToolUsageStats.Entry)
128 |         self._tool_stats_lock = threading.Lock()
129 | 
130 |     @property
131 |     def token_estimator_name(self) -> str:
132 |         """
133 |         Get the name of the registered token count estimator used.
134 |         """
135 |         return self._token_estimator_name
136 | 
137 |     @dataclass(kw_only=True)
138 |     class Entry:
139 |         num_times_called: int = 0
140 |         input_tokens: int = 0
141 |         output_tokens: int = 0
142 | 
143 |         def update_on_call(self, input_tokens: int, output_tokens: int) -> None:
144 |             """
145 |             Update the entry with the number of tokens used for a single call.
146 |             """
147 |             self.num_times_called += 1
148 |             self.input_tokens += input_tokens
149 |             self.output_tokens += output_tokens
150 | 
151 |     def _estimate_token_count(self, text: str) -> int:
152 |         return self._token_count_estimator.estimate_token_count(text)
153 | 
154 |     def get_stats(self, tool_name: str) -> ToolUsageStats.Entry:
155 |         """
156 |         Get (a copy of) the current usage statistics for a specific tool.
157 |         """
158 |         with self._tool_stats_lock:
159 |             return copy(self._tool_stats[tool_name])
160 | 
161 |     def record_tool_usage(self, tool_name: str, input_str: str, output_str: str) -> None:
162 |         input_tokens = self._estimate_token_count(input_str)
163 |         output_tokens = self._estimate_token_count(output_str)
164 |         with self._tool_stats_lock:
165 |             entry = self._tool_stats[tool_name]
166 |             entry.update_on_call(input_tokens, output_tokens)
167 | 
168 |     def get_tool_stats_dict(self) -> dict[str, dict[str, int]]:
169 |         with self._tool_stats_lock:
170 |             return {name: asdict(entry) for name, entry in self._tool_stats.items()}
171 | 
172 |     def clear(self) -> None:
173 |         with self._tool_stats_lock:
174 |             self._tool_stats.clear()
175 | 
```

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

```python
  1 | """
  2 | Elixir-specific test configuration and fixtures.
  3 | """
  4 | 
  5 | import os
  6 | import subprocess
  7 | import time
  8 | from pathlib import Path
  9 | 
 10 | import pytest
 11 | 
 12 | 
 13 | def ensure_elixir_test_repo_compiled(repo_path: str) -> None:
 14 |     """Ensure the Elixir test repository dependencies are installed and project is compiled.
 15 | 
 16 |     Next LS requires the project to be fully compiled and indexed before providing
 17 |     complete references and symbol resolution. This function:
 18 |     1. Installs dependencies via 'mix deps.get'
 19 |     2. Compiles the project via 'mix compile'
 20 | 
 21 |     This is essential in CI environments where dependencies aren't pre-installed.
 22 | 
 23 |     Args:
 24 |         repo_path: Path to the Elixir project root directory
 25 | 
 26 |     """
 27 |     # Check if this looks like an Elixir project
 28 |     mix_file = os.path.join(repo_path, "mix.exs")
 29 |     if not os.path.exists(mix_file):
 30 |         return
 31 | 
 32 |     # Check if already compiled (optimization for repeated runs)
 33 |     build_path = os.path.join(repo_path, "_build")
 34 |     deps_path = os.path.join(repo_path, "deps")
 35 | 
 36 |     if os.path.exists(build_path) and os.path.exists(deps_path):
 37 |         print(f"Elixir test repository already compiled in {repo_path}")
 38 |         return
 39 | 
 40 |     try:
 41 |         print("Installing dependencies and compiling Elixir test repository for optimal Next LS performance...")
 42 | 
 43 |         # First, install dependencies with increased timeout for CI
 44 |         print("=" * 60)
 45 |         print("Step 1/2: Installing Elixir dependencies...")
 46 |         print("=" * 60)
 47 |         start_time = time.time()
 48 | 
 49 |         deps_result = subprocess.run(
 50 |             ["mix", "deps.get"],
 51 |             cwd=repo_path,
 52 |             capture_output=True,
 53 |             text=True,
 54 |             timeout=180,
 55 |             check=False,  # 3 minutes for dependency installation (CI can be slow)
 56 |         )
 57 | 
 58 |         deps_duration = time.time() - start_time
 59 |         print(f"Dependencies installation completed in {deps_duration:.2f} seconds")
 60 | 
 61 |         # Always log the output for transparency
 62 |         if deps_result.stdout.strip():
 63 |             print("Dependencies stdout:")
 64 |             print("-" * 40)
 65 |             print(deps_result.stdout)
 66 |             print("-" * 40)
 67 | 
 68 |         if deps_result.stderr.strip():
 69 |             print("Dependencies stderr:")
 70 |             print("-" * 40)
 71 |             print(deps_result.stderr)
 72 |             print("-" * 40)
 73 | 
 74 |         if deps_result.returncode != 0:
 75 |             print(f"⚠️  Warning: Dependencies installation failed with exit code {deps_result.returncode}")
 76 |             # Continue anyway - some projects might not have dependencies
 77 |         else:
 78 |             print("✓ Dependencies installed successfully")
 79 | 
 80 |         # Then compile the project with increased timeout for CI
 81 |         print("=" * 60)
 82 |         print("Step 2/2: Compiling Elixir project...")
 83 |         print("=" * 60)
 84 |         start_time = time.time()
 85 | 
 86 |         compile_result = subprocess.run(
 87 |             ["mix", "compile"],
 88 |             cwd=repo_path,
 89 |             capture_output=True,
 90 |             text=True,
 91 |             timeout=300,
 92 |             check=False,  # 5 minutes for compilation (Credo compilation can be slow in CI)
 93 |         )
 94 | 
 95 |         compile_duration = time.time() - start_time
 96 |         print(f"Compilation completed in {compile_duration:.2f} seconds")
 97 | 
 98 |         # Always log the output for transparency
 99 |         if compile_result.stdout.strip():
100 |             print("Compilation stdout:")
101 |             print("-" * 40)
102 |             print(compile_result.stdout)
103 |             print("-" * 40)
104 | 
105 |         if compile_result.stderr.strip():
106 |             print("Compilation stderr:")
107 |             print("-" * 40)
108 |             print(compile_result.stderr)
109 |             print("-" * 40)
110 | 
111 |         if compile_result.returncode == 0:
112 |             print(f"✓ Elixir test repository compiled successfully in {repo_path}")
113 |         else:
114 |             print(f"⚠️  Warning: Compilation completed with exit code {compile_result.returncode}")
115 |             # Still continue - warnings are often non-fatal
116 | 
117 |         print("=" * 60)
118 |         print(f"Total setup time: {time.time() - (start_time - compile_duration - deps_duration):.2f} seconds")
119 |         print("=" * 60)
120 | 
121 |     except subprocess.TimeoutExpired as e:
122 |         print("=" * 60)
123 |         print(f"❌ TIMEOUT: Elixir setup timed out after {e.timeout} seconds")
124 |         print(f"Command: {' '.join(e.cmd)}")
125 |         print("This may indicate slow CI environment - Next LS may still work but with reduced functionality")
126 | 
127 |         # Try to get partial output if available
128 |         if hasattr(e, "stdout") and e.stdout:
129 |             print("Partial stdout before timeout:")
130 |             print("-" * 40)
131 |             print(e.stdout)
132 |             print("-" * 40)
133 |         if hasattr(e, "stderr") and e.stderr:
134 |             print("Partial stderr before timeout:")
135 |             print("-" * 40)
136 |             print(e.stderr)
137 |             print("-" * 40)
138 |         print("=" * 60)
139 | 
140 |     except FileNotFoundError:
141 |         print("❌ ERROR: 'mix' command not found - Elixir test repository may not be compiled")
142 |         print("Please ensure Elixir is installed and available in PATH")
143 |     except Exception as e:
144 |         print(f"❌ ERROR: Failed to prepare Elixir test repository: {e}")
145 | 
146 | 
147 | @pytest.fixture(scope="session", autouse=True)
148 | def setup_elixir_test_environment():
149 |     """Automatically prepare Elixir test environment for all Elixir tests.
150 | 
151 |     This fixture runs once per test session and automatically:
152 |     1. Installs dependencies via 'mix deps.get'
153 |     2. Compiles the Elixir test repository via 'mix compile'
154 | 
155 |     It uses autouse=True so it runs automatically without needing to be explicitly
156 |     requested by tests. This ensures Next LS has a fully prepared project to work with.
157 | 
158 |     Uses generous timeouts (3-5 minutes) to accommodate slow CI environments.
159 |     All output is logged for transparency and debugging.
160 |     """
161 |     # Get the test repo path relative to this conftest.py file
162 |     test_repo_path = Path(__file__).parent.parent.parent / "resources" / "repos" / "elixir" / "test_repo"
163 |     ensure_elixir_test_repo_compiled(str(test_repo_path))
164 |     return str(test_repo_path)
165 | 
166 | 
167 | @pytest.fixture(scope="session")
168 | def elixir_test_repo_path(setup_elixir_test_environment):
169 |     """Get the path to the prepared Elixir test repository.
170 | 
171 |     This fixture depends on setup_elixir_test_environment to ensure dependencies
172 |     are installed and compilation has completed before returning the path.
173 |     """
174 |     return setup_elixir_test_environment
175 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/erlang/conftest.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Erlang-specific test configuration and fixtures.
  3 | """
  4 | 
  5 | import os
  6 | import subprocess
  7 | import time
  8 | from pathlib import Path
  9 | 
 10 | import pytest
 11 | 
 12 | 
 13 | def ensure_erlang_test_repo_compiled(repo_path: str) -> None:
 14 |     """Ensure the Erlang test repository dependencies are installed and project is compiled.
 15 | 
 16 |     Erlang LS requires the project to be fully compiled and indexed before providing
 17 |     complete references and symbol resolution. This function:
 18 |     1. Installs dependencies via 'rebar3 deps'
 19 |     2. Compiles the project via 'rebar3 compile'
 20 | 
 21 |     This is essential in CI environments where dependencies aren't pre-installed.
 22 | 
 23 |     Args:
 24 |         repo_path: Path to the Erlang project root directory
 25 | 
 26 |     """
 27 |     # Check if this looks like an Erlang project
 28 |     rebar_config = os.path.join(repo_path, "rebar.config")
 29 |     if not os.path.exists(rebar_config):
 30 |         return
 31 | 
 32 |     # Check if already compiled (optimization for repeated runs)
 33 |     build_path = os.path.join(repo_path, "_build")
 34 |     deps_path = os.path.join(repo_path, "deps")
 35 | 
 36 |     if os.path.exists(build_path) and os.path.exists(deps_path):
 37 |         print(f"Erlang test repository already compiled in {repo_path}")
 38 |         return
 39 | 
 40 |     try:
 41 |         print("Installing dependencies and compiling Erlang test repository for optimal Erlang LS performance...")
 42 | 
 43 |         # First, install dependencies with increased timeout for CI
 44 |         print("=" * 60)
 45 |         print("Step 1/2: Installing Erlang dependencies...")
 46 |         print("=" * 60)
 47 |         start_time = time.time()
 48 | 
 49 |         deps_result = subprocess.run(
 50 |             ["rebar3", "deps"],
 51 |             cwd=repo_path,
 52 |             capture_output=True,
 53 |             text=True,
 54 |             timeout=180,
 55 |             check=False,  # 3 minutes for dependency installation (CI can be slow)
 56 |         )
 57 | 
 58 |         deps_duration = time.time() - start_time
 59 |         print(f"Dependencies installation completed in {deps_duration:.2f} seconds")
 60 | 
 61 |         # Always log the output for transparency
 62 |         if deps_result.stdout.strip():
 63 |             print("Dependencies stdout:")
 64 |             print("-" * 40)
 65 |             print(deps_result.stdout)
 66 |             print("-" * 40)
 67 | 
 68 |         if deps_result.stderr.strip():
 69 |             print("Dependencies stderr:")
 70 |             print("-" * 40)
 71 |             print(deps_result.stderr)
 72 |             print("-" * 40)
 73 | 
 74 |         if deps_result.returncode != 0:
 75 |             print(f"⚠️  Warning: Dependencies installation failed with exit code {deps_result.returncode}")
 76 |             # Continue anyway - some projects might not have dependencies
 77 |         else:
 78 |             print("✓ Dependencies installed successfully")
 79 | 
 80 |         # Then compile the project with increased timeout for CI
 81 |         print("=" * 60)
 82 |         print("Step 2/2: Compiling Erlang project...")
 83 |         print("=" * 60)
 84 |         start_time = time.time()
 85 | 
 86 |         compile_result = subprocess.run(
 87 |             ["rebar3", "compile"],
 88 |             cwd=repo_path,
 89 |             capture_output=True,
 90 |             text=True,
 91 |             timeout=300,
 92 |             check=False,  # 5 minutes for compilation (Dialyzer can be slow in CI)
 93 |         )
 94 | 
 95 |         compile_duration = time.time() - start_time
 96 |         print(f"Compilation completed in {compile_duration:.2f} seconds")
 97 | 
 98 |         # Always log the output for transparency
 99 |         if compile_result.stdout.strip():
100 |             print("Compilation stdout:")
101 |             print("-" * 40)
102 |             print(compile_result.stdout)
103 |             print("-" * 40)
104 | 
105 |         if compile_result.stderr.strip():
106 |             print("Compilation stderr:")
107 |             print("-" * 40)
108 |             print(compile_result.stderr)
109 |             print("-" * 40)
110 | 
111 |         if compile_result.returncode == 0:
112 |             print(f"✓ Erlang test repository compiled successfully in {repo_path}")
113 |         else:
114 |             print(f"⚠️  Warning: Compilation completed with exit code {compile_result.returncode}")
115 |             # Still continue - warnings are often non-fatal
116 | 
117 |         print("=" * 60)
118 |         print(f"Total setup time: {time.time() - (start_time - compile_duration - deps_duration):.2f} seconds")
119 |         print("=" * 60)
120 | 
121 |     except subprocess.TimeoutExpired as e:
122 |         print("=" * 60)
123 |         print(f"❌ TIMEOUT: Erlang setup timed out after {e.timeout} seconds")
124 |         print(f"Command: {' '.join(e.cmd)}")
125 |         print("This may indicate slow CI environment - Erlang LS may still work but with reduced functionality")
126 | 
127 |         # Try to get partial output if available
128 |         if hasattr(e, "stdout") and e.stdout:
129 |             print("Partial stdout before timeout:")
130 |             print("-" * 40)
131 |             print(e.stdout)
132 |             print("-" * 40)
133 |         if hasattr(e, "stderr") and e.stderr:
134 |             print("Partial stderr before timeout:")
135 |             print("-" * 40)
136 |             print(e.stderr)
137 |             print("-" * 40)
138 |         print("=" * 60)
139 | 
140 |     except FileNotFoundError:
141 |         print("❌ ERROR: 'rebar3' command not found - Erlang test repository may not be compiled")
142 |         print("Please ensure rebar3 is installed and available in PATH")
143 |     except Exception as e:
144 |         print(f"❌ ERROR: Failed to prepare Erlang test repository: {e}")
145 | 
146 | 
147 | @pytest.fixture(scope="session", autouse=True)
148 | def setup_erlang_test_environment():
149 |     """Automatically prepare Erlang test environment for all Erlang tests.
150 | 
151 |     This fixture runs once per test session and automatically:
152 |     1. Installs dependencies via 'rebar3 deps'
153 |     2. Compiles the Erlang test repository via 'rebar3 compile'
154 | 
155 |     It uses autouse=True so it runs automatically without needing to be explicitly
156 |     requested by tests. This ensures Erlang LS has a fully prepared project to work with.
157 | 
158 |     Uses generous timeouts (3-5 minutes) to accommodate slow CI environments.
159 |     All output is logged for transparency and debugging.
160 |     """
161 |     # Get the test repo path relative to this conftest.py file
162 |     test_repo_path = Path(__file__).parent.parent.parent / "resources" / "repos" / "erlang" / "test_repo"
163 |     ensure_erlang_test_repo_compiled(str(test_repo_path))
164 |     return str(test_repo_path)
165 | 
166 | 
167 | @pytest.fixture(scope="session")
168 | def erlang_test_repo_path(setup_erlang_test_environment):
169 |     """Get the path to the prepared Erlang test repository.
170 | 
171 |     This fixture depends on setup_erlang_test_environment to ensure dependencies
172 |     are installed and compilation has completed before returning the path.
173 |     """
174 |     return setup_erlang_test_environment
175 | 
```

--------------------------------------------------------------------------------
/.serena/memories/serena_core_concepts_and_architecture.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Serena Core Concepts and Architecture
  2 | 
  3 | ## High-Level Architecture
  4 | 
  5 | Serena is built around a dual-layer architecture:
  6 | 
  7 | 1. **SerenaAgent** - The main orchestrator that manages projects, tools, and user interactions
  8 | 2. **SolidLanguageServer** - A unified wrapper around Language Server Protocol (LSP) implementations
  9 | 
 10 | ## Core Components
 11 | 
 12 | ### 1. SerenaAgent (`src/serena/agent.py`)
 13 | 
 14 | The central coordinator that:
 15 | - Manages active projects and their configurations
 16 | - Coordinates between different tools and contexts
 17 | - Handles language server lifecycle
 18 | - Manages memory persistence
 19 | - Provides MCP (Model Context Protocol) server interface
 20 | 
 21 | Key responsibilities:
 22 | - **Project Management** - Activating, switching between projects
 23 | - **Tool Registry** - Loading and managing available tools based on context/mode
 24 | - **Language Server Integration** - Starting/stopping language servers per project
 25 | - **Memory Management** - Persistent storage of project knowledge
 26 | - **Task Execution** - Coordinating complex multi-step operations
 27 | 
 28 | ### 2. SolidLanguageServer (`src/solidlsp/ls.py`)
 29 | 
 30 | A unified abstraction over multiple language servers that provides:
 31 | - **Language-agnostic interface** for symbol operations
 32 | - **Caching layer** for performance optimization
 33 | - **Error handling and recovery** for unreliable language servers
 34 | - **Uniform API** regardless of underlying LSP implementation
 35 | 
 36 | Core capabilities:
 37 | - Symbol discovery and navigation
 38 | - Code completion and hover information
 39 | - Find references and definitions
 40 | - Document and workspace symbol search
 41 | - File watching and change notifications
 42 | 
 43 | ### 3. Tool System (`src/serena/tools/`)
 44 | 
 45 | Modular tool architecture with several categories:
 46 | 
 47 | #### File Tools (`file_tools.py`)
 48 | - File system operations (read, write, list directories)
 49 | - Text search and pattern matching
 50 | - Regex-based replacements
 51 | 
 52 | #### Symbol Tools (`symbol_tools.py`)  
 53 | - Language-aware symbol finding and navigation
 54 | - Symbol body replacement and insertion
 55 | - Reference finding across codebase
 56 | 
 57 | #### Memory Tools (`memory_tools.py`)
 58 | - Project knowledge persistence
 59 | - Memory retrieval and management
 60 | - Onboarding information storage
 61 | 
 62 | #### Configuration Tools (`config_tools.py`)
 63 | - Project activation and switching
 64 | - Mode and context management
 65 | - Tool inclusion/exclusion
 66 | 
 67 | ### 4. Configuration System (`src/serena/config/`)
 68 | 
 69 | Multi-layered configuration supporting:
 70 | - **Contexts** - Define available tools and their behavior
 71 | - **Modes** - Specify operational patterns (interactive, editing, etc.)
 72 | - **Projects** - Per-project settings and language server configs
 73 | - **Tool Sets** - Grouped tool collections for different use cases
 74 | 
 75 | ## Language Server Integration
 76 | 
 77 | ### Language Support Model
 78 | 
 79 | Each supported language has:
 80 | 1. **Language Server Implementation** (`src/solidlsp/language_servers/`)
 81 | 2. **Runtime Dependencies** - Managed downloads of language servers
 82 | 3. **Test Repository** (`test/resources/repos/<language>/`)
 83 | 4. **Test Suite** (`test/solidlsp/<language>/`)
 84 | 
 85 | ### Language Server Lifecycle
 86 | 
 87 | 1. **Discovery** - Find language servers or download them automatically
 88 | 2. **Initialization** - Start server process and perform LSP handshake
 89 | 3. **Project Setup** - Open workspace and configure language-specific settings
 90 | 4. **Operation** - Handle requests/responses with caching and error recovery
 91 | 5. **Shutdown** - Clean shutdown of server processes
 92 | 
 93 | ### Supported Languages
 94 | 
 95 | Current language support includes:
 96 | - **C#** - Microsoft.CodeAnalysis.LanguageServer (.NET 9)
 97 | - **Python** - Pyright or Jedi
 98 | - **TypeScript/JavaScript** - TypeScript Language Server
 99 | - **Rust** - rust-analyzer
100 | - **Go** - gopls
101 | - **Java** - Eclipse JDT Language Server
102 | - **Kotlin** - Kotlin Language Server
103 | - **PHP** - Intelephense
104 | - **Ruby** - Solargraph
105 | - **Clojure** - clojure-lsp
106 | - **Elixir** - ElixirLS
107 | - **Dart** - Dart Language Server
108 | - **C/C++** - clangd
109 | - **Terraform** - terraform-ls
110 | 
111 | ## Memory and Knowledge Management
112 | 
113 | ### Memory System
114 | - **Markdown-based storage** in `.serena/memories/` directory
115 | - **Contextual retrieval** - memories loaded based on relevance
116 | - **Project-specific** knowledge persistence
117 | - **Onboarding support** - guided setup for new projects
118 | 
119 | ### Knowledge Categories
120 | - **Project Structure** - Directory layouts, build systems
121 | - **Architecture Patterns** - How the codebase is organized
122 | - **Development Workflows** - Testing, building, deployment
123 | - **Domain Knowledge** - Business logic and requirements
124 | 
125 | ## MCP Server Interface
126 | 
127 | Serena exposes its functionality through Model Context Protocol:
128 | - **Tool Discovery** - AI agents can enumerate available tools
129 | - **Context-Aware Operations** - Tools behave based on active project/mode
130 | - **Stateful Sessions** - Maintains project state across interactions
131 | - **Error Handling** - Graceful degradation when tools fail
132 | 
133 | ## Error Handling and Resilience
134 | 
135 | ### Language Server Reliability
136 | - **Timeout Management** - Configurable timeouts for LSP requests
137 | - **Process Recovery** - Automatic restart of crashed language servers
138 | - **Fallback Behavior** - Graceful degradation when LSP unavailable
139 | - **Caching Strategy** - Reduces impact of server failures
140 | 
141 | ### Project Activation Safety
142 | - **Validation** - Verify project structure before activation
143 | - **Error Isolation** - Project failures don't affect other projects
144 | - **Recovery Mechanisms** - Automatic cleanup and retry logic
145 | 
146 | ## Performance Considerations
147 | 
148 | ### Caching Strategy
149 | - **Symbol Cache** - In-memory caching of expensive symbol operations
150 | - **File System Cache** - Reduced disk I/O for repeated operations
151 | - **Language Server Cache** - Persistent cache across sessions
152 | 
153 | ### Resource Management
154 | - **Language Server Pooling** - Reuse servers across projects when possible
155 | - **Memory Management** - Automatic cleanup of unused resources
156 | - **Background Operations** - Async operations don't block user interactions
157 | 
158 | ## Extension Points
159 | 
160 | ### Adding New Languages
161 | 1. Implement language server class in `src/solidlsp/language_servers/`
162 | 2. Add runtime dependencies configuration
163 | 3. Create test repository and test suite
164 | 4. Update language enumeration and configuration
165 | 
166 | ### Adding New Tools
167 | 1. Inherit from `Tool` base class in `tools_base.py`
168 | 2. Implement required methods and parameter validation
169 | 3. Register tool in appropriate tool registry
170 | 4. Add to context/mode configurations as needed
171 | 
172 | ### Custom Contexts and Modes
173 | - Define new contexts in YAML configuration files
174 | - Specify tool sets and operational patterns
175 | - Configure for specific development workflows
```

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

```python
  1 | import logging
  2 | import os
  3 | import pathlib
  4 | import subprocess
  5 | import threading
  6 | from typing import cast
  7 | 
  8 | from overrides import override
  9 | 
 10 | from solidlsp.ls import SolidLanguageServer
 11 | from solidlsp.ls_config import LanguageServerConfig
 12 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 13 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 14 | from solidlsp.settings import SolidLSPSettings
 15 | 
 16 | log = logging.getLogger(__name__)
 17 | 
 18 | 
 19 | class Gopls(SolidLanguageServer):
 20 |     """
 21 |     Provides Go specific instantiation of the LanguageServer class using gopls.
 22 |     """
 23 | 
 24 |     @override
 25 |     def is_ignored_dirname(self, dirname: str) -> bool:
 26 |         # For Go projects, we should ignore:
 27 |         # - vendor: third-party dependencies vendored into the project
 28 |         # - node_modules: if the project has JavaScript components
 29 |         # - dist/build: common output directories
 30 |         return super().is_ignored_dirname(dirname) or dirname in ["vendor", "node_modules", "dist", "build"]
 31 | 
 32 |     @staticmethod
 33 |     def _determine_log_level(line: str) -> int:
 34 |         """Classify gopls stderr output to avoid false-positive errors."""
 35 |         line_lower = line.lower()
 36 | 
 37 |         # File discovery messages that are not actual errors
 38 |         if any(
 39 |             [
 40 |                 "discover.go:" in line_lower,
 41 |                 "walker.go:" in line_lower,
 42 |                 "walking of {file://" in line_lower,
 43 |                 "bus: -> discover" in line_lower,
 44 |             ]
 45 |         ):
 46 |             return logging.DEBUG
 47 | 
 48 |         return SolidLanguageServer._determine_log_level(line)
 49 | 
 50 |     @staticmethod
 51 |     def _get_go_version() -> str | None:
 52 |         """Get the installed Go version or None if not found."""
 53 |         try:
 54 |             result = subprocess.run(["go", "version"], capture_output=True, text=True, check=False)
 55 |             if result.returncode == 0:
 56 |                 return result.stdout.strip()
 57 |         except FileNotFoundError:
 58 |             return None
 59 |         return None
 60 | 
 61 |     @staticmethod
 62 |     def _get_gopls_version() -> str | None:
 63 |         """Get the installed gopls version or None if not found."""
 64 |         try:
 65 |             result = subprocess.run(["gopls", "version"], capture_output=True, text=True, check=False)
 66 |             if result.returncode == 0:
 67 |                 return result.stdout.strip()
 68 |         except FileNotFoundError:
 69 |             return None
 70 |         return None
 71 | 
 72 |     @staticmethod
 73 |     def _setup_runtime_dependency() -> bool:
 74 |         """
 75 |         Check if required Go runtime dependencies are available.
 76 |         Raises RuntimeError with helpful message if dependencies are missing.
 77 |         """
 78 |         go_version = Gopls._get_go_version()
 79 |         if not go_version:
 80 |             raise RuntimeError(
 81 |                 "Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH."
 82 |             )
 83 | 
 84 |         gopls_version = Gopls._get_gopls_version()
 85 |         if not gopls_version:
 86 |             raise RuntimeError(
 87 |                 "Found a Go version but gopls is not installed.\n"
 88 |                 "Please install gopls as described in https://pkg.go.dev/golang.org/x/tools/gopls#section-readme\n\n"
 89 |                 "After installation, make sure it is added to your PATH (it might be installed in a different location than Go)."
 90 |             )
 91 | 
 92 |         return True
 93 | 
 94 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 95 |         self._setup_runtime_dependency()
 96 | 
 97 |         super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), "go", solidlsp_settings)
 98 |         self.server_ready = threading.Event()
 99 |         self.request_id = 0
100 | 
101 |     @staticmethod
102 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
103 |         """
104 |         Returns the initialize params for the Go 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 |                     "definition": {"dynamicRegistration": True},
113 |                     "documentSymbol": {
114 |                         "dynamicRegistration": True,
115 |                         "hierarchicalDocumentSymbolSupport": True,
116 |                         "symbolKind": {"valueSet": list(range(1, 27))},
117 |                     },
118 |                 },
119 |                 "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
120 |             },
121 |             "processId": os.getpid(),
122 |             "rootPath": repository_absolute_path,
123 |             "rootUri": root_uri,
124 |             "workspaceFolders": [
125 |                 {
126 |                     "uri": root_uri,
127 |                     "name": os.path.basename(repository_absolute_path),
128 |                 }
129 |             ],
130 |         }
131 |         return cast(InitializeParams, initialize_params)
132 | 
133 |     def _start_server(self) -> None:
134 |         """Start gopls server process"""
135 | 
136 |         def register_capability_handler(params: dict) -> None:
137 |             return
138 | 
139 |         def window_log_message(msg: dict) -> None:
140 |             log.info(f"LSP: window/logMessage: {msg}")
141 | 
142 |         def do_nothing(params: dict) -> None:
143 |             return
144 | 
145 |         self.server.on_request("client/registerCapability", register_capability_handler)
146 |         self.server.on_notification("window/logMessage", window_log_message)
147 |         self.server.on_notification("$/progress", do_nothing)
148 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
149 | 
150 |         log.info("Starting gopls server process")
151 |         self.server.start()
152 |         initialize_params = self._get_initialize_params(self.repository_root_path)
153 | 
154 |         log.info("Sending initialize request from LSP client to LSP server and awaiting response")
155 |         init_response = self.server.send.initialize(initialize_params)
156 | 
157 |         # Verify server capabilities
158 |         assert "textDocumentSync" in init_response["capabilities"]
159 |         assert "completionProvider" in init_response["capabilities"]
160 |         assert "definitionProvider" in init_response["capabilities"]
161 | 
162 |         self.server.notify.initialized({})
163 |         self.completions_available.set()
164 | 
165 |         # gopls server is typically ready immediately after initialization
166 |         self.server_ready.set()
167 |         self.server_ready.wait()
168 | 
```

--------------------------------------------------------------------------------
/docs/02-usage/040_workflow.md:
--------------------------------------------------------------------------------

```markdown
  1 | # The Project Workflow
  2 | 
  3 | Serena uses a project-based workflow.
  4 | A **project** is simply a directory on your filesystem that contains code and other files
  5 | that you want Serena to work with.
  6 | 
  7 | Assuming that you have project you want to work with (which may initially be empty),
  8 | setting up a project with Serena typically involves the following steps:
  9 | 
 10 | 1. **Project creation**: Configuring project settings for Serena (and indexing the project, if desired)
 11 | 2. **Project activation**: Making Serena aware of the project you want to work with
 12 | 3. **Onboarding**: Getting Serena familiar with the project (creating memories)
 13 | 4. **Working on coding tasks**: Using Serena to help you with actual coding tasks in the project
 14 | 
 15 | (project-creation-indexing)=
 16 | ## Project Creation & Indexing
 17 | 
 18 | You can create a project either  
 19 |  * implicitly, by just activating a directory as a project while already in a conversation; this will use default settings for your project (skip to the next section).
 20 |  * explicitly, using the project creation command, or
 21 | 
 22 | ### Explicit Project Creation
 23 | 
 24 | To explicitly create a project, use the following command while in the project directory:
 25 | 
 26 |     <serena> project create [options]
 27 | 
 28 | For instance, when using `uvx`, run
 29 | 
 30 |     uvx --from git+https://github.com/oraios/serena serena project create [options]
 31 | 
 32 |  * For an empty project, you will need to specify the programming language
 33 |    (e.g., `--language python`). 
 34 |  * For an existing project, the main programming language will be detected automatically,
 35 |    but you can choose to explicitly specify multiple languages by passing the `--language` parameter
 36 |    multiple times (e.g. `--language python --language typescript`).
 37 |  * You can optionally specify a custom project name with `--name "My Project"`.
 38 |  * You can immediately index the project after creation with `--index`.
 39 | 
 40 | After creation, you can adjust the project settings in the generated `.serena/project.yml` file.
 41 | 
 42 | (indexing)=
 43 | ### Indexing
 44 | 
 45 | Especially for larger project, it is advisable to index the project after creation (in order to avoid
 46 | delays during MCP server startup or the first tool application):
 47 | 
 48 | While in the project directory, run this command:
 49 |    
 50 |     <serena> project index
 51 | 
 52 | Indexing has to be called only once. During regular usage, Serena will automatically update the index whenever files change.
 53 | 
 54 | ## Project Activation
 55 |    
 56 | Project activation makes Serena aware of the project you want to work with.
 57 | You can either choose to do this
 58 |  * while in a conversation, by telling the LLM to activate a project, e.g.,
 59 |        
 60 |       * "Activate the project /path/to/my_project" (for first-time activation with auto-creation)
 61 |       * "Activate the project my_project"
 62 |    
 63 |    Note that this option requires the `activate_project` tool to be active, 
 64 |    which it isn't in single-project [contexts](contexts) like `ide` or `claude-code` *if* a project is provided at startup.
 65 |    (The tool is deactivated, because we assume that in these contexts, user will only work on the single, open project and have
 66 |    no need to switch it.)
 67 | 
 68 |  * when the MCP server starts, by passing the project path or name as a command-line argument
 69 |    (e.g. when using a single-project mode like `ide` or `claude-code`): `--project <path|name>`
 70 | 
 71 | 
 72 | ## Onboarding & Memories
 73 | 
 74 | By default, Serena will perform an **onboarding process** when
 75 | it is started for the first time for a project.
 76 | The goal of the onboarding is for Serena to get familiar with the project
 77 | and to store memories, which it can then draw upon in future interactions.
 78 | If an LLM should fail to complete the onboarding and does not actually write the
 79 | respective memories to disk, you may need to ask it to do so explicitly.
 80 | 
 81 | The onboarding will usually read a lot of content from the project, thus filling
 82 | up the context. It can therefore be advisable to switch to another conversation
 83 | once the onboarding is complete.
 84 | After the onboarding, we recommend that you have a quick look at the memories and,
 85 | if necessary, edit them or add additional ones.
 86 | 
 87 | **Memories** are files stored in `.serena/memories/` in the project directory,
 88 | which the agent can choose to read in subsequent interactions.
 89 | Feel free to read and adjust them as needed; you can also add new ones manually.
 90 | Every file in the `.serena/memories/` directory is a memory file.
 91 | Whenever Serena starts working on a project, the list of memories is
 92 | provided, and the agent can decide to read them.
 93 | We found that memories can significantly improve the user experience with Serena.
 94 | 
 95 | 
 96 | ## Preparing Your Project
 97 | 
 98 | When using Serena to work on your project, it can be helpful to follow a few best practices.
 99 | 
100 | ### Structure Your Codebase
101 | 
102 | Serena uses the code structure for finding, reading and editing code. This means that it will
103 | work well with well-structured code but may perform poorly on fully unstructured one (like a "God class"
104 | with enormous, non-modular functions).
105 | 
106 | Furthermore, for languages that are not statically typed, the use of type annotations (if supported) 
107 | are highly beneficial.
108 | 
109 | ### Start from a Clean State
110 | 
111 | It is best to start a code generation task from a clean git state. Not only will
112 | this make it easier for you to inspect the changes, but also the model itself will
113 | have a chance of seeing what it has changed by calling `git diff` and thereby
114 | correct itself or continue working in a followup conversation if needed.
115 | 
116 | ### Use Platform-Native Line Endings
117 | 
118 | **Important**: since Serena will write to files using the system-native line endings
119 | and it might want to look at the git diff, it is important to
120 | set `git config core.autocrlf` to `true` on Windows.
121 | With `git config core.autocrlf` set to `false` on Windows, you may end up with huge diffs
122 | due to line endings only. 
123 | It is generally a good idea to globally enable this git setting on Windows:
124 | 
125 | ```shell
126 | git config --global core.autocrlf true
127 | ```
128 | 
129 | ### Logging, Linting, and Automated Tests
130 | 
131 | Serena can successfully complete tasks in an _agent loop_, where it iteratively
132 | acquires information, performs actions, and reflects on the results.
133 | However, Serena cannot use a debugger; it must rely on the results of program executions,
134 | linting results, and test results to assess the correctness of its actions.
135 | Therefore, software that is designed to meaningful interpretable outputs (e.g. log messages)
136 | and that has a good test coverage is much easier to work with for Serena.
137 | 
138 | We generally recommend to start an editing task from a state where all linting checks and tests pass.
```

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

```python
  1 | import logging
  2 | import os
  3 | import pathlib
  4 | from typing import cast
  5 | 
  6 | from solidlsp.ls import SolidLanguageServer
  7 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
  8 | from solidlsp.settings import SolidLSPSettings
  9 | 
 10 | from ..ls_config import LanguageServerConfig
 11 | from ..lsp_protocol_handler.lsp_types import InitializeParams
 12 | from .common import RuntimeDependency, RuntimeDependencyCollection
 13 | 
 14 | log = logging.getLogger(__name__)
 15 | 
 16 | 
 17 | class DartLanguageServer(SolidLanguageServer):
 18 |     """
 19 |     Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart.
 20 |     """
 21 | 
 22 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
 23 |         """
 24 |         Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 25 |         """
 26 |         executable_path = self._setup_runtime_dependencies(solidlsp_settings)
 27 |         super().__init__(
 28 |             config, repository_root_path, ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), "dart", solidlsp_settings
 29 |         )
 30 | 
 31 |     @classmethod
 32 |     def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str:
 33 |         deps = RuntimeDependencyCollection(
 34 |             [
 35 |                 RuntimeDependency(
 36 |                     id="DartLanguageServer",
 37 |                     description="Dart Language Server for Linux (x64)",
 38 |                     url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip",
 39 |                     platform_id="linux-x64",
 40 |                     archive_type="zip",
 41 |                     binary_name="dart-sdk/bin/dart",
 42 |                 ),
 43 |                 RuntimeDependency(
 44 |                     id="DartLanguageServer",
 45 |                     description="Dart Language Server for Windows (x64)",
 46 |                     url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip",
 47 |                     platform_id="win-x64",
 48 |                     archive_type="zip",
 49 |                     binary_name="dart-sdk/bin/dart.exe",
 50 |                 ),
 51 |                 RuntimeDependency(
 52 |                     id="DartLanguageServer",
 53 |                     description="Dart Language Server for Windows (arm64)",
 54 |                     url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-arm64-release.zip",
 55 |                     platform_id="win-arm64",
 56 |                     archive_type="zip",
 57 |                     binary_name="dart-sdk/bin/dart.exe",
 58 |                 ),
 59 |                 RuntimeDependency(
 60 |                     id="DartLanguageServer",
 61 |                     description="Dart Language Server for macOS (x64)",
 62 |                     url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip",
 63 |                     platform_id="osx-x64",
 64 |                     archive_type="zip",
 65 |                     binary_name="dart-sdk/bin/dart",
 66 |                 ),
 67 |                 RuntimeDependency(
 68 |                     id="DartLanguageServer",
 69 |                     description="Dart Language Server for macOS (arm64)",
 70 |                     url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip",
 71 |                     platform_id="osx-arm64",
 72 |                     archive_type="zip",
 73 |                     binary_name="dart-sdk/bin/dart",
 74 |                 ),
 75 |             ]
 76 |         )
 77 | 
 78 |         dart_ls_dir = cls.ls_resources_dir(solidlsp_settings)
 79 |         dart_executable_path = deps.binary_path(dart_ls_dir)
 80 | 
 81 |         if not os.path.exists(dart_executable_path):
 82 |             deps.install(dart_ls_dir)
 83 | 
 84 |         assert os.path.exists(dart_executable_path)
 85 |         os.chmod(dart_executable_path, 0o755)
 86 | 
 87 |         return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2"
 88 | 
 89 |     @staticmethod
 90 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
 91 |         """
 92 |         Returns the initialize params for the Dart Language Server.
 93 |         """
 94 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
 95 |         initialize_params = {
 96 |             "capabilities": {},
 97 |             "initializationOptions": {
 98 |                 "onlyAnalyzeProjectsWithOpenFiles": False,
 99 |                 "closingLabels": False,
100 |                 "outline": False,
101 |                 "flutterOutline": False,
102 |                 "allowOpenUri": False,
103 |             },
104 |             "trace": "verbose",
105 |             "processId": os.getpid(),
106 |             "rootPath": repository_absolute_path,
107 |             "rootUri": pathlib.Path(repository_absolute_path).as_uri(),
108 |             "workspaceFolders": [
109 |                 {
110 |                     "uri": root_uri,
111 |                     "name": os.path.basename(repository_absolute_path),
112 |                 }
113 |             ],
114 |         }
115 | 
116 |         return cast(InitializeParams, initialize_params)
117 | 
118 |     def _start_server(self) -> None:
119 |         """
120 |         Start the language server and yield when the server is ready.
121 |         """
122 | 
123 |         def execute_client_command_handler(params: dict) -> list:
124 |             return []
125 | 
126 |         def do_nothing(params: dict) -> None:
127 |             return
128 | 
129 |         def check_experimental_status(params: dict) -> None:
130 |             pass
131 | 
132 |         def window_log_message(msg: dict) -> None:
133 |             log.info(f"LSP: window/logMessage: {msg}")
134 | 
135 |         self.server.on_request("client/registerCapability", do_nothing)
136 |         self.server.on_notification("language/status", do_nothing)
137 |         self.server.on_notification("window/logMessage", window_log_message)
138 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
139 |         self.server.on_notification("$/progress", do_nothing)
140 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
141 |         self.server.on_notification("language/actionableNotification", do_nothing)
142 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
143 | 
144 |         log.info("Starting dart-language-server server process")
145 |         self.server.start()
146 |         initialize_params = self._get_initialize_params(self.repository_root_path)
147 |         log.debug("Sending initialize request to dart-language-server")
148 |         init_response = self.server.send_request("initialize", initialize_params)  # type: ignore
149 |         log.info(f"Received initialize response from dart-language-server: {init_response}")
150 | 
151 |         self.server.notify.initialized({})
152 | 
```

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

```python
  1 | import logging
  2 | import os
  3 | import pathlib
  4 | import subprocess
  5 | import threading
  6 | from typing import Any
  7 | 
  8 | from overrides import override
  9 | 
 10 | from solidlsp.ls import SolidLanguageServer
 11 | from solidlsp.ls_config import LanguageServerConfig
 12 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 13 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 14 | from solidlsp.settings import SolidLSPSettings
 15 | 
 16 | log = logging.getLogger(__name__)
 17 | 
 18 | 
 19 | class RLanguageServer(SolidLanguageServer):
 20 |     """R Language Server implementation using the languageserver R package."""
 21 | 
 22 |     @override
 23 |     def _get_wait_time_for_cross_file_referencing(self) -> float:
 24 |         return 5.0  # R language server needs extra time for workspace indexing in CI environments
 25 | 
 26 |     @override
 27 |     def is_ignored_dirname(self, dirname: str) -> bool:
 28 |         # For R projects, ignore common directories
 29 |         return super().is_ignored_dirname(dirname) or dirname in [
 30 |             "renv",  # R environment management
 31 |             "packrat",  # Legacy R package management
 32 |             ".Rproj.user",  # RStudio project files
 33 |             "vignettes",  # Package vignettes (often large)
 34 |         ]
 35 | 
 36 |     @staticmethod
 37 |     def _check_r_installation() -> None:
 38 |         """Check if R and languageserver are available."""
 39 |         try:
 40 |             # Check R installation
 41 |             result = subprocess.run(["R", "--version"], capture_output=True, text=True, check=False)
 42 |             if result.returncode != 0:
 43 |                 raise RuntimeError("R is not installed or not in PATH")
 44 | 
 45 |             # Check languageserver package
 46 |             result = subprocess.run(
 47 |                 ["R", "--vanilla", "--quiet", "--slave", "-e", "if (!require('languageserver', quietly=TRUE)) quit(status=1)"],
 48 |                 capture_output=True,
 49 |                 text=True,
 50 |                 check=False,
 51 |             )
 52 | 
 53 |             if result.returncode != 0:
 54 |                 raise RuntimeError(
 55 |                     "R languageserver package is not installed.\nInstall it with: R -e \"install.packages('languageserver')\""
 56 |                 )
 57 | 
 58 |         except FileNotFoundError:
 59 |             raise RuntimeError("R is not installed. Please install R from https://www.r-project.org/")
 60 | 
 61 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 62 |         # Check R installation
 63 |         self._check_r_installation()
 64 | 
 65 |         # R command to start language server
 66 |         # Use --vanilla for minimal startup and --quiet to suppress all output except LSP
 67 |         # Set specific options to improve parsing stability
 68 |         r_cmd = 'R --vanilla --quiet --slave -e "options(languageserver.debug_mode = FALSE); languageserver::run()"'
 69 | 
 70 |         super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=r_cmd, cwd=repository_root_path), "r", solidlsp_settings)
 71 |         self.server_ready = threading.Event()
 72 | 
 73 |     @staticmethod
 74 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
 75 |         """Initialize params for R Language Server."""
 76 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
 77 |         initialize_params = {
 78 |             "locale": "en",
 79 |             "capabilities": {
 80 |                 "textDocument": {
 81 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
 82 |                     "completion": {
 83 |                         "dynamicRegistration": True,
 84 |                         "completionItem": {
 85 |                             "snippetSupport": True,
 86 |                             "commitCharactersSupport": True,
 87 |                             "documentationFormat": ["markdown", "plaintext"],
 88 |                             "deprecatedSupport": True,
 89 |                             "preselectSupport": True,
 90 |                         },
 91 |                     },
 92 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
 93 |                     "definition": {"dynamicRegistration": True},
 94 |                     "references": {"dynamicRegistration": True},
 95 |                     "documentSymbol": {
 96 |                         "dynamicRegistration": True,
 97 |                         "hierarchicalDocumentSymbolSupport": True,
 98 |                         "symbolKind": {"valueSet": list(range(1, 27))},
 99 |                     },
100 |                     "formatting": {"dynamicRegistration": True},
101 |                     "rangeFormatting": {"dynamicRegistration": True},
102 |                 },
103 |                 "workspace": {
104 |                     "workspaceFolders": True,
105 |                     "didChangeConfiguration": {"dynamicRegistration": True},
106 |                     "symbol": {
107 |                         "dynamicRegistration": True,
108 |                         "symbolKind": {"valueSet": list(range(1, 27))},
109 |                     },
110 |                 },
111 |             },
112 |             "processId": os.getpid(),
113 |             "rootPath": repository_absolute_path,
114 |             "rootUri": root_uri,
115 |             "workspaceFolders": [
116 |                 {
117 |                     "uri": root_uri,
118 |                     "name": os.path.basename(repository_absolute_path),
119 |                 }
120 |             ],
121 |         }
122 |         return initialize_params  # type: ignore
123 | 
124 |     def _start_server(self) -> None:
125 |         """Start R Language Server process."""
126 | 
127 |         def window_log_message(msg: dict) -> None:
128 |             log.info(f"R LSP: window/logMessage: {msg}")
129 | 
130 |         def do_nothing(params: Any) -> None:
131 |             return
132 | 
133 |         def register_capability_handler(params: Any) -> None:
134 |             return
135 | 
136 |         # Register LSP message handlers
137 |         self.server.on_request("client/registerCapability", register_capability_handler)
138 |         self.server.on_notification("window/logMessage", window_log_message)
139 |         self.server.on_notification("$/progress", do_nothing)
140 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
141 | 
142 |         log.info("Starting R Language Server process")
143 |         self.server.start()
144 | 
145 |         initialize_params = self._get_initialize_params(self.repository_root_path)
146 |         log.info(
147 |             "Sending initialize request to R Language Server",
148 |         )
149 | 
150 |         init_response = self.server.send.initialize(initialize_params)
151 | 
152 |         # Verify server capabilities
153 |         capabilities = init_response.get("capabilities", {})
154 |         assert "textDocumentSync" in capabilities
155 |         if "completionProvider" in capabilities:
156 |             log.info("R LSP completion provider available")
157 |         if "definitionProvider" in capabilities:
158 |             log.info("R LSP definition provider available")
159 | 
160 |         self.server.notify.initialized({})
161 |         self.completions_available.set()
162 | 
163 |         # R Language Server is ready after initialization
164 |         self.server_ready.set()
165 | 
```

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

```python
  1 | import logging
  2 | import os
  3 | import pathlib
  4 | import platform
  5 | import shutil
  6 | import subprocess
  7 | from typing import Any
  8 | 
  9 | from overrides import override
 10 | 
 11 | from solidlsp.ls import SolidLanguageServer
 12 | from solidlsp.ls_config import LanguageServerConfig
 13 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 14 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 15 | from solidlsp.settings import SolidLSPSettings
 16 | 
 17 | log = logging.getLogger(__name__)
 18 | 
 19 | 
 20 | class JuliaLanguageServer(SolidLanguageServer):
 21 |     """
 22 |     Language server implementation for Julia using LanguageServer.jl.
 23 |     """
 24 | 
 25 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 26 |         julia_executable = self._setup_runtime_dependency()  # PASS LOGGER
 27 |         julia_code = "using LanguageServer; runserver()"
 28 | 
 29 |         julia_ls_cmd: str | list[str]
 30 |         if platform.system() == "Windows":
 31 |             # On Windows, pass as list (Serena handles shell=True differently)
 32 |             julia_ls_cmd = [julia_executable, "--startup-file=no", "--history-file=no", "-e", julia_code, repository_root_path]
 33 |         else:
 34 |             # On Linux/macOS, build shell-escaped string
 35 |             import shlex
 36 | 
 37 |             julia_ls_cmd = (
 38 |                 f"{shlex.quote(julia_executable)} "
 39 |                 f"--startup-file=no "
 40 |                 f"--history-file=no "
 41 |                 f"-e {shlex.quote(julia_code)} "
 42 |                 f"{shlex.quote(repository_root_path)}"
 43 |             )
 44 | 
 45 |         log.info(f"[JULIA DEBUG] Command: {julia_ls_cmd}")
 46 | 
 47 |         super().__init__(
 48 |             config, repository_root_path, ProcessLaunchInfo(cmd=julia_ls_cmd, cwd=repository_root_path), "julia", solidlsp_settings
 49 |         )
 50 | 
 51 |     @staticmethod
 52 |     def _setup_runtime_dependency() -> str:
 53 |         """
 54 |         Check if the Julia runtime is available and return its full path.
 55 |         Raises RuntimeError with a helpful message if the dependency is missing.
 56 |         """
 57 |         # First check if julia is in PATH
 58 |         julia_path = shutil.which("julia")
 59 | 
 60 |         # If not found in PATH, check common installation locations
 61 |         if julia_path is None:
 62 |             common_locations = [
 63 |                 os.path.expanduser("~/.juliaup/bin/julia"),
 64 |                 os.path.expanduser("~/.julia/bin/julia"),
 65 |                 "/usr/local/bin/julia",
 66 |                 "/usr/bin/julia",
 67 |             ]
 68 | 
 69 |             for location in common_locations:
 70 |                 if os.path.isfile(location) and os.access(location, os.X_OK):
 71 |                     julia_path = location
 72 |                     break
 73 | 
 74 |         if julia_path is None:
 75 |             raise RuntimeError(
 76 |                 "Julia is not installed or not in your PATH. "
 77 |                 "Please install Julia from https://julialang.org/downloads/ and ensure it is accessible. "
 78 |                 f"Checked locations: {common_locations}"
 79 |             )
 80 | 
 81 |         # Check if LanguageServer.jl is installed
 82 |         check_cmd = [julia_path, "-e", "using LanguageServer"]
 83 |         try:
 84 |             result = subprocess.run(check_cmd, check=False, capture_output=True, text=True, timeout=10)
 85 |             if result.returncode != 0:
 86 |                 # LanguageServer.jl not found, install it
 87 |                 JuliaLanguageServer._install_language_server(julia_path)
 88 |         except subprocess.TimeoutExpired:
 89 |             # Assume it needs installation
 90 |             JuliaLanguageServer._install_language_server(julia_path)
 91 | 
 92 |         return julia_path
 93 | 
 94 |     @staticmethod
 95 |     def _install_language_server(julia_path: str) -> None:
 96 |         """Install LanguageServer.jl package."""
 97 |         log.info("LanguageServer.jl not found. Installing... (this may take a minute)")
 98 | 
 99 |         install_cmd = [julia_path, "-e", 'using Pkg; Pkg.add("LanguageServer")']
100 | 
101 |         try:
102 |             result = subprocess.run(install_cmd, check=False, capture_output=True, text=True, timeout=300)  # 5 minutes for installation
103 | 
104 |             if result.returncode == 0:
105 |                 log.info("LanguageServer.jl installed successfully!")
106 |             else:
107 |                 raise RuntimeError(f"Failed to install LanguageServer.jl: {result.stderr}")
108 |         except subprocess.TimeoutExpired:
109 |             raise RuntimeError(
110 |                 "LanguageServer.jl installation timed out. Please install manually: julia -e 'using Pkg; Pkg.add(\"LanguageServer\")'"
111 |             )
112 | 
113 |     @override
114 |     def is_ignored_dirname(self, dirname: str) -> bool:
115 |         """Define language-specific directories to ignore for Julia projects."""
116 |         return super().is_ignored_dirname(dirname) or dirname in [".julia", "build", "dist"]
117 | 
118 |     def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
119 |         """
120 |         Returns the initialize params for the Julia Language Server.
121 |         """
122 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
123 |         initialize_params: InitializeParams = {  # type: ignore
124 |             "processId": os.getpid(),
125 |             "rootPath": repository_absolute_path,
126 |             "rootUri": root_uri,
127 |             "capabilities": {
128 |                 "workspace": {"workspaceFolders": True},
129 |                 "textDocument": {
130 |                     "definition": {"dynamicRegistration": True},
131 |                     "references": {"dynamicRegistration": True},
132 |                     "documentSymbol": {"dynamicRegistration": True},
133 |                 },
134 |             },
135 |             "workspaceFolders": [
136 |                 {
137 |                     "uri": root_uri,
138 |                     "name": os.path.basename(repository_absolute_path),
139 |                 }
140 |             ],
141 |         }
142 |         return initialize_params  # type: ignore
143 | 
144 |     def _start_server(self) -> None:
145 |         """Start the LanguageServer.jl server process."""
146 | 
147 |         def do_nothing(params: Any) -> None:
148 |             return
149 | 
150 |         def window_log_message(msg: dict) -> None:
151 |             log.info(f"LSP: window/logMessage: {msg}")
152 | 
153 |         self.server.on_notification("window/logMessage", window_log_message)
154 |         self.server.on_notification("$/progress", do_nothing)
155 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
156 | 
157 |         log.info("Starting LanguageServer.jl server process")
158 |         self.server.start()
159 | 
160 |         initialize_params = self._get_initialize_params(self.repository_root_path)
161 |         log.info("Sending initialize request to Julia Language Server")
162 | 
163 |         init_response = self.server.send.initialize(initialize_params)
164 |         assert "definitionProvider" in init_response["capabilities"]
165 |         assert "referencesProvider" in init_response["capabilities"]
166 |         assert "documentSymbolProvider" in init_response["capabilities"]
167 | 
168 |         self.server.notify.initialized({})
169 |         self.completions_available.set()
170 |         log.info("Julia Language Server is initialized and ready.")
171 | 
```

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

```python
  1 | from __future__ import annotations
  2 | 
  3 | import logging
  4 | import os
  5 | import platform
  6 | import subprocess
  7 | from collections.abc import Iterable, Mapping, Sequence
  8 | from dataclasses import dataclass, replace
  9 | from typing import Any, cast
 10 | 
 11 | from solidlsp.ls_utils import FileUtils, PlatformUtils
 12 | from solidlsp.util.subprocess_util import subprocess_kwargs
 13 | 
 14 | log = logging.getLogger(__name__)
 15 | 
 16 | 
 17 | @dataclass(kw_only=True)
 18 | class RuntimeDependency:
 19 |     """Represents a runtime dependency for a language server."""
 20 | 
 21 |     id: str
 22 |     platform_id: str | None = None
 23 |     url: str | None = None
 24 |     archive_type: str | None = None
 25 |     binary_name: str | None = None
 26 |     command: str | list[str] | None = None
 27 |     package_name: str | None = None
 28 |     package_version: str | None = None
 29 |     extract_path: str | None = None
 30 |     description: str | None = None
 31 | 
 32 | 
 33 | class RuntimeDependencyCollection:
 34 |     """Utility to handle installation of runtime dependencies."""
 35 | 
 36 |     def __init__(self, dependencies: Sequence[RuntimeDependency], overrides: Iterable[Mapping[str, Any]] = ()) -> None:
 37 |         """Initialize the collection with a list of dependencies and optional overrides.
 38 | 
 39 |         :param dependencies: List of base RuntimeDependency instances. The combination of 'id' and 'platform_id' must be unique.
 40 |         :param overrides: List of dictionaries which represent overrides or additions to the base dependencies.
 41 |             Each entry must contain at least the 'id' key, and optionally 'platform_id' to uniquely identify the dependency to override.
 42 |         """
 43 |         self._id_and_platform_id_to_dep: dict[tuple[str, str | None], RuntimeDependency] = {}
 44 |         for dep in dependencies:
 45 |             dep_key = (dep.id, dep.platform_id)
 46 |             if dep_key in self._id_and_platform_id_to_dep:
 47 |                 raise ValueError(f"Duplicate runtime dependency with id '{dep.id}' and platform_id '{dep.platform_id}':\n{dep}")
 48 |             self._id_and_platform_id_to_dep[dep_key] = dep
 49 | 
 50 |         for dep_values_override in overrides:
 51 |             override_key = cast(tuple[str, str | None], (dep_values_override["id"], dep_values_override.get("platform_id")))
 52 |             base_dep = self._id_and_platform_id_to_dep.get(override_key)
 53 |             if base_dep is None:
 54 |                 new_runtime_dep = RuntimeDependency(**dep_values_override)
 55 |                 self._id_and_platform_id_to_dep[override_key] = new_runtime_dep
 56 |             else:
 57 |                 self._id_and_platform_id_to_dep[override_key] = replace(base_dep, **dep_values_override)
 58 | 
 59 |     def get_dependencies_for_platform(self, platform_id: str) -> list[RuntimeDependency]:
 60 |         return [d for d in self._id_and_platform_id_to_dep.values() if d.platform_id in (platform_id, "any", "platform-agnostic", None)]
 61 | 
 62 |     def get_dependencies_for_current_platform(self) -> list[RuntimeDependency]:
 63 |         return self.get_dependencies_for_platform(PlatformUtils.get_platform_id().value)
 64 | 
 65 |     def get_single_dep_for_current_platform(self, dependency_id: str | None = None) -> RuntimeDependency:
 66 |         deps = self.get_dependencies_for_current_platform()
 67 |         if dependency_id is not None:
 68 |             deps = [d for d in deps if d.id == dependency_id]
 69 |         if len(deps) != 1:
 70 |             raise RuntimeError(
 71 |                 f"Expected exactly one runtime dependency for platform-{PlatformUtils.get_platform_id().value} and {dependency_id=}, found {len(deps)}"
 72 |             )
 73 |         return deps[0]
 74 | 
 75 |     def binary_path(self, target_dir: str) -> str:
 76 |         dep = self.get_single_dep_for_current_platform()
 77 |         if not dep.binary_name:
 78 |             return target_dir
 79 |         return os.path.join(target_dir, dep.binary_name)
 80 | 
 81 |     def install(self, target_dir: str) -> dict[str, str]:
 82 |         """Install all dependencies for the current platform into *target_dir*.
 83 | 
 84 |         Returns a mapping from dependency id to the resolved binary path.
 85 |         """
 86 |         os.makedirs(target_dir, exist_ok=True)
 87 |         results: dict[str, str] = {}
 88 |         for dep in self.get_dependencies_for_current_platform():
 89 |             if dep.url:
 90 |                 self._install_from_url(dep, target_dir)
 91 |             if dep.command:
 92 |                 self._run_command(dep.command, target_dir)
 93 |             if dep.binary_name:
 94 |                 results[dep.id] = os.path.join(target_dir, dep.binary_name)
 95 |             else:
 96 |                 results[dep.id] = target_dir
 97 |         return results
 98 | 
 99 |     @staticmethod
100 |     def _run_command(command: str | list[str], cwd: str) -> None:
101 |         kwargs = subprocess_kwargs()
102 |         if not PlatformUtils.get_platform_id().is_windows():
103 |             import pwd
104 | 
105 |             kwargs["user"] = pwd.getpwuid(os.getuid()).pw_name  # type: ignore
106 | 
107 |         is_windows = platform.system() == "Windows"
108 |         if not isinstance(command, str) and not is_windows:
109 |             # Since we are using the shell, we need to convert the command list to a single string
110 |             # on Linux/macOS
111 |             command = " ".join(command)
112 | 
113 |         log.info("Running command %s in '%s'", f"'{command}'" if isinstance(command, str) else command, cwd)
114 | 
115 |         completed_process = subprocess.run(
116 |             command,
117 |             shell=True,
118 |             check=True,
119 |             cwd=cwd,
120 |             stdout=subprocess.PIPE,
121 |             stderr=subprocess.STDOUT,
122 |             **kwargs,
123 |         )  # type: ignore
124 |         if completed_process.returncode != 0:
125 |             log.warning("Command '%s' failed with return code %d", command, completed_process.returncode)
126 |             log.warning("Command output:\n%s", completed_process.stdout)
127 |         else:
128 |             log.info(
129 |                 "Command completed successfully",
130 |             )
131 | 
132 |     @staticmethod
133 |     def _install_from_url(dep: RuntimeDependency, target_dir: str) -> None:
134 |         if not dep.url:
135 |             raise ValueError(f"Dependency {dep.id} has no URL")
136 | 
137 |         if dep.archive_type in ("gz", "binary") and dep.binary_name:
138 |             dest = os.path.join(target_dir, dep.binary_name)
139 |             FileUtils.download_and_extract_archive(dep.url, dest, dep.archive_type)
140 |         else:
141 |             FileUtils.download_and_extract_archive(dep.url, target_dir, dep.archive_type or "zip")
142 | 
143 | 
144 | def quote_windows_path(path: str) -> str:
145 |     """
146 |     Quote a path for Windows command execution if needed.
147 | 
148 |     On Windows, paths need to be quoted for proper command execution.
149 |     The function checks if the path is already quoted to avoid double-quoting.
150 |     On other platforms, the path is returned unchanged.
151 | 
152 |     Args:
153 |         path: The file path to potentially quote
154 | 
155 |     Returns:
156 |         The quoted path on Windows (if not already quoted), unchanged path on other platforms
157 | 
158 |     """
159 |     if platform.system() == "Windows":
160 |         # Check if already quoted to avoid double-quoting
161 |         if path.startswith('"') and path.endswith('"'):
162 |             return path
163 |         return f'"{path}"'
164 |     return path
165 | 
```

--------------------------------------------------------------------------------
/test/conftest.py:
--------------------------------------------------------------------------------

```python
  1 | import logging
  2 | import os
  3 | from collections.abc import Iterator
  4 | from pathlib import Path
  5 | 
  6 | import pytest
  7 | from blib2to3.pgen2.parse import contextmanager
  8 | from sensai.util.logging import configure
  9 | 
 10 | from serena.config.serena_config import SerenaPaths
 11 | from serena.constants import SERENA_MANAGED_DIR_NAME
 12 | from serena.project import Project
 13 | from serena.util.file_system import GitignoreParser
 14 | from solidlsp.ls import SolidLanguageServer
 15 | from solidlsp.ls_config import Language, LanguageServerConfig
 16 | from solidlsp.settings import SolidLSPSettings
 17 | 
 18 | from .solidlsp.clojure import is_clojure_cli_available
 19 | 
 20 | configure(level=logging.INFO)
 21 | 
 22 | log = logging.getLogger(__name__)
 23 | 
 24 | 
 25 | @pytest.fixture(scope="session")
 26 | def resources_dir() -> Path:
 27 |     """Path to the test resources directory."""
 28 |     current_dir = Path(__file__).parent
 29 |     return current_dir / "resources"
 30 | 
 31 | 
 32 | class LanguageParamRequest:
 33 |     param: Language
 34 | 
 35 | 
 36 | def get_repo_path(language: Language) -> Path:
 37 |     return Path(__file__).parent / "resources" / "repos" / language / "test_repo"
 38 | 
 39 | 
 40 | def _create_ls(
 41 |     language: Language, repo_path: str | None = None, ignored_paths: list[str] | None = None, trace_lsp_communication: bool = False
 42 | ) -> SolidLanguageServer:
 43 |     ignored_paths = ignored_paths or []
 44 |     if repo_path is None:
 45 |         repo_path = str(get_repo_path(language))
 46 |     gitignore_parser = GitignoreParser(str(repo_path))
 47 |     for spec in gitignore_parser.get_ignore_specs():
 48 |         ignored_paths.extend(spec.patterns)
 49 |     config = LanguageServerConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication)
 50 |     return SolidLanguageServer.create(
 51 |         config,
 52 |         repo_path,
 53 |         solidlsp_settings=SolidLSPSettings(
 54 |             solidlsp_dir=SerenaPaths().serena_user_home_dir, project_data_relative_path=SERENA_MANAGED_DIR_NAME
 55 |         ),
 56 |     )
 57 | 
 58 | 
 59 | @contextmanager
 60 | def start_ls_context(
 61 |     language: Language, repo_path: str | None = None, ignored_paths: list[str] | None = None, trace_lsp_communication: bool = False
 62 | ) -> Iterator[SolidLanguageServer]:
 63 |     ls = _create_ls(language, repo_path, ignored_paths, trace_lsp_communication)
 64 |     log.info(f"Starting language server for {language} {repo_path}")
 65 |     ls.start()
 66 |     try:
 67 |         log.info(f"Language server started for {language} {repo_path}")
 68 |         yield ls
 69 |     finally:
 70 |         log.info(f"Stopping language server for {language} {repo_path}")
 71 |         try:
 72 |             ls.stop(shutdown_timeout=5)
 73 |         except Exception as e:
 74 |             log.warning(f"Warning: Error stopping language server: {e}")
 75 |             # try to force cleanup
 76 |             if hasattr(ls, "server") and hasattr(ls.server, "process"):
 77 |                 try:
 78 |                     ls.server.process.terminate()
 79 |                 except:
 80 |                     pass
 81 | 
 82 | 
 83 | @contextmanager
 84 | def start_default_ls_context(language: Language) -> Iterator[SolidLanguageServer]:
 85 |     with start_ls_context(language) as ls:
 86 |         yield ls
 87 | 
 88 | 
 89 | def _create_default_project(language: Language) -> Project:
 90 |     repo_path = str(get_repo_path(language))
 91 |     return Project.load(repo_path)
 92 | 
 93 | 
 94 | @pytest.fixture(scope="session")
 95 | def repo_path(request: LanguageParamRequest) -> Path:
 96 |     """Get the repository path for a specific language.
 97 | 
 98 |     This fixture requires a language parameter via pytest.mark.parametrize:
 99 | 
100 |     Example:
101 |     ```
102 |     @pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True)
103 |     def test_python_repo(repo_path):
104 |         assert (repo_path / "src").exists()
105 |     ```
106 | 
107 |     """
108 |     if not hasattr(request, "param"):
109 |         raise ValueError("Language parameter must be provided via pytest.mark.parametrize")
110 | 
111 |     language = request.param
112 |     return get_repo_path(language)
113 | 
114 | 
115 | # Note: using module scope here to avoid restarting LS for each test function but still terminate between test modules
116 | @pytest.fixture(scope="module")
117 | def language_server(request: LanguageParamRequest):
118 |     """Create a language server instance configured for the specified language.
119 | 
120 |     This fixture requires a language parameter via pytest.mark.parametrize:
121 | 
122 |     Example:
123 |     ```
124 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
125 |     def test_python_server(language_server: SyncLanguageServer) -> None:
126 |         # Use the Python language server
127 |         pass
128 |     ```
129 | 
130 |     You can also test multiple languages in a single test:
131 |     ```
132 |     @pytest.mark.parametrize("language_server", [Language.PYTHON, Language.TYPESCRIPT], indirect=True)
133 |     def test_multiple_languages(language_server: SyncLanguageServer) -> None:
134 |         # This test will run once for each language
135 |         pass
136 |     ```
137 | 
138 |     """
139 |     if not hasattr(request, "param"):
140 |         raise ValueError("Language parameter must be provided via pytest.mark.parametrize")
141 | 
142 |     language = request.param
143 |     with start_default_ls_context(language) as ls:
144 |         yield ls
145 | 
146 | 
147 | @pytest.fixture(scope="module")
148 | def project(request: LanguageParamRequest):
149 |     """Create a Project for the specified language.
150 | 
151 |     This fixture requires a language parameter via pytest.mark.parametrize:
152 | 
153 |     Example:
154 |     ```
155 |     @pytest.mark.parametrize("project", [Language.PYTHON], indirect=True)
156 |     def test_python_project(project: Project) -> None:
157 |         # Use the Python project to test something
158 |         pass
159 |     ```
160 | 
161 |     You can also test multiple languages in a single test:
162 |     ```
163 |     @pytest.mark.parametrize("project", [Language.PYTHON, Language.TYPESCRIPT], indirect=True)
164 |     def test_multiple_languages(project: SyncLanguageServer) -> None:
165 |         # This test will run once for each language
166 |         pass
167 |     ```
168 | 
169 |     """
170 |     if not hasattr(request, "param"):
171 |         raise ValueError("Language parameter must be provided via pytest.mark.parametrize")
172 | 
173 |     language = request.param
174 |     project = _create_default_project(language)
175 |     yield project
176 |     project.shutdown(timeout=5)
177 | 
178 | 
179 | is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true"
180 | """
181 | Flag indicating whether the tests are running in the GitHub CI environment.
182 | """
183 | 
184 | 
185 | def _determine_disabled_languages() -> list[Language]:
186 |     """
187 |     Determine which language tests should be disabled (based on the environment)
188 | 
189 |     :return: the list of disabled languages
190 |     """
191 |     result: list[Language] = []
192 | 
193 |     java_tests_enabled = True
194 |     if not java_tests_enabled:
195 |         result.append(Language.JAVA)
196 | 
197 |     clojure_tests_enabled = is_clojure_cli_available()
198 |     if not clojure_tests_enabled:
199 |         result.append(Language.CLOJURE)
200 | 
201 |     al_tests_enabled = True
202 |     if not al_tests_enabled:
203 |         result.append(Language.AL)
204 | 
205 |     return result
206 | 
207 | 
208 | _disabled_languages = _determine_disabled_languages()
209 | 
210 | 
211 | def language_tests_enabled(language: Language) -> bool:
212 |     """
213 |     Check if tests for the given language are enabled in the current environment.
214 | 
215 |     :param language: the language to check
216 |     :return: True if tests for the language are enabled, False otherwise
217 |     """
218 |     return language not in _disabled_languages
219 | 
```

--------------------------------------------------------------------------------
/test/resources/repos/vue/test_repo/src/components/CalculatorInput.vue:
--------------------------------------------------------------------------------

```vue
  1 | <script setup lang="ts">
  2 | import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
  3 | import { useCalculatorStore } from '@/stores/calculator'
  4 | import { useFormatter } from '@/composables/useFormatter'
  5 | import CalculatorButton from './CalculatorButton.vue'
  6 | import type { Operation } from '@/types'
  7 | 
  8 | // Get the calculator store
  9 | const store = useCalculatorStore()
 10 | 
 11 | // Use composable for formatting
 12 | const formatter = useFormatter(2)
 13 | 
 14 | // Local refs for component state
 15 | const isOperationPending = ref(false)
 16 | const lastOperation = ref<Operation>(null)
 17 | const keyboardEnabled = ref(true)
 18 | const operationHistory = ref<string[]>([])
 19 | 
 20 | // Template refs - demonstrates template ref pattern
 21 | const displayRef = ref<HTMLDivElement | null>(null)
 22 | const equalsButtonRef = ref<InstanceType<typeof CalculatorButton> | null>(null)
 23 | 
 24 | // Computed property for button styling
 25 | const getOperationClass = computed(() => (op: Operation) => {
 26 |   return lastOperation.value === op ? 'active' : ''
 27 | })
 28 | 
 29 | // Computed formatted display value using composable
 30 | const formattedDisplay = computed(() => {
 31 |   const value = parseFloat(store.display)
 32 |   return isNaN(value) ? store.display : formatter.formatNumber(value)
 33 | })
 34 | 
 35 | // Watch for operation changes - demonstrates watch
 36 | watch(lastOperation, (newOp, oldOp) => {
 37 |   if (newOp !== oldOp && newOp) {
 38 |     operationHistory.value.push(newOp)
 39 |     // Keep only last 10 operations
 40 |     if (operationHistory.value.length > 10) {
 41 |       operationHistory.value.shift()
 42 |     }
 43 |   }
 44 | })
 45 | 
 46 | // Watch store display changes - demonstrates watch with callback
 47 | watch(
 48 |   () => store.display,
 49 |   (newDisplay) => {
 50 |     if (displayRef.value) {
 51 |       // Trigger animation on display change
 52 |       displayRef.value.classList.add('display-updated')
 53 |       setTimeout(() => {
 54 |         displayRef.value?.classList.remove('display-updated')
 55 |       }, 300)
 56 |     }
 57 |   }
 58 | )
 59 | 
 60 | // Lifecycle hook - demonstrates onMounted
 61 | onMounted(() => {
 62 |   console.log('CalculatorInput mounted')
 63 |   // Add keyboard event listener
 64 |   window.addEventListener('keydown', handleKeyboard)
 65 | 
 66 |   // Focus on the display element
 67 |   if (displayRef.value) {
 68 |     displayRef.value.focus()
 69 |   }
 70 | })
 71 | 
 72 | // Lifecycle hook - demonstrates onBeforeUnmount
 73 | onBeforeUnmount(() => {
 74 |   console.log('CalculatorInput unmounting')
 75 |   // Clean up keyboard event listener
 76 |   window.removeEventListener('keydown', handleKeyboard)
 77 | })
 78 | 
 79 | // Handle number button clicks
 80 | const handleDigit = (digit: number) => {
 81 |   store.appendDigit(digit)
 82 |   isOperationPending.value = false
 83 | }
 84 | 
 85 | // Handle operation button clicks
 86 | const handleOperation = (operation: Operation) => {
 87 |   isOperationPending.value = true
 88 |   lastOperation.value = operation
 89 | 
 90 |   switch (operation) {
 91 |     case 'add':
 92 |       store.add()
 93 |       break
 94 |     case 'subtract':
 95 |       store.subtract()
 96 |       break
 97 |     case 'multiply':
 98 |       store.multiply()
 99 |       break
100 |     case 'divide':
101 |       store.divide()
102 |       break
103 |   }
104 | }
105 | 
106 | // Handle equals button
107 | const handleEquals = () => {
108 |   store.equals()
109 |   isOperationPending.value = false
110 |   lastOperation.value = null
111 | 
112 |   // Access exposed method from child component
113 |   if (equalsButtonRef.value) {
114 |     console.log('Equals button press count:', equalsButtonRef.value.pressCount)
115 |   }
116 | }
117 | 
118 | // Handle clear button
119 | const handleClear = () => {
120 |   store.clear()
121 |   isOperationPending.value = false
122 |   lastOperation.value = null
123 |   operationHistory.value = []
124 | }
125 | 
126 | // Keyboard handler - demonstrates event handling
127 | const handleKeyboard = (event: KeyboardEvent) => {
128 |   if (!keyboardEnabled.value) return
129 | 
130 |   const key = event.key
131 | 
132 |   if (key >= '0' && key <= '9') {
133 |     handleDigit(parseInt(key))
134 |   } else if (key === '+') {
135 |     handleOperation('add')
136 |   } else if (key === '-') {
137 |     handleOperation('subtract')
138 |   } else if (key === '*') {
139 |     handleOperation('multiply')
140 |   } else if (key === '/') {
141 |     event.preventDefault()
142 |     handleOperation('divide')
143 |   } else if (key === 'Enter' || key === '=') {
144 |     handleEquals()
145 |   } else if (key === 'Escape' || key === 'c' || key === 'C') {
146 |     handleClear()
147 |   }
148 | }
149 | 
150 | // Toggle keyboard input
151 | const toggleKeyboard = () => {
152 |   keyboardEnabled.value = !keyboardEnabled.value
153 | }
154 | 
155 | // Array of digits for rendering
156 | const digits = [7, 8, 9, 4, 5, 6, 1, 2, 3, 0]
157 | </script>
158 | 
159 | <template>
160 |   <div class="calculator-input">
161 |     <div ref="displayRef" class="display" tabindex="0">
162 |       {{ formattedDisplay }}
163 |     </div>
164 | 
165 |     <div class="keyboard-toggle">
166 |       <label>
167 |         <input type="checkbox" v-model="keyboardEnabled" @change="toggleKeyboard" />
168 |         Enable Keyboard Input
169 |       </label>
170 |     </div>
171 | 
172 |     <div class="buttons">
173 |       <CalculatorButton
174 |         v-for="digit in digits"
175 |         :key="digit"
176 |         :label="digit"
177 |         variant="digit"
178 |         @click="handleDigit"
179 |       />
180 | 
181 |       <CalculatorButton
182 |         label="+"
183 |         variant="operation"
184 |         :active="lastOperation === 'add'"
185 |         @click="() => handleOperation('add')"
186 |       />
187 | 
188 |       <CalculatorButton
189 |         label="-"
190 |         variant="operation"
191 |         :active="lastOperation === 'subtract'"
192 |         @click="() => handleOperation('subtract')"
193 |       />
194 | 
195 |       <CalculatorButton
196 |         label="×"
197 |         variant="operation"
198 |         :active="lastOperation === 'multiply'"
199 |         @click="() => handleOperation('multiply')"
200 |       />
201 | 
202 |       <CalculatorButton
203 |         label="÷"
204 |         variant="operation"
205 |         :active="lastOperation === 'divide'"
206 |         @click="() => handleOperation('divide')"
207 |       />
208 | 
209 |       <CalculatorButton
210 |         ref="equalsButtonRef"
211 |         label="="
212 |         variant="equals"
213 |         size="large"
214 |         @click="handleEquals"
215 |       />
216 | 
217 |       <CalculatorButton
218 |         label="C"
219 |         variant="clear"
220 |         @click="handleClear"
221 |       />
222 |     </div>
223 | 
224 |     <div v-if="isOperationPending" class="pending-indicator">
225 |       Operation pending: {{ lastOperation }}
226 |     </div>
227 | 
228 |     <div v-if="operationHistory.length > 0" class="operation-history">
229 |       Recent operations: {{ operationHistory.join(', ') }}
230 |     </div>
231 |   </div>
232 | </template>
233 | 
234 | <style scoped>
235 | .calculator-input {
236 |   display: flex;
237 |   flex-direction: column;
238 |   gap: 1rem;
239 |   padding: 1rem;
240 |   background: #f5f5f5;
241 |   border-radius: 8px;
242 | }
243 | 
244 | .display {
245 |   font-size: 2rem;
246 |   text-align: right;
247 |   padding: 1rem;
248 |   background: white;
249 |   border-radius: 4px;
250 |   min-height: 3rem;
251 |   transition: background-color 0.3s;
252 |   outline: none;
253 | }
254 | 
255 | .display:focus {
256 |   box-shadow: 0 0 0 2px #2196f3;
257 | }
258 | 
259 | .display.display-updated {
260 |   background-color: #e3f2fd;
261 | }
262 | 
263 | .keyboard-toggle {
264 |   display: flex;
265 |   justify-content: center;
266 |   padding: 0.5rem;
267 | }
268 | 
269 | .keyboard-toggle label {
270 |   display: flex;
271 |   align-items: center;
272 |   gap: 0.5rem;
273 |   cursor: pointer;
274 |   font-size: 0.9rem;
275 | }
276 | 
277 | .buttons {
278 |   display: grid;
279 |   grid-template-columns: repeat(4, 1fr);
280 |   gap: 0.5rem;
281 | }
282 | 
283 | .pending-indicator {
284 |   font-size: 0.9rem;
285 |   color: #666;
286 |   text-align: center;
287 |   font-style: italic;
288 | }
289 | 
290 | .operation-history {
291 |   font-size: 0.8rem;
292 |   color: #999;
293 |   text-align: center;
294 |   padding: 0.5rem;
295 |   background: white;
296 |   border-radius: 4px;
297 |   max-height: 3rem;
298 |   overflow: auto;
299 | }
300 | </style>
301 | 
```

--------------------------------------------------------------------------------
/.serena/memories/adding_new_language_support_guide.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Adding New Language Support to Serena
  2 | 
  3 | This guide explains how to add support for a new programming language to Serena.
  4 | 
  5 | ## Overview
  6 | 
  7 | Adding a new language involves:
  8 | 
  9 | 1. **Language Server Implementation** - Creating a language-specific server class
 10 | 2. **Language Registration** - Adding the language to enums and configurations  
 11 | 3. **Test Repository** - Creating a minimal test project
 12 | 4. **Test Suite** - Writing comprehensive tests
 13 | 5. **Runtime Dependencies** - Configuring automatic language server downloads
 14 | 
 15 | ## Step 1: Language Server Implementation
 16 | 
 17 | ### 1.1 Create Language Server Class
 18 | 
 19 | Create a new file in `src/solidlsp/language_servers/` (e.g., `new_language_server.py`).
 20 | Have a look at `intelephense.py` for a reference implementation of a language server which downloads all its dependencies, at `gopls.py` for an LS that needs some preinstalled
 21 | dependencies, and on `pyright_server.py` that does not need any additional dependencies
 22 | because the language server can be installed directly as python package.
 23 | 
 24 | ```python
 25 | from solidlsp.ls import SolidLanguageServer
 26 | from solidlsp.ls_config import LanguageServerConfig
 27 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 28 | 
 29 | 
 30 | class NewLanguageServer(SolidLanguageServer):
 31 |     """
 32 |     Language server implementation for NewLanguage.
 33 |     """
 34 | 
 35 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str):
 36 |         # Determine language server command
 37 |         cmd = self._get_language_server_command()
 38 | 
 39 |         super().__init__(config,
 40 |             ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
 41 |             "new_language",,
 42 | 
 43 |     def _get_language_server_command(self) -> list[str]:
 44 |         """Get the command to start the language server."""
 45 |         # Example: return ["new-language-server", "--stdio"]
 46 |         pass
 47 | 
 48 |     @override
 49 |     def is_ignored_dirname(self, dirname: str) -> bool:
 50 |         """Define language-specific directories to ignore."""
 51 |         return super().is_ignored_dirname(dirname) or dirname in ["build",
 52 |             "dist", "target"]
 53 | ```
 54 | 
 55 | ### 1.2 Language Server Discovery and Installation
 56 | 
 57 | For languages requiring automatic installation, implement download logic similar to C#:
 58 | 
 59 | ```python
 60 | @classmethod
 61 | def _ensure_server_installed(cls) -> str:
 62 |     """Ensure language server is installed and return path."""
 63 |     # Check system installation first
 64 |     system_server = shutil.which("new-language-server")
 65 |     if system_server:
 66 |         return system_server
 67 |     
 68 |     # Download and install if needed
 69 |     server_path = cls._download_and_install_server()
 70 |     return server_path
 71 | 
 72 | def _download_and_install_server(cls) -> str:
 73 |     """Download and install the language server."""
 74 |     # Implementation specific to your language server
 75 |     pass
 76 | ```
 77 | 
 78 | ### 1.3 LSP Initialization
 79 | 
 80 | Override initialization methods if needed:
 81 | 
 82 | ```python
 83 | def _get_initialize_params(self) -> InitializeParams:
 84 |     """Return language-specific initialization parameters."""
 85 |     return {
 86 |         "processId": os.getpid(),
 87 |         "rootUri": PathUtils.path_to_uri(self.repository_root_path),
 88 |         "capabilities": {
 89 |             # Language-specific capabilities
 90 |         }
 91 |     }
 92 | 
 93 | def _start_server(self):
 94 |     """Start the language server with custom handlers."""
 95 |     # Set up notification handlers
 96 |     self.server.on_notification("window/logMessage", self._handle_log_message)
 97 |     
 98 |     # Start server and initialize
 99 |     self.server.start()
100 |     init_response = self.server.send.initialize(self._get_initialize_params())
101 |     self.server.notify.initialized({})
102 | ```
103 | 
104 | ## Step 2: Language Registration
105 | 
106 | ### 2.1 Add to Language Enum
107 | 
108 | In `src/solidlsp/ls_config.py`, add your language to the `Language` enum:
109 | 
110 | ```python
111 | class Language(str, Enum):
112 |     # Existing languages...
113 |     NEW_LANGUAGE = "new_language"
114 |     
115 |     def get_source_fn_matcher(self) -> FilenameMatcher:
116 |         match self:
117 |             # Existing cases...
118 |             case self.NEW_LANGUAGE:
119 |                 return FilenameMatcher("*.newlang", "*.nl")  # File extensions
120 | ```
121 | 
122 | ### 2.2 Update Language Server Factory
123 | 
124 | In `src/solidlsp/ls.py`, add your language to the `create` method:
125 | 
126 | ```python
127 | @classmethod
128 | def create(cls, config: LanguageServerConfig, repository_root_path: str) -> "SolidLanguageServer":
129 |     match config.code_language:
130 |         # Existing cases...
131 |         case Language.NEW_LANGUAGE:
132 |             from solidlsp.language_servers.new_language_server import NewLanguageServer
133 |             return NewLanguageServer(config, repository_root_path)
134 | ```
135 | 
136 | ## Step 3: Test Repository
137 | 
138 | ### 3.1 Create Test Project
139 | 
140 | Create a minimal project in `test/resources/repos/new_language/test_repo/`:
141 | 
142 | ```
143 | test/resources/repos/new_language/test_repo/
144 | ├── main.newlang              # Main source file
145 | ├── lib/
146 | │   └── helper.newlang       # Additional source for testing
147 | ├── project.toml             # Project configuration (if applicable)
148 | └── .gitignore              # Ignore build artifacts
149 | ```
150 | 
151 | ### 3.2 Example Source Files
152 | 
153 | Create meaningful source files that demonstrate:
154 | 
155 | - **Classes/Types** - For symbol testing
156 | - **Functions/Methods** - For reference finding
157 | - **Imports/Dependencies** - For cross-file operations
158 | - **Nested Structures** - For hierarchical symbol testing
159 | 
160 | Example `main.newlang`:
161 | ```
162 | import lib.helper
163 | 
164 | class Calculator {
165 |     func add(a: Int, b: Int) -> Int {
166 |         return a + b
167 |     }
168 |     
169 |     func subtract(a: Int, b: Int) -> Int {
170 |         return helper.subtract(a, b)  // Reference to imported function
171 |     }
172 | }
173 | 
174 | class Program {
175 |     func main() {
176 |         let calc = Calculator()
177 |         let result = calc.add(5, 3)  // Reference to add method
178 |         print(result)
179 |     }
180 | }
181 | ```
182 | 
183 | ## Step 4: Test Suite
184 | 
185 | Testing the language server implementation is of crucial importance, and the tests will
186 | form the main part of the review process. Make sure that the tests are up to the standard
187 | of Serena to make the review go smoother.
188 | 
189 | General rules for tests:
190 | 
191 | 1. Tests for symbols and references should always check that the expected symbol names and references were actually found.
192 |    Just testing that a list came back or that the result is not None is insufficient.
193 | 2. Tests should never be skipped, the only exception is skipping based on some package being available or on an unsupported OS.
194 | 3. Tests should run in CI, check if there is a suitable GitHub action for installing the dependencies.
195 | 
196 | ### 4.1 Basic Tests
197 | 
198 | Create `test/solidlsp/new_language/test_new_language_basic.py`.
199 | Have a look at the structure of existing tests, for example, in `test/solidlsp/php/test_php_basic.py`
200 | You should at least test:
201 | 
202 | 1. Finding symbols
203 | 2. Finding within-file references
204 | 3. Finding cross-file references
205 | 
206 | Have a look at `test/solidlsp/php/test_php_basic.py` as an example for what should be tested.
207 | Don't forget to add a new language marker to `pytest.ini`.
208 | 
209 | ### 4.2 Integration Tests
210 | 
211 | Consider adding new cases to the parametrized tests in `test_serena_agent.py` for the new language.
212 | 
213 | 
214 | ### 5 Documentation
215 | 
216 | Update:
217 | 
218 | - **README.md** - Add language to supported languages list
219 | - **CHANGELOG.md** - Document the new language support
220 | - **Language-specific docs** - Installation requirements, known issues
221 | 
```

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

```python
  1 | """
  2 | Provides Scala specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Scala.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import shutil
  9 | import subprocess
 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 PlatformUtils
 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 | if not PlatformUtils.get_platform_id().value.startswith("win"):
 21 |     pass
 22 | 
 23 | 
 24 | log = logging.getLogger(__name__)
 25 | 
 26 | 
 27 | class ScalaLanguageServer(SolidLanguageServer):
 28 |     """
 29 |     Provides Scala specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Scala.
 30 |     """
 31 | 
 32 |     def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
 33 |         """
 34 |         Creates a ScalaLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 35 |         """
 36 |         scala_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
 37 |         super().__init__(
 38 |             config,
 39 |             repository_root_path,
 40 |             ProcessLaunchInfo(cmd=scala_lsp_executable_path, cwd=repository_root_path),
 41 |             config.code_language.value,
 42 |             solidlsp_settings,
 43 |         )
 44 | 
 45 |     @override
 46 |     def is_ignored_dirname(self, dirname: str) -> bool:
 47 |         return super().is_ignored_dirname(dirname) or dirname in [
 48 |             ".bloop",
 49 |             ".metals",
 50 |             "target",
 51 |         ]
 52 | 
 53 |     @classmethod
 54 |     def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]:
 55 |         """
 56 |         Setup runtime dependencies for Scala Language Server and return the command to start the server.
 57 |         """
 58 |         assert shutil.which("java") is not None, "JDK is not installed or not in PATH."
 59 | 
 60 |         metals_version = "1.6.4"
 61 | 
 62 |         metals_home = os.path.join(cls.ls_resources_dir(solidlsp_settings), "metals-lsp")
 63 |         os.makedirs(metals_home, exist_ok=True)
 64 |         metals_executable = os.path.join(metals_home, metals_version, "metals")
 65 |         coursier_command_path = shutil.which("coursier")
 66 |         cs_command_path = shutil.which("cs")
 67 |         assert cs_command_path is not None or coursier_command_path is not None, "coursier is not installed or not in PATH."
 68 | 
 69 |         if not os.path.exists(metals_executable):
 70 |             if not cs_command_path:
 71 |                 assert coursier_command_path is not None
 72 |                 log.info("'cs' command not found. Trying to install it using 'coursier'.")
 73 |                 try:
 74 |                     log.info("Running 'coursier setup --yes' to install 'cs'...")
 75 |                     subprocess.run([coursier_command_path, "setup", "--yes"], check=True, capture_output=True, text=True)
 76 |                 except subprocess.CalledProcessError as e:
 77 |                     raise RuntimeError(f"Failed to set up 'cs' command with 'coursier setup'. Stderr: {e.stderr}")
 78 | 
 79 |                 cs_command_path = shutil.which("cs")
 80 |                 if not cs_command_path:
 81 |                     raise RuntimeError(
 82 |                         "'cs' command not found after running 'coursier setup'. Please check your PATH or install it manually."
 83 |                     )
 84 |                 log.info("'cs' command installed successfully.")
 85 | 
 86 |             log.info(f"metals executable not found at {metals_executable}, bootstrapping...")
 87 |             subprocess.run(["mkdir", "-p", os.path.join(metals_home, metals_version)], check=True)
 88 |             artifact = f"org.scalameta:metals_2.13:{metals_version}"
 89 |             cmd = [
 90 |                 cs_command_path,
 91 |                 "bootstrap",
 92 |                 "--java-opt",
 93 |                 "-XX:+UseG1GC",
 94 |                 "--java-opt",
 95 |                 "-XX:+UseStringDeduplication",
 96 |                 "--java-opt",
 97 |                 "-Xss4m",
 98 |                 "--java-opt",
 99 |                 "-Xms100m",
100 |                 "--java-opt",
101 |                 "-Dmetals.client=Serena",
102 |                 artifact,
103 |                 "-o",
104 |                 metals_executable,
105 |                 "-f",
106 |             ]
107 |             log.info("Bootstrapping metals...")
108 |             subprocess.run(cmd, cwd=metals_home, check=True)
109 |             log.info("Bootstrapping metals finished.")
110 |         return [metals_executable]
111 | 
112 |     @staticmethod
113 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
114 |         """
115 |         Returns the initialize params for the Scala Language Server.
116 |         """
117 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
118 |         initialize_params = {
119 |             "locale": "en",
120 |             "processId": os.getpid(),
121 |             "rootPath": repository_absolute_path,
122 |             "rootUri": root_uri,
123 |             "initializationOptions": {
124 |                 "compilerOptions": {
125 |                     "completionCommand": None,
126 |                     "isCompletionItemDetailEnabled": True,
127 |                     "isCompletionItemDocumentationEnabled": True,
128 |                     "isCompletionItemResolve": True,
129 |                     "isHoverDocumentationEnabled": True,
130 |                     "isSignatureHelpDocumentationEnabled": True,
131 |                     "overrideDefFormat": "ascli",
132 |                     "snippetAutoIndent": False,
133 |                 },
134 |                 "debuggingProvider": True,
135 |                 "decorationProvider": False,
136 |                 "didFocusProvider": False,
137 |                 "doctorProvider": False,
138 |                 "executeClientCommandProvider": False,
139 |                 "globSyntax": "uri",
140 |                 "icons": "unicode",
141 |                 "inputBoxProvider": False,
142 |                 "isVirtualDocumentSupported": False,
143 |                 "isExitOnShutdown": True,
144 |                 "isHttpEnabled": True,
145 |                 "openFilesOnRenameProvider": False,
146 |                 "quickPickProvider": False,
147 |                 "renameFileThreshold": 200,
148 |                 "statusBarProvider": "false",
149 |                 "treeViewProvider": False,
150 |                 "testExplorerProvider": False,
151 |                 "openNewWindowProvider": False,
152 |                 "copyWorksheetOutputProvider": False,
153 |                 "doctorVisibilityProvider": False,
154 |             },
155 |             "capabilities": {"textDocument": {"documentSymbol": {"hierarchicalDocumentSymbolSupport": True}}},
156 |         }
157 |         return initialize_params  # type: ignore
158 | 
159 |     def _start_server(self) -> None:
160 |         """
161 |         Starts the Scala Language Server
162 |         """
163 |         log.info("Starting Scala server process")
164 |         self.server.start()
165 | 
166 |         log.info("Sending initialize request from LSP client to LSP server and awaiting response")
167 | 
168 |         initialize_params = self._get_initialize_params(self.repository_root_path)
169 |         self.server.send.initialize(initialize_params)
170 |         self.server.notify.initialized({})
171 | 
172 |     @override
173 |     def _get_wait_time_for_cross_file_referencing(self) -> float:
174 |         return 5
175 | 
```

--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/models.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Models module that demonstrates various Python class patterns.
  3 | """
  4 | 
  5 | from abc import ABC, abstractmethod
  6 | from typing import Any, Generic, TypeVar
  7 | 
  8 | T = TypeVar("T")
  9 | 
 10 | 
 11 | class BaseModel(ABC):
 12 |     """
 13 |     Abstract base class for all models.
 14 |     """
 15 | 
 16 |     def __init__(self, id: str, name: str | None = None):
 17 |         self.id = id
 18 |         self.name = name or id
 19 | 
 20 |     @abstractmethod
 21 |     def to_dict(self) -> dict[str, Any]:
 22 |         """Convert model to dictionary representation"""
 23 | 
 24 |     @classmethod
 25 |     def from_dict(cls, data: dict[str, Any]) -> "BaseModel":
 26 |         """Create a model instance from dictionary data"""
 27 |         id = data.get("id", "")
 28 |         name = data.get("name")
 29 |         return cls(id=id, name=name)
 30 | 
 31 | 
 32 | class User(BaseModel):
 33 |     """
 34 |     User model representing a system user.
 35 |     """
 36 | 
 37 |     def __init__(self, id: str, name: str | None = None, email: str = "", roles: list[str] | None = None):
 38 |         super().__init__(id, name)
 39 |         self.email = email
 40 |         self.roles = roles or []
 41 | 
 42 |     def to_dict(self) -> dict[str, Any]:
 43 |         return {"id": self.id, "name": self.name, "email": self.email, "roles": self.roles}
 44 | 
 45 |     @classmethod
 46 |     def from_dict(cls, data: dict[str, Any]) -> "User":
 47 |         instance = super().from_dict(data)
 48 |         instance.email = data.get("email", "")
 49 |         instance.roles = data.get("roles", [])
 50 |         return instance
 51 | 
 52 |     def has_role(self, role: str) -> bool:
 53 |         """Check if user has a specific role"""
 54 |         return role in self.roles
 55 | 
 56 | 
 57 | class Item(BaseModel):
 58 |     """
 59 |     Item model representing a product or service.
 60 |     """
 61 | 
 62 |     def __init__(self, id: str, name: str | None = None, price: float = 0.0, category: str = ""):
 63 |         super().__init__(id, name)
 64 |         self.price = price
 65 |         self.category = category
 66 | 
 67 |     def to_dict(self) -> dict[str, Any]:
 68 |         return {"id": self.id, "name": self.name, "price": self.price, "category": self.category}
 69 | 
 70 |     def get_display_price(self) -> str:
 71 |         """Format price for display"""
 72 |         return f"${self.price:.2f}"
 73 | 
 74 | 
 75 | # Generic type example
 76 | class Collection(Generic[T]):
 77 |     def __init__(self, items: list[T] | None = None):
 78 |         self.items = items or []
 79 | 
 80 |     def add(self, item: T) -> None:
 81 |         self.items.append(item)
 82 | 
 83 |     def get_all(self) -> list[T]:
 84 |         return self.items
 85 | 
 86 | 
 87 | # Factory function
 88 | def create_user_object(id: str, name: str, email: str, roles: list[str] | None = None) -> User:
 89 |     """Factory function to create a user"""
 90 |     return User(id=id, name=name, email=email, roles=roles)
 91 | 
 92 | 
 93 | # Multiple inheritance examples
 94 | 
 95 | 
 96 | class Loggable:
 97 |     """
 98 |     Mixin class that provides logging functionality.
 99 |     Example of a common mixin pattern used with multiple inheritance.
100 |     """
101 | 
102 |     def __init__(self, **kwargs):
103 |         super().__init__(**kwargs)
104 |         self.log_entries: list[str] = []
105 | 
106 |     def log(self, message: str) -> None:
107 |         """Add a log entry"""
108 |         self.log_entries.append(message)
109 | 
110 |     def get_logs(self) -> list[str]:
111 |         """Get all log entries"""
112 |         return self.log_entries
113 | 
114 | 
115 | class Serializable:
116 |     """
117 |     Mixin class that provides JSON serialization capabilities.
118 |     Another example of a mixin for multiple inheritance.
119 |     """
120 | 
121 |     def __init__(self, **kwargs):
122 |         super().__init__(**kwargs)
123 | 
124 |     def to_json(self) -> dict[str, Any]:
125 |         """Convert to JSON-serializable dictionary"""
126 |         return self.to_dict() if hasattr(self, "to_dict") else {}
127 | 
128 |     @classmethod
129 |     def from_json(cls, data: dict[str, Any]) -> Any:
130 |         """Create instance from JSON data"""
131 |         return cls.from_dict(data) if hasattr(cls, "from_dict") else cls(**data)
132 | 
133 | 
134 | class Auditable:
135 |     """
136 |     Mixin for tracking creation and modification timestamps.
137 |     """
138 | 
139 |     def __init__(self, **kwargs):
140 |         super().__init__(**kwargs)
141 |         self.created_at: str = kwargs.get("created_at", "")
142 |         self.updated_at: str = kwargs.get("updated_at", "")
143 | 
144 |     def update_timestamp(self, timestamp: str) -> None:
145 |         """Update the last modified timestamp"""
146 |         self.updated_at = timestamp
147 | 
148 | 
149 | # Diamond inheritance pattern
150 | class BaseService(ABC):
151 |     """
152 |     Base class for service objects - demonstrates diamond inheritance pattern.
153 |     """
154 | 
155 |     def __init__(self, name: str = "base"):
156 |         self.service_name = name
157 | 
158 |     @abstractmethod
159 |     def get_service_info(self) -> dict[str, str]:
160 |         """Get service information"""
161 | 
162 | 
163 | class DataService(BaseService):
164 |     """
165 |     Data handling service.
166 |     """
167 | 
168 |     def __init__(self, **kwargs):
169 |         name = kwargs.pop("name", "data")
170 |         super().__init__(name=name)
171 |         self.data_source = kwargs.get("data_source", "default")
172 | 
173 |     def get_service_info(self) -> dict[str, str]:
174 |         return {"service_type": "data", "service_name": self.service_name, "data_source": self.data_source}
175 | 
176 | 
177 | class NetworkService(BaseService):
178 |     """
179 |     Network connectivity service.
180 |     """
181 | 
182 |     def __init__(self, **kwargs):
183 |         name = kwargs.pop("name", "network")
184 |         super().__init__(name=name)
185 |         self.endpoint = kwargs.get("endpoint", "localhost")
186 | 
187 |     def get_service_info(self) -> dict[str, str]:
188 |         return {"service_type": "network", "service_name": self.service_name, "endpoint": self.endpoint}
189 | 
190 | 
191 | class DataSyncService(DataService, NetworkService):
192 |     """
193 |     Service that syncs data over network - example of diamond inheritance.
194 |     Inherits from both DataService and NetworkService, which both inherit from BaseService.
195 |     """
196 | 
197 |     def __init__(self, **kwargs):
198 |         super().__init__(**kwargs)
199 |         self.sync_interval = kwargs.get("sync_interval", 60)
200 | 
201 |     def get_service_info(self) -> dict[str, str]:
202 |         info = super().get_service_info()
203 |         info.update({"service_type": "data_sync", "sync_interval": str(self.sync_interval)})
204 |         return info
205 | 
206 | 
207 | # Multiple inheritance with mixins
208 | 
209 | 
210 | class LoggableUser(User, Loggable):
211 |     """
212 |     User class with logging capabilities.
213 |     Example of extending a concrete class with a mixin.
214 |     """
215 | 
216 |     def __init__(self, id: str, name: str | None = None, email: str = "", roles: list[str] | None = None):
217 |         super().__init__(id=id, name=name, email=email, roles=roles)
218 | 
219 |     def add_role(self, role: str) -> None:
220 |         """Add a role to the user and log the action"""
221 |         if role not in self.roles:
222 |             self.roles.append(role)
223 |             self.log(f"Added role '{role}' to user {self.id}")
224 | 
225 | 
226 | class TrackedItem(Item, Serializable, Auditable):
227 |     """
228 |     Item with serialization and auditing capabilities.
229 |     Example of a class inheriting from a concrete class and multiple mixins.
230 |     """
231 | 
232 |     def __init__(
233 |         self, id: str, name: str | None = None, price: float = 0.0, category: str = "", created_at: str = "", updated_at: str = ""
234 |     ):
235 |         super().__init__(id=id, name=name, price=price, category=category, created_at=created_at, updated_at=updated_at)
236 |         self.stock_level = 0
237 | 
238 |     def update_stock(self, quantity: int) -> None:
239 |         """Update stock level and timestamp"""
240 |         self.stock_level = quantity
241 |         self.update_timestamp(f"stock_update_{quantity}")
242 | 
243 |     def to_dict(self) -> dict[str, Any]:
244 |         result = super().to_dict()
245 |         result.update({"stock_level": self.stock_level, "created_at": self.created_at, "updated_at": self.updated_at})
246 |         return result
247 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/rego/test_rego_basic.py:
--------------------------------------------------------------------------------

```python
  1 | """Tests for Rego language server (Regal) functionality."""
  2 | 
  3 | import os
  4 | import sys
  5 | 
  6 | import pytest
  7 | 
  8 | from solidlsp.ls import SolidLanguageServer
  9 | from solidlsp.ls_config import Language
 10 | from solidlsp.ls_utils import SymbolUtils
 11 | 
 12 | 
 13 | @pytest.mark.rego
 14 | @pytest.mark.skipif(
 15 |     sys.platform == "win32", reason="Regal LSP has Windows path handling bug - see https://github.com/StyraInc/regal/issues/1683"
 16 | )
 17 | class TestRegoLanguageServer:
 18 |     """Test Regal language server functionality for Rego."""
 19 | 
 20 |     @pytest.mark.parametrize("language_server", [Language.REGO], indirect=True)
 21 |     def test_request_document_symbols_authz(self, language_server: SolidLanguageServer) -> None:
 22 |         """Test that document symbols can be retrieved from authz.rego."""
 23 |         file_path = os.path.join("policies", "authz.rego")
 24 |         symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
 25 | 
 26 |         assert symbols is not None
 27 |         assert len(symbols) > 0
 28 | 
 29 |         # Extract symbol names
 30 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 31 |         symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
 32 | 
 33 |         # Verify specific Rego rules/functions are found
 34 |         assert "allow" in symbol_names, "allow rule not found"
 35 |         assert "allow_read" in symbol_names, "allow_read rule not found"
 36 |         assert "is_admin" in symbol_names, "is_admin function not found"
 37 |         assert "admin_roles" in symbol_names, "admin_roles constant not found"
 38 | 
 39 |     @pytest.mark.parametrize("language_server", [Language.REGO], indirect=True)
 40 |     def test_request_document_symbols_helpers(self, language_server: SolidLanguageServer) -> None:
 41 |         """Test that document symbols can be retrieved from helpers.rego."""
 42 |         file_path = os.path.join("utils", "helpers.rego")
 43 |         symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
 44 | 
 45 |         assert symbols is not None
 46 |         assert len(symbols) > 0
 47 | 
 48 |         # Extract symbol names
 49 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 50 |         symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
 51 | 
 52 |         # Verify specific helper functions are found
 53 |         assert "is_valid_user" in symbol_names, "is_valid_user function not found"
 54 |         assert "is_valid_email" in symbol_names, "is_valid_email function not found"
 55 |         assert "is_valid_username" in symbol_names, "is_valid_username function not found"
 56 | 
 57 |     @pytest.mark.parametrize("language_server", [Language.REGO], indirect=True)
 58 |     def test_find_symbol_full_tree(self, language_server: SolidLanguageServer) -> None:
 59 |         """Test finding symbols across entire workspace using symbol tree."""
 60 |         symbols = language_server.request_full_symbol_tree()
 61 | 
 62 |         # Use SymbolUtils to check for expected symbols
 63 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "allow"), "allow rule not found in symbol tree"
 64 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "is_valid_user"), "is_valid_user function not found in symbol tree"
 65 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "is_admin"), "is_admin function not found in symbol tree"
 66 | 
 67 |     @pytest.mark.parametrize("language_server", [Language.REGO], indirect=True)
 68 |     def test_request_definition_within_file(self, language_server: SolidLanguageServer) -> None:
 69 |         """Test go-to-definition for symbols within the same file."""
 70 |         # In authz.rego, check_permission references admin_roles
 71 |         file_path = os.path.join("policies", "authz.rego")
 72 | 
 73 |         # Get document symbols
 74 |         symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
 75 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 76 | 
 77 |         # Find the is_admin symbol which references admin_roles
 78 |         is_admin_symbol = next((s for s in symbol_list if s.get("name") == "is_admin"), None)
 79 |         assert is_admin_symbol is not None, "is_admin symbol should always be found in authz.rego"
 80 |         assert "range" in is_admin_symbol, "is_admin symbol should have a range"
 81 | 
 82 |         # Request definition from within is_admin (line 25, which references admin_roles at line 21)
 83 |         # Line 25 is: admin_roles[_] == user.role
 84 |         line = is_admin_symbol["range"]["start"]["line"] + 1
 85 |         char = 4  # Position at "admin_roles"
 86 | 
 87 |         definitions = language_server.request_definition(file_path, line, char)
 88 |         assert definitions is not None and len(definitions) > 0, "Should find definition for admin_roles"
 89 | 
 90 |         # Verify the definition points to admin_roles in the same file
 91 |         assert any("authz.rego" in defn.get("relativePath", "") for defn in definitions), "Definition should be in authz.rego"
 92 | 
 93 |     @pytest.mark.parametrize("language_server", [Language.REGO], indirect=True)
 94 |     def test_request_definition_across_files(self, language_server: SolidLanguageServer) -> None:
 95 |         """Test go-to-definition for symbols across files (cross-file references)."""
 96 |         # In authz.rego line 11, the allow rule calls utils.is_valid_user
 97 |         # This function is defined in utils/helpers.rego
 98 |         file_path = os.path.join("policies", "authz.rego")
 99 | 
100 |         # Get document symbols
101 |         symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
102 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
103 | 
104 |         # Find the allow symbol
105 |         allow_symbol = next((s for s in symbol_list if s.get("name") == "allow"), None)
106 |         assert allow_symbol is not None, "allow symbol should always be found in authz.rego"
107 |         assert "range" in allow_symbol, "allow symbol should have a range"
108 | 
109 |         # Request definition from line 11 where utils.is_valid_user is called
110 |         # Line 11: utils.is_valid_user(input.user)
111 |         line = 10  # 0-indexed, so line 11 in file is line 10 in LSP
112 |         char = 7  # Position at "is_valid_user" in "utils.is_valid_user"
113 | 
114 |         definitions = language_server.request_definition(file_path, line, char)
115 |         assert definitions is not None and len(definitions) > 0, "Should find cross-file definition for is_valid_user"
116 | 
117 |         # Verify the definition points to helpers.rego (cross-file)
118 |         assert any(
119 |             "helpers.rego" in defn.get("relativePath", "") for defn in definitions
120 |         ), "Definition should be in utils/helpers.rego (cross-file reference)"
121 | 
122 |     @pytest.mark.parametrize("language_server", [Language.REGO], indirect=True)
123 |     def test_find_symbols_validation(self, language_server: SolidLanguageServer) -> None:
124 |         """Test finding symbols in validation.rego which has imports."""
125 |         file_path = os.path.join("policies", "validation.rego")
126 |         symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
127 | 
128 |         assert symbols is not None
129 |         assert len(symbols) > 0
130 | 
131 |         # Extract symbol names
132 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
133 |         symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
134 | 
135 |         # Verify expected symbols
136 |         assert "validate_user_input" in symbol_names, "validate_user_input rule not found"
137 |         assert "has_valid_credentials" in symbol_names, "has_valid_credentials function not found"
138 |         assert "validate_request" in symbol_names, "validate_request rule not found"
139 | 
```
Page 4/21FirstPrevNextLast