This is page 3 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/035_tools.md:
--------------------------------------------------------------------------------
```markdown
1 | # List of Tools
2 |
3 | Find the full list of Serena's tools below (output of `<serena> tools list --all`).
4 |
5 | Note that in most configurations, only a subset of these tools will be enabled simultaneously (see the section on [configuration](../02-usage/050_configuration) for details).
6 |
7 | * `activate_project`: Activates a project based on the project name or path.
8 | * `check_onboarding_performed`: Checks whether project onboarding was already performed.
9 | * `create_text_file`: Creates/overwrites a file in the project directory.
10 | * `delete_lines`: Deletes a range of lines within a file.
11 | * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
12 | * `execute_shell_command`: Executes a shell command.
13 | * `find_file`: Finds files in the given relative paths
14 | * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
15 | * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filter
16 | ed by type).
17 | * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools,
18 | contexts, and modes.
19 | * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
20 | * `initial_instructions`: Provides instructions on how to use the Serena toolbox.
21 | * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
22 | * `insert_at_line`: Inserts content at a given line in a file.
23 | * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
24 | * `jet_brains_find_referencing_symbols`: Finds symbols that reference the given symbol
25 | * `jet_brains_find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (option
26 | ally filtered by type).
27 | * `jet_brains_get_symbols_overview`: Retrieves an overview of the top-level symbols within a specified file
28 | * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
29 | * `list_memories`: Lists memories in Serena's project-specific memory store.
30 | * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
31 | * `open_dashboard`: Opens the Serena web dashboard in the default web browser.
32 | * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with th
33 | e necessary context).
34 | * `read_file`: Reads a file within the project directory.
35 | * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
36 | * `remove_project`: Removes a project from the Serena configuration.
37 | * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities.
38 | * `replace_lines`: Replaces a range of lines within a file with new content.
39 | * `replace_content`: Replaces content in a file (optionally using regular expressions).
40 | * `replace_symbol_body`: Replaces the full definition of a symbol.
41 | * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
42 | * `search_for_pattern`: Performs a search for a pattern in the project.
43 | * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
44 | * `switch_modes`: Activates modes by providing a list of their names
45 | * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
46 | * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
47 | * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
48 | * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
49 |
```
--------------------------------------------------------------------------------
/src/interprompt/prompt_factory.py:
--------------------------------------------------------------------------------
```python
1 | import logging
2 | import os
3 | from typing import Any
4 |
5 | from .multilang_prompt import DEFAULT_LANG_CODE, LanguageFallbackMode, MultiLangPromptCollection, PromptList
6 |
7 | log = logging.getLogger(__name__)
8 |
9 |
10 | class PromptFactoryBase:
11 | """Base class for auto-generated prompt factory classes."""
12 |
13 | def __init__(self, prompts_dir: str | list[str], lang_code: str = DEFAULT_LANG_CODE, fallback_mode=LanguageFallbackMode.EXCEPTION):
14 | """
15 | :param prompts_dir: the directory containing the prompt templates and prompt lists.
16 | If a list is provided, will look for prompt templates in the dirs from left to right
17 | (first one containing the desired template wins).
18 | :param lang_code: the language code to use for retrieving the prompt templates and prompt lists.
19 | Leave as `default` for single-language use cases.
20 | :param fallback_mode: the fallback mode to use when a prompt template or prompt list is not found for the requested language.
21 | Irrelevant for single-language use cases.
22 | """
23 | self.lang_code = lang_code
24 | self._prompt_collection = MultiLangPromptCollection(prompts_dir, fallback_mode=fallback_mode)
25 |
26 | def _render_prompt(self, prompt_name: str, params: dict[str, Any]) -> str:
27 | del params["self"]
28 | return self._prompt_collection.render_prompt_template(prompt_name, params, lang_code=self.lang_code)
29 |
30 | def _get_prompt_list(self, prompt_name: str) -> PromptList:
31 | return self._prompt_collection.get_prompt_list(prompt_name, self.lang_code)
32 |
33 |
34 | def autogenerate_prompt_factory_module(prompts_dir: str, target_module_path: str) -> None:
35 | """
36 | Auto-generates a prompt factory module for the given prompt directory.
37 | The generated `PromptFactory` class is meant to be the central entry class for retrieving and rendering prompt templates and prompt
38 | lists in your application.
39 | It will contain one method per prompt template and prompt list, and is useful for both single- and multi-language use cases.
40 |
41 | :param prompts_dir: the directory containing the prompt templates and prompt lists
42 | :param target_module_path: the path to the target module file (.py). Important: The module will be overwritten!
43 | """
44 | generated_code = """
45 | # ruff: noqa
46 | # black: skip
47 | # mypy: ignore-errors
48 |
49 | # NOTE: This module is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually!
50 |
51 | from interprompt.multilang_prompt import PromptList
52 | from interprompt.prompt_factory import PromptFactoryBase
53 | from typing import Any
54 |
55 |
56 | class PromptFactory(PromptFactoryBase):
57 | \"""
58 | A class for retrieving and rendering prompt templates and prompt lists.
59 | \"""
60 | """
61 | # ---- add methods based on prompt template names and parameters and prompt list names ----
62 | prompt_collection = MultiLangPromptCollection(prompts_dir)
63 |
64 | for template_name in prompt_collection.get_prompt_template_names():
65 | template_parameters = prompt_collection.get_prompt_template_parameters(template_name)
66 | if len(template_parameters) == 0:
67 | method_params_str = ""
68 | else:
69 | method_params_str = ", *, " + ", ".join([f"{param}: Any" for param in template_parameters])
70 | generated_code += f"""
71 | def create_{template_name}(self{method_params_str}) -> str:
72 | return self._render_prompt('{template_name}', locals())
73 | """
74 | for prompt_list_name in prompt_collection.get_prompt_list_names():
75 | generated_code += f"""
76 | def get_list_{prompt_list_name}(self) -> PromptList:
77 | return self._get_prompt_list('{prompt_list_name}')
78 | """
79 | os.makedirs(os.path.dirname(target_module_path), exist_ok=True)
80 | with open(target_module_path, "w", encoding="utf-8") as f:
81 | f.write(generated_code)
82 | log.info(f"Prompt factory generated successfully in {target_module_path}")
83 |
```
--------------------------------------------------------------------------------
/src/serena/resources/config/prompt_templates/system_prompt.yml:
--------------------------------------------------------------------------------
```yaml
1 | # The system prompt template. Note that many clients will not allow configuration of the actual system prompt,
2 | # in which case this prompt will be given as a regular message on the call of a simple tool which the agent
3 | # is encouraged (via the tool description) to call at the beginning of the conversation.
4 | prompts:
5 | system_prompt: |
6 | You are a professional coding agent.
7 | You have access to semantic coding tools upon which you rely heavily for all your work.
8 | You operate in a resource-efficient and intelligent manner, always keeping in mind to not read or generate
9 | content that is not needed for the task at hand.
10 |
11 | Some tasks may require you to understand the architecture of large parts of the codebase, while for others,
12 | it may be enough to read a small set of symbols or a single file.
13 | You avoid reading entire files unless it is absolutely necessary, instead relying on intelligent step-by-step
14 | acquisition of information. {% if 'ToolMarkerSymbolicRead' in available_markers %}Once you have read a full file, it does not make
15 | sense to analyse it with the symbolic read tools; you already have the information.{% endif %}
16 |
17 | You can achieve intelligent reading of code by using the symbolic tools for getting an overview of symbols and
18 | the relations between them, and then only reading the bodies of symbols that are necessary to complete the task at hand.
19 | You can use the standard tools like list_dir, find_file and search_for_pattern if you need to.
20 | Where appropriate, you pass the `relative_path` parameter to restrict the search to a specific file or directory.
21 | {% if 'search_for_pattern' in available_tools %}
22 | If you are unsure about a symbol's name or location{% if 'find_symbol' in available_tools %} (to the extent that substring_matching for the symbol name is not enough){% endif %}, you can use the `search_for_pattern` tool, which allows fast
23 | and flexible search for patterns in the codebase.{% if 'ToolMarkerSymbolicRead' in available_markers %} In this way, you can first find candidates for symbols or files,
24 | and then proceed with the symbolic tools.{% endif %}
25 | {% endif %}
26 |
27 | {% if 'ToolMarkerSymbolicRead' in available_markers %}
28 | Symbols are identified by their `name_path` and `relative_path` (see the description of the `find_symbol` tool).
29 | You can get information about the symbols in a file by using the `get_symbols_overview` tool or use the `find_symbol` to search.
30 | You only read the bodies of symbols when you need to (e.g. if you want to fully understand or edit it).
31 | For example, if you are working with Python code and already know that you need to read the body of the constructor of the class Foo, you can directly
32 | use `find_symbol` with name path pattern `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit,
33 | you can use `find_symbol` with name path pattern `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding
34 | to read the desired methods with `include_body=True`.
35 | You can understand relationships between symbols by using the `find_referencing_symbols` tool.
36 | {% endif %}
37 |
38 | {% if 'read_memory' in available_tools %}
39 | You generally have access to memories and it may be useful for you to read them.
40 | You infer whether memories are relevant based on their names.
41 | {% endif %}
42 |
43 | The context and modes of operation are described below. These determine how to interact with your user
44 | and which kinds of interactions are expected of you.
45 |
46 | Context description:
47 | {{ context_system_prompt }}
48 |
49 | Modes descriptions:
50 | {% for prompt in mode_system_prompts %}
51 | {{ prompt }}
52 | {% endfor %}
53 |
54 | You have hereby read the 'Serena Instructions Manual' and do not need to read it again.
55 |
```
--------------------------------------------------------------------------------
/test/resources/repos/vue/test_repo/src/stores/calculator.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defineStore } from 'pinia'
2 | import type { HistoryEntry, Operation, CalculatorState } from '@/types'
3 |
4 | export const useCalculatorStore = defineStore('calculator', {
5 | state: (): CalculatorState => ({
6 | currentValue: 0,
7 | previousValue: null,
8 | operation: null,
9 | history: [],
10 | displayValue: '0'
11 | }),
12 |
13 | getters: {
14 | /**
15 | * Get the most recent history entries (last 10)
16 | */
17 | recentHistory: (state): HistoryEntry[] => {
18 | return state.history.slice(-10).reverse()
19 | },
20 |
21 | /**
22 | * Check if calculator has any history
23 | */
24 | hasHistory: (state): boolean => {
25 | return state.history.length > 0
26 | },
27 |
28 | /**
29 | * Get the current display text
30 | */
31 | display: (state): string => {
32 | return state.displayValue
33 | }
34 | },
35 |
36 | actions: {
37 | /**
38 | * Set a number value
39 | */
40 | setNumber(value: number) {
41 | this.currentValue = value
42 | this.displayValue = value.toString()
43 | },
44 |
45 | /**
46 | * Append a digit to the current value
47 | */
48 | appendDigit(digit: number) {
49 | if (this.displayValue === '0') {
50 | this.displayValue = digit.toString()
51 | } else {
52 | this.displayValue += digit.toString()
53 | }
54 | this.currentValue = parseFloat(this.displayValue)
55 | },
56 |
57 | /**
58 | * Add two numbers
59 | */
60 | add() {
61 | if (this.previousValue !== null && this.operation) {
62 | this.executeOperation()
63 | }
64 | this.previousValue = this.currentValue
65 | this.operation = 'add'
66 | this.displayValue = '0'
67 | },
68 |
69 | /**
70 | * Subtract two numbers
71 | */
72 | subtract() {
73 | if (this.previousValue !== null && this.operation) {
74 | this.executeOperation()
75 | }
76 | this.previousValue = this.currentValue
77 | this.operation = 'subtract'
78 | this.displayValue = '0'
79 | },
80 |
81 | /**
82 | * Multiply two numbers
83 | */
84 | multiply() {
85 | if (this.previousValue !== null && this.operation) {
86 | this.executeOperation()
87 | }
88 | this.previousValue = this.currentValue
89 | this.operation = 'multiply'
90 | this.displayValue = '0'
91 | },
92 |
93 | /**
94 | * Divide two numbers
95 | */
96 | divide() {
97 | if (this.previousValue !== null && this.operation) {
98 | this.executeOperation()
99 | }
100 | this.previousValue = this.currentValue
101 | this.operation = 'divide'
102 | this.displayValue = '0'
103 | },
104 |
105 | /**
106 | * Execute the pending operation
107 | */
108 | executeOperation() {
109 | if (this.previousValue === null || this.operation === null) {
110 | return
111 | }
112 |
113 | let result = 0
114 | const prev = this.previousValue
115 | const current = this.currentValue
116 | let expression = ''
117 |
118 | switch (this.operation) {
119 | case 'add':
120 | result = prev + current
121 | expression = `${prev} + ${current}`
122 | break
123 | case 'subtract':
124 | result = prev - current
125 | expression = `${prev} - ${current}`
126 | break
127 | case 'multiply':
128 | result = prev * current
129 | expression = `${prev} × ${current}`
130 | break
131 | case 'divide':
132 | if (current === 0) {
133 | this.displayValue = 'Error'
134 | this.clear()
135 | return
136 | }
137 | result = prev / current
138 | expression = `${prev} ÷ ${current}`
139 | break
140 | }
141 |
142 | // Add to history
143 | this.history.push({
144 | expression,
145 | result,
146 | timestamp: new Date()
147 | })
148 |
149 | this.currentValue = result
150 | this.displayValue = result.toString()
151 | this.previousValue = null
152 | this.operation = null
153 | },
154 |
155 | /**
156 | * Calculate the equals operation
157 | */
158 | equals() {
159 | this.executeOperation()
160 | },
161 |
162 | /**
163 | * Clear the calculator state
164 | */
165 | clear() {
166 | this.currentValue = 0
167 | this.previousValue = null
168 | this.operation = null
169 | this.displayValue = '0'
170 | },
171 |
172 | /**
173 | * Clear all history
174 | */
175 | clearHistory() {
176 | this.history = []
177 | }
178 | }
179 | })
180 |
```
--------------------------------------------------------------------------------
/.serena/memories/serena_repository_structure.md:
--------------------------------------------------------------------------------
```markdown
1 | # Serena Repository Structure
2 |
3 | ## Overview
4 | Serena is a multi-language code assistant that combines two main components:
5 | 1. **Serena Core** - The main agent framework with tools and MCP server
6 | 2. **SolidLSP** - A unified Language Server Protocol wrapper for multiple programming languages
7 |
8 | ## Top-Level Structure
9 |
10 | ```
11 | serena/
12 | ├── src/ # Main source code
13 | │ ├── serena/ # Serena agent framework
14 | │ ├── solidlsp/ # LSP wrapper library
15 | │ └── interprompt/ # Multi-language prompt templates
16 | ├── test/ # Test suites
17 | │ ├── serena/ # Serena agent tests
18 | │ ├── solidlsp/ # Language server tests
19 | │ └── resources/repos/ # Test repositories for each language
20 | ├── scripts/ # Build and utility scripts
21 | ├── resources/ # Static resources and configurations
22 | ├── pyproject.toml # Python project configuration
23 | ├── README.md # Project documentation
24 | └── CHANGELOG.md # Version history
25 | ```
26 |
27 | ## Source Code Organization
28 |
29 | ### Serena Core (`src/serena/`)
30 | - **`agent.py`** - Main SerenaAgent class that orchestrates everything
31 | - **`tools/`** - MCP tools for file operations, symbols, memory, etc.
32 | - `file_tools.py` - File system operations (read, write, search)
33 | - `symbol_tools.py` - Symbol-based code operations (find, edit)
34 | - `memory_tools.py` - Knowledge persistence and retrieval
35 | - `config_tools.py` - Project and mode management
36 | - `workflow_tools.py` - Onboarding and meta-operations
37 | - **`config/`** - Configuration management
38 | - `serena_config.py` - Main configuration classes
39 | - `context_mode.py` - Context and mode definitions
40 | - **`util/`** - Utility modules
41 | - **`mcp.py`** - MCP server implementation
42 | - **`cli.py`** - Command-line interface
43 |
44 | ### SolidLSP (`src/solidlsp/`)
45 | - **`ls.py`** - Main SolidLanguageServer class
46 | - **`language_servers/`** - Language-specific implementations
47 | - `csharp_language_server.py` - C# (Microsoft.CodeAnalysis.LanguageServer)
48 | - `python_server.py` - Python (Pyright)
49 | - `typescript_language_server.py` - TypeScript
50 | - `rust_analyzer.py` - Rust
51 | - `gopls.py` - Go
52 | - And many more...
53 | - **`ls_config.py`** - Language server configuration
54 | - **`ls_types.py`** - LSP type definitions
55 | - **`ls_utils.py`** - Utilities for working with LSP data
56 |
57 | ### Interprompt (`src/interprompt/`)
58 | - Multi-language prompt template system
59 | - Jinja2-based templating with language fallbacks
60 |
61 | ## Test Structure
62 |
63 | ### Language Server Tests (`test/solidlsp/`)
64 | Each language has its own test directory:
65 | ```
66 | test/solidlsp/
67 | ├── csharp/
68 | │ └── test_csharp_basic.py
69 | ├── python/
70 | │ └── test_python_basic.py
71 | ├── typescript/
72 | │ └── test_typescript_basic.py
73 | └── ...
74 | ```
75 |
76 | ### Test Resources (`test/resources/repos/`)
77 | Contains minimal test projects for each language:
78 | ```
79 | test/resources/repos/
80 | ├── csharp/test_repo/
81 | │ ├── serena.sln
82 | │ ├── TestProject.csproj
83 | │ ├── Program.cs
84 | │ └── Models/Person.cs
85 | ├── python/test_repo/
86 | ├── typescript/test_repo/
87 | └── ...
88 | ```
89 |
90 | ### Test Infrastructure
91 | - **`test/conftest.py`** - Shared test fixtures and utilities
92 | - **`create_ls()`** function - Creates language server instances for testing
93 | - **`language_server` fixture** - Parametrized fixture for multi-language tests
94 |
95 | ## Key Configuration Files
96 |
97 | - **`pyproject.toml`** - Python dependencies, build config, and tool settings
98 | - **`.serena/`** directories - Project-specific Serena configuration and memories
99 | - **`CLAUDE.md`** - Instructions for AI assistants working on the project
100 |
101 | ## Dependencies Management
102 |
103 | The project uses modern Python tooling:
104 | - **uv** for fast dependency resolution and virtual environments
105 | - **pytest** for testing with language-specific markers (`@pytest.mark.csharp`)
106 | - **ruff** for linting and formatting
107 | - **mypy** for type checking
108 |
109 | ## Build and Development
110 |
111 | - **Docker support** - Full containerized development environment
112 | - **GitHub Actions** - CI/CD with language server testing
113 | - **Development scripts** in `scripts/` directory
```
--------------------------------------------------------------------------------
/src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "RoslynExtensionsOptions": {
3 | "EnableDecompilationSupport": false,
4 | "EnableAnalyzersSupport": true,
5 | "EnableImportCompletion": true,
6 | "EnableAsyncCompletion": false,
7 | "DocumentAnalysisTimeoutMs": 30000,
8 | "DiagnosticWorkersThreadCount": 18,
9 | "AnalyzeOpenDocumentsOnly": true,
10 | "InlayHintsOptions": {
11 | "EnableForParameters": false,
12 | "ForLiteralParameters": false,
13 | "ForIndexerParameters": false,
14 | "ForObjectCreationParameters": false,
15 | "ForOtherParameters": false,
16 | "SuppressForParametersThatDifferOnlyBySuffix": false,
17 | "SuppressForParametersThatMatchMethodIntent": false,
18 | "SuppressForParametersThatMatchArgumentName": false,
19 | "EnableForTypes": false,
20 | "ForImplicitVariableTypes": false,
21 | "ForLambdaParameterTypes": false,
22 | "ForImplicitObjectCreation": false
23 | },
24 | "LocationPaths": null
25 | },
26 | "FormattingOptions": {
27 | "OrganizeImports": false,
28 | "EnableEditorConfigSupport": true,
29 | "NewLine": "\n",
30 | "UseTabs": false,
31 | "TabSize": 4,
32 | "IndentationSize": 4,
33 | "SpacingAfterMethodDeclarationName": false,
34 | "SeparateImportDirectiveGroups": false,
35 | "SpaceWithinMethodDeclarationParenthesis": false,
36 | "SpaceBetweenEmptyMethodDeclarationParentheses": false,
37 | "SpaceAfterMethodCallName": false,
38 | "SpaceWithinMethodCallParentheses": false,
39 | "SpaceBetweenEmptyMethodCallParentheses": false,
40 | "SpaceAfterControlFlowStatementKeyword": true,
41 | "SpaceWithinExpressionParentheses": false,
42 | "SpaceWithinCastParentheses": false,
43 | "SpaceWithinOtherParentheses": false,
44 | "SpaceAfterCast": false,
45 | "SpaceBeforeOpenSquareBracket": false,
46 | "SpaceBetweenEmptySquareBrackets": false,
47 | "SpaceWithinSquareBrackets": false,
48 | "SpaceAfterColonInBaseTypeDeclaration": true,
49 | "SpaceAfterComma": true,
50 | "SpaceAfterDot": false,
51 | "SpaceAfterSemicolonsInForStatement": true,
52 | "SpaceBeforeColonInBaseTypeDeclaration": true,
53 | "SpaceBeforeComma": false,
54 | "SpaceBeforeDot": false,
55 | "SpaceBeforeSemicolonsInForStatement": false,
56 | "SpacingAroundBinaryOperator": "single",
57 | "IndentBraces": false,
58 | "IndentBlock": true,
59 | "IndentSwitchSection": true,
60 | "IndentSwitchCaseSection": true,
61 | "IndentSwitchCaseSectionWhenBlock": true,
62 | "LabelPositioning": "oneLess",
63 | "WrappingPreserveSingleLine": true,
64 | "WrappingKeepStatementsOnSingleLine": true,
65 | "NewLinesForBracesInTypes": true,
66 | "NewLinesForBracesInMethods": true,
67 | "NewLinesForBracesInProperties": true,
68 | "NewLinesForBracesInAccessors": true,
69 | "NewLinesForBracesInAnonymousMethods": true,
70 | "NewLinesForBracesInControlBlocks": true,
71 | "NewLinesForBracesInAnonymousTypes": true,
72 | "NewLinesForBracesInObjectCollectionArrayInitializers": true,
73 | "NewLinesForBracesInLambdaExpressionBody": true,
74 | "NewLineForElse": true,
75 | "NewLineForCatch": true,
76 | "NewLineForFinally": true,
77 | "NewLineForMembersInObjectInit": true,
78 | "NewLineForMembersInAnonymousTypes": true,
79 | "NewLineForClausesInQuery": true
80 | },
81 | "FileOptions": {
82 | "SystemExcludeSearchPatterns": [
83 | "**/node_modules/**/*",
84 | "**/bin/**/*",
85 | "**/obj/**/*",
86 | "**/.git/**/*",
87 | "**/.git",
88 | "**/.svn",
89 | "**/.hg",
90 | "**/CVS",
91 | "**/.DS_Store",
92 | "**/Thumbs.db"
93 | ],
94 | "ExcludeSearchPatterns": []
95 | },
96 | "RenameOptions": {
97 | "RenameOverloads": false,
98 | "RenameInStrings": false,
99 | "RenameInComments": false
100 | },
101 | "ImplementTypeOptions": {
102 | "InsertionBehavior": 0,
103 | "PropertyGenerationBehavior": 0
104 | },
105 | "DotNetCliOptions": {
106 | "LocationPaths": null
107 | },
108 | "Plugins": {
109 | "LocationPaths": null
110 | }
111 | }
```
--------------------------------------------------------------------------------
/test/resources/repos/powershell/test_repo/utils.ps1:
--------------------------------------------------------------------------------
```
1 | # Utility functions for PowerShell operations
2 |
3 | <#
4 | .SYNOPSIS
5 | Converts a string to uppercase.
6 | .PARAMETER InputString
7 | The string to convert.
8 | .OUTPUTS
9 | System.String - The uppercase string.
10 | #>
11 | function Convert-ToUpperCase {
12 | [CmdletBinding()]
13 | [OutputType([string])]
14 | param(
15 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
16 | [string]$InputString
17 | )
18 |
19 | return $InputString.ToUpper()
20 | }
21 |
22 | <#
23 | .SYNOPSIS
24 | Converts a string to lowercase.
25 | .PARAMETER InputString
26 | The string to convert.
27 | .OUTPUTS
28 | System.String - The lowercase string.
29 | #>
30 | function Convert-ToLowerCase {
31 | [CmdletBinding()]
32 | [OutputType([string])]
33 | param(
34 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
35 | [string]$InputString
36 | )
37 |
38 | return $InputString.ToLower()
39 | }
40 |
41 | <#
42 | .SYNOPSIS
43 | Removes leading and trailing whitespace from a string.
44 | .PARAMETER InputString
45 | The string to trim.
46 | .OUTPUTS
47 | System.String - The trimmed string.
48 | #>
49 | function Remove-Whitespace {
50 | [CmdletBinding()]
51 | [OutputType([string])]
52 | param(
53 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
54 | [string]$InputString
55 | )
56 |
57 | return $InputString.Trim()
58 | }
59 |
60 | <#
61 | .SYNOPSIS
62 | Creates a backup of a file.
63 | .PARAMETER FilePath
64 | The path to the file to backup.
65 | .PARAMETER BackupDirectory
66 | The directory where the backup will be created.
67 | .OUTPUTS
68 | System.String - The path to the backup file.
69 | #>
70 | function Backup-File {
71 | [CmdletBinding()]
72 | [OutputType([string])]
73 | param(
74 | [Parameter(Mandatory = $true)]
75 | [string]$FilePath,
76 |
77 | [Parameter(Mandatory = $false)]
78 | [string]$BackupDirectory = "."
79 | )
80 |
81 | if (-not (Test-Path $FilePath)) {
82 | throw "File not found: $FilePath"
83 | }
84 |
85 | $fileName = Split-Path $FilePath -Leaf
86 | $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
87 | $backupName = "$fileName.$timestamp.bak"
88 | $backupPath = Join-Path $BackupDirectory $backupName
89 |
90 | Copy-Item -Path $FilePath -Destination $backupPath
91 | return $backupPath
92 | }
93 |
94 | <#
95 | .SYNOPSIS
96 | Checks if an array contains a specific element.
97 | .PARAMETER Array
98 | The array to search.
99 | .PARAMETER Element
100 | The element to find.
101 | .OUTPUTS
102 | System.Boolean - True if the element is found, false otherwise.
103 | #>
104 | function Test-ArrayContains {
105 | [CmdletBinding()]
106 | [OutputType([bool])]
107 | param(
108 | [Parameter(Mandatory = $true)]
109 | [array]$Array,
110 |
111 | [Parameter(Mandatory = $true)]
112 | $Element
113 | )
114 |
115 | return $Array -contains $Element
116 | }
117 |
118 | <#
119 | .SYNOPSIS
120 | Writes a log message with timestamp.
121 | .PARAMETER Message
122 | The message to log.
123 | .PARAMETER Level
124 | The log level (Info, Warning, Error).
125 | #>
126 | function Write-LogMessage {
127 | [CmdletBinding()]
128 | param(
129 | [Parameter(Mandatory = $true)]
130 | [string]$Message,
131 |
132 | [Parameter(Mandatory = $false)]
133 | [ValidateSet("Info", "Warning", "Error")]
134 | [string]$Level = "Info"
135 | )
136 |
137 | $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
138 | $logEntry = "[$timestamp] [$Level] $Message"
139 |
140 | switch ($Level) {
141 | "Info" { Write-Host $logEntry -ForegroundColor White }
142 | "Warning" { Write-Host $logEntry -ForegroundColor Yellow }
143 | "Error" { Write-Host $logEntry -ForegroundColor Red }
144 | }
145 | }
146 |
147 | <#
148 | .SYNOPSIS
149 | Validates if a string is a valid email address.
150 | .PARAMETER Email
151 | The email address to validate.
152 | .OUTPUTS
153 | System.Boolean - True if the email is valid, false otherwise.
154 | #>
155 | function Test-ValidEmail {
156 | [CmdletBinding()]
157 | [OutputType([bool])]
158 | param(
159 | [Parameter(Mandatory = $true)]
160 | [string]$Email
161 | )
162 |
163 | $emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
164 | return $Email -match $emailRegex
165 | }
166 |
167 | <#
168 | .SYNOPSIS
169 | Checks if a string is a valid number.
170 | .PARAMETER Value
171 | The string to check.
172 | .OUTPUTS
173 | System.Boolean - True if the string is a valid number, false otherwise.
174 | #>
175 | function Test-IsNumber {
176 | [CmdletBinding()]
177 | [OutputType([bool])]
178 | param(
179 | [Parameter(Mandatory = $true)]
180 | [string]$Value
181 | )
182 |
183 | $number = 0
184 | return [double]::TryParse($Value, [ref]$number)
185 | }
186 |
```
--------------------------------------------------------------------------------
/test/resources/repos/vue/test_repo/src/components/CalculatorButton.vue:
--------------------------------------------------------------------------------
```vue
1 | <script setup lang="ts">
2 | import { computed, ref } from 'vue'
3 |
4 | /**
5 | * Props interface for CalculatorButton.
6 | * Demonstrates: defineProps with TypeScript interface
7 | */
8 | interface Props {
9 | label: string | number
10 | variant?: 'digit' | 'operation' | 'equals' | 'clear'
11 | disabled?: boolean
12 | active?: boolean
13 | size?: 'small' | 'medium' | 'large'
14 | }
15 |
16 | /**
17 | * Emits interface for CalculatorButton.
18 | * Demonstrates: defineEmits with TypeScript
19 | */
20 | interface Emits {
21 | click: [value: string | number]
22 | hover: [isHovering: boolean]
23 | focus: []
24 | blur: []
25 | }
26 |
27 | // Define props with defaults
28 | const props = withDefaults(defineProps<Props>(), {
29 | variant: 'digit',
30 | disabled: false,
31 | active: false,
32 | size: 'medium'
33 | })
34 |
35 | // Define emits
36 | const emit = defineEmits<Emits>()
37 |
38 | // Local state
39 | const isHovered = ref(false)
40 | const isFocused = ref(false)
41 | const pressCount = ref(0)
42 |
43 | // Computed classes based on props and state
44 | const buttonClass = computed(() => {
45 | const classes = ['calc-button', `calc-button--${props.variant}`, `calc-button--${props.size}`]
46 |
47 | if (props.active) classes.push('calc-button--active')
48 | if (props.disabled) classes.push('calc-button--disabled')
49 | if (isHovered.value) classes.push('calc-button--hovered')
50 | if (isFocused.value) classes.push('calc-button--focused')
51 |
52 | return classes.join(' ')
53 | })
54 |
55 | // Computed aria label for accessibility
56 | const ariaLabel = computed(() => {
57 | const variantText = {
58 | digit: 'Number',
59 | operation: 'Operation',
60 | equals: 'Equals',
61 | clear: 'Clear'
62 | }[props.variant]
63 |
64 | return `${variantText}: ${props.label}`
65 | })
66 |
67 | // Event handlers that emit events
68 | const handleClick = () => {
69 | if (!props.disabled) {
70 | pressCount.value++
71 | emit('click', props.label)
72 | }
73 | }
74 |
75 | const handleMouseEnter = () => {
76 | isHovered.value = true
77 | emit('hover', true)
78 | }
79 |
80 | const handleMouseLeave = () => {
81 | isHovered.value = false
82 | emit('hover', false)
83 | }
84 |
85 | const handleFocus = () => {
86 | isFocused.value = true
87 | emit('focus')
88 | }
89 |
90 | const handleBlur = () => {
91 | isFocused.value = false
92 | emit('blur')
93 | }
94 |
95 | // Expose internal state for parent access via template refs
96 | // Demonstrates: defineExpose
97 | defineExpose({
98 | pressCount,
99 | isHovered,
100 | isFocused,
101 | simulateClick: handleClick
102 | })
103 | </script>
104 |
105 | <template>
106 | <button
107 | :class="buttonClass"
108 | :disabled="disabled"
109 | :aria-label="ariaLabel"
110 | @click="handleClick"
111 | @mouseenter="handleMouseEnter"
112 | @mouseleave="handleMouseLeave"
113 | @focus="handleFocus"
114 | @blur="handleBlur"
115 | >
116 | <span class="calc-button__label">{{ label }}</span>
117 | <span v-if="pressCount > 0" class="calc-button__badge">{{ pressCount }}</span>
118 | </button>
119 | </template>
120 |
121 | <style scoped>
122 | .calc-button {
123 | position: relative;
124 | padding: 1rem;
125 | font-size: 1.2rem;
126 | border: none;
127 | border-radius: 4px;
128 | cursor: pointer;
129 | transition: all 0.2s;
130 | font-weight: 500;
131 | }
132 |
133 | .calc-button--small {
134 | padding: 0.5rem;
135 | font-size: 1rem;
136 | }
137 |
138 | .calc-button--medium {
139 | padding: 1rem;
140 | font-size: 1.2rem;
141 | }
142 |
143 | .calc-button--large {
144 | padding: 1.5rem;
145 | font-size: 1.5rem;
146 | }
147 |
148 | .calc-button--digit {
149 | background: white;
150 | color: #333;
151 | }
152 |
153 | .calc-button--digit:hover:not(:disabled) {
154 | background: #e0e0e0;
155 | }
156 |
157 | .calc-button--operation {
158 | background: #2196f3;
159 | color: white;
160 | }
161 |
162 | .calc-button--operation:hover:not(:disabled) {
163 | background: #1976d2;
164 | }
165 |
166 | .calc-button--operation.calc-button--active {
167 | background: #1565c0;
168 | }
169 |
170 | .calc-button--equals {
171 | background: #4caf50;
172 | color: white;
173 | }
174 |
175 | .calc-button--equals:hover:not(:disabled) {
176 | background: #45a049;
177 | }
178 |
179 | .calc-button--clear {
180 | background: #f44336;
181 | color: white;
182 | }
183 |
184 | .calc-button--clear:hover:not(:disabled) {
185 | background: #da190b;
186 | }
187 |
188 | .calc-button--disabled {
189 | opacity: 0.5;
190 | cursor: not-allowed;
191 | }
192 |
193 | .calc-button--hovered {
194 | transform: translateY(-2px);
195 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
196 | }
197 |
198 | .calc-button--focused {
199 | outline: 2px solid #2196f3;
200 | outline-offset: 2px;
201 | }
202 |
203 | .calc-button__label {
204 | display: block;
205 | }
206 |
207 | .calc-button__badge {
208 | position: absolute;
209 | top: -5px;
210 | right: -5px;
211 | background: #ff5722;
212 | color: white;
213 | border-radius: 50%;
214 | width: 20px;
215 | height: 20px;
216 | font-size: 0.7rem;
217 | display: flex;
218 | align-items: center;
219 | justify-content: center;
220 | }
221 | </style>
222 |
```
--------------------------------------------------------------------------------
/test/solidlsp/perl/test_perl_basic.py:
--------------------------------------------------------------------------------
```python
1 | import platform
2 | from pathlib import Path
3 |
4 | import pytest
5 |
6 | from solidlsp import SolidLanguageServer
7 | from solidlsp.ls_config import Language
8 |
9 |
10 | @pytest.mark.perl
11 | @pytest.mark.skipif(platform.system() == "Windows", reason="Perl::LanguageServer does not support native Windows operation")
12 | class TestPerlLanguageServer:
13 | """
14 | Tests for Perl::LanguageServer integration.
15 |
16 | Perl::LanguageServer provides comprehensive LSP support for Perl including:
17 | - Document symbols (functions, variables)
18 | - Go to definition (including cross-file)
19 | - Find references (including cross-file) - this was not available in PLS
20 | """
21 |
22 | @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True)
23 | @pytest.mark.parametrize("repo_path", [Language.PERL], indirect=True)
24 | def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
25 | """Test that the language server starts and stops successfully."""
26 | # The fixture already handles start and stop
27 | assert language_server.is_running()
28 | assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
29 |
30 | @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True)
31 | def test_document_symbols(self, language_server: SolidLanguageServer) -> None:
32 | """Test that document symbols are correctly identified."""
33 | # Request document symbols
34 | all_symbols, _ = language_server.request_document_symbols("main.pl").get_all_symbols_and_roots()
35 |
36 | assert all_symbols, "Expected to find symbols in main.pl"
37 | assert len(all_symbols) > 0, "Expected at least one symbol"
38 |
39 | # DEBUG: Print all symbols
40 | print("\n=== All symbols in main.pl ===")
41 | for s in all_symbols:
42 | line = s.get("range", {}).get("start", {}).get("line", "?")
43 | print(f"Line {line}: {s.get('name')} (kind={s.get('kind')})")
44 |
45 | # Check that we can find function symbols
46 | function_symbols = [s for s in all_symbols if s.get("kind") == 12] # 12 = Function/Method
47 | assert len(function_symbols) >= 2, f"Expected at least 2 functions (greet, use_helper_function), found {len(function_symbols)}"
48 |
49 | function_names = [s.get("name") for s in function_symbols]
50 | assert "greet" in function_names, f"Expected 'greet' function in symbols, found: {function_names}"
51 | assert "use_helper_function" in function_names, f"Expected 'use_helper_function' in symbols, found: {function_names}"
52 |
53 | # @pytest.mark.skip(reason="Perl::LanguageServer cross-file definition tracking needs configuration")
54 | @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True)
55 | def test_find_definition_across_files(self, language_server: SolidLanguageServer) -> None:
56 | definition_location_list = language_server.request_definition("main.pl", 17, 0)
57 |
58 | assert len(definition_location_list) == 1
59 | definition_location = definition_location_list[0]
60 | print(f"Found definition: {definition_location}")
61 | assert definition_location["uri"].endswith("helper.pl")
62 | assert definition_location["range"]["start"]["line"] == 4 # add method on line 2 (0-indexed 1)
63 |
64 | @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True)
65 | def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
66 | """Test finding references to a function across multiple files."""
67 | reference_locations = language_server.request_references("helper.pl", 4, 5)
68 |
69 | assert len(reference_locations) >= 2, f"Expected at least 2 references to helper_function, found {len(reference_locations)}"
70 |
71 | main_pl_refs = [ref for ref in reference_locations if ref["uri"].endswith("main.pl")]
72 | assert len(main_pl_refs) >= 2, f"Expected at least 2 references in main.pl, found {len(main_pl_refs)}"
73 |
74 | main_pl_lines = sorted([ref["range"]["start"]["line"] for ref in main_pl_refs])
75 | assert 17 in main_pl_lines, f"Expected reference at line 18 (0-indexed 17), found: {main_pl_lines}"
76 | assert 20 in main_pl_lines, f"Expected reference at line 21 (0-indexed 20), found: {main_pl_lines}"
77 |
```
--------------------------------------------------------------------------------
/.serena/project.yml:
--------------------------------------------------------------------------------
```yaml
1 | ignore_all_files_in_gitignore: true
2 |
3 | # list of additional paths to ignore
4 | # same syntax as gitignore, so you can use * and **
5 | # Was previously called `ignored_dirs`, please update your config if you are using that.
6 | # Added (renamed)on 2025-04-07
7 | ignored_paths: []
8 |
9 | # whether the project is in read-only mode
10 | # If set to true, all editing tools will be disabled and attempts to use them will result in an error
11 | # Added on 2025-04-18
12 | read_only: false
13 |
14 |
15 | # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
16 | # Below is the complete list of tools for convenience.
17 | # To make sure you have the latest list of tools, and to view their descriptions,
18 | # execute `uv run scripts/print_tool_overview.py`.
19 | #
20 | # * `activate_project`: Activates a project by name.
21 | # * `check_onboarding_performed`: Checks whether project onboarding was already performed.
22 | # * `create_text_file`: Creates/overwrites a file in the project directory.
23 | # * `delete_lines`: Deletes a range of lines within a file.
24 | # * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
25 | # * `execute_shell_command`: Executes a shell command.
26 | # * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
27 | # * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
28 | # * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
29 | # * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
30 | # * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
31 | # * `initial_instructions`: Gets the initial instructions for the current project.
32 | # Should only be used in settings where the system prompt cannot be set,
33 | # e.g. in clients you have no control over, like Claude Desktop.
34 | # * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
35 | # * `insert_at_line`: Inserts content at a given line in a file.
36 | # * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
37 | # * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
38 | # * `list_memories`: Lists memories in Serena's project-specific memory store.
39 | # * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
40 | # * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
41 | # * `read_file`: Reads a file within the project directory.
42 | # * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
43 | # * `remove_project`: Removes a project from the Serena configuration.
44 | # * `replace_lines`: Replaces a range of lines within a file with new content.
45 | # * `replace_symbol_body`: Replaces the full definition of a symbol.
46 | # * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
47 | # * `search_for_pattern`: Performs a search for a pattern in the project.
48 | # * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
49 | # * `switch_modes`: Activates modes by providing a list of their names
50 | # * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
51 | # * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
52 | # * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
53 | # * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
54 | excluded_tools: []
55 |
56 | # initial prompt for the project. It will always be given to the LLM upon activating the project
57 | # (contrary to the memories, which are loaded on demand).
58 | initial_prompt: ""
59 |
60 | project_name: "serena"
61 | languages:
62 | - python
63 | - typescript
64 | included_optional_tools: []
65 | encoding: utf-8
66 |
```
--------------------------------------------------------------------------------
/src/solidlsp/util/zip.py:
--------------------------------------------------------------------------------
```python
1 | import fnmatch
2 | import logging
3 | import os
4 | import sys
5 | import zipfile
6 | from pathlib import Path
7 | from typing import Optional
8 |
9 | log = logging.getLogger(__name__)
10 |
11 |
12 | class SafeZipExtractor:
13 | """
14 | A utility class for extracting ZIP archives safely.
15 |
16 | Features:
17 | - Handles long file paths on Windows
18 | - Skips files that fail to extract, continuing with the rest
19 | - Creates necessary directories automatically
20 | - Optional include/exclude pattern filters
21 | """
22 |
23 | def __init__(
24 | self,
25 | archive_path: Path,
26 | extract_dir: Path,
27 | verbose: bool = True,
28 | include_patterns: Optional[list[str]] = None,
29 | exclude_patterns: Optional[list[str]] = None,
30 | ) -> None:
31 | """
32 | Initialize the SafeZipExtractor.
33 |
34 | :param archive_path: Path to the ZIP archive file
35 | :param extract_dir: Directory where files will be extracted
36 | :param verbose: Whether to log status messages
37 | :param include_patterns: List of glob patterns for files to extract (None = all files)
38 | :param exclude_patterns: List of glob patterns for files to skip
39 | """
40 | self.archive_path = Path(archive_path)
41 | self.extract_dir = Path(extract_dir)
42 | self.verbose = verbose
43 | self.include_patterns = include_patterns or []
44 | self.exclude_patterns = exclude_patterns or []
45 |
46 | def extract_all(self) -> None:
47 | """
48 | Extract all files from the archive, skipping any that fail.
49 | """
50 | if not self.archive_path.exists():
51 | raise FileNotFoundError(f"Archive not found: {self.archive_path}")
52 |
53 | if self.verbose:
54 | log.info(f"Extracting from: {self.archive_path} to {self.extract_dir}")
55 |
56 | with zipfile.ZipFile(self.archive_path, "r") as zip_ref:
57 | for member in zip_ref.infolist():
58 | if self._should_extract(member.filename):
59 | self._extract_member(zip_ref, member)
60 | elif self.verbose:
61 | log.info(f"Skipped: {member.filename}")
62 |
63 | def _should_extract(self, filename: str) -> bool:
64 | """
65 | Determine whether a file should be extracted based on include/exclude patterns.
66 |
67 | :param filename: The file name from the archive
68 | :return: True if the file should be extracted
69 | """
70 | # If include_patterns is set, only extract if it matches at least one pattern
71 | if self.include_patterns:
72 | if not any(fnmatch.fnmatch(filename, pattern) for pattern in self.include_patterns):
73 | return False
74 |
75 | # If exclude_patterns is set, skip if it matches any pattern
76 | if self.exclude_patterns:
77 | if any(fnmatch.fnmatch(filename, pattern) for pattern in self.exclude_patterns):
78 | return False
79 |
80 | return True
81 |
82 | def _extract_member(self, zip_ref: zipfile.ZipFile, member: zipfile.ZipInfo) -> None:
83 | """
84 | Extract a single member from the archive with error handling.
85 |
86 | :param zip_ref: Open ZipFile object
87 | :param member: ZipInfo object representing the file
88 | """
89 | try:
90 | target_path = self.extract_dir / member.filename
91 |
92 | # Ensure directory structure exists
93 | target_path.parent.mkdir(parents=True, exist_ok=True)
94 |
95 | # Handle long paths on Windows
96 | final_path = self._normalize_path(target_path)
97 |
98 | # Extract file
99 | with zip_ref.open(member) as source, open(final_path, "wb") as target:
100 | target.write(source.read())
101 |
102 | if self.verbose:
103 | log.info(f"Extracted: {member.filename}")
104 |
105 | except Exception as e:
106 | log.error(f"Failed to extract {member.filename}: {e}")
107 |
108 | @staticmethod
109 | def _normalize_path(path: Path) -> Path:
110 | """
111 | Adjust path to handle long paths on Windows.
112 |
113 | :param path: Original path
114 | :return: Normalized path
115 | """
116 | if sys.platform.startswith("win"):
117 | return Path(rf"\\?\{os.path.abspath(path)}")
118 | return path # type: ignore
119 |
120 |
121 | # Example usage:
122 | # extractor = SafeZipExtractor(
123 | # archive_path=Path("file.nupkg"),
124 | # extract_dir=Path("extract_dir"),
125 | # include_patterns=["*.dll", "*.xml"],
126 | # exclude_patterns=["*.pdb"]
127 | # )
128 | # extractor.extract_all()
129 |
```
--------------------------------------------------------------------------------
/src/serena/resources/serena_config.template.yml:
--------------------------------------------------------------------------------
```yaml
1 | language_backend: LSP
2 | # the language backend to use for code understanding and manipulation.
3 | # Possible values are:
4 | # * LSP: Use the language server protocol (LSP), spawning freely available language servers
5 | # via the SolidLSP library that is part of Serena.
6 | # * JetBrains: Use the Serena plugin in your JetBrains IDE.
7 | # (requires the plugin to be installed and the project being worked on to be open
8 | # in your IDE).
9 |
10 | gui_log_window: False
11 | # whether to open a graphical window with Serena's logs.
12 | # This is mainly supported on Windows and (partly) on Linux; not available on macOS.
13 | # If you prefer a browser-based tool, use the `web_dashboard` option instead.
14 | # Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html
15 | #
16 | # Being able to inspect logs is useful both for troubleshooting and for monitoring the tool calls,
17 | # especially when using the agno playground, since the tool calls are not always shown,
18 | # and the input params are never shown in the agno UI.
19 | # When used as MCP server for Claude Desktop, the logs are primarily for troubleshooting.
20 | # Note: unfortunately, the various entities starting the Serena server or agent do so in
21 | # mysterious ways, often starting multiple instances of the process without shutting down
22 | # previous instances. This can lead to multiple log windows being opened, and only the last
23 | # window being updated. Since we can't control how agno or Claude Desktop start Serena,
24 | # we have to live with this limitation for now.
25 |
26 | web_dashboard: True
27 | # whether to open the Serena web dashboard (which will be accessible through your web browser) that
28 | # provides access to Serena's configuration and state as well as the current session logs.
29 | # The web dashboard is supported on all platforms.
30 | # Further information: https://oraios.github.io/serena/02-usage/060_dashboard.html
31 |
32 | web_dashboard_listen_address: 127.0.0.1
33 | # The address where the web dashboard will listen on
34 |
35 | web_dashboard_open_on_launch: True
36 | # whether to open a browser window with the web dashboard when Serena starts (provided that web_dashboard
37 | # is enabled).
38 | # If set to false, you can still open the dashboard manually by
39 | # a) telling the LLM to "open the dashboard" (provided that the open_dashboard tool is enabled) or by
40 | # b) manually navigating to http://localhost:24282/dashboard/ in your web browser (actual port
41 | # may be higher if you have multiple instances running; try ports 24283, 24284, etc.)
42 | # See also: https://oraios.github.io/serena/02-usage/060_dashboard.html
43 |
44 | log_level: 20
45 | # the minimum log level for the GUI log window and the dashboard (10 = debug, 20 = info, 30 = warning, 40 = error)
46 |
47 | trace_lsp_communication: False
48 | # whether to trace the communication between Serena and the language servers.
49 | # This is useful for debugging language server issues.
50 |
51 | ls_specific_settings: {}
52 | # Added on 23.08.2025
53 | # Advanced configuration option allowing to configure language server implementation specific options. Maps the language
54 | # (same entry as in project.yml) to the options.
55 | # Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
56 | # No documentation on options means no options are available.
57 |
58 | tool_timeout: 240
59 | # timeout, in seconds, after which tool executions are terminated
60 |
61 | excluded_tools: []
62 | # list of tools to be globally excluded
63 |
64 | included_optional_tools: []
65 | # list of optional tools (which are disabled by default) to be included
66 |
67 | default_max_tool_answer_chars: 150000
68 | # Used as default for tools where the apply method has a default maximal answer length.
69 | # Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default
70 | # through the global configuration.
71 |
72 | token_count_estimator: CHAR_COUNT
73 | # the name of the token count estimator to use for tool usage statistics.
74 | # See the `RegisteredTokenCountEstimator` enum for available options.
75 | #
76 | # By default, a very naive character count estimator is used, which simply counts the number of characters.
77 | # You can configure this to TIKTOKEN_GPT4 to use a local tiktoken-based estimator for GPT-4 (will download tiktoken
78 | # data files on first run), or ANTHROPIC_CLAUDE_SONNET_4 which will use the (free of cost) Anthropic API to
79 | # estimate the token count using the Claude Sonnet 4 tokenizer.
80 |
81 | # The list of registered project paths (updated automatically).
82 | projects: []
83 |
```
--------------------------------------------------------------------------------
/lessons_learned.md:
--------------------------------------------------------------------------------
```markdown
1 | # Lessons Learned
2 |
3 | In this document we briefly collect what we have learned while developing and using Serena,
4 | what works well and what doesn't.
5 |
6 | ## What Worked
7 |
8 | ### Separate Tool Logic From MCP Implementation
9 |
10 | MCP is just another protocol, one should let the details of it creep into the application logic.
11 | The official docs suggest using function annotations to define tools and prompts. While that may be
12 | useful for small projects to get going fast, it is not wise for more serious projects. In Serena,
13 | all tools are defined independently and then converted to instances of `MCPTool` using our `make_tool`
14 | function.
15 |
16 | ### Autogenerated PromptFactory
17 |
18 | Prompt templates are central for most LLM applications, so one needs good representations of them in the code,
19 | while at the same time they often need to be customizable and exposed to users. In Serena we address these conflicting
20 | needs by defining prompt templates (in jinja format) in separate yamls that users can easily modify and by autogenerated
21 | a `PromptFactory` class with meaningful method and parameter names from these yamls. The latter is committed to our code.
22 | We separated out the generation logic into the [interprompt](/src/interprompt/README.md) subpackage that can be used as a library.
23 |
24 | ### Tempfiles and Snapshots for Testing of Editing Tools
25 |
26 | We test most aspects of Serena by having a small "project" for each supported language in `tests/resources`.
27 | For the editing tools, which would change the code in these projects, we use tempfiles to copy over the code.
28 | The pretty awesome [syrupy](https://github.com/syrupy-project/syrupy) pytest plugin helped in developing
29 | snapshot tests.
30 |
31 | ### Dashboard and GUI for Logging
32 |
33 | It is very useful to know what the MCP Server is doing. We collect and display logs in a GUI or a web dashboard,
34 | which helps a lot in seeing what's going on and in identifying any issues.
35 |
36 | ### Unrestricted Bash Tool
37 |
38 | We know it's not particularly safe to permit unlimited shell commands outside a sandbox, but we did quite some
39 | evaluations and so far... nothing bad has happened. Seems like the current versions of the AI overlords rarely want to execute `sudo rm - rf /`.
40 | Still, we are working on a safer approach as well as better integration with sandboxing.
41 |
42 | ### Multilspy
43 |
44 | The [multilspy](https://github.com/microsoft/multilspy/) project helped us a lot in getting started and stands at the core of Serena.
45 | Many more well known python implementations of language servers were subpar in code quality and design (for example, missing types).
46 |
47 | ### Developing Serena with Serena
48 |
49 | We clearly notice that the better the tool gets, the easier it is to make it even better
50 |
51 | ## Prompting
52 |
53 | ### Shouting and Emotive Language May Be Needed
54 |
55 | When developing the `ReplaceRegexTool` we were initially not able to make Claude 4 (in Claude Desktop) use wildcards to save on output tokens. Neither
56 | examples nor explicit instructions helped. It was only after adding
57 |
58 | ```
59 | IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD!
60 | ```
61 |
62 | to the initial instructions and to the tool description that Claude finally started following the instructions.
63 |
64 | ## What Didn't Work
65 |
66 | ### Lifespan Handling by MCP Clients
67 |
68 | The MCP technology is clearly very green. Even though there is a lifespan context in the MCP SDK,
69 | many clients, including Claude Desktop, fail to properly clean up, leaving zombie processes behind.
70 | We mitigate this through the GUI window and the dashboard, so the user sees whether Serena is running
71 | and can terminate it there.
72 |
73 | ### Trusting Asyncio
74 |
75 | Running multiple asyncio apps led to non-deterministic
76 | event loop contamination and deadlocks, which were very hard to debug
77 | and understand. We solved this with a large hammer, by putting all asyncio apps into a separate
78 | process. It made the code much more complex and slightly enhanced RAM requirements, but it seems
79 | like that was the only way to reliably overcome asyncio deadlock issues.
80 |
81 | ### Cross-OS Tkinter GUI
82 |
83 | Different OS have different limitations when it comes to starting a window or dealing with Tkinter
84 | installations. This was so messy to get right that we pivoted to a web-dashboard instead
85 |
86 | ### Editing Based on Line Numbers
87 |
88 | Not only are LLMs notoriously bad in counting, but also the line numbers change after edit operations,
89 | and LLMs are also often too dumb to understand that they should update the line numbers information they had
90 | received before. We pivoted to string-matching and symbol-name based editing.
```
--------------------------------------------------------------------------------
/test/resources/repos/vue/test_repo/src/components/CalculatorDisplay.vue:
--------------------------------------------------------------------------------
```vue
1 | <script setup lang="ts">
2 | import { computed, ref } from 'vue'
3 | import { storeToRefs } from 'pinia'
4 | import { useCalculatorStore } from '@/stores/calculator'
5 | import type { HistoryEntry } from '@/types'
6 |
7 | // Get the calculator store
8 | const store = useCalculatorStore()
9 |
10 | // Use storeToRefs to get reactive references to store state
11 | const { recentHistory, hasHistory, currentValue, operation } = storeToRefs(store)
12 |
13 | // Local ref for display options
14 | const showFullHistory = ref(false)
15 | const maxHistoryItems = ref(5)
16 |
17 | // Computed property for filtered history
18 | const displayedHistory = computed((): HistoryEntry[] => {
19 | if (showFullHistory.value) {
20 | return recentHistory.value
21 | }
22 | return recentHistory.value.slice(0, maxHistoryItems.value)
23 | })
24 |
25 | // Format date for display
26 | const formatDate = (date: Date): string => {
27 | return new Date(date).toLocaleTimeString()
28 | }
29 |
30 | // Format the current calculation status
31 | const currentCalculation = computed((): string => {
32 | if (operation.value && store.previousValue !== null) {
33 | const opSymbol = {
34 | add: '+',
35 | subtract: '-',
36 | multiply: '×',
37 | divide: '÷'
38 | }[operation.value]
39 | return `${store.previousValue} ${opSymbol} ${currentValue.value}`
40 | }
41 | return currentValue.value.toString()
42 | })
43 |
44 | // Toggle history view
45 | const toggleHistoryView = () => {
46 | showFullHistory.value = !showFullHistory.value
47 | }
48 |
49 | // Clear history handler
50 | const clearHistory = () => {
51 | store.clearHistory()
52 | }
53 |
54 | // Check if history is empty
55 | const isHistoryEmpty = computed((): boolean => {
56 | return !hasHistory.value
57 | })
58 | </script>
59 |
60 | <template>
61 | <div class="calculator-display">
62 | <div class="current-calculation">
63 | <h3>Current Calculation</h3>
64 | <div class="calculation-value">
65 | {{ currentCalculation }}
66 | </div>
67 | </div>
68 |
69 | <div class="history-section">
70 | <div class="history-header">
71 | <h3>History</h3>
72 | <button
73 | v-if="hasHistory"
74 | @click="toggleHistoryView"
75 | class="btn-toggle"
76 | >
77 | {{ showFullHistory ? 'Show Less' : 'Show All' }}
78 | </button>
79 | <button
80 | v-if="hasHistory"
81 | @click="clearHistory"
82 | class="btn-clear-history"
83 | >
84 | Clear History
85 | </button>
86 | </div>
87 |
88 | <div v-if="isHistoryEmpty" class="empty-history">
89 | No calculations yet
90 | </div>
91 |
92 | <div v-else class="history-list">
93 | <div
94 | v-for="(entry, index) in displayedHistory"
95 | :key="`${entry.timestamp}-${index}`"
96 | class="history-item"
97 | >
98 | <span class="expression">{{ entry.expression }}</span>
99 | <span class="result">= {{ entry.result }}</span>
100 | <span class="timestamp">{{ formatDate(entry.timestamp) }}</span>
101 | </div>
102 | </div>
103 |
104 | <div v-if="!showFullHistory && recentHistory.length > maxHistoryItems" class="history-count">
105 | Showing {{ maxHistoryItems }} of {{ recentHistory.length }} entries
106 | </div>
107 | </div>
108 | </div>
109 | </template>
110 |
111 | <style scoped>
112 | .calculator-display {
113 | display: flex;
114 | flex-direction: column;
115 | gap: 1.5rem;
116 | padding: 1rem;
117 | background: white;
118 | border-radius: 8px;
119 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
120 | }
121 |
122 | .current-calculation {
123 | padding: 1rem;
124 | background: #e3f2fd;
125 | border-radius: 4px;
126 | }
127 |
128 | .current-calculation h3 {
129 | margin: 0 0 0.5rem 0;
130 | font-size: 1rem;
131 | color: #1976d2;
132 | }
133 |
134 | .calculation-value {
135 | font-size: 1.5rem;
136 | font-weight: bold;
137 | color: #333;
138 | }
139 |
140 | .history-section {
141 | display: flex;
142 | flex-direction: column;
143 | gap: 1rem;
144 | }
145 |
146 | .history-header {
147 | display: flex;
148 | align-items: center;
149 | gap: 1rem;
150 | }
151 |
152 | .history-header h3 {
153 | margin: 0;
154 | flex: 1;
155 | }
156 |
157 | .btn-toggle,
158 | .btn-clear-history {
159 | padding: 0.5rem 1rem;
160 | border: none;
161 | border-radius: 4px;
162 | cursor: pointer;
163 | font-size: 0.9rem;
164 | }
165 |
166 | .btn-toggle {
167 | background: #2196f3;
168 | color: white;
169 | }
170 |
171 | .btn-toggle:hover {
172 | background: #1976d2;
173 | }
174 |
175 | .btn-clear-history {
176 | background: #f44336;
177 | color: white;
178 | }
179 |
180 | .btn-clear-history:hover {
181 | background: #da190b;
182 | }
183 |
184 | .empty-history {
185 | padding: 2rem;
186 | text-align: center;
187 | color: #999;
188 | font-style: italic;
189 | }
190 |
191 | .history-list {
192 | display: flex;
193 | flex-direction: column;
194 | gap: 0.5rem;
195 | }
196 |
197 | .history-item {
198 | display: grid;
199 | grid-template-columns: 2fr 1fr 1fr;
200 | gap: 1rem;
201 | padding: 0.75rem;
202 | background: #f5f5f5;
203 | border-radius: 4px;
204 | align-items: center;
205 | }
206 |
207 | .expression {
208 | font-weight: 500;
209 | }
210 |
211 | .result {
212 | font-weight: bold;
213 | color: #4caf50;
214 | }
215 |
216 | .timestamp {
217 | font-size: 0.8rem;
218 | color: #666;
219 | text-align: right;
220 | }
221 |
222 | .history-count {
223 | text-align: center;
224 | font-size: 0.9rem;
225 | color: #666;
226 | padding: 0.5rem;
227 | }
228 | </style>
229 |
```
--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/examples/user_management.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Example demonstrating user management with the test_repo module.
3 |
4 | This example showcases:
5 | - Creating and managing users
6 | - Using various object types and relationships
7 | - Type annotations and complex Python patterns
8 | """
9 |
10 | import logging
11 | from dataclasses import dataclass
12 | from typing import Any
13 |
14 | from test_repo.models import User, create_user_object
15 | from test_repo.services import UserService
16 |
17 | # Set up logging
18 | logging.basicConfig(level=logging.INFO)
19 | logger = logging.getLogger(__name__)
20 |
21 |
22 | @dataclass
23 | class UserStats:
24 | """Statistics about user activity."""
25 |
26 | user_id: str
27 | login_count: int = 0
28 | last_active_days: int = 0
29 | engagement_score: float = 0.0
30 |
31 | def is_active(self) -> bool:
32 | """Check if the user is considered active."""
33 | return self.last_active_days < 30
34 |
35 |
36 | class UserManager:
37 | """Example class demonstrating complex user management."""
38 |
39 | def __init__(self, service: UserService):
40 | self.service = service
41 | self.active_users: dict[str, User] = {}
42 | self.user_stats: dict[str, UserStats] = {}
43 |
44 | def register_user(self, name: str, email: str, roles: list[str] | None = None) -> User:
45 | """Register a new user."""
46 | logger.info(f"Registering new user: {name} ({email})")
47 | user = self.service.create_user(name=name, email=email, roles=roles)
48 | self.active_users[user.id] = user
49 | self.user_stats[user.id] = UserStats(user_id=user.id)
50 | return user
51 |
52 | def get_user(self, user_id: str) -> User | None:
53 | """Get a user by ID."""
54 | if user_id in self.active_users:
55 | return self.active_users[user_id]
56 |
57 | # Try to fetch from service
58 | user = self.service.get_user(user_id)
59 | if user:
60 | self.active_users[user.id] = user
61 | return user
62 |
63 | def update_user_stats(self, user_id: str, login_count: int, days_since_active: int) -> None:
64 | """Update statistics for a user."""
65 | if user_id not in self.user_stats:
66 | self.user_stats[user_id] = UserStats(user_id=user_id)
67 |
68 | stats = self.user_stats[user_id]
69 | stats.login_count = login_count
70 | stats.last_active_days = days_since_active
71 |
72 | # Calculate engagement score based on activity
73 | engagement = (100 - min(days_since_active, 100)) * 0.8
74 | engagement += min(login_count, 20) * 0.2
75 | stats.engagement_score = engagement
76 |
77 | def get_active_users(self) -> list[User]:
78 | """Get all active users."""
79 | active_user_ids = [user_id for user_id, stats in self.user_stats.items() if stats.is_active()]
80 | return [self.active_users[user_id] for user_id in active_user_ids if user_id in self.active_users]
81 |
82 | def get_user_by_email(self, email: str) -> User | None:
83 | """Find a user by their email address."""
84 | for user in self.active_users.values():
85 | if user.email == email:
86 | return user
87 | return None
88 |
89 |
90 | # Example function demonstrating type annotations
91 | def process_user_data(users: list[User], include_inactive: bool = False, transform_func: callable | None = None) -> dict[str, Any]:
92 | """Process user data with optional transformations."""
93 | result: dict[str, Any] = {"users": [], "total": 0, "admin_count": 0}
94 |
95 | for user in users:
96 | if transform_func:
97 | user_data = transform_func(user.to_dict())
98 | else:
99 | user_data = user.to_dict()
100 |
101 | result["users"].append(user_data)
102 | result["total"] += 1
103 |
104 | if "admin" in user.roles:
105 | result["admin_count"] += 1
106 |
107 | return result
108 |
109 |
110 | def main():
111 | """Main function demonstrating the usage of UserManager."""
112 | # Initialize service and manager
113 | service = UserService()
114 | manager = UserManager(service)
115 |
116 | # Register some users
117 | admin = manager.register_user("Admin User", "[email protected]", ["admin"])
118 | user1 = manager.register_user("Regular User", "[email protected]", ["user"])
119 | user2 = manager.register_user("Another User", "[email protected]", ["user"])
120 |
121 | # Update some stats
122 | manager.update_user_stats(admin.id, 100, 5)
123 | manager.update_user_stats(user1.id, 50, 10)
124 | manager.update_user_stats(user2.id, 10, 45) # Inactive user
125 |
126 | # Get active users
127 | active_users = manager.get_active_users()
128 | logger.info(f"Active users: {len(active_users)}")
129 |
130 | # Process user data
131 | user_data = process_user_data(active_users, transform_func=lambda u: {**u, "full_name": u.get("name", "")})
132 |
133 | logger.info(f"Processed {user_data['total']} users, {user_data['admin_count']} admins")
134 |
135 | # Example of calling create_user directly
136 | external_user = create_user_object(id="ext123", name="External User", email="[email protected]", roles=["external"])
137 | logger.info(f"Created external user: {external_user.name}")
138 |
139 |
140 | if __name__ == "__main__":
141 | main()
142 |
```
--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Example demonstrating user management with the test_repo module.
3 |
4 | This example showcases:
5 | - Creating and managing users
6 | - Using various object types and relationships
7 | - Type annotations and complex Python patterns
8 | """
9 |
10 | import logging
11 | from dataclasses import dataclass
12 | from typing import Any
13 |
14 | from test_repo.models import User, create_user_object
15 | from test_repo.services import UserService
16 |
17 | # Set up logging
18 | logging.basicConfig(level=logging.INFO)
19 | logger = logging.getLogger(__name__)
20 |
21 |
22 | @dataclass
23 | class UserStats:
24 | """Statistics about user activity."""
25 |
26 | user_id: str
27 | login_count: int = 0
28 | last_active_days: int = 0
29 | engagement_score: float = 0.0
30 |
31 | def is_active(self) -> bool:
32 | """Check if the user is considered active."""
33 | return self.last_active_days < 30
34 |
35 |
36 | class UserManager:
37 | """Example class demonstrating complex user management."""
38 |
39 | def __init__(self, service: UserService):
40 | self.service = service
41 | self.active_users: dict[str, User] = {}
42 | self.user_stats: dict[str, UserStats] = {}
43 |
44 | def register_user(self, name: str, email: str, roles: list[str] | None = None) -> User:
45 | """Register a new user."""
46 | logger.info(f"Registering new user: {name} ({email})")
47 | user = self.service.create_user(name=name, email=email, roles=roles)
48 | self.active_users[user.id] = user
49 | self.user_stats[user.id] = UserStats(user_id=user.id)
50 | return user
51 |
52 | def get_user(self, user_id: str) -> User | None:
53 | """Get a user by ID."""
54 | if user_id in self.active_users:
55 | return self.active_users[user_id]
56 |
57 | # Try to fetch from service
58 | user = self.service.get_user(user_id)
59 | if user:
60 | self.active_users[user.id] = user
61 | return user
62 |
63 | def update_user_stats(self, user_id: str, login_count: int, days_since_active: int) -> None:
64 | """Update statistics for a user."""
65 | if user_id not in self.user_stats:
66 | self.user_stats[user_id] = UserStats(user_id=user_id)
67 |
68 | stats = self.user_stats[user_id]
69 | stats.login_count = login_count
70 | stats.last_active_days = days_since_active
71 |
72 | # Calculate engagement score based on activity
73 | engagement = (100 - min(days_since_active, 100)) * 0.8
74 | engagement += min(login_count, 20) * 0.2
75 | stats.engagement_score = engagement
76 |
77 | def get_active_users(self) -> list[User]:
78 | """Get all active users."""
79 | active_user_ids = [user_id for user_id, stats in self.user_stats.items() if stats.is_active()]
80 | return [self.active_users[user_id] for user_id in active_user_ids if user_id in self.active_users]
81 |
82 | def get_user_by_email(self, email: str) -> User | None:
83 | """Find a user by their email address."""
84 | for user in self.active_users.values():
85 | if user.email == email:
86 | return user
87 | return None
88 |
89 |
90 | # Example function demonstrating type annotations
91 | def process_user_data(users: list[User], include_inactive: bool = False, transform_func: callable | None = None) -> dict[str, Any]:
92 | """Process user data with optional transformations."""
93 | result: dict[str, Any] = {"users": [], "total": 0, "admin_count": 0}
94 |
95 | for user in users:
96 | if transform_func:
97 | user_data = transform_func(user.to_dict())
98 | else:
99 | user_data = user.to_dict()
100 |
101 | result["users"].append(user_data)
102 | result["total"] += 1
103 |
104 | if "admin" in user.roles:
105 | result["admin_count"] += 1
106 |
107 | return result
108 |
109 |
110 | def main():
111 | """Main function demonstrating the usage of UserManager."""
112 | # Initialize service and manager
113 | service = UserService()
114 | manager = UserManager(service)
115 |
116 | # Register some users
117 | admin = manager.register_user("Admin User", "[email protected]", ["admin"])
118 | user1 = manager.register_user("Regular User", "[email protected]", ["user"])
119 | user2 = manager.register_user("Another User", "[email protected]", ["user"])
120 |
121 | # Update some stats
122 | manager.update_user_stats(admin.id, 100, 5)
123 | manager.update_user_stats(user1.id, 50, 10)
124 | manager.update_user_stats(user2.id, 10, 45) # Inactive user
125 |
126 | # Get active users
127 | active_users = manager.get_active_users()
128 | logger.info(f"Active users: {len(active_users)}")
129 |
130 | # Process user data
131 | user_data = process_user_data(active_users, transform_func=lambda u: {**u, "full_name": u.get("name", "")})
132 |
133 | logger.info(f"Processed {user_data['total']} users, {user_data['admin_count']} admins")
134 |
135 | # Example of calling create_user directly
136 | external_user = create_user_object(id="ext123", name="External User", email="[email protected]", roles=["external"])
137 | logger.info(f"Created external user: {external_user.name}")
138 |
139 |
140 | if __name__ == "__main__":
141 | main()
142 |
```
--------------------------------------------------------------------------------
/test/solidlsp/r/test_r_basic.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Basic tests for R Language Server integration
3 | """
4 |
5 | import os
6 | from pathlib import Path
7 |
8 | import pytest
9 |
10 | from solidlsp import SolidLanguageServer
11 | from solidlsp.ls_config import Language
12 |
13 |
14 | @pytest.mark.r
15 | class TestRLanguageServer:
16 | """Test basic functionality of the R language server."""
17 |
18 | @pytest.mark.parametrize("language_server", [Language.R], indirect=True)
19 | @pytest.mark.parametrize("repo_path", [Language.R], indirect=True)
20 | def test_server_initialization(self, language_server: SolidLanguageServer, repo_path: Path):
21 | """Test that the R language server initializes properly."""
22 | assert language_server is not None
23 | assert language_server.language_id == "r"
24 | assert language_server.is_running()
25 | assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
26 |
27 | @pytest.mark.parametrize("language_server", [Language.R], indirect=True)
28 | def test_symbol_retrieval(self, language_server: SolidLanguageServer):
29 | """Test R document symbol extraction."""
30 | all_symbols, _root_symbols = language_server.request_document_symbols(os.path.join("R", "utils.R")).get_all_symbols_and_roots()
31 |
32 | # Should find the three exported functions
33 | function_symbols = [s for s in all_symbols if s.get("kind") == 12] # Function kind
34 | assert len(function_symbols) >= 3
35 |
36 | # Check that we found the expected functions
37 | function_names = {s.get("name") for s in function_symbols}
38 | expected_functions = {"calculate_mean", "process_data", "create_data_frame"}
39 | assert expected_functions.issubset(function_names), f"Expected functions {expected_functions} but found {function_names}"
40 |
41 | @pytest.mark.parametrize("language_server", [Language.R], indirect=True)
42 | def test_find_definition_across_files(self, language_server: SolidLanguageServer):
43 | """Test finding function definitions across files."""
44 | analysis_file = os.path.join("examples", "analysis.R")
45 |
46 | # In analysis.R line 7: create_data_frame(n = 50)
47 | # The function create_data_frame is defined in R/utils.R
48 | # Find definition of create_data_frame function call (0-indexed: line 6)
49 | definition_location_list = language_server.request_definition(analysis_file, 6, 17) # cursor on 'create_data_frame'
50 |
51 | assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
52 | assert len(definition_location_list) >= 1
53 | definition_location = definition_location_list[0]
54 | assert definition_location["uri"].endswith("utils.R")
55 | # Definition should be around line 37 (0-indexed: 36) where create_data_frame is defined
56 | assert definition_location["range"]["start"]["line"] >= 35
57 |
58 | @pytest.mark.parametrize("language_server", [Language.R], indirect=True)
59 | def test_find_references_across_files(self, language_server: SolidLanguageServer):
60 | """Test finding function references across files."""
61 | analysis_file = os.path.join("examples", "analysis.R")
62 |
63 | # Test from usage side: find references to calculate_mean from its usage in analysis.R
64 | # In analysis.R line 13: calculate_mean(clean_data$value)
65 | # calculate_mean function call is at line 13 (0-indexed: line 12)
66 | references = language_server.request_references(analysis_file, 12, 15) # cursor on 'calculate_mean'
67 |
68 | assert references, f"Expected non-empty references for calculate_mean but got {references=}"
69 |
70 | # Must find the definition in utils.R (cross-file reference)
71 | reference_files = [ref["uri"] for ref in references]
72 | assert any(uri.endswith("utils.R") for uri in reference_files), "Cross-file reference to definition in utils.R not found"
73 |
74 | # Verify we actually found the right location in utils.R
75 | utils_refs = [ref for ref in references if ref["uri"].endswith("utils.R")]
76 | assert len(utils_refs) >= 1, "Should find at least one reference in utils.R"
77 | utils_ref = utils_refs[0]
78 | # Should be around line 6 where calculate_mean is defined (0-indexed: line 5)
79 | assert (
80 | utils_ref["range"]["start"]["line"] == 5
81 | ), f"Expected reference at line 5 in utils.R, got line {utils_ref['range']['start']['line']}"
82 |
83 | def test_file_matching(self):
84 | """Test that R files are properly matched."""
85 | from solidlsp.ls_config import Language
86 |
87 | matcher = Language.R.get_source_fn_matcher()
88 |
89 | assert matcher.is_relevant_filename("script.R")
90 | assert matcher.is_relevant_filename("analysis.r")
91 | assert not matcher.is_relevant_filename("script.py")
92 | assert not matcher.is_relevant_filename("README.md")
93 |
94 | def test_r_language_enum(self):
95 | """Test R language enum value."""
96 | assert Language.R == "r"
97 | assert str(Language.R) == "r"
98 |
```
--------------------------------------------------------------------------------
/test/serena/config/test_serena_config.py:
--------------------------------------------------------------------------------
```python
1 | import shutil
2 | import tempfile
3 | from pathlib import Path
4 |
5 | import pytest
6 |
7 | from serena.config.serena_config import ProjectConfig
8 | from solidlsp.ls_config import Language
9 |
10 |
11 | class TestProjectConfigAutogenerate:
12 | """Test class for ProjectConfig autogeneration functionality."""
13 |
14 | def setup_method(self):
15 | """Set up test environment before each test method."""
16 | # Create a temporary directory for testing
17 | self.test_dir = tempfile.mkdtemp()
18 | self.project_path = Path(self.test_dir)
19 |
20 | def teardown_method(self):
21 | """Clean up test environment after each test method."""
22 | # Remove the temporary directory
23 | shutil.rmtree(self.test_dir)
24 |
25 | def test_autogenerate_empty_directory(self):
26 | """Test that autogenerate raises ValueError with helpful message for empty directory."""
27 | with pytest.raises(ValueError) as exc_info:
28 | ProjectConfig.autogenerate(self.project_path, save_to_disk=False)
29 |
30 | error_message = str(exc_info.value)
31 | assert "No source files found" in error_message
32 |
33 | def test_autogenerate_with_python_files(self):
34 | """Test successful autogeneration with Python source files."""
35 | # Create a Python file
36 | python_file = self.project_path / "main.py"
37 | python_file.write_text("def hello():\n print('Hello, world!')\n")
38 |
39 | # Run autogenerate
40 | config = ProjectConfig.autogenerate(self.project_path, save_to_disk=False)
41 |
42 | # Verify the configuration
43 | assert config.project_name == self.project_path.name
44 | assert config.languages == [Language.PYTHON]
45 |
46 | def test_autogenerate_with_js_files(self):
47 | """Test successful autogeneration with JavaScript source files."""
48 | # Create files for multiple languages
49 | (self.project_path / "small.js").write_text("console.log('JS');")
50 |
51 | # Run autogenerate - should pick Python as dominant
52 | config = ProjectConfig.autogenerate(self.project_path, save_to_disk=False)
53 |
54 | assert config.languages == [Language.TYPESCRIPT]
55 |
56 | def test_autogenerate_with_multiple_languages(self):
57 | """Test autogeneration picks dominant language when multiple are present."""
58 | # Create files for multiple languages
59 | (self.project_path / "main.py").write_text("print('Python')")
60 | (self.project_path / "util.py").write_text("def util(): pass")
61 | (self.project_path / "small.js").write_text("console.log('JS');")
62 |
63 | # Run autogenerate - should pick Python as dominant
64 | config = ProjectConfig.autogenerate(self.project_path, save_to_disk=False)
65 |
66 | assert config.languages == [Language.PYTHON]
67 |
68 | def test_autogenerate_saves_to_disk(self):
69 | """Test that autogenerate can save the configuration to disk."""
70 | # Create a Go file
71 | go_file = self.project_path / "main.go"
72 | go_file.write_text("package main\n\nfunc main() {}\n")
73 |
74 | # Run autogenerate with save_to_disk=True
75 | config = ProjectConfig.autogenerate(self.project_path, save_to_disk=True)
76 |
77 | # Verify the configuration file was created
78 | config_path = self.project_path / ".serena" / "project.yml"
79 | assert config_path.exists()
80 |
81 | # Verify the content
82 | assert config.languages == [Language.GO]
83 |
84 | def test_autogenerate_nonexistent_path(self):
85 | """Test that autogenerate raises FileNotFoundError for non-existent path."""
86 | non_existent = self.project_path / "does_not_exist"
87 |
88 | with pytest.raises(FileNotFoundError) as exc_info:
89 | ProjectConfig.autogenerate(non_existent, save_to_disk=False)
90 |
91 | assert "Project root not found" in str(exc_info.value)
92 |
93 | def test_autogenerate_with_gitignored_files_only(self):
94 | """Test autogenerate behavior when only gitignored files exist."""
95 | # Create a .gitignore that ignores all Python files
96 | gitignore = self.project_path / ".gitignore"
97 | gitignore.write_text("*.py\n")
98 |
99 | # Create Python files that will be ignored
100 | (self.project_path / "ignored.py").write_text("print('ignored')")
101 |
102 | # Should still raise ValueError as no source files are detected
103 | with pytest.raises(ValueError) as exc_info:
104 | ProjectConfig.autogenerate(self.project_path, save_to_disk=False)
105 |
106 | assert "No source files found" in str(exc_info.value)
107 |
108 | def test_autogenerate_custom_project_name(self):
109 | """Test autogenerate with custom project name."""
110 | # Create a TypeScript file
111 | ts_file = self.project_path / "index.ts"
112 | ts_file.write_text("const greeting: string = 'Hello';\n")
113 |
114 | # Run autogenerate with custom name
115 | custom_name = "my-custom-project"
116 | config = ProjectConfig.autogenerate(self.project_path, project_name=custom_name, save_to_disk=False)
117 |
118 | assert config.project_name == custom_name
119 | assert config.languages == [Language.TYPESCRIPT]
120 |
```
--------------------------------------------------------------------------------
/test/solidlsp/groovy/test_groovy_basic.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | from pathlib import Path
3 |
4 | import pytest
5 |
6 | from serena.constants import SERENA_MANAGED_DIR_NAME
7 | from solidlsp import SolidLanguageServer
8 | from solidlsp.ls_config import Language, LanguageServerConfig
9 | from solidlsp.ls_utils import SymbolUtils
10 | from solidlsp.settings import SolidLSPSettings
11 |
12 |
13 | @pytest.mark.groovy
14 | class TestGroovyLanguageServer:
15 | @classmethod
16 | def setup_class(cls):
17 | """
18 | Set up test class with Groovy test repository.
19 | """
20 | cls.test_repo_path = Path(__file__).parent.parent.parent / "resources" / "repos" / "groovy" / "test_repo"
21 |
22 | if not cls.test_repo_path.exists():
23 | pytest.skip("Groovy test repository not found")
24 |
25 | # Use JAR path from environment variable
26 | ls_jar_path = os.environ.get("GROOVY_LS_JAR_PATH")
27 | if not ls_jar_path or not os.path.exists(ls_jar_path):
28 | pytest.skip(
29 | "Groovy Language Server JAR not found. Set GROOVY_LS_JAR_PATH environment variable to run tests.",
30 | allow_module_level=True,
31 | )
32 |
33 | # Get JAR options from environment variable
34 | ls_jar_options = os.environ.get("GROOVY_LS_JAR_OPTIONS", "")
35 | ls_java_home_path = os.environ.get("GROOVY_LS_JAVA_HOME_PATH")
36 |
37 | groovy_settings = {"ls_jar_path": ls_jar_path, "ls_jar_options": ls_jar_options}
38 | if ls_java_home_path:
39 | groovy_settings["ls_java_home_path"] = ls_java_home_path
40 |
41 | # Create language server directly with Groovy-specific settings
42 | repo_path = str(cls.test_repo_path)
43 | config = LanguageServerConfig(code_language=Language.GROOVY, ignored_paths=[], trace_lsp_communication=False)
44 |
45 | solidlsp_settings = SolidLSPSettings(
46 | solidlsp_dir=str(Path.home() / ".serena"),
47 | project_data_relative_path=SERENA_MANAGED_DIR_NAME,
48 | ls_specific_settings={Language.GROOVY: groovy_settings},
49 | )
50 |
51 | cls.language_server = SolidLanguageServer.create(config, repo_path, solidlsp_settings=solidlsp_settings)
52 | cls.language_server.start()
53 |
54 | @classmethod
55 | def teardown_class(cls):
56 | """
57 | Clean up language server.
58 | """
59 | if hasattr(cls, "language_server"):
60 | cls.language_server.stop()
61 |
62 | def test_find_symbol(self) -> None:
63 | symbols = self.language_server.request_full_symbol_tree()
64 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree"
65 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree"
66 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model class not found in symbol tree"
67 | assert SymbolUtils.symbol_tree_contains_name(symbols, "ModelUser"), "ModelUser class not found in symbol tree"
68 |
69 | def test_find_referencing_class_symbols(self) -> None:
70 | file_path = os.path.join("src", "main", "groovy", "com", "example", "Utils.groovy")
71 | refs = self.language_server.request_references(file_path, 3, 6)
72 | assert any("Main.groovy" in ref.get("relativePath", "") for ref in refs), "Utils should be referenced from Main.groovy"
73 |
74 | file_path = os.path.join("src", "main", "groovy", "com", "example", "Model.groovy")
75 | symbols = self.language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
76 | model_symbol = None
77 | for sym in symbols[0]:
78 | if sym.get("name") == "com.example.Model" and sym.get("kind") == 5:
79 | model_symbol = sym
80 | break
81 | assert model_symbol is not None, "Could not find 'Model' class symbol in Model.groovy"
82 |
83 | if "selectionRange" in model_symbol:
84 | sel_start = model_symbol["selectionRange"]["start"]
85 | else:
86 | sel_start = model_symbol["range"]["start"]
87 | refs = self.language_server.request_references(file_path, sel_start["line"], sel_start["character"])
88 |
89 | main_refs = [ref for ref in refs if "Main.groovy" in ref.get("relativePath", "")]
90 | assert len(main_refs) >= 2, f"Model should be referenced from Main.groovy at least 2 times, found {len(main_refs)}"
91 |
92 | model_user_refs = [ref for ref in refs if "ModelUser.groovy" in ref.get("relativePath", "")]
93 | assert len(model_user_refs) >= 1, f"Model should be referenced from ModelUser.groovy at least 1 time, found {len(model_user_refs)}"
94 |
95 | def test_overview_methods(self) -> None:
96 | symbols = self.language_server.request_full_symbol_tree()
97 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview"
98 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview"
99 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview"
100 | assert SymbolUtils.symbol_tree_contains_name(symbols, "ModelUser"), "ModelUser missing from overview"
101 |
```
--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/scripts/run_app.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python
2 | """
3 | Main entry point script for the test_repo application.
4 |
5 | This script demonstrates how a typical application entry point would be structured,
6 | with command-line arguments, configuration loading, and service initialization.
7 | """
8 |
9 | import argparse
10 | import json
11 | import logging
12 | import os
13 | import sys
14 | from typing import Any
15 |
16 | # Add parent directory to path to make imports work
17 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
18 |
19 | from test_repo.models import Item, User
20 | from test_repo.services import ItemService, UserService
21 |
22 | # Configure logging
23 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
24 | logger = logging.getLogger(__name__)
25 |
26 |
27 | def parse_args():
28 | """Parse command line arguments."""
29 | parser = argparse.ArgumentParser(description="Test Repo Application")
30 |
31 | parser.add_argument("--config", type=str, default="config.json", help="Path to configuration file")
32 |
33 | parser.add_argument("--mode", choices=["user", "item", "both"], default="both", help="Operation mode")
34 |
35 | parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
36 |
37 | return parser.parse_args()
38 |
39 |
40 | def load_config(config_path: str) -> dict[str, Any]:
41 | """Load configuration from a JSON file."""
42 | if not os.path.exists(config_path):
43 | logger.warning(f"Configuration file not found: {config_path}")
44 | return {}
45 |
46 | try:
47 | with open(config_path, encoding="utf-8") as f:
48 | return json.load(f)
49 | except json.JSONDecodeError:
50 | logger.error(f"Invalid JSON in configuration file: {config_path}")
51 | return {}
52 | except Exception as e:
53 | logger.error(f"Error loading configuration: {e}")
54 | return {}
55 |
56 |
57 | def create_sample_users(service: UserService, count: int = 3) -> list[User]:
58 | """Create sample users for demonstration."""
59 | users = []
60 |
61 | # Create admin user
62 | admin = service.create_user(name="Admin User", email="[email protected]", roles=["admin"])
63 | users.append(admin)
64 |
65 | # Create regular users
66 | for i in range(count - 1):
67 | user = service.create_user(name=f"User {i + 1}", email=f"user{i + 1}@example.com", roles=["user"])
68 | users.append(user)
69 |
70 | return users
71 |
72 |
73 | def create_sample_items(service: ItemService, count: int = 5) -> list[Item]:
74 | """Create sample items for demonstration."""
75 | categories = ["Electronics", "Books", "Clothing", "Food", "Other"]
76 | items = []
77 |
78 | for i in range(count):
79 | category = categories[i % len(categories)]
80 | item = service.create_item(name=f"Item {i + 1}", price=10.0 * (i + 1), category=category)
81 | items.append(item)
82 |
83 | return items
84 |
85 |
86 | def run_user_operations(service: UserService, config: dict[str, Any]) -> None:
87 | """Run operations related to users."""
88 | logger.info("Running user operations")
89 |
90 | # Get configuration
91 | user_count = config.get("user_count", 3)
92 |
93 | # Create users
94 | users = create_sample_users(service, user_count)
95 | logger.info(f"Created {len(users)} users")
96 |
97 | # Demonstrate some operations
98 | for user in users:
99 | logger.info(f"User: {user.name} (ID: {user.id})")
100 |
101 | # Access a method to demonstrate method calls
102 | if user.has_role("admin"):
103 | logger.info(f"{user.name} is an admin")
104 |
105 | # Lookup a user
106 | found_user = service.get_user(users[0].id)
107 | if found_user:
108 | logger.info(f"Found user: {found_user.name}")
109 |
110 |
111 | def run_item_operations(service: ItemService, config: dict[str, Any]) -> None:
112 | """Run operations related to items."""
113 | logger.info("Running item operations")
114 |
115 | # Get configuration
116 | item_count = config.get("item_count", 5)
117 |
118 | # Create items
119 | items = create_sample_items(service, item_count)
120 | logger.info(f"Created {len(items)} items")
121 |
122 | # Demonstrate some operations
123 | total_price = 0.0
124 | for item in items:
125 | price_display = item.get_display_price()
126 | logger.info(f"Item: {item.name}, Price: {price_display}")
127 | total_price += item.price
128 |
129 | logger.info(f"Total price of all items: ${total_price:.2f}")
130 |
131 |
132 | def main():
133 | """Main entry point for the application."""
134 | # Parse command line arguments
135 | args = parse_args()
136 |
137 | # Configure logging level
138 | if args.verbose:
139 | logging.getLogger().setLevel(logging.DEBUG)
140 |
141 | logger.info("Starting Test Repo Application")
142 |
143 | # Load configuration
144 | config = load_config(args.config)
145 | logger.debug(f"Loaded configuration: {config}")
146 |
147 | # Initialize services
148 | user_service = UserService()
149 | item_service = ItemService()
150 |
151 | # Run operations based on mode
152 | if args.mode in ("user", "both"):
153 | run_user_operations(user_service, config)
154 |
155 | if args.mode in ("item", "both"):
156 | run_item_operations(item_service, config)
157 |
158 | logger.info("Application completed successfully")
159 |
160 |
161 | item_reference = Item(id="1", name="Item 1", price=10.0, category="Electronics")
162 |
163 | if __name__ == "__main__":
164 | main()
165 |
```
--------------------------------------------------------------------------------
/test/resources/repos/vue/test_repo/src/App.vue:
--------------------------------------------------------------------------------
```vue
1 | <script setup lang="ts">
2 | import { ref, computed, watch, watchEffect, onMounted } from 'vue'
3 | import { useCalculatorStore } from '@/stores/calculator'
4 | import { useThemeProvider } from '@/composables/useTheme'
5 | import { useTimeFormatter } from '@/composables/useFormatter'
6 | import CalculatorInput from '@/components/CalculatorInput.vue'
7 | import CalculatorDisplay from '@/components/CalculatorDisplay.vue'
8 |
9 | // Get the calculator store
10 | const store = useCalculatorStore()
11 |
12 | // Use theme composable with provide/inject - provides theme to all child components
13 | const themeManager = useThemeProvider()
14 |
15 | // Use time formatter composable
16 | const timeFormatter = useTimeFormatter()
17 |
18 | // Local ref for app title
19 | const appTitle = ref('Vue Calculator')
20 |
21 | // Computed property for app version
22 | const appVersion = computed((): string => {
23 | return '1.0.0'
24 | })
25 |
26 | // Computed property for greeting message
27 | const greetingMessage = computed((): string => {
28 | const hour = new Date().getHours()
29 | if (hour < 12) return 'Good morning'
30 | if (hour < 18) return 'Good afternoon'
31 | return 'Good evening'
32 | })
33 |
34 | // Get statistics from store
35 | const totalCalculations = computed((): number => {
36 | return store.history.length
37 | })
38 |
39 | // Check if calculator is active
40 | const isCalculatorActive = computed((): boolean => {
41 | return store.currentValue !== 0 || store.previousValue !== null
42 | })
43 |
44 | // Get last calculation time
45 | const lastCalculationTime = computed((): string => {
46 | if (store.history.length === 0) return 'No calculations yet'
47 | const lastEntry = store.history[store.history.length - 1]
48 | return timeFormatter.getRelativeTime(lastEntry.timestamp)
49 | })
50 |
51 | // Watch for calculation count changes - demonstrates watchEffect
52 | watchEffect(() => {
53 | document.title = `Calculator (${totalCalculations.value} calculations)`
54 | })
55 |
56 | // Watch for theme changes - demonstrates watch
57 | watch(
58 | () => themeManager.isDarkMode.value,
59 | (isDark) => {
60 | console.log(`Theme changed to ${isDark ? 'dark' : 'light'} mode`)
61 | }
62 | )
63 |
64 | // Lifecycle hook
65 | onMounted(() => {
66 | console.log('App component mounted')
67 | console.log(`Initial calculations: ${totalCalculations.value}`)
68 | })
69 |
70 | // Toggle theme method
71 | const toggleTheme = () => {
72 | themeManager.toggleDarkMode()
73 | }
74 |
75 | <template>
76 | <div :class="['app', { 'dark-mode': themeManager.isDarkMode }]">
77 | <header class="app-header">
78 | <h1>{{ appTitle }}</h1>
79 | <div class="header-info">
80 | <span class="greeting">{{ greetingMessage }}!</span>
81 | <span class="version">v{{ appVersion }}</span>
82 | <button @click="toggleTheme" class="btn-theme">
83 | {{ themeManager.isDarkMode ? '☀️' : '🌙' }}
84 | </button>
85 | </div>
86 | </header>
87 |
88 | <main class="app-main">
89 | <div class="calculator-container">
90 | <div class="stats-bar">
91 | <span>Total Calculations: {{ totalCalculations }}</span>
92 | <span :class="{ 'active-indicator': isCalculatorActive }">
93 | {{ isCalculatorActive ? 'Active' : 'Idle' }}
94 | </span>
95 | <span class="last-calc-time">{{ lastCalculationTime }}</span>
96 | </div>
97 |
98 | <div class="calculator-grid">
99 | <CalculatorInput />
100 | <CalculatorDisplay />
101 | </div>
102 | </div>
103 | </main>
104 |
105 | <footer class="app-footer">
106 | <p>Built with Vue 3 + Pinia + TypeScript</p>
107 | </footer>
108 | </div>
109 | </template>
110 |
111 | <style scoped>
112 | .app {
113 | min-height: 100vh;
114 | display: flex;
115 | flex-direction: column;
116 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
117 | transition: background 0.3s ease;
118 | }
119 |
120 | .app.dark-mode {
121 | background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
122 | }
123 |
124 | .app-header {
125 | padding: 2rem;
126 | color: white;
127 | text-align: center;
128 | }
129 |
130 | .app-header h1 {
131 | margin: 0 0 1rem 0;
132 | font-size: 2.5rem;
133 | }
134 |
135 | .header-info {
136 | display: flex;
137 | justify-content: center;
138 | align-items: center;
139 | gap: 1rem;
140 | }
141 |
142 | .greeting {
143 | font-size: 1.1rem;
144 | }
145 |
146 | .version {
147 | font-size: 0.9rem;
148 | opacity: 0.8;
149 | }
150 |
151 | .btn-theme {
152 | background: rgba(255, 255, 255, 0.2);
153 | border: none;
154 | border-radius: 50%;
155 | width: 40px;
156 | height: 40px;
157 | font-size: 1.2rem;
158 | cursor: pointer;
159 | transition: background 0.2s;
160 | }
161 |
162 | .btn-theme:hover {
163 | background: rgba(255, 255, 255, 0.3);
164 | }
165 |
166 | .app-main {
167 | flex: 1;
168 | display: flex;
169 | justify-content: center;
170 | align-items: center;
171 | padding: 2rem;
172 | }
173 |
174 | .calculator-container {
175 | width: 100%;
176 | max-width: 1200px;
177 | display: flex;
178 | flex-direction: column;
179 | gap: 1rem;
180 | }
181 |
182 | .stats-bar {
183 | display: flex;
184 | justify-content: space-between;
185 | align-items: center;
186 | gap: 1rem;
187 | padding: 1rem;
188 | background: rgba(255, 255, 255, 0.9);
189 | border-radius: 8px;
190 | font-weight: 500;
191 | }
192 |
193 | .last-calc-time {
194 | font-size: 0.9rem;
195 | color: #666;
196 | font-style: italic;
197 | }
198 |
199 | .active-indicator {
200 | color: #4caf50;
201 | font-weight: bold;
202 | }
203 |
204 | .active-indicator:not(.active-indicator) {
205 | color: #999;
206 | }
207 |
208 | .calculator-grid {
209 | display: grid;
210 | grid-template-columns: 1fr 1fr;
211 | gap: 2rem;
212 | }
213 |
214 | @media (max-width: 768px) {
215 | .calculator-grid {
216 | grid-template-columns: 1fr;
217 | }
218 | }
219 |
220 | .app-footer {
221 | padding: 1rem;
222 | text-align: center;
223 | color: white;
224 | opacity: 0.8;
225 | }
226 |
227 | .app-footer p {
228 | margin: 0;
229 | }
230 | </style>
231 |
```
--------------------------------------------------------------------------------
/src/serena/tools/workflow_tools.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Tools supporting the general workflow of the agent
3 | """
4 |
5 | import json
6 | import platform
7 |
8 | from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
9 |
10 |
11 | class CheckOnboardingPerformedTool(Tool):
12 | """
13 | Checks whether project onboarding was already performed.
14 | """
15 |
16 | def apply(self) -> str:
17 | """
18 | Checks whether project onboarding was already performed.
19 | You should always call this tool before beginning to actually work on the project/after activating a project.
20 | """
21 | from .memory_tools import ListMemoriesTool
22 |
23 | list_memories_tool = self.agent.get_tool(ListMemoriesTool)
24 | memories = json.loads(list_memories_tool.apply())
25 | if len(memories) == 0:
26 | return (
27 | "Onboarding not performed yet (no memories available). "
28 | + "You should perform onboarding by calling the `onboarding` tool before proceeding with the task."
29 | )
30 | else:
31 | return f"""The onboarding was already performed, below is the list of available memories.
32 | Do not read them immediately, just remember that they exist and that you can read them later, if it is necessary
33 | for the current task.
34 | Some memories may be based on previous conversations, others may be general for the current project.
35 | You should be able to tell which one you need based on the name of the memory.
36 |
37 | {memories}"""
38 |
39 |
40 | class OnboardingTool(Tool):
41 | """
42 | Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
43 | """
44 |
45 | def apply(self) -> str:
46 | """
47 | Call this tool if onboarding was not performed yet.
48 | You will call this tool at most once per conversation.
49 |
50 | :return: instructions on how to create the onboarding information
51 | """
52 | system = platform.system()
53 | return self.prompt_factory.create_onboarding_prompt(system=system)
54 |
55 |
56 | class ThinkAboutCollectedInformationTool(Tool):
57 | """
58 | Thinking tool for pondering the completeness of collected information.
59 | """
60 |
61 | def apply(self) -> str:
62 | """
63 | Think about the collected information and whether it is sufficient and relevant.
64 | This tool should ALWAYS be called after you have completed a non-trivial sequence of searching steps like
65 | find_symbol, find_referencing_symbols, search_files_for_pattern, read_file, etc.
66 | """
67 | return self.prompt_factory.create_think_about_collected_information()
68 |
69 |
70 | class ThinkAboutTaskAdherenceTool(Tool):
71 | """
72 | Thinking tool for determining whether the agent is still on track with the current task.
73 | """
74 |
75 | def apply(self) -> str:
76 | """
77 | Think about the task at hand and whether you are still on track.
78 | Especially important if the conversation has been going on for a while and there
79 | has been a lot of back and forth.
80 |
81 | This tool should ALWAYS be called before you insert, replace, or delete code.
82 | """
83 | return self.prompt_factory.create_think_about_task_adherence()
84 |
85 |
86 | class ThinkAboutWhetherYouAreDoneTool(Tool):
87 | """
88 | Thinking tool for determining whether the task is truly completed.
89 | """
90 |
91 | def apply(self) -> str:
92 | """
93 | Whenever you feel that you are done with what the user has asked for, it is important to call this tool.
94 | """
95 | return self.prompt_factory.create_think_about_whether_you_are_done()
96 |
97 |
98 | class SummarizeChangesTool(Tool, ToolMarkerOptional):
99 | """
100 | Provides instructions for summarizing the changes made to the codebase.
101 | """
102 |
103 | def apply(self) -> str:
104 | """
105 | Summarize the changes you have made to the codebase.
106 | This tool should always be called after you have fully completed any non-trivial coding task,
107 | but only after the think_about_whether_you_are_done call.
108 | """
109 | return self.prompt_factory.create_summarize_changes()
110 |
111 |
112 | class PrepareForNewConversationTool(Tool):
113 | """
114 | Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
115 | """
116 |
117 | def apply(self) -> str:
118 | """
119 | Instructions for preparing for a new conversation. This tool should only be called on explicit user request.
120 | """
121 | return self.prompt_factory.create_prepare_for_new_conversation()
122 |
123 |
124 | class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject):
125 | """
126 | Provides instructions on how to use the Serena toolbox.
127 | Should only be used in settings where the system prompt is not read automatically by the client.
128 |
129 | NOTE: Some MCP clients (including Claude Desktop) do not read the system prompt automatically!
130 | """
131 |
132 | def apply(self) -> str:
133 | """
134 | Provides the 'Serena Instructions Manual', which contains essential information on how to use the Serena toolbox.
135 | IMPORTANT: If you have not yet read the manual, call this tool immediately after you are given your task by the user,
136 | as it will critically inform you!
137 | """
138 | return self.agent.create_system_prompt()
139 |
```
--------------------------------------------------------------------------------
/test/solidlsp/elixir/test_elixir_basic.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Basic integration tests for the Elixir language server functionality.
3 |
4 | These tests validate the functionality of the language server APIs
5 | like request_references using the test repository.
6 | """
7 |
8 | import os
9 |
10 | import pytest
11 |
12 | from solidlsp import SolidLanguageServer
13 | from solidlsp.ls_config import Language
14 |
15 | from . import EXPERT_UNAVAILABLE, EXPERT_UNAVAILABLE_REASON
16 |
17 | # These marks will be applied to all tests in this module
18 | pytestmark = [pytest.mark.elixir, pytest.mark.skipif(EXPERT_UNAVAILABLE, reason=f"Next LS not available: {EXPERT_UNAVAILABLE_REASON}")]
19 |
20 |
21 | class TestElixirBasic:
22 | """Basic Elixir language server functionality tests."""
23 |
24 | @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
25 | def test_request_references_function_definition(self, language_server: SolidLanguageServer):
26 | """Test finding references to a function definition."""
27 | file_path = os.path.join("lib", "models.ex")
28 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
29 |
30 | # Find the User module's 'new' function
31 | user_new_symbol = None
32 | for symbol in symbols[0]: # Top level symbols
33 | if symbol.get("name") == "User" and symbol.get("kind") == 2: # Module
34 | for child in symbol.get("children", []):
35 | if child.get("name", "").startswith("def new(") and child.get("kind") == 12: # Function
36 | user_new_symbol = child
37 | break
38 | break
39 |
40 | if not user_new_symbol or "selectionRange" not in user_new_symbol:
41 | pytest.skip("User.new function or its selectionRange not found")
42 |
43 | sel_start = user_new_symbol["selectionRange"]["start"]
44 | references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
45 |
46 | assert references is not None
47 | assert len(references) > 0
48 |
49 | # Should find at least one reference (the definition itself)
50 | found_definition = any(ref["uri"].endswith("models.ex") for ref in references)
51 | assert found_definition, "Should find the function definition"
52 |
53 | @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
54 | def test_request_references_create_user_function(self, language_server: SolidLanguageServer):
55 | """Test finding references to create_user function."""
56 | file_path = os.path.join("lib", "services.ex")
57 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
58 |
59 | # Find the UserService module's 'create_user' function
60 | create_user_symbol = None
61 | for symbol in symbols[0]: # Top level symbols
62 | if symbol.get("name") == "UserService" and symbol.get("kind") == 2: # Module
63 | for child in symbol.get("children", []):
64 | if child.get("name", "").startswith("def create_user(") and child.get("kind") == 12: # Function
65 | create_user_symbol = child
66 | break
67 | break
68 |
69 | if not create_user_symbol or "selectionRange" not in create_user_symbol:
70 | pytest.skip("UserService.create_user function or its selectionRange not found")
71 |
72 | sel_start = create_user_symbol["selectionRange"]["start"]
73 | references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
74 |
75 | assert references is not None
76 | assert len(references) > 0
77 |
78 | @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
79 | def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer):
80 | """Test finding symbols that reference a specific function."""
81 | file_path = os.path.join("lib", "models.ex")
82 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
83 |
84 | # Find the User module's 'new' function
85 | user_new_symbol = None
86 | for symbol in symbols[0]: # Top level symbols
87 | if symbol.get("name") == "User" and symbol.get("kind") == 2: # Module
88 | for child in symbol.get("children", []):
89 | if child.get("name", "").startswith("def new(") and child.get("kind") == 12: # Function
90 | user_new_symbol = child
91 | break
92 | break
93 |
94 | if not user_new_symbol or "selectionRange" not in user_new_symbol:
95 | pytest.skip("User.new function or its selectionRange not found")
96 |
97 | sel_start = user_new_symbol["selectionRange"]["start"]
98 | referencing_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
99 |
100 | assert referencing_symbols is not None
101 |
102 | @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
103 | def test_timeout_enumeration_bug(self, language_server: SolidLanguageServer):
104 | """Test that enumeration doesn't timeout (regression test)."""
105 | # This should complete without timing out
106 | symbols = language_server.request_document_symbols("lib/models.ex").get_all_symbols_and_roots()
107 | assert symbols is not None
108 |
109 | # Test multiple symbol requests in succession
110 | for _ in range(3):
111 | symbols = language_server.request_document_symbols("lib/services.ex").get_all_symbols_and_roots()
112 | assert symbols is not None
113 |
```
--------------------------------------------------------------------------------
/test/serena/util/test_exception.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | from unittest.mock import MagicMock, Mock, patch
3 |
4 | import pytest
5 |
6 | from serena.util.exception import is_headless_environment, show_fatal_exception_safe
7 |
8 |
9 | class TestHeadlessEnvironmentDetection:
10 | """Test class for headless environment detection functionality."""
11 |
12 | def test_is_headless_no_display(self):
13 | """Test that environment without DISPLAY is detected as headless on Linux."""
14 | with patch("sys.platform", "linux"):
15 | with patch.dict(os.environ, {}, clear=True):
16 | assert is_headless_environment() is True
17 |
18 | def test_is_headless_ssh_connection(self):
19 | """Test that SSH sessions are detected as headless."""
20 | with patch("sys.platform", "linux"):
21 | with patch.dict(os.environ, {"SSH_CONNECTION": "192.168.1.1 22 192.168.1.2 22", "DISPLAY": ":0"}):
22 | assert is_headless_environment() is True
23 |
24 | with patch.dict(os.environ, {"SSH_CLIENT": "192.168.1.1 22 22", "DISPLAY": ":0"}):
25 | assert is_headless_environment() is True
26 |
27 | def test_is_headless_wsl(self):
28 | """Test that WSL environment is detected as headless."""
29 | # Skip this test on Windows since os.uname doesn't exist
30 | if not hasattr(os, "uname"):
31 | pytest.skip("os.uname not available on this platform")
32 |
33 | with patch("sys.platform", "linux"):
34 | with patch("os.uname") as mock_uname:
35 | mock_uname.return_value = Mock(release="5.15.153.1-microsoft-standard-WSL2")
36 | with patch.dict(os.environ, {"DISPLAY": ":0"}):
37 | assert is_headless_environment() is True
38 |
39 | def test_is_headless_docker(self):
40 | """Test that Docker containers are detected as headless."""
41 | with patch("sys.platform", "linux"):
42 | # Test with CI environment variable
43 | with patch.dict(os.environ, {"CI": "true", "DISPLAY": ":0"}):
44 | assert is_headless_environment() is True
45 |
46 | # Test with CONTAINER environment variable
47 | with patch.dict(os.environ, {"CONTAINER": "docker", "DISPLAY": ":0"}):
48 | assert is_headless_environment() is True
49 |
50 | # Test with .dockerenv file
51 | with patch("os.path.exists") as mock_exists:
52 | mock_exists.return_value = True
53 | with patch.dict(os.environ, {"DISPLAY": ":0"}):
54 | assert is_headless_environment() is True
55 |
56 | def test_is_not_headless_windows(self):
57 | """Test that Windows is never detected as headless."""
58 | with patch("sys.platform", "win32"):
59 | # Even without DISPLAY, Windows should not be headless
60 | with patch.dict(os.environ, {}, clear=True):
61 | assert is_headless_environment() is False
62 |
63 |
64 | class TestShowFatalExceptionSafe:
65 | """Test class for safe fatal exception display functionality."""
66 |
67 | @patch("serena.util.exception.is_headless_environment", return_value=True)
68 | @patch("serena.util.exception.log")
69 | def test_show_fatal_exception_safe_headless(self, mock_log, mock_is_headless):
70 | """Test that GUI is not attempted in headless environment."""
71 | test_exception = ValueError("Test error")
72 |
73 | # The import should never happen in headless mode
74 | with patch("serena.gui_log_viewer.show_fatal_exception") as mock_show_gui:
75 | show_fatal_exception_safe(test_exception)
76 | mock_show_gui.assert_not_called()
77 |
78 | # Verify debug log about skipping GUI
79 | mock_log.debug.assert_called_once_with("Skipping GUI error display in headless environment")
80 |
81 | @patch("serena.util.exception.is_headless_environment", return_value=False)
82 | @patch("serena.util.exception.log")
83 | def test_show_fatal_exception_safe_with_gui(self, mock_log, mock_is_headless):
84 | """Test that GUI is attempted when not in headless environment."""
85 | test_exception = ValueError("Test error")
86 |
87 | # Mock the GUI function
88 | with patch("serena.gui_log_viewer.show_fatal_exception") as mock_show_gui:
89 | show_fatal_exception_safe(test_exception)
90 | mock_show_gui.assert_called_once_with(test_exception)
91 |
92 | @patch("serena.util.exception.is_headless_environment", return_value=False)
93 | @patch("serena.util.exception.log")
94 | def test_show_fatal_exception_safe_gui_failure(self, mock_log, mock_is_headless):
95 | """Test graceful handling when GUI display fails."""
96 | test_exception = ValueError("Test error")
97 | gui_error = ImportError("No module named 'tkinter'")
98 |
99 | # Mock the GUI function to raise an exception
100 | with patch("serena.gui_log_viewer.show_fatal_exception", side_effect=gui_error):
101 | show_fatal_exception_safe(test_exception)
102 |
103 | # Verify debug log about GUI failure
104 | mock_log.debug.assert_called_with(f"Failed to show GUI error dialog: {gui_error}")
105 |
106 | def test_show_fatal_exception_safe_prints_to_stderr(self):
107 | """Test that exceptions are always printed to stderr."""
108 | test_exception = ValueError("Test error message")
109 |
110 | with patch("sys.stderr", new_callable=MagicMock) as mock_stderr:
111 | with patch("serena.util.exception.is_headless_environment", return_value=True):
112 | with patch("serena.util.exception.log"):
113 | show_fatal_exception_safe(test_exception)
114 |
115 | # Verify print was called with the correct arguments
116 | mock_stderr.write.assert_any_call("Fatal exception: Test error message")
117 |
```
--------------------------------------------------------------------------------
/src/serena/resources/config/prompt_templates/simple_tool_outputs.yml:
--------------------------------------------------------------------------------
```yaml
1 | # Some of Serena's tools are just outputting a fixed text block without doing anything else.
2 | # Such tools are meant to encourage the agent to think in a certain way, to stay on track
3 | # and so on. The (templates for) outputs of these tools are contained here.
4 | prompts:
5 | onboarding_prompt: |
6 | You are viewing the project for the first time.
7 | Your task is to assemble relevant high-level information about the project which
8 | will be saved to memory files in the following steps.
9 | The information should be sufficient to understand what the project is about,
10 | and the most important commands for developing code.
11 | The project is being developed on the system: {{ system }}.
12 |
13 | You need to identify at least the following information:
14 | * the project's purpose
15 | * the tech stack used
16 | * the code style and conventions used (including naming, type hints, docstrings, etc.)
17 | * which commands to run when a task is completed (linting, formatting, testing, etc.)
18 | * the rough structure of the codebase
19 | * the commands for testing, formatting, and linting
20 | * the commands for running the entrypoints of the project
21 | * the util commands for the system, like `git`, `ls`, `cd`, `grep`, `find`, etc. Keep in mind that the system is {{ system }},
22 | so the commands might be different than on a regular unix system.
23 | * whether there are particular guidelines, styles, design patterns, etc. that one should know about
24 |
25 | This list is not exhaustive, you can add more information if you think it is relevant.
26 |
27 | For doing that, you will need to acquire information about the project with the corresponding tools.
28 | Read only the necessary files and directories to avoid loading too much data into memory.
29 | If you cannot find everything you need from the project itself, you should ask the user for more information.
30 |
31 | After collecting all the information, you will use the `write_memory` tool (in multiple calls) to save it to various memory files.
32 | A particularly important memory file will be the `suggested_commands.md` file, which should contain
33 | a list of commands that the user should know about to develop code in this project.
34 | Moreover, you should create memory files for the style and conventions and a dedicated memory file for
35 | what should be done when a task is completed.
36 | **Important**: after done with the onboarding task, remember to call the `write_memory` to save the collected information!
37 |
38 | think_about_collected_information: |
39 | Have you collected all the information you need for solving the current task? If not, can the missing information be acquired by using the available tools,
40 | in particular the tools related to symbol discovery? Or do you need to ask the user for more information?
41 | Think about it step by step and give a summary of the missing information and how it could be acquired.
42 |
43 | think_about_task_adherence: |
44 | Are you deviating from the task at hand? Do you need any additional information to proceed?
45 | Have you loaded all relevant memory files to see whether your implementation is fully aligned with the
46 | code style, conventions, and guidelines of the project? If not, adjust your implementation accordingly
47 | before modifying any code into the codebase.
48 | Note that it is better to stop and ask the user for clarification
49 | than to perform large changes which might not be aligned with the user's intentions.
50 | If you feel like the conversation is deviating too much from the original task, apologize and suggest to the user
51 | how to proceed. If the conversation became too long, create a summary of the current progress and suggest to the user
52 | to start a new conversation based on that summary.
53 |
54 | think_about_whether_you_are_done: |
55 | Have you already performed all the steps required by the task? Is it appropriate to run tests and linting, and if so,
56 | have you done that already? Is it appropriate to adjust non-code files like documentation and config and have you done that already?
57 | Should new tests be written to cover the changes?
58 | Note that a task that is just about exploring the codebase does not require running tests or linting.
59 | Read the corresponding memory files to see what should be done when a task is completed.
60 |
61 | summarize_changes: |
62 | Summarize all the changes you have made to the codebase over the course of the conversation.
63 | Explore the diff if needed (e.g. by using `git diff`) to ensure that you have not missed anything.
64 | Explain whether and how the changes are covered by tests. Explain how to best use the new code, how to understand it,
65 | which existing code it affects and interacts with. Are there any dangers (like potential breaking changes or potential new problems)
66 | that the user should be aware of? Should any new documentation be written or existing documentation updated?
67 | You can use tools to explore the codebase prior to writing the summary, but don't write any new code in this step until
68 | the summary is complete.
69 |
70 | prepare_for_new_conversation: |
71 | You have not yet completed the current task but we are running out of context.
72 | {mode_prepare_for_new_conversation}
73 | Imagine that you are handing over the task to another person who has access to the
74 | same tools and memory files as you do, but has not been part of the conversation so far.
75 | Write a summary that can be used in the next conversation to a memory file using the `write_memory` tool.
76 |
```
--------------------------------------------------------------------------------
/src/solidlsp/lsp_protocol_handler/server.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | This file provides the implementation of the JSON-RPC client, that launches and
3 | communicates with the language server.
4 |
5 | The initial implementation of this file was obtained from
6 | https://github.com/predragnikolic/OLSP under the MIT License with the following terms:
7 |
8 | MIT License
9 |
10 | Copyright (c) 2023 Предраг Николић
11 |
12 | Permission is hereby granted, free of charge, to any person obtaining a copy
13 | of this software and associated documentation files (the "Software"), to deal
14 | in the Software without restriction, including without limitation the rights
15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 | copies of the Software, and to permit persons to whom the Software is
17 | furnished to do so, subject to the following conditions:
18 |
19 | The above copyright notice and this permission notice shall be included in all
20 | copies or substantial portions of the Software.
21 |
22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 | SOFTWARE.
29 | """
30 |
31 | import dataclasses
32 | import json
33 | import logging
34 | import os
35 | from typing import Any, Union
36 |
37 | from .lsp_types import ErrorCodes
38 |
39 | StringDict = dict[str, Any]
40 | PayloadLike = Union[list[StringDict], StringDict, None, bool]
41 | CONTENT_LENGTH = "Content-Length: "
42 | ENCODING = "utf-8"
43 | log = logging.getLogger(__name__)
44 |
45 |
46 | @dataclasses.dataclass
47 | class ProcessLaunchInfo:
48 | """
49 | This class is used to store the information required to launch a (language server) process.
50 | """
51 |
52 | cmd: str | list[str]
53 | """
54 | the command used to launch the process.
55 | Specification as a list is preferred (as it is more robust and avoids incorrect quoting of arguments);
56 | the string variant is supported for backward compatibility only
57 | """
58 |
59 | env: dict[str, str] = dataclasses.field(default_factory=dict)
60 | """
61 | the environment variables to set for the process
62 | """
63 |
64 | cwd: str = os.getcwd()
65 | """
66 | the working directory for the process
67 | """
68 |
69 |
70 | class LSPError(Exception):
71 | def __init__(self, code: ErrorCodes, message: str) -> None:
72 | super().__init__(message)
73 | self.code = code
74 |
75 | def to_lsp(self) -> StringDict:
76 | return {"code": self.code, "message": super().__str__()}
77 |
78 | @classmethod
79 | def from_lsp(cls, d: StringDict) -> "LSPError":
80 | return LSPError(d["code"], d["message"])
81 |
82 | def __str__(self) -> str:
83 | return f"{super().__str__()} ({self.code})"
84 |
85 |
86 | def make_response(request_id: Any, params: PayloadLike) -> StringDict:
87 | return {"jsonrpc": "2.0", "id": request_id, "result": params}
88 |
89 |
90 | def make_error_response(request_id: Any, err: LSPError) -> StringDict:
91 | return {"jsonrpc": "2.0", "id": request_id, "error": err.to_lsp()}
92 |
93 |
94 | # LSP methods that expect NO params field at all (not even empty object).
95 | # These methods use Void/unit type in their protocol definition.
96 | # - shutdown: HLS uses Haskell's Void type, rust-analyzer expects unit
97 | # - exit: Similar - notification with no params
98 | # Sending params:{} to these methods causes parse errors like "Cannot parse Void"
99 | # See: https://www.jsonrpc.org/specification ("params MAY be omitted")
100 | _NO_PARAMS_METHODS = frozenset({"shutdown", "exit"})
101 |
102 |
103 | def _build_params_field(method: str, params: PayloadLike) -> StringDict:
104 | """Build the params portion of a JSON-RPC message based on LSP method requirements.
105 |
106 | LSP methods with Void/unit type (shutdown, exit) must omit params field entirely
107 | to satisfy HLS and rust-analyzer. Other methods send empty {} for None params
108 | to maintain Delphi/FPC LSP compatibility (PR #851).
109 |
110 | Returns a dict that can be merged into the message using ** unpacking.
111 | """
112 | if method in _NO_PARAMS_METHODS:
113 | return {} # Omit params entirely for Void-type methods
114 | elif params is not None:
115 | return {"params": params}
116 | else:
117 | return {"params": {}} # Keep {} for Delphi/FPC compatibility
118 |
119 |
120 | def make_notification(method: str, params: PayloadLike) -> StringDict:
121 | """Create a JSON-RPC 2.0 notification message."""
122 | return {"jsonrpc": "2.0", "method": method, **_build_params_field(method, params)}
123 |
124 |
125 | def make_request(method: str, request_id: Any, params: PayloadLike) -> StringDict:
126 | """Create a JSON-RPC 2.0 request message."""
127 | return {"jsonrpc": "2.0", "method": method, "id": request_id, **_build_params_field(method, params)}
128 |
129 |
130 | class StopLoopException(Exception):
131 | pass
132 |
133 |
134 | def create_message(payload: PayloadLike) -> tuple[bytes, bytes, bytes]:
135 | body = json.dumps(payload, check_circular=False, ensure_ascii=False, separators=(",", ":")).encode(ENCODING)
136 | return (
137 | f"Content-Length: {len(body)}\r\n".encode(ENCODING),
138 | "Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n".encode(ENCODING),
139 | body,
140 | )
141 |
142 |
143 | class MessageType:
144 | error = 1
145 | warning = 2
146 | info = 3
147 | log = 4
148 |
149 |
150 | def content_length(line: bytes) -> int | None:
151 | if line.startswith(b"Content-Length: "):
152 | _, value = line.split(b"Content-Length: ")
153 | value = value.strip()
154 | try:
155 | return int(value)
156 | except ValueError:
157 | raise ValueError(f"Invalid Content-Length header: {value!r}")
158 | return None
159 |
```
--------------------------------------------------------------------------------
/src/solidlsp/language_servers/regal_server.py:
--------------------------------------------------------------------------------
```python
1 | """Regal Language Server implementation for Rego policy files."""
2 |
3 | import logging
4 | import os
5 | import shutil
6 | import threading
7 |
8 | from overrides import override
9 |
10 | from solidlsp.ls import SolidLanguageServer
11 | from solidlsp.ls_config import LanguageServerConfig
12 | from solidlsp.ls_utils import PathUtils
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 RegalLanguageServer(SolidLanguageServer):
21 | """
22 | Provides Rego specific instantiation of the LanguageServer class using Regal.
23 |
24 | Regal is the official linter and language server for Rego (Open Policy Agent's policy language).
25 | See: https://github.com/StyraInc/regal
26 | """
27 |
28 | @override
29 | def is_ignored_dirname(self, dirname: str) -> bool:
30 | return super().is_ignored_dirname(dirname) or dirname in [".regal", ".opa"]
31 |
32 | def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
33 | """
34 | Creates a RegalLanguageServer instance.
35 |
36 | This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
37 |
38 | :param config: Language server configuration
39 | :param repository_root_path: Path to the repository root
40 | :param solidlsp_settings: Settings for solidlsp
41 | """
42 | # Regal should be installed system-wide (via CI or user installation)
43 | regal_executable_path = shutil.which("regal")
44 | if not regal_executable_path:
45 | raise RuntimeError(
46 | "Regal language server not found. Please install it from https://github.com/StyraInc/regal or via your package manager."
47 | )
48 |
49 | super().__init__(
50 | config,
51 | repository_root_path,
52 | ProcessLaunchInfo(cmd=f"{regal_executable_path} language-server", cwd=repository_root_path),
53 | "rego",
54 | solidlsp_settings,
55 | )
56 | self.server_ready = threading.Event()
57 |
58 | @staticmethod
59 | def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
60 | """
61 | Returns the initialize params for the Regal Language Server.
62 |
63 | :param repository_absolute_path: Absolute path to the repository
64 | :return: LSP initialization parameters
65 | """
66 | root_uri = PathUtils.path_to_uri(repository_absolute_path)
67 | return {
68 | "processId": os.getpid(),
69 | "locale": "en",
70 | "rootPath": repository_absolute_path,
71 | "rootUri": root_uri,
72 | "capabilities": {
73 | "textDocument": {
74 | "synchronization": {"didSave": True, "dynamicRegistration": True},
75 | "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
76 | "definition": {"dynamicRegistration": True},
77 | "references": {"dynamicRegistration": True},
78 | "documentSymbol": {
79 | "dynamicRegistration": True,
80 | "hierarchicalDocumentSymbolSupport": True,
81 | "symbolKind": {"valueSet": list(range(1, 27))}, # type: ignore[arg-type]
82 | },
83 | "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, # type: ignore[list-item]
84 | "codeAction": {"dynamicRegistration": True},
85 | "formatting": {"dynamicRegistration": True},
86 | },
87 | "workspace": {
88 | "workspaceFolders": True,
89 | "didChangeConfiguration": {"dynamicRegistration": True},
90 | "symbol": {"dynamicRegistration": True},
91 | },
92 | },
93 | "workspaceFolders": [
94 | {
95 | "name": os.path.basename(repository_absolute_path),
96 | "uri": root_uri,
97 | }
98 | ],
99 | }
100 |
101 | def _start_server(self) -> None:
102 | """Start Regal language server process and wait for initialization."""
103 |
104 | def register_capability_handler(params) -> None: # type: ignore[no-untyped-def]
105 | return
106 |
107 | def window_log_message(msg) -> None: # type: ignore[no-untyped-def]
108 | log.info(f"LSP: window/logMessage: {msg}")
109 |
110 | def do_nothing(params) -> None: # type: ignore[no-untyped-def]
111 | return
112 |
113 | self.server.on_request("client/registerCapability", register_capability_handler)
114 | self.server.on_notification("window/logMessage", window_log_message)
115 | self.server.on_notification("$/progress", do_nothing)
116 | self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
117 |
118 | log.info("Starting Regal language server process")
119 | self.server.start()
120 | initialize_params = self._get_initialize_params(self.repository_root_path)
121 |
122 | log.info(
123 | "Sending initialize request from LSP client to LSP server and awaiting response",
124 | )
125 | init_response = self.server.send.initialize(initialize_params)
126 |
127 | # Verify server capabilities
128 | assert "capabilities" in init_response
129 | assert "textDocumentSync" in init_response["capabilities"]
130 |
131 | self.server.notify.initialized({})
132 | self.completions_available.set()
133 |
134 | # Regal server is ready immediately after initialization
135 | self.server_ready.set()
136 | self.server_ready.wait()
137 |
```
--------------------------------------------------------------------------------
/docs/autogen_rst.py:
--------------------------------------------------------------------------------
```python
1 | import logging
2 | import os
3 | import shutil
4 | from pathlib import Path
5 | from typing import Optional, List
6 |
7 | log = logging.getLogger(os.path.basename(__file__))
8 |
9 | TOP_LEVEL_PACKAGE = "serena"
10 | PROJECT_NAME = "Serena"
11 |
12 | def module_template(module_qualname: str):
13 | module_name = module_qualname.split(".")[-1]
14 | title = module_name.replace("_", r"\_")
15 | return f"""{title}
16 | {"=" * len(title)}
17 |
18 | .. automodule:: {module_qualname}
19 | :members:
20 | :undoc-members:
21 | """
22 |
23 |
24 | def index_template(package_name: str, doc_references: Optional[List[str]] = None, text_prefix=""):
25 | doc_references = doc_references or ""
26 | if doc_references:
27 | doc_references = "\n" + "\n".join(f"* :doc:`{ref}`" for ref in doc_references) + "\n"
28 |
29 | dirname = package_name.split(".")[-1]
30 | title = dirname.replace("_", r"\_")
31 | if title == TOP_LEVEL_PACKAGE:
32 | title = "API Reference"
33 | return f"{title}\n{'=' * len(title)}" + text_prefix + doc_references
34 |
35 |
36 | def write_to_file(content: str, path: str):
37 | os.makedirs(os.path.dirname(path), exist_ok=True)
38 | with open(path, "w") as f:
39 | f.write(content)
40 | os.chmod(path, 0o666)
41 |
42 |
43 | _SUBTITLE = (
44 | f"\n Here is the autogenerated documentation of the {PROJECT_NAME} API. \n \n "
45 | "The Table of Contents to the left has the same structure as the "
46 | "repository's package code. The links at each page point to the submodules and subpackages. \n"
47 | )
48 |
49 |
50 | def make_rst(src_root, rst_root, clean=False, overwrite=False, package_prefix=""):
51 | """Creates/updates documentation in form of rst files for modules and packages.
52 |
53 | Does not delete any existing rst files. Thus, rst files for packages or modules that have been removed or renamed
54 | should be deleted by hand.
55 |
56 | This method should be executed from the project's top-level directory
57 |
58 | :param src_root: path to library base directory, typically "src/<library_name>"
59 | :param rst_root: path to the root directory to which .rst files will be written
60 | :param clean: whether to completely clean the target directory beforehand, removing any existing .rst files
61 | :param overwrite: whether to overwrite existing rst files. This should be used with caution as it will delete
62 | all manual changes to documentation files
63 | :package_prefix: a prefix to prepend to each module (for the case where the src_root is not the base package),
64 | which, if not empty, should end with a "."
65 | :return:
66 | """
67 | rst_root = os.path.abspath(rst_root)
68 |
69 | if clean and os.path.isdir(rst_root):
70 | shutil.rmtree(rst_root)
71 |
72 | base_package_name = package_prefix + os.path.basename(src_root)
73 |
74 | # TODO: reduce duplication with same logic for subpackages below
75 | files_in_dir = os.listdir(src_root)
76 | module_names = [f[:-3] for f in files_in_dir if f.endswith(".py") and not f.startswith("_")]
77 | subdir_refs = [
78 | f"{f}/index"
79 | for f in files_in_dir
80 | if os.path.isdir(os.path.join(src_root, f))
81 | and not f.startswith("_")
82 | and not f.startswith(".")
83 | ]
84 | package_index_rst_path = os.path.join(
85 | rst_root,
86 | "index.rst",
87 | )
88 | log.info(f"Writing {package_index_rst_path}")
89 | write_to_file(
90 | index_template(
91 | base_package_name,
92 | doc_references=module_names + subdir_refs,
93 | text_prefix=_SUBTITLE,
94 | ),
95 | package_index_rst_path,
96 | )
97 |
98 | for root, dirnames, filenames in os.walk(src_root):
99 | if os.path.basename(root).startswith("_"):
100 | continue
101 | base_package_relpath = os.path.relpath(root, start=src_root)
102 | base_package_qualname = package_prefix + os.path.relpath(
103 | root,
104 | start=os.path.dirname(src_root),
105 | ).replace(os.path.sep, ".")
106 |
107 | for dirname in dirnames:
108 | if dirname.startswith("_"):
109 | log.debug(f"Skipping {dirname}")
110 | continue
111 | files_in_dir = os.listdir(os.path.join(root, dirname))
112 | module_names = [
113 | f[:-3] for f in files_in_dir if f.endswith(".py") and not f.startswith("_")
114 | ]
115 | subdir_refs = [
116 | f"{f}/index"
117 | for f in files_in_dir
118 | if os.path.isdir(os.path.join(root, dirname, f)) and not f.startswith("_")
119 | ]
120 | package_qualname = f"{base_package_qualname}.{dirname}"
121 | package_index_rst_path = os.path.join(
122 | rst_root,
123 | base_package_relpath,
124 | dirname,
125 | "index.rst",
126 | )
127 | log.info(f"Writing {package_index_rst_path}")
128 | write_to_file(
129 | index_template(package_qualname, doc_references=module_names + subdir_refs),
130 | package_index_rst_path,
131 | )
132 |
133 | for filename in filenames:
134 | base_name, ext = os.path.splitext(filename)
135 | if ext == ".py" and not filename.startswith("_"):
136 | module_qualname = f"{base_package_qualname}.{filename[:-3]}"
137 |
138 | module_rst_path = os.path.join(rst_root, base_package_relpath, f"{base_name}.rst")
139 | if os.path.exists(module_rst_path) and not overwrite:
140 | log.debug(f"{module_rst_path} already exists, skipping it")
141 |
142 | log.info(f"Writing module documentation to {module_rst_path}")
143 | write_to_file(module_template(module_qualname), module_rst_path)
144 |
145 |
146 | if __name__ == "__main__":
147 | logging.basicConfig(level=logging.INFO)
148 | docs_root = Path(__file__).parent
149 | enable_module_docs = False
150 | if enable_module_docs:
151 | make_rst(
152 | docs_root / ".." / "src" / "serena",
153 | docs_root / "serena",
154 | clean=True,
155 | )
156 |
```
--------------------------------------------------------------------------------
/test/solidlsp/matlab/test_matlab_basic.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Basic integration tests for the MATLAB language server functionality.
3 |
4 | These tests validate the functionality of the language server APIs
5 | like request_document_symbols using the MATLAB test repository.
6 |
7 | Requirements:
8 | - MATLAB R2021b or later must be installed
9 | - MATLAB_PATH environment variable should be set to MATLAB installation directory
10 | - Node.js and npm must be installed
11 | """
12 |
13 | import os
14 |
15 | import pytest
16 |
17 | from solidlsp import SolidLanguageServer
18 | from solidlsp.ls_config import Language
19 |
20 | # Skip all tests if MATLAB is not available
21 | pytestmark = pytest.mark.matlab
22 |
23 | # Check if MATLAB is available
24 | MATLAB_AVAILABLE = os.environ.get("MATLAB_PATH") is not None or any(
25 | os.path.exists(p)
26 | for p in [
27 | "/Applications/MATLAB_R2024b.app",
28 | "/Applications/MATLAB_R2025b.app",
29 | "/Volumes/S1/Applications/MATLAB_R2024b.app",
30 | "/Volumes/S1/Applications/MATLAB_R2025b.app",
31 | ]
32 | )
33 |
34 |
35 | @pytest.mark.skipif(not MATLAB_AVAILABLE, reason="MATLAB installation not found")
36 | class TestMatlabLanguageServerBasics:
37 | """Test basic functionality of the MATLAB language server."""
38 |
39 | @pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
40 | def test_matlab_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
41 | """Test that MATLAB language server can be initialized successfully."""
42 | assert language_server is not None
43 | assert language_server.language == Language.MATLAB
44 |
45 | @pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
46 | def test_matlab_request_document_symbols_class(self, language_server: SolidLanguageServer) -> None:
47 | """Test request_document_symbols for MATLAB class file."""
48 | # Test getting symbols from Calculator.m (class file)
49 | all_symbols, _root_symbols = language_server.request_document_symbols("Calculator.m").get_all_symbols_and_roots()
50 |
51 | # Extract class symbols (LSP Symbol Kind 5 for class)
52 | class_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 5]
53 | class_names = [symbol["name"] for symbol in class_symbols]
54 |
55 | # Should find the Calculator class
56 | assert "Calculator" in class_names, "Should find Calculator class"
57 |
58 | # Extract method symbols (LSP Symbol Kind 6 for method or 12 for function)
59 | method_symbols = [symbol for symbol in all_symbols if symbol.get("kind") in [6, 12]]
60 | method_names = [symbol["name"] for symbol in method_symbols]
61 |
62 | # Should find key methods
63 | expected_methods = ["add", "subtract", "multiply", "divide"]
64 | for method in expected_methods:
65 | assert method in method_names, f"Should find {method} method in Calculator class"
66 |
67 | @pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
68 | def test_matlab_request_document_symbols_function(self, language_server: SolidLanguageServer) -> None:
69 | """Test request_document_symbols for MATLAB function file."""
70 | # Test getting symbols from lib/mathUtils.m (function file)
71 | all_symbols, _root_symbols = language_server.request_document_symbols("lib/mathUtils.m").get_all_symbols_and_roots()
72 |
73 | # Extract function symbols (LSP Symbol Kind 12 for function)
74 | function_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 12]
75 | function_names = [symbol["name"] for symbol in function_symbols]
76 |
77 | # Should find the main mathUtils function
78 | assert "mathUtils" in function_names, "Should find mathUtils function"
79 |
80 | # Should also find nested/local functions
81 | expected_local_functions = ["computeFactorial", "computeFibonacci", "checkPrime", "computeStats"]
82 | for func in expected_local_functions:
83 | assert func in function_names, f"Should find {func} local function"
84 |
85 | @pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
86 | def test_matlab_request_document_symbols_script(self, language_server: SolidLanguageServer) -> None:
87 | """Test request_document_symbols for MATLAB script file."""
88 | # Test getting symbols from main.m (script file)
89 | all_symbols, _root_symbols = language_server.request_document_symbols("main.m").get_all_symbols_and_roots()
90 |
91 | # Scripts may have variables and sections, but less structured symbols
92 | # Just verify we can get symbols without errors
93 | assert all_symbols is not None
94 |
95 |
96 | @pytest.mark.skipif(not MATLAB_AVAILABLE, reason="MATLAB installation not found")
97 | class TestMatlabLanguageServerReferences:
98 | """Test find references functionality of the MATLAB language server."""
99 |
100 | @pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
101 | def test_matlab_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
102 | """Test finding references within a single MATLAB file."""
103 | # Find references to 'result' variable in Calculator.m
104 | # This is a basic test to verify references work
105 | references = language_server.request_references("Calculator.m", 25, 12) # 'result' in add method
106 |
107 | # Should find at least the definition
108 | assert references is not None
109 |
110 | @pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True)
111 | def test_matlab_find_references_cross_file(self, language_server: SolidLanguageServer) -> None:
112 | """Test finding references across MATLAB files."""
113 | # Find references to Calculator class used in main.m
114 | references = language_server.request_references("main.m", 11, 8) # 'Calculator' reference
115 |
116 | # Should find references in both main.m and Calculator.m
117 | assert references is not None
118 |
```