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