This is page 2 of 21. Use http://codebase.md/oraios/serena?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .devcontainer
│ └── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── config.yml
│ │ ├── feature_request.md
│ │ └── issue--bug--performance-problem--question-.md
│ └── workflows
│ ├── codespell.yml
│ ├── docker.yml
│ ├── docs.yaml
│ ├── junie.yml
│ ├── publish.yml
│ └── pytest.yml
├── .gitignore
├── .serena
│ ├── .gitignore
│ ├── memories
│ │ ├── adding_new_language_support_guide.md
│ │ ├── serena_core_concepts_and_architecture.md
│ │ ├── serena_repository_structure.md
│ │ └── suggested_commands.md
│ └── project.yml
├── .vscode
│ └── settings.json
├── CHANGELOG.md
├── CLAUDE.md
├── compose.yaml
├── CONTRIBUTING.md
├── docker_build_and_run.sh
├── DOCKER.md
├── Dockerfile
├── docs
│ ├── _config.yml
│ ├── _static
│ │ └── images
│ │ └── jetbrains-marketplace-button.png
│ ├── .gitignore
│ ├── 01-about
│ │ ├── 000_intro.md
│ │ ├── 010_llm-integration.md
│ │ ├── 020_programming-languages.md
│ │ ├── 030_serena-in-action.md
│ │ ├── 035_tools.md
│ │ ├── 040_comparison-to-other-agents.md
│ │ └── 050_acknowledgements.md
│ ├── 02-usage
│ │ ├── 000_intro.md
│ │ ├── 010_prerequisites.md
│ │ ├── 020_running.md
│ │ ├── 025_jetbrains_plugin.md
│ │ ├── 030_clients.md
│ │ ├── 040_workflow.md
│ │ ├── 050_configuration.md
│ │ ├── 060_dashboard.md
│ │ ├── 070_security.md
│ │ └── 999_additional-usage.md
│ ├── 03-special-guides
│ │ ├── 000_intro.md
│ │ ├── custom_agent.md
│ │ ├── groovy_setup_guide_for_serena.md
│ │ ├── scala_setup_guide_for_serena.md
│ │ └── serena_on_chatgpt.md
│ ├── autogen_rst.py
│ ├── create_toc.py
│ └── index.md
├── flake.lock
├── flake.nix
├── lessons_learned.md
├── LICENSE
├── llms-install.md
├── pyproject.toml
├── README.md
├── repo_dir_sync.py
├── resources
│ ├── jetbrains-marketplace-button.cdr
│ ├── serena-icons.cdr
│ ├── serena-logo-dark-mode.svg
│ ├── serena-logo.cdr
│ ├── serena-logo.svg
│ └── vscode_sponsor_logo.png
├── roadmap.md
├── scripts
│ ├── agno_agent.py
│ ├── demo_run_tools.py
│ ├── gen_prompt_factory.py
│ ├── mcp_server.py
│ ├── print_mode_context_options.py
│ ├── print_tool_overview.py
│ └── profile_tool_call.py
├── src
│ ├── interprompt
│ │ ├── __init__.py
│ │ ├── .syncCommitId.remote
│ │ ├── .syncCommitId.this
│ │ ├── jinja_template.py
│ │ ├── multilang_prompt.py
│ │ ├── prompt_factory.py
│ │ └── util
│ │ ├── __init__.py
│ │ └── class_decorators.py
│ ├── README.md
│ ├── serena
│ │ ├── __init__.py
│ │ ├── agent.py
│ │ ├── agno.py
│ │ ├── analytics.py
│ │ ├── cli.py
│ │ ├── code_editor.py
│ │ ├── config
│ │ │ ├── __init__.py
│ │ │ ├── context_mode.py
│ │ │ └── serena_config.py
│ │ ├── constants.py
│ │ ├── dashboard.py
│ │ ├── generated
│ │ │ └── generated_prompt_factory.py
│ │ ├── gui_log_viewer.py
│ │ ├── ls_manager.py
│ │ ├── mcp.py
│ │ ├── project.py
│ │ ├── prompt_factory.py
│ │ ├── resources
│ │ │ ├── config
│ │ │ │ ├── contexts
│ │ │ │ │ ├── agent.yml
│ │ │ │ │ ├── chatgpt.yml
│ │ │ │ │ ├── claude-code.yml
│ │ │ │ │ ├── codex.yml
│ │ │ │ │ ├── context.template.yml
│ │ │ │ │ ├── desktop-app.yml
│ │ │ │ │ ├── ide.yml
│ │ │ │ │ └── oaicompat-agent.yml
│ │ │ │ ├── internal_modes
│ │ │ │ │ └── jetbrains.yml
│ │ │ │ ├── modes
│ │ │ │ │ ├── editing.yml
│ │ │ │ │ ├── interactive.yml
│ │ │ │ │ ├── mode.template.yml
│ │ │ │ │ ├── no-memories.yml
│ │ │ │ │ ├── no-onboarding.yml
│ │ │ │ │ ├── onboarding.yml
│ │ │ │ │ ├── one-shot.yml
│ │ │ │ │ └── planning.yml
│ │ │ │ └── prompt_templates
│ │ │ │ ├── simple_tool_outputs.yml
│ │ │ │ └── system_prompt.yml
│ │ │ ├── dashboard
│ │ │ │ ├── dashboard.css
│ │ │ │ ├── dashboard.js
│ │ │ │ ├── index.html
│ │ │ │ ├── jquery.min.js
│ │ │ │ ├── serena-icon-16.png
│ │ │ │ ├── serena-icon-32.png
│ │ │ │ ├── serena-icon-48.png
│ │ │ │ ├── serena-logo-dark-mode.svg
│ │ │ │ ├── serena-logo.svg
│ │ │ │ ├── serena-logs-dark-mode.png
│ │ │ │ └── serena-logs.png
│ │ │ ├── project.template.yml
│ │ │ └── serena_config.template.yml
│ │ ├── symbol.py
│ │ ├── task_executor.py
│ │ ├── text_utils.py
│ │ ├── tools
│ │ │ ├── __init__.py
│ │ │ ├── cmd_tools.py
│ │ │ ├── config_tools.py
│ │ │ ├── file_tools.py
│ │ │ ├── jetbrains_plugin_client.py
│ │ │ ├── jetbrains_tools.py
│ │ │ ├── memory_tools.py
│ │ │ ├── symbol_tools.py
│ │ │ ├── tools_base.py
│ │ │ └── workflow_tools.py
│ │ └── util
│ │ ├── class_decorators.py
│ │ ├── cli_util.py
│ │ ├── exception.py
│ │ ├── file_system.py
│ │ ├── general.py
│ │ ├── git.py
│ │ ├── gui.py
│ │ ├── inspection.py
│ │ ├── logging.py
│ │ ├── shell.py
│ │ └── thread.py
│ └── solidlsp
│ ├── __init__.py
│ ├── .gitignore
│ ├── language_servers
│ │ ├── al_language_server.py
│ │ ├── bash_language_server.py
│ │ ├── clangd_language_server.py
│ │ ├── clojure_lsp.py
│ │ ├── common.py
│ │ ├── csharp_language_server.py
│ │ ├── dart_language_server.py
│ │ ├── eclipse_jdtls.py
│ │ ├── elixir_tools
│ │ │ ├── __init__.py
│ │ │ ├── elixir_tools.py
│ │ │ └── README.md
│ │ ├── elm_language_server.py
│ │ ├── erlang_language_server.py
│ │ ├── fortran_language_server.py
│ │ ├── fsharp_language_server.py
│ │ ├── gopls.py
│ │ ├── groovy_language_server.py
│ │ ├── haskell_language_server.py
│ │ ├── intelephense.py
│ │ ├── jedi_server.py
│ │ ├── julia_server.py
│ │ ├── kotlin_language_server.py
│ │ ├── lua_ls.py
│ │ ├── marksman.py
│ │ ├── matlab_language_server.py
│ │ ├── nixd_ls.py
│ │ ├── omnisharp
│ │ │ ├── initialize_params.json
│ │ │ ├── runtime_dependencies.json
│ │ │ └── workspace_did_change_configuration.json
│ │ ├── omnisharp.py
│ │ ├── pascal_server.py
│ │ ├── perl_language_server.py
│ │ ├── powershell_language_server.py
│ │ ├── pyright_server.py
│ │ ├── r_language_server.py
│ │ ├── regal_server.py
│ │ ├── ruby_lsp.py
│ │ ├── rust_analyzer.py
│ │ ├── scala_language_server.py
│ │ ├── solargraph.py
│ │ ├── sourcekit_lsp.py
│ │ ├── taplo_server.py
│ │ ├── terraform_ls.py
│ │ ├── typescript_language_server.py
│ │ ├── vts_language_server.py
│ │ ├── vue_language_server.py
│ │ ├── yaml_language_server.py
│ │ └── zls.py
│ ├── ls_config.py
│ ├── ls_exceptions.py
│ ├── ls_handler.py
│ ├── ls_request.py
│ ├── ls_types.py
│ ├── ls_utils.py
│ ├── ls.py
│ ├── lsp_protocol_handler
│ │ ├── lsp_constants.py
│ │ ├── lsp_requests.py
│ │ ├── lsp_types.py
│ │ └── server.py
│ ├── settings.py
│ └── util
│ ├── cache.py
│ ├── subprocess_util.py
│ └── zip.py
├── sync.py
├── test
│ ├── __init__.py
│ ├── conftest.py
│ ├── resources
│ │ └── repos
│ │ ├── al
│ │ │ └── test_repo
│ │ │ ├── app.json
│ │ │ └── src
│ │ │ ├── Codeunits
│ │ │ │ ├── CustomerMgt.Codeunit.al
│ │ │ │ └── PaymentProcessorImpl.Codeunit.al
│ │ │ ├── Enums
│ │ │ │ └── CustomerType.Enum.al
│ │ │ ├── Interfaces
│ │ │ │ └── IPaymentProcessor.Interface.al
│ │ │ ├── Pages
│ │ │ │ ├── CustomerCard.Page.al
│ │ │ │ └── CustomerList.Page.al
│ │ │ ├── TableExtensions
│ │ │ │ └── Item.TableExt.al
│ │ │ └── Tables
│ │ │ └── Customer.Table.al
│ │ ├── bash
│ │ │ └── test_repo
│ │ │ ├── config.sh
│ │ │ ├── main.sh
│ │ │ └── utils.sh
│ │ ├── clojure
│ │ │ └── test_repo
│ │ │ ├── deps.edn
│ │ │ └── src
│ │ │ └── test_app
│ │ │ ├── core.clj
│ │ │ └── utils.clj
│ │ ├── csharp
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── Models
│ │ │ │ └── Person.cs
│ │ │ ├── Program.cs
│ │ │ ├── serena.sln
│ │ │ └── TestProject.csproj
│ │ ├── dart
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── lib
│ │ │ │ ├── helper.dart
│ │ │ │ ├── main.dart
│ │ │ │ └── models.dart
│ │ │ └── pubspec.yaml
│ │ ├── elixir
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── lib
│ │ │ │ ├── examples.ex
│ │ │ │ ├── ignored_dir
│ │ │ │ │ └── ignored_module.ex
│ │ │ │ ├── models.ex
│ │ │ │ ├── services.ex
│ │ │ │ ├── test_repo.ex
│ │ │ │ └── utils.ex
│ │ │ ├── mix.exs
│ │ │ ├── mix.lock
│ │ │ ├── scripts
│ │ │ │ └── build_script.ex
│ │ │ └── test
│ │ │ ├── models_test.exs
│ │ │ └── test_repo_test.exs
│ │ ├── elm
│ │ │ └── test_repo
│ │ │ ├── elm.json
│ │ │ ├── Main.elm
│ │ │ └── Utils.elm
│ │ ├── erlang
│ │ │ └── test_repo
│ │ │ ├── hello.erl
│ │ │ ├── ignored_dir
│ │ │ │ └── ignored_module.erl
│ │ │ ├── include
│ │ │ │ ├── records.hrl
│ │ │ │ └── types.hrl
│ │ │ ├── math_utils.erl
│ │ │ ├── rebar.config
│ │ │ ├── src
│ │ │ │ ├── app.erl
│ │ │ │ ├── models.erl
│ │ │ │ ├── services.erl
│ │ │ │ └── utils.erl
│ │ │ └── test
│ │ │ ├── models_tests.erl
│ │ │ └── utils_tests.erl
│ │ ├── fortran
│ │ │ └── test_repo
│ │ │ ├── main.f90
│ │ │ └── modules
│ │ │ ├── geometry.f90
│ │ │ └── math_utils.f90
│ │ ├── fsharp
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── Calculator.fs
│ │ │ ├── Models
│ │ │ │ └── Person.fs
│ │ │ ├── Program.fs
│ │ │ ├── README.md
│ │ │ └── TestProject.fsproj
│ │ ├── go
│ │ │ └── test_repo
│ │ │ └── main.go
│ │ ├── groovy
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── build.gradle
│ │ │ └── src
│ │ │ └── main
│ │ │ └── groovy
│ │ │ └── com
│ │ │ └── example
│ │ │ ├── Main.groovy
│ │ │ ├── Model.groovy
│ │ │ ├── ModelUser.groovy
│ │ │ └── Utils.groovy
│ │ ├── haskell
│ │ │ └── test_repo
│ │ │ ├── app
│ │ │ │ └── Main.hs
│ │ │ ├── haskell-test-repo.cabal
│ │ │ ├── package.yaml
│ │ │ ├── src
│ │ │ │ ├── Calculator.hs
│ │ │ │ └── Helper.hs
│ │ │ └── stack.yaml
│ │ ├── java
│ │ │ └── test_repo
│ │ │ ├── pom.xml
│ │ │ └── src
│ │ │ └── main
│ │ │ └── java
│ │ │ └── test_repo
│ │ │ ├── Main.java
│ │ │ ├── Model.java
│ │ │ ├── ModelUser.java
│ │ │ └── Utils.java
│ │ ├── julia
│ │ │ └── test_repo
│ │ │ ├── lib
│ │ │ │ └── helper.jl
│ │ │ └── main.jl
│ │ ├── kotlin
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── build.gradle.kts
│ │ │ └── src
│ │ │ └── main
│ │ │ └── kotlin
│ │ │ └── test_repo
│ │ │ ├── Main.kt
│ │ │ ├── Model.kt
│ │ │ ├── ModelUser.kt
│ │ │ └── Utils.kt
│ │ ├── lua
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── main.lua
│ │ │ ├── src
│ │ │ │ ├── calculator.lua
│ │ │ │ └── utils.lua
│ │ │ └── tests
│ │ │ └── test_calculator.lua
│ │ ├── markdown
│ │ │ └── test_repo
│ │ │ ├── api.md
│ │ │ ├── CONTRIBUTING.md
│ │ │ ├── guide.md
│ │ │ └── README.md
│ │ ├── matlab
│ │ │ └── test_repo
│ │ │ ├── Calculator.m
│ │ │ └── main.m
│ │ ├── nix
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── default.nix
│ │ │ ├── flake.nix
│ │ │ ├── lib
│ │ │ │ └── utils.nix
│ │ │ ├── modules
│ │ │ │ └── example.nix
│ │ │ └── scripts
│ │ │ └── hello.sh
│ │ ├── pascal
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── lib
│ │ │ │ └── helper.pas
│ │ │ └── main.pas
│ │ ├── perl
│ │ │ └── test_repo
│ │ │ ├── helper.pl
│ │ │ └── main.pl
│ │ ├── php
│ │ │ └── test_repo
│ │ │ ├── helper.php
│ │ │ ├── index.php
│ │ │ └── simple_var.php
│ │ ├── powershell
│ │ │ └── test_repo
│ │ │ ├── main.ps1
│ │ │ ├── PowerShellEditorServices.json
│ │ │ └── utils.ps1
│ │ ├── python
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── custom_test
│ │ │ │ ├── __init__.py
│ │ │ │ └── advanced_features.py
│ │ │ ├── examples
│ │ │ │ ├── __init__.py
│ │ │ │ └── user_management.py
│ │ │ ├── ignore_this_dir_with_postfix
│ │ │ │ └── ignored_module.py
│ │ │ ├── scripts
│ │ │ │ ├── __init__.py
│ │ │ │ └── run_app.py
│ │ │ └── test_repo
│ │ │ ├── __init__.py
│ │ │ ├── complex_types.py
│ │ │ ├── models.py
│ │ │ ├── name_collisions.py
│ │ │ ├── nested_base.py
│ │ │ ├── nested.py
│ │ │ ├── overloaded.py
│ │ │ ├── services.py
│ │ │ ├── utils.py
│ │ │ └── variables.py
│ │ ├── r
│ │ │ └── test_repo
│ │ │ ├── .Rbuildignore
│ │ │ ├── DESCRIPTION
│ │ │ ├── examples
│ │ │ │ └── analysis.R
│ │ │ ├── NAMESPACE
│ │ │ └── R
│ │ │ ├── models.R
│ │ │ └── utils.R
│ │ ├── rego
│ │ │ └── test_repo
│ │ │ ├── policies
│ │ │ │ ├── authz.rego
│ │ │ │ └── validation.rego
│ │ │ └── utils
│ │ │ └── helpers.rego
│ │ ├── ruby
│ │ │ └── test_repo
│ │ │ ├── .solargraph.yml
│ │ │ ├── examples
│ │ │ │ └── user_management.rb
│ │ │ ├── lib.rb
│ │ │ ├── main.rb
│ │ │ ├── models.rb
│ │ │ ├── nested.rb
│ │ │ ├── services.rb
│ │ │ └── variables.rb
│ │ ├── rust
│ │ │ ├── test_repo
│ │ │ │ ├── Cargo.lock
│ │ │ │ ├── Cargo.toml
│ │ │ │ └── src
│ │ │ │ ├── lib.rs
│ │ │ │ └── main.rs
│ │ │ └── test_repo_2024
│ │ │ ├── Cargo.lock
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── main.rs
│ │ ├── scala
│ │ │ ├── build.sbt
│ │ │ ├── project
│ │ │ │ ├── build.properties
│ │ │ │ ├── metals.sbt
│ │ │ │ └── plugins.sbt
│ │ │ └── src
│ │ │ └── main
│ │ │ └── scala
│ │ │ └── com
│ │ │ └── example
│ │ │ ├── Main.scala
│ │ │ └── Utils.scala
│ │ ├── swift
│ │ │ └── test_repo
│ │ │ ├── Package.swift
│ │ │ └── src
│ │ │ ├── main.swift
│ │ │ └── utils.swift
│ │ ├── terraform
│ │ │ └── test_repo
│ │ │ ├── data.tf
│ │ │ ├── main.tf
│ │ │ ├── outputs.tf
│ │ │ └── variables.tf
│ │ ├── toml
│ │ │ └── test_repo
│ │ │ ├── Cargo.toml
│ │ │ ├── config.toml
│ │ │ └── pyproject.toml
│ │ ├── typescript
│ │ │ └── test_repo
│ │ │ ├── .serena
│ │ │ │ └── project.yml
│ │ │ ├── index.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── use_helper.ts
│ │ │ └── ws_manager.js
│ │ ├── vue
│ │ │ └── test_repo
│ │ │ ├── .gitignore
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── src
│ │ │ │ ├── App.vue
│ │ │ │ ├── components
│ │ │ │ │ ├── CalculatorButton.vue
│ │ │ │ │ ├── CalculatorDisplay.vue
│ │ │ │ │ └── CalculatorInput.vue
│ │ │ │ ├── composables
│ │ │ │ │ ├── useFormatter.ts
│ │ │ │ │ └── useTheme.ts
│ │ │ │ ├── main.ts
│ │ │ │ ├── stores
│ │ │ │ │ └── calculator.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── tsconfig.node.json
│ │ │ └── vite.config.ts
│ │ ├── yaml
│ │ │ └── test_repo
│ │ │ ├── config.yaml
│ │ │ ├── data.yaml
│ │ │ └── services.yml
│ │ └── zig
│ │ └── test_repo
│ │ ├── .gitignore
│ │ ├── build.zig
│ │ ├── src
│ │ │ ├── calculator.zig
│ │ │ ├── main.zig
│ │ │ └── math_utils.zig
│ │ └── zls.json
│ ├── serena
│ │ ├── __init__.py
│ │ ├── __snapshots__
│ │ │ └── test_symbol_editing.ambr
│ │ ├── config
│ │ │ ├── __init__.py
│ │ │ └── test_serena_config.py
│ │ ├── test_cli_project_commands.py
│ │ ├── test_edit_marker.py
│ │ ├── test_mcp.py
│ │ ├── test_serena_agent.py
│ │ ├── test_symbol_editing.py
│ │ ├── test_symbol.py
│ │ ├── test_task_executor.py
│ │ ├── test_text_utils.py
│ │ ├── test_tool_parameter_types.py
│ │ └── util
│ │ ├── test_exception.py
│ │ └── test_file_system.py
│ └── solidlsp
│ ├── al
│ │ └── test_al_basic.py
│ ├── bash
│ │ ├── __init__.py
│ │ └── test_bash_basic.py
│ ├── clojure
│ │ ├── __init__.py
│ │ └── test_clojure_basic.py
│ ├── csharp
│ │ └── test_csharp_basic.py
│ ├── dart
│ │ ├── __init__.py
│ │ └── test_dart_basic.py
│ ├── elixir
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_elixir_basic.py
│ │ ├── test_elixir_ignored_dirs.py
│ │ ├── test_elixir_integration.py
│ │ └── test_elixir_symbol_retrieval.py
│ ├── elm
│ │ └── test_elm_basic.py
│ ├── erlang
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_erlang_basic.py
│ │ ├── test_erlang_ignored_dirs.py
│ │ └── test_erlang_symbol_retrieval.py
│ ├── fortran
│ │ ├── __init__.py
│ │ └── test_fortran_basic.py
│ ├── fsharp
│ │ └── test_fsharp_basic.py
│ ├── go
│ │ └── test_go_basic.py
│ ├── groovy
│ │ └── test_groovy_basic.py
│ ├── haskell
│ │ ├── __init__.py
│ │ └── test_haskell_basic.py
│ ├── java
│ │ └── test_java_basic.py
│ ├── julia
│ │ └── test_julia_basic.py
│ ├── kotlin
│ │ └── test_kotlin_basic.py
│ ├── lua
│ │ └── test_lua_basic.py
│ ├── markdown
│ │ ├── __init__.py
│ │ └── test_markdown_basic.py
│ ├── matlab
│ │ ├── __init__.py
│ │ └── test_matlab_basic.py
│ ├── nix
│ │ └── test_nix_basic.py
│ ├── pascal
│ │ ├── __init__.py
│ │ └── test_pascal_basic.py
│ ├── perl
│ │ └── test_perl_basic.py
│ ├── php
│ │ └── test_php_basic.py
│ ├── powershell
│ │ ├── __init__.py
│ │ └── test_powershell_basic.py
│ ├── python
│ │ ├── test_python_basic.py
│ │ ├── test_retrieval_with_ignored_dirs.py
│ │ └── test_symbol_retrieval.py
│ ├── r
│ │ ├── __init__.py
│ │ └── test_r_basic.py
│ ├── rego
│ │ └── test_rego_basic.py
│ ├── ruby
│ │ ├── test_ruby_basic.py
│ │ └── test_ruby_symbol_retrieval.py
│ ├── rust
│ │ ├── test_rust_2024_edition.py
│ │ ├── test_rust_analyzer_detection.py
│ │ └── test_rust_basic.py
│ ├── scala
│ │ └── test_scala_language_server.py
│ ├── swift
│ │ └── test_swift_basic.py
│ ├── terraform
│ │ └── test_terraform_basic.py
│ ├── test_lsp_protocol_handler_server.py
│ ├── toml
│ │ ├── __init__.py
│ │ ├── test_toml_basic.py
│ │ ├── test_toml_edge_cases.py
│ │ ├── test_toml_ignored_dirs.py
│ │ └── test_toml_symbol_retrieval.py
│ ├── typescript
│ │ └── test_typescript_basic.py
│ ├── util
│ │ └── test_zip.py
│ ├── vue
│ │ ├── __init__.py
│ │ ├── test_vue_basic.py
│ │ ├── test_vue_error_cases.py
│ │ ├── test_vue_rename.py
│ │ └── test_vue_symbol_retrieval.py
│ ├── yaml_ls
│ │ ├── __init__.py
│ │ └── test_yaml_basic.py
│ └── zig
│ └── test_zig_basic.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/test/resources/repos/bash/test_repo/utils.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # Utility functions for bash scripting
4 |
5 | # String manipulation functions
6 | function to_uppercase() {
7 | echo "${1^^}"
8 | }
9 |
10 | function to_lowercase() {
11 | echo "${1,,}"
12 | }
13 |
14 | function trim_whitespace() {
15 | local var="$1"
16 | var="${var#"${var%%[![:space:]]*}"}"
17 | var="${var%"${var##*[![:space:]]}"}"
18 | echo "$var"
19 | }
20 |
21 | # File operations
22 | function backup_file() {
23 | local file="$1"
24 | local backup_dir="${2:-./backups}"
25 |
26 | if [[ ! -f "$file" ]]; then
27 | echo "Error: File '$file' does not exist" >&2
28 | return 1
29 | fi
30 |
31 | mkdir -p "$backup_dir"
32 | cp "$file" "${backup_dir}/$(basename "$file").$(date +%Y%m%d_%H%M%S).bak"
33 | echo "Backup created for $file"
34 | }
35 |
36 | # Array operations
37 | function contains_element() {
38 | local element="$1"
39 | shift
40 | local array=("$@")
41 |
42 | for item in "${array[@]}"; do
43 | if [[ "$item" == "$element" ]]; then
44 | return 0
45 | fi
46 | done
47 | return 1
48 | }
49 |
50 | # Logging functions
51 | function log_message() {
52 | local level="$1"
53 | local message="$2"
54 | local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
55 |
56 | case "$level" in
57 | "ERROR")
58 | echo "[$timestamp] ERROR: $message" >&2
59 | ;;
60 | "WARN")
61 | echo "[$timestamp] WARN: $message" >&2
62 | ;;
63 | "INFO")
64 | echo "[$timestamp] INFO: $message"
65 | ;;
66 | "DEBUG")
67 | [[ "${DEBUG:-false}" == "true" ]] && echo "[$timestamp] DEBUG: $message"
68 | ;;
69 | *)
70 | echo "[$timestamp] $message"
71 | ;;
72 | esac
73 | }
74 |
75 | # Validation functions
76 | function is_valid_email() {
77 | local email="$1"
78 | [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
79 | }
80 |
81 | function is_number() {
82 | [[ $1 =~ ^[0-9]+$ ]]
83 | }
84 |
```
--------------------------------------------------------------------------------
/test/solidlsp/erlang/test_erlang_basic.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Basic integration tests for the Erlang 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 pytest
9 |
10 | from solidlsp import SolidLanguageServer
11 | from solidlsp.ls_config import Language
12 |
13 | from . import ERLANG_LS_UNAVAILABLE, ERLANG_LS_UNAVAILABLE_REASON
14 |
15 |
16 | @pytest.mark.erlang
17 | @pytest.mark.skipif(ERLANG_LS_UNAVAILABLE, reason=f"Erlang LS not available: {ERLANG_LS_UNAVAILABLE_REASON}")
18 | class TestErlangLanguageServerBasics:
19 | """Test basic functionality of the Erlang language server."""
20 |
21 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
22 | def test_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
23 | """Test that the Erlang language server initializes properly."""
24 | assert language_server is not None
25 | assert language_server.language == Language.ERLANG
26 |
27 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
28 | def test_document_symbols(self, language_server: SolidLanguageServer) -> None:
29 | """Test document symbols retrieval for Erlang files."""
30 | try:
31 | file_path = "hello.erl"
32 | symbols_tuple = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
33 | assert isinstance(symbols_tuple, tuple)
34 | assert len(symbols_tuple) == 2
35 |
36 | all_symbols, root_symbols = symbols_tuple
37 | assert isinstance(all_symbols, list)
38 | assert isinstance(root_symbols, list)
39 | except Exception as e:
40 | if "not fully initialized" in str(e):
41 | pytest.skip("Erlang language server not fully initialized")
42 | else:
43 | raise
44 |
```
--------------------------------------------------------------------------------
/src/serena/util/general.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | from dataclasses import MISSING, Field
3 | from typing import Any, Literal, cast, overload
4 |
5 | from ruamel.yaml import YAML
6 | from ruamel.yaml.comments import CommentedMap
7 |
8 | from serena.constants import SERENA_FILE_ENCODING
9 |
10 |
11 | def _create_YAML(preserve_comments: bool = False) -> YAML:
12 | """
13 | Creates a YAML that can load/save with comments if preserve_comments is True.
14 | """
15 | typ = None if preserve_comments else "safe"
16 | result = YAML(typ=typ)
17 | result.preserve_quotes = preserve_comments
18 | return result
19 |
20 |
21 | @overload
22 | def load_yaml(path: str, preserve_comments: Literal[False]) -> dict: ...
23 | @overload
24 | def load_yaml(path: str, preserve_comments: Literal[True]) -> CommentedMap: ...
25 | def load_yaml(path: str, preserve_comments: bool = False) -> dict | CommentedMap:
26 | with open(path, encoding=SERENA_FILE_ENCODING) as f:
27 | yaml = _create_YAML(preserve_comments)
28 | return yaml.load(f)
29 |
30 |
31 | def save_yaml(path: str, data: dict | CommentedMap, preserve_comments: bool = False) -> None:
32 | yaml = _create_YAML(preserve_comments)
33 | os.makedirs(os.path.dirname(path), exist_ok=True)
34 | with open(path, "w", encoding=SERENA_FILE_ENCODING) as f:
35 | yaml.dump(data, f)
36 |
37 |
38 | def get_dataclass_default(cls: type, field_name: str) -> Any:
39 | """
40 | Gets the default value of a dataclass field.
41 |
42 | :param cls: The dataclass type.
43 | :param field_name: The name of the field.
44 | :return: The default value of the field (either from default or default_factory).
45 | """
46 | field = cast(Field, cls.__dataclass_fields__[field_name]) # type: ignore[attr-defined]
47 |
48 | if field.default is not MISSING:
49 | return field.default
50 |
51 | if field.default_factory is not MISSING: # default_factory is a function
52 | return field.default_factory()
53 |
54 | raise AttributeError(f"{field_name} has no default")
55 |
```
--------------------------------------------------------------------------------
/docs/02-usage/060_dashboard.md:
--------------------------------------------------------------------------------
```markdown
1 | # The Dashboard and GUI Tool
2 |
3 | Serena comes with built-in tools for monitoring and managing the current session:
4 |
5 | * the **web-based dashboard** (enabled by default)
6 |
7 | The dashboard provides detailed information on your Serena session, the current configuration and provides access to logs.
8 | Some settings (e.g. the current set of active programming languages) can also be directly modified through the dashboard.
9 |
10 | The dashboard is supported on all platforms.
11 |
12 | By default, it will be accessible at `http://localhost:24282/dashboard/index.html`,
13 | but a higher port may be used if the default port is unavailable/multiple instances are running.
14 |
15 | * the **GUI tool** (disabled by default)
16 |
17 | The GUI tool is a native application window which displays logs.
18 | It furthermore allows you to shut down the agent and to access the dashboard's URL (if it is running).
19 |
20 | This is mainly supported on Windows, but it may also work on Linux; macOS is unsupported.
21 |
22 | Both can be configured in Serena's [configuration](050_configuration) file (`serena_config.yml`).
23 | If enabled, they will automatically be opened as soon as the Serena agent/MCP server is started.
24 | For the dashboard, this can be disabled if desired (see below).
25 |
26 | ## Disabling Automatic Browser Opening
27 |
28 | If you prefer not to have the dashboard open automatically (e.g., to avoid focus stealing), you can disable it
29 | by setting `web_dashboard_open_on_launch: False` in your `serena_config.yml`.
30 |
31 | When automatic opening is disabled, you can still access the dashboard by:
32 | * asking the LLM to "open the Serena dashboard", which will open the dashboard in your default browser
33 | (the tool `open_dashboard` is enabled for this purpose, provided that the dashboard is active,
34 | not opened by default and the GUI tool, which can provide the URL, is not enabled)
35 | * navigating directly to the URL (see above)
36 |
```
--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/nested_base.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Module to test parsing of classes with nested module paths in base classes.
3 | """
4 |
5 | from typing import Generic, TypeVar
6 |
7 | T = TypeVar("T")
8 |
9 |
10 | class BaseModule:
11 | """Base module class for nested module tests."""
12 |
13 |
14 | class SubModule:
15 | """Sub-module class for nested paths."""
16 |
17 | class NestedBase:
18 | """Nested base class."""
19 |
20 | def base_method(self):
21 | """Base method."""
22 | return "base"
23 |
24 | class NestedLevel2:
25 | """Nested level 2."""
26 |
27 | def nested_level_2_method(self):
28 | """Nested level 2 method."""
29 | return "nested_level_2"
30 |
31 | class GenericBase(Generic[T]):
32 | """Generic nested base class."""
33 |
34 | def generic_method(self, value: T) -> T:
35 | """Generic method."""
36 | return value
37 |
38 |
39 | # Classes extending base classes with single-level nesting
40 | class FirstLevel(SubModule):
41 | """Class extending a class from a nested module path."""
42 |
43 | def first_level_method(self):
44 | """First level method."""
45 | return "first"
46 |
47 |
48 | # Classes extending base classes with multi-level nesting
49 | class TwoLevel(SubModule.NestedBase):
50 | """Class extending a doubly-nested base class."""
51 |
52 | def multi_level_method(self):
53 | """Multi-level method."""
54 | return "multi"
55 |
56 | def base_method(self):
57 | """Override of base method."""
58 | return "overridden"
59 |
60 |
61 | class ThreeLevel(SubModule.NestedBase.NestedLevel2):
62 | """Class extending a triply-nested base class."""
63 |
64 | def three_level_method(self):
65 | """Three-level method."""
66 | return "three"
67 |
68 |
69 | # Class extending a generic base class with nesting
70 | class GenericExtension(SubModule.GenericBase[str]):
71 | """Class extending a generic nested base class."""
72 |
73 | def generic_extension_method(self, text: str) -> str:
74 | """Extension method."""
75 | return f"Extended: {text}"
76 |
```
--------------------------------------------------------------------------------
/src/serena/util/inspection.py:
--------------------------------------------------------------------------------
```python
1 | import logging
2 | import os
3 | from collections.abc import Generator
4 | from typing import TypeVar
5 |
6 | from serena.util.file_system import find_all_non_ignored_files
7 | from solidlsp.ls_config import Language
8 |
9 | T = TypeVar("T")
10 |
11 | log = logging.getLogger(__name__)
12 |
13 |
14 | def iter_subclasses(cls: type[T], recursive: bool = True) -> Generator[type[T], None, None]:
15 | """Iterate over all subclasses of a class. If recursive is True, also iterate over all subclasses of all subclasses."""
16 | for subclass in cls.__subclasses__():
17 | yield subclass
18 | if recursive:
19 | yield from iter_subclasses(subclass, recursive)
20 |
21 |
22 | def determine_programming_language_composition(repo_path: str) -> dict[Language, float]:
23 | """
24 | Determine the programming language composition of a repository.
25 |
26 | :param repo_path: Path to the repository to analyze
27 |
28 | :return: Dictionary mapping languages to percentages of files matching each language
29 | """
30 | all_files = find_all_non_ignored_files(repo_path)
31 |
32 | if not all_files:
33 | return {}
34 |
35 | # Count files for each language
36 | language_counts: dict[Language, int] = {}
37 | total_files = len(all_files)
38 |
39 | for language in Language.iter_all(include_experimental=False):
40 | matcher = language.get_source_fn_matcher()
41 | count = 0
42 |
43 | for file_path in all_files:
44 | # Use just the filename for matching, not the full path
45 | filename = os.path.basename(file_path)
46 | if matcher.is_relevant_filename(filename):
47 | count += 1
48 |
49 | if count > 0:
50 | language_counts[language] = count
51 |
52 | # Convert counts to percentages
53 | language_percentages: dict[Language, float] = {}
54 | for language, count in language_counts.items():
55 | percentage = (count / total_files) * 100
56 | language_percentages[language] = round(percentage, 2)
57 |
58 | return language_percentages
59 |
```
--------------------------------------------------------------------------------
/src/solidlsp/ls_exceptions.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | This module contains the exceptions raised by the framework.
3 | """
4 |
5 | from solidlsp.ls_config import Language
6 |
7 |
8 | class SolidLSPException(Exception):
9 | def __init__(self, message: str, cause: Exception | None = None) -> None:
10 | """
11 | Initializes the exception with the given message.
12 |
13 | :param message: the message describing the exception
14 | :param cause: the original exception that caused this exception, if any.
15 | For exceptions raised during request handling, this is typically
16 | * an LSPError for errors returned by the LSP server
17 | * LanguageServerTerminatedException for errors due to the language server having terminated.
18 | """
19 | self.cause = cause
20 | super().__init__(message)
21 |
22 | def is_language_server_terminated(self) -> bool:
23 | """
24 | :return: True if the exception is caused by the language server having terminated as indicated
25 | by the causing exception being an instance of LanguageServerTerminatedException.
26 | """
27 | from .ls_handler import LanguageServerTerminatedException
28 |
29 | return isinstance(self.cause, LanguageServerTerminatedException)
30 |
31 | def get_affected_language(self) -> Language | None:
32 | """
33 | :return: the affected language for the case where the exception is caused by the language server having terminated
34 | """
35 | from .ls_handler import LanguageServerTerminatedException
36 |
37 | if isinstance(self.cause, LanguageServerTerminatedException):
38 | return self.cause.language
39 | return None
40 |
41 | def __str__(self) -> str:
42 | """
43 | Returns a string representation of the exception.
44 | """
45 | s = super().__str__()
46 | if self.cause:
47 | if "\n" in s:
48 | s += "\n"
49 | else:
50 | s += " "
51 | s += f"(caused by {self.cause})"
52 | return s
53 |
```
--------------------------------------------------------------------------------
/docs/01-about/050_acknowledgements.md:
--------------------------------------------------------------------------------
```markdown
1 | # Acknowledgements
2 |
3 | ## Sponsors
4 |
5 | We are very grateful to our [sponsors](https://github.com/sponsors/oraios), who help us drive Serena's development.
6 | The core team (the founders of [Oraios AI](https://oraios-ai.de/)) put in a lot of work in order to turn Serena into a useful open source project.
7 | So far, there is no business model behind this project, and sponsors are our only source of income from it.
8 |
9 | Sponsors help us dedicate more time to the project, managing contributions, and working on larger features (like better tooling based on more advanced
10 | LSP features, VSCode integration, debugging via the DAP, and several others).
11 | If you find this project useful to your work, or would like to accelerate the development of Serena, consider becoming a sponsor.
12 |
13 | We are proud to announce that the Visual Studio Code team, together with Microsoft’s Open Source Programs Office and GitHub Open Source
14 | have decided to sponsor Serena with a one-time contribution!
15 |
16 | ## Community Contributions
17 |
18 | A significant part of Serena, especially support for various languages, was contributed by the open source community.
19 | We are very grateful for the many contributors who made this possible and who played an important role in making Serena
20 | what it is today.
21 |
22 | ## Technologies
23 |
24 | We built Serena on top of multiple existing open-source technologies, the most important ones being:
25 |
26 | 1. [multilspy](https://github.com/microsoft/multilspy).
27 | A library which wraps language server implementations and adapts them for interaction via Python
28 | and which provided the basis for our library Solid-LSP (src/solidlsp).
29 | Solid-LSP provides pure synchronous LSP calls and extends the original library with the symbolic logic
30 | that Serena required.
31 | 2. [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk)
32 | 3. All the language servers that we use through Solid-LSP.
33 |
34 | Without these projects, Serena would not have been possible (or would have been significantly more difficult to build).
35 |
```
--------------------------------------------------------------------------------
/src/solidlsp/lsp_protocol_handler/lsp_constants.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | This module contains constants used in the LSP protocol.
3 | """
4 |
5 |
6 | class LSPConstants:
7 | """
8 | This class contains constants used in the LSP protocol.
9 | """
10 |
11 | # the key for uri used to represent paths
12 | URI = "uri"
13 |
14 | # the key for range, which is a from and to position within a text document
15 | RANGE = "range"
16 |
17 | # A key used in LocationLink type, used as the span of the origin link
18 | ORIGIN_SELECTION_RANGE = "originSelectionRange"
19 |
20 | # A key used in LocationLink type, used as the target uri of the link
21 | TARGET_URI = "targetUri"
22 |
23 | # A key used in LocationLink type, used as the target range of the link
24 | TARGET_RANGE = "targetRange"
25 |
26 | # A key used in LocationLink type, used as the target selection range of the link
27 | TARGET_SELECTION_RANGE = "targetSelectionRange"
28 |
29 | # key for the textDocument field in the request
30 | TEXT_DOCUMENT = "textDocument"
31 |
32 | # key used to represent the language a document is in - "java", "csharp", etc.
33 | LANGUAGE_ID = "languageId"
34 |
35 | # key used to represent the version of a document (a shared value between the client and server)
36 | VERSION = "version"
37 |
38 | # key used to represent the text of a document being sent from the client to the server on open
39 | TEXT = "text"
40 |
41 | # key used to represent a position (line and colnum) within a text document
42 | POSITION = "position"
43 |
44 | # key used to represent the line number of a position
45 | LINE = "line"
46 |
47 | # key used to represent the column number of a position
48 | CHARACTER = "character"
49 |
50 | # key used to represent the changes made to a document
51 | CONTENT_CHANGES = "contentChanges"
52 |
53 | # key used to represent name of symbols
54 | NAME = "name"
55 |
56 | # key used to represent the kind of symbols
57 | KIND = "kind"
58 |
59 | # key used to represent children in document symbols
60 | CHILDREN = "children"
61 |
62 | # key used to represent the location in symbols
63 | LOCATION = "location"
64 |
65 | # Severity level of the diagnostic
66 | SEVERITY = "severity"
67 |
68 | # The message of the diagnostic
69 | MESSAGE = "message"
70 |
```
--------------------------------------------------------------------------------
/src/serena/tools/cmd_tools.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Tools supporting the execution of (external) commands
3 | """
4 |
5 | import os.path
6 |
7 | from serena.tools import Tool, ToolMarkerCanEdit
8 | from serena.util.shell import execute_shell_command
9 |
10 |
11 | class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit):
12 | """
13 | Executes a shell command.
14 | """
15 |
16 | def apply(
17 | self,
18 | command: str,
19 | cwd: str | None = None,
20 | capture_stderr: bool = True,
21 | max_answer_chars: int = -1,
22 | ) -> str:
23 | """
24 | Execute a shell command and return its output. If there is a memory about suggested commands, read that first.
25 | Never execute unsafe shell commands!
26 | IMPORTANT: Do not use this tool to start
27 | * long-running processes (e.g. servers) that are not intended to terminate quickly,
28 | * processes that require user interaction.
29 |
30 | :param command: the shell command to execute
31 | :param cwd: the working directory to execute the command in. If None, the project root will be used.
32 | :param capture_stderr: whether to capture and return stderr output
33 | :param max_answer_chars: if the output is longer than this number of characters,
34 | no content will be returned. -1 means using the default value, don't adjust unless there is no other way to get the content
35 | required for the task.
36 | :return: a JSON object containing the command's stdout and optionally stderr output
37 | """
38 | if cwd is None:
39 | _cwd = self.get_project_root()
40 | else:
41 | if os.path.isabs(cwd):
42 | _cwd = cwd
43 | else:
44 | _cwd = os.path.join(self.get_project_root(), cwd)
45 | if not os.path.isdir(_cwd):
46 | raise FileNotFoundError(
47 | f"Specified a relative working directory ({cwd}), but the resulting path is not a directory: {_cwd}"
48 | )
49 |
50 | result = execute_shell_command(command, cwd=_cwd, capture_stderr=capture_stderr)
51 | result = result.json()
52 | return self._limit_length(result, max_answer_chars)
53 |
```
--------------------------------------------------------------------------------
/src/serena/resources/config/contexts/chatgpt.yml:
--------------------------------------------------------------------------------
```yaml
1 | description: A configuration specific for ChatGPT, which has a limit of 30 tools and requires short descriptions.
2 | prompt: |
3 | You are running in desktop app context where the tools give you access to the code base as well as some
4 | access to the file system, if configured. You interact with the user through a chat interface that is separated
5 | from the code base. As a consequence, if you are in interactive mode, your communication with the user should
6 | involve high-level thinking and planning as well as some summarization of any code edits that you make.
7 | For viewing the code edits the user will view them in a separate code editor window, and the back-and-forth
8 | between the chat and the code editor should be minimized as well as facilitated by you.
9 | If complex changes have been made, advise the user on how to review them in the code editor.
10 | If complex relationships that the user asked for should be visualized or explained, consider creating
11 | a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering
12 | options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions.
13 | excluded_tools: []
14 | included_optional_tools:
15 | - switch_modes
16 |
17 | tool_description_overrides:
18 | find_symbol: |
19 | Retrieves symbols matching `name_path_pattern` in a file.
20 | Use `depth > 0` to include children. `name_path_pattern` can be: "foo": any symbol named "foo"; "foo/bar": "bar" within "foo"; "/foo/bar": only top-level "foo/bar"
21 | replace_content: |
22 | Replaces content in files. Preferred for smaller edits where symbol-level tools aren't appropriate.
23 | Use mode "regex" with wildcards (.*?) to match large sections efficiently: "beginning.*?end" instead of specifying exact content.
24 | Essential for multi-line replacements.
25 | search_for_pattern: |
26 | Flexible pattern search across codebase. Prefer symbolic operations when possible.
27 | Uses DOTALL matching. Use non-greedy quantifiers (.*?) to avoid over-matching.
28 | Supports file filtering via globs and code-only restriction.
```
--------------------------------------------------------------------------------
/src/serena/util/thread.py:
--------------------------------------------------------------------------------
```python
1 | import threading
2 | from collections.abc import Callable
3 | from enum import Enum
4 | from typing import Generic, TypeVar
5 |
6 | from sensai.util.string import ToStringMixin
7 |
8 |
9 | class TimeoutException(Exception):
10 | def __init__(self, message: str, timeout: float) -> None:
11 | super().__init__(message)
12 | self.timeout = timeout
13 |
14 |
15 | T = TypeVar("T")
16 |
17 |
18 | class ExecutionResult(Generic[T], ToStringMixin):
19 |
20 | class Status(Enum):
21 | SUCCESS = "success"
22 | TIMEOUT = "timeout"
23 | EXCEPTION = "error"
24 |
25 | def __init__(self) -> None:
26 | self.result_value: T | None = None
27 | self.status: ExecutionResult.Status | None = None
28 | self.exception: Exception | None = None
29 |
30 | def set_result_value(self, value: T) -> None:
31 | self.result_value = value
32 | self.status = ExecutionResult.Status.SUCCESS
33 |
34 | def set_timed_out(self, exception: TimeoutException) -> None:
35 | self.exception = exception
36 | self.status = ExecutionResult.Status.TIMEOUT
37 |
38 | def set_exception(self, exception: Exception) -> None:
39 | self.exception = exception
40 | self.status = ExecutionResult.Status.EXCEPTION
41 |
42 |
43 | def execute_with_timeout(func: Callable[[], T], timeout: float, function_name: str) -> ExecutionResult[T]:
44 | """
45 | Executes the given function with a timeout
46 |
47 | :param func: the function to execute
48 | :param timeout: the timeout in seconds
49 | :param function_name: the name of the function (for error messages)
50 | :returns: the execution result
51 | """
52 | execution_result: ExecutionResult[T] = ExecutionResult()
53 |
54 | def target() -> None:
55 | try:
56 | value = func()
57 | execution_result.set_result_value(value)
58 | except Exception as e:
59 | execution_result.set_exception(e)
60 |
61 | thread = threading.Thread(target=target, daemon=True)
62 | thread.start()
63 | thread.join(timeout=timeout)
64 |
65 | if thread.is_alive():
66 | timeout_exception = TimeoutException(f"Execution of '{function_name}' timed out after {timeout} seconds.", timeout)
67 | execution_result.set_timed_out(timeout_exception)
68 |
69 | return execution_result
70 |
```
--------------------------------------------------------------------------------
/docs/02-usage/999_additional-usage.md:
--------------------------------------------------------------------------------
```markdown
1 | # Additional Usage Pointers
2 |
3 | ## Prompting Strategies
4 |
5 | We found that it is often a good idea to spend some time conceptualizing and planning a task
6 | before actually implementing it, especially for non-trivial task. This helps both in achieving
7 | better results and in increasing the feeling of control and staying in the loop. You can
8 | make a detailed plan in one session, where Serena may read a lot of your code to build up the context,
9 | and then continue with the implementation in another (potentially after creating suitable memories).
10 |
11 | ## Running Out of Context
12 |
13 | For long and complicated tasks, or tasks where Serena has read a lot of content, you
14 | may come close to the limits of context tokens. In that case, it is often a good idea to continue
15 | in a new conversation. Serena has a dedicated tool to create a summary of the current state
16 | of the progress and all relevant info for continuing it. You can request to create this summary and
17 | write it to a memory. Then, in a new conversation, you can just ask Serena to read the memory and
18 | continue with the task. In our experience, this worked really well. On the up-side, since in a
19 | single session there is no summarization involved, Serena does not usually get lost (unlike some
20 | other agents that summarize under the hood), and it is also instructed to occasionally check whether
21 | it's on the right track.
22 |
23 | Serena instructs the LLM to be economical in general, so the problem of running out of context
24 | should not occur too often, unless the task is very large or complicated.
25 |
26 | ## Serena and Git Worktrees
27 |
28 | [git-worktree](https://git-scm.com/docs/git-worktree) can be an excellent way to parallelize your work. More on this in [Anthropic: Run parallel Claude Code sessions with Git worktrees](https://docs.claude.com/en/docs/claude-code/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees).
29 |
30 | When it comes to serena AND git-worktree AND larger projects (that take longer to index),
31 | the recommended way is to COPY your `$ORIG_PROJECT/.serena/cache` to `$GIT_WORKTREE/.serena/cache`.
32 | Perform [pre-indexing of your project](indexing) to avoid having to re-index per each worktree you create.
33 |
```
--------------------------------------------------------------------------------
/test/resources/repos/bash/test_repo/config.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # Configuration script for project setup
4 |
5 | # Environment variables
6 | export PROJECT_NAME="bash-test-project"
7 | export PROJECT_VERSION="1.0.0"
8 | export LOG_LEVEL="INFO"
9 | export CONFIG_DIR="./config"
10 |
11 | # Default settings
12 | DEFAULT_TIMEOUT=30
13 | DEFAULT_RETRIES=3
14 | DEFAULT_PORT=8080
15 |
16 | # Configuration arrays
17 | declare -A ENVIRONMENTS=(
18 | ["dev"]="development"
19 | ["prod"]="production"
20 | ["test"]="testing"
21 | )
22 |
23 | declare -A DATABASE_CONFIGS=(
24 | ["host"]="localhost"
25 | ["port"]="5432"
26 | ["name"]="myapp_db"
27 | ["user"]="dbuser"
28 | )
29 |
30 | # Function to load configuration
31 | load_config() {
32 | local env="${1:-dev}"
33 | local config_file="${CONFIG_DIR}/${env}.conf"
34 |
35 | if [[ -f "$config_file" ]]; then
36 | echo "Loading configuration from $config_file"
37 | source "$config_file"
38 | else
39 | echo "Warning: Configuration file $config_file not found, using defaults"
40 | fi
41 | }
42 |
43 | # Function to validate configuration
44 | validate_config() {
45 | local errors=0
46 |
47 | if [[ -z "$PROJECT_NAME" ]]; then
48 | echo "Error: PROJECT_NAME is not set" >&2
49 | ((errors++))
50 | fi
51 |
52 | if [[ -z "$PROJECT_VERSION" ]]; then
53 | echo "Error: PROJECT_VERSION is not set" >&2
54 | ((errors++))
55 | fi
56 |
57 | if [[ $DEFAULT_PORT -lt 1024 || $DEFAULT_PORT -gt 65535 ]]; then
58 | echo "Error: Invalid port number $DEFAULT_PORT" >&2
59 | ((errors++))
60 | fi
61 |
62 | return $errors
63 | }
64 |
65 | # Function to print configuration
66 | print_config() {
67 | echo "=== Current Configuration ==="
68 | echo "Project Name: $PROJECT_NAME"
69 | echo "Version: $PROJECT_VERSION"
70 | echo "Log Level: $LOG_LEVEL"
71 | echo "Default Port: $DEFAULT_PORT"
72 | echo "Default Timeout: $DEFAULT_TIMEOUT"
73 | echo "Default Retries: $DEFAULT_RETRIES"
74 |
75 | echo "\n=== Environments ==="
76 | for env in "${!ENVIRONMENTS[@]}"; do
77 | echo " $env: ${ENVIRONMENTS[$env]}"
78 | done
79 |
80 | echo "\n=== Database Configuration ==="
81 | for key in "${!DATABASE_CONFIGS[@]}"; do
82 | echo " $key: ${DATABASE_CONFIGS[$key]}"
83 | done
84 | }
85 |
86 | # Initialize configuration if this script is run directly
87 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
88 | load_config "$1"
89 | validate_config
90 | print_config
91 | fi
92 |
```
--------------------------------------------------------------------------------
/src/serena/util/exception.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | import sys
3 |
4 | from serena.agent import log
5 |
6 |
7 | def is_headless_environment() -> bool:
8 | """
9 | Detect if we're running in a headless environment where GUI operations would fail.
10 |
11 | Returns True if:
12 | - No DISPLAY variable on Linux/Unix
13 | - Running in SSH session
14 | - Running in WSL without X server
15 | - Running in Docker container
16 | """
17 | # Check if we're on Windows - GUI usually works there
18 | if sys.platform == "win32":
19 | return False
20 |
21 | # Check for DISPLAY variable (required for X11)
22 | if not os.environ.get("DISPLAY"): # type: ignore
23 | return True
24 |
25 | # Check for SSH session
26 | if os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_CLIENT"):
27 | return True
28 |
29 | # Check for common CI/container environments
30 | if os.environ.get("CI") or os.environ.get("CONTAINER") or os.path.exists("/.dockerenv"):
31 | return True
32 |
33 | # Check for WSL (only on Unix-like systems where os.uname exists)
34 | if hasattr(os, "uname"):
35 | if "microsoft" in os.uname().release.lower():
36 | # In WSL, even with DISPLAY set, X server might not be running
37 | # This is a simplified check - could be improved
38 | return True
39 |
40 | return False
41 |
42 |
43 | def show_fatal_exception_safe(e: Exception) -> None:
44 | """
45 | Shows the given exception in the GUI log viewer on the main thread and ensures that the exception is logged or at
46 | least printed to stderr.
47 | """
48 | # Log the error and print it to stderr
49 | log.error(f"Fatal exception: {e}", exc_info=e)
50 | print(f"Fatal exception: {e}", file=sys.stderr)
51 |
52 | # Don't attempt GUI in headless environments
53 | if is_headless_environment():
54 | log.debug("Skipping GUI error display in headless environment")
55 | return
56 |
57 | # attempt to show the error in the GUI
58 | try:
59 | # NOTE: The import can fail on macOS if Tk is not available (depends on Python interpreter installation, which uv
60 | # used as a base); while tkinter as such is always available, its dependencies can be unavailable on macOS.
61 | from serena.gui_log_viewer import show_fatal_exception
62 |
63 | show_fatal_exception(e)
64 | except Exception as gui_error:
65 | log.debug(f"Failed to show GUI error dialog: {gui_error}")
66 |
```
--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/services.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Services module demonstrating function usage and dependencies.
3 | """
4 |
5 | from typing import Any
6 |
7 | from .models import Item, User
8 |
9 |
10 | class UserService:
11 | """Service for user-related operations"""
12 |
13 | def __init__(self, user_db: dict[str, User] | None = None):
14 | self.users = user_db or {}
15 |
16 | def create_user(self, id: str, name: str, email: str) -> User:
17 | """Create a new user and store it"""
18 | if id in self.users:
19 | raise ValueError(f"User with ID {id} already exists")
20 |
21 | user = User(id=id, name=name, email=email)
22 | self.users[id] = user
23 | return user
24 |
25 | def get_user(self, id: str) -> User | None:
26 | """Get a user by ID"""
27 | return self.users.get(id)
28 |
29 | def list_users(self) -> list[User]:
30 | """Get a list of all users"""
31 | return list(self.users.values())
32 |
33 | def delete_user(self, id: str) -> bool:
34 | """Delete a user by ID"""
35 | if id in self.users:
36 | del self.users[id]
37 | return True
38 | return False
39 |
40 |
41 | class ItemService:
42 | """Service for item-related operations"""
43 |
44 | def __init__(self, item_db: dict[str, Item] | None = None):
45 | self.items = item_db or {}
46 |
47 | def create_item(self, id: str, name: str, price: float, category: str) -> Item:
48 | """Create a new item and store it"""
49 | if id in self.items:
50 | raise ValueError(f"Item with ID {id} already exists")
51 |
52 | item = Item(id=id, name=name, price=price, category=category)
53 | self.items[id] = item
54 | return item
55 |
56 | def get_item(self, id: str) -> Item | None:
57 | """Get an item by ID"""
58 | return self.items.get(id)
59 |
60 | def list_items(self, category: str | None = None) -> list[Item]:
61 | """List all items, optionally filtered by category"""
62 | if category:
63 | return [item for item in self.items.values() if item.category == category]
64 | return list(self.items.values())
65 |
66 |
67 | # Factory function for services
68 | def create_service_container() -> dict[str, Any]:
69 | """Create a container with all services"""
70 | container = {"user_service": UserService(), "item_service": ItemService()}
71 | return container
72 |
73 |
74 | user_var_str = "user_var"
75 |
76 |
77 | user_service = UserService()
78 | user_service.create_user("1", "Alice", "[email protected]")
79 |
```
--------------------------------------------------------------------------------
/test/solidlsp/ruby/test_ruby_basic.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | from pathlib import Path
3 |
4 | import pytest
5 |
6 | from solidlsp import SolidLanguageServer
7 | from solidlsp.ls_config import Language
8 | from solidlsp.ls_utils import SymbolUtils
9 |
10 |
11 | @pytest.mark.ruby
12 | class TestRubyLanguageServer:
13 | @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
14 | def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
15 | symbols = language_server.request_full_symbol_tree()
16 | assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoClass"), "DemoClass not found in symbol tree"
17 | assert SymbolUtils.symbol_tree_contains_name(symbols, "helper_function"), "helper_function not found in symbol tree"
18 | assert SymbolUtils.symbol_tree_contains_name(symbols, "print_value"), "print_value not found in symbol tree"
19 |
20 | @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
21 | def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
22 | file_path = os.path.join("main.rb")
23 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
24 | helper_symbol = None
25 | for sym in symbols[0]:
26 | if sym.get("name") == "helper_function":
27 | helper_symbol = sym
28 | break
29 | print(helper_symbol)
30 | assert helper_symbol is not None, "Could not find 'helper_function' symbol in main.rb"
31 |
32 | @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
33 | @pytest.mark.parametrize("repo_path", [Language.RUBY], indirect=True)
34 | def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
35 | # Test finding Calculator.add method definition from line 17: Calculator.new.add(demo.value, 10)
36 | definition_location_list = language_server.request_definition(
37 | str(repo_path / "main.rb"), 16, 17
38 | ) # add method at line 17 (0-indexed 16), position 17
39 |
40 | assert len(definition_location_list) == 1
41 | definition_location = definition_location_list[0]
42 | print(f"Found definition: {definition_location}")
43 | assert definition_location["uri"].endswith("lib.rb")
44 | assert definition_location["range"]["start"]["line"] == 1 # add method on line 2 (0-indexed 1)
45 |
```
--------------------------------------------------------------------------------
/test/solidlsp/julia/test_julia_basic.py:
--------------------------------------------------------------------------------
```python
1 | import pytest
2 |
3 | from solidlsp.ls import SolidLanguageServer
4 | from solidlsp.ls_config import Language
5 |
6 |
7 | @pytest.mark.julia
8 | class TestJuliaLanguageServer:
9 | @pytest.mark.parametrize("language_server", [Language.JULIA], indirect=True)
10 | def test_julia_symbols(self, language_server: SolidLanguageServer):
11 | """
12 | Test if we can find the top-level symbols in the main.jl file.
13 | """
14 | all_symbols, _ = language_server.request_document_symbols("main.jl").get_all_symbols_and_roots()
15 | symbol_names = {s["name"] for s in all_symbols}
16 | assert "calculate_sum" in symbol_names
17 | assert "main" in symbol_names
18 |
19 | @pytest.mark.parametrize("language_server", [Language.JULIA], indirect=True)
20 | def test_julia_within_file_references(self, language_server: SolidLanguageServer):
21 | """
22 | Test finding references to a function within the same file.
23 | """
24 | # Find references to 'calculate_sum' - the function name starts at line 2, column 9
25 | # LSP uses 0-based indexing
26 | references = language_server.request_references("main.jl", line=2, column=9)
27 |
28 | # Should find at least the definition and the call site
29 | assert len(references) >= 1, f"Expected at least 1 reference, got {len(references)}"
30 |
31 | # Verify at least one reference is in main.jl
32 | reference_paths = [ref["relativePath"] for ref in references]
33 | assert "main.jl" in reference_paths
34 |
35 | @pytest.mark.parametrize("language_server", [Language.JULIA], indirect=True)
36 | def test_julia_cross_file_references(self, language_server: SolidLanguageServer):
37 | """
38 | Test finding references to a function defined in another file.
39 | """
40 | # The 'say_hello' function name starts at line 1, column 13 in lib/helper.jl
41 | # LSP uses 0-based indexing
42 | references = language_server.request_references("lib/helper.jl", line=1, column=13)
43 |
44 | # Should find at least the call site in main.jl
45 | assert len(references) >= 1, f"Expected at least 1 reference, got {len(references)}"
46 |
47 | # Verify at least one reference points to the usage
48 | reference_paths = [ref["relativePath"] for ref in references]
49 | # The reference might be in either file (definition or usage)
50 | assert "main.jl" in reference_paths or "lib/helper.jl" in reference_paths
51 |
```
--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/overloaded.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Module demonstrating function and method overloading with typing.overload
3 | """
4 |
5 | from typing import Any, overload
6 |
7 |
8 | # Example of function overloading
9 | @overload
10 | def process_data(data: str) -> dict[str, str]: ...
11 |
12 |
13 | @overload
14 | def process_data(data: int) -> dict[str, int]: ...
15 |
16 |
17 | @overload
18 | def process_data(data: list[str | int]) -> dict[str, list[str | int]]: ...
19 |
20 |
21 | def process_data(data: str | int | list[str | int]) -> dict[str, Any]:
22 | """
23 | Process data based on its type.
24 |
25 | - If string: returns a dict with 'value': <string>
26 | - If int: returns a dict with 'value': <int>
27 | - If list: returns a dict with 'value': <list>
28 | """
29 | return {"value": data}
30 |
31 |
32 | # Class with overloaded methods
33 | class DataProcessor:
34 | """
35 | A class demonstrating method overloading.
36 | """
37 |
38 | @overload
39 | def transform(self, input_value: str) -> str: ...
40 |
41 | @overload
42 | def transform(self, input_value: int) -> int: ...
43 |
44 | @overload
45 | def transform(self, input_value: list[Any]) -> list[Any]: ...
46 |
47 | def transform(self, input_value: str | int | list[Any]) -> str | int | list[Any]:
48 | """
49 | Transform input based on its type.
50 |
51 | - If string: returns the string in uppercase
52 | - If int: returns the int multiplied by 2
53 | - If list: returns the list sorted
54 | """
55 | if isinstance(input_value, str):
56 | return input_value.upper()
57 | elif isinstance(input_value, int):
58 | return input_value * 2
59 | elif isinstance(input_value, list):
60 | try:
61 | return sorted(input_value)
62 | except TypeError:
63 | return input_value
64 | return input_value
65 |
66 | @overload
67 | def fetch(self, id: int) -> dict[str, Any]: ...
68 |
69 | @overload
70 | def fetch(self, id: str, cache: bool = False) -> dict[str, Any] | None: ...
71 |
72 | def fetch(self, id: int | str, cache: bool = False) -> dict[str, Any] | None:
73 | """
74 | Fetch data for a given ID.
75 |
76 | Args:
77 | id: The ID to fetch, either numeric or string
78 | cache: Whether to use cache for string IDs
79 |
80 | Returns:
81 | Data dictionary or None if not found
82 |
83 | """
84 | # Implementation would actually fetch data
85 | if isinstance(id, int):
86 | return {"id": id, "type": "numeric"}
87 | else:
88 | return {"id": id, "type": "string", "cached": cache}
89 |
```
--------------------------------------------------------------------------------
/docs/01-about/040_comparison-to-other-agents.md:
--------------------------------------------------------------------------------
```markdown
1 | # Comparison with Other Coding Agents
2 |
3 | To our knowledge, Serena is the first fully-featured coding agent where the
4 | entire functionality is made available through an MCP server,
5 | thus not requiring additional API keys or subscriptions if access to an LLM
6 | is already available through an MCP-compatible client.
7 |
8 | ## Subscription-Based Coding Agents
9 |
10 | Many prominent subscription-based coding agents are parts of IDEs like
11 | Windsurf, Cursor and VSCode.
12 | Serena's functionality is similar to Cursor's Agent, Windsurf's Cascade or
13 | VSCode's agent mode.
14 |
15 | Serena has the advantage of not requiring a subscription.
16 |
17 | More technical differences are:
18 |
19 | * Serena navigates and edits code using a language server, so it has a symbolic
20 | understanding of the code.
21 | IDE-based tools often use a text search-based or purely text file-based approach, which is often
22 | less powerful, especially for large codebases.
23 | * Serena is not bound to a specific interface (IDE or CLI).
24 | Serena's MCP server can be used with any MCP client (including some IDEs).
25 | * Serena is not bound to a specific large language model or API.
26 | * Serena is open-source and has a small codebase, so it can be easily extended
27 | and modified.
28 |
29 | ## API-Based Coding Agents
30 |
31 | An alternative to subscription-based agents are API-based agents like Claude
32 | Code, Cline, Aider, Roo Code and others, where the usage costs map directly
33 | to the API costs of the underlying LLM.
34 | Some of them (like Cline) can even be included in IDEs as an extension.
35 | They are often very powerful and their main downside are the (potentially very
36 | high) API costs.
37 | Serena itself can be used as an API-based agent (see the [section on Agno](../03-special-guides/custom_agent.md)).
38 |
39 | The main difference between Serena and other API-based agents is that Serena can
40 | also be used as an MCP server, thus not requiring
41 | an API key and bypassing the API costs.
42 |
43 | ## Other MCP-Based Coding Agents
44 |
45 | There are other MCP servers designed for coding, like [DesktopCommander](https://github.com/wonderwhy-er/DesktopCommanderMCP) and
46 | [codemcp](https://github.com/ezyang/codemcp).
47 | However, to the best of our knowledge, none of them provide semantic code
48 | retrieval and editing tools; they rely purely on text-based analysis.
49 | It is the integration of language servers and the MCP that makes Serena unique
50 | and so powerful for challenging coding tasks, especially in the context of
51 | larger codebases.
```
--------------------------------------------------------------------------------
/test/resources/repos/typescript/test_repo/ws_manager.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Dummy WebSocket manager class for testing ambiguous regex matching.
3 | */
4 | class WebSocketManager {
5 | constructor() {
6 | console.log("WebSocketManager initializing\nStatus OK");
7 | this.ws = null;
8 | this.statusElement = document.getElementById("status");
9 | }
10 |
11 | /**
12 | * Connects to the WebSocket server.
13 | */
14 | connectToServer() {
15 | if (this.ws?.readyState === WebSocket.OPEN) {
16 | this.updateConnectionStatus("Already connected", true);
17 | return;
18 | }
19 |
20 | try {
21 | this.ws = new WebSocket("ws://localhost:4402");
22 | this.updateConnectionStatus("Connecting...", false);
23 |
24 | this.ws.onopen = () => {
25 | console.log("Connected to server");
26 | this.updateConnectionStatus("Connected", true);
27 | };
28 |
29 | this.ws.onmessage = (event) => {
30 | console.log("Received message:", event.data);
31 | try {
32 | const data = JSON.parse(event.data);
33 | this.handleMessage(data);
34 | } catch (error) {
35 | console.error("Failed to parse message:", error);
36 | this.updateConnectionStatus("Parse error", false);
37 | }
38 | };
39 |
40 | this.ws.onclose = (event) => {
41 | console.log("Connection closed");
42 | const message = event.reason || undefined;
43 | this.updateConnectionStatus("Disconnected", false, message);
44 | this.ws = null;
45 | };
46 |
47 | this.ws.onerror = (error) => {
48 | console.error("WebSocket error:", error);
49 | this.updateConnectionStatus("Connection error", false);
50 | };
51 | } catch (error) {
52 | console.error("Failed to connect to server:", error);
53 | this.updateConnectionStatus("Connection failed", false);
54 | }
55 | }
56 |
57 | /**
58 | * Updates the connection status display.
59 | */
60 | updateConnectionStatus(status, isConnected, message) {
61 | if (this.statusElement) {
62 | const text = message ? `${status}: ${message}` : status;
63 | this.statusElement.textContent = text;
64 | this.statusElement.style.color = isConnected ? "green" : "red";
65 | }
66 | }
67 |
68 | /**
69 | * Handles incoming messages.
70 | */
71 | handleMessage(data) {
72 | console.log("Handling:", data);
73 | }
74 | }
```
--------------------------------------------------------------------------------
/test/solidlsp/scala/test_scala_language_server.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 |
3 | import pytest
4 |
5 | from solidlsp.language_servers.scala_language_server import ScalaLanguageServer
6 | from solidlsp.ls_config import Language, LanguageServerConfig
7 | from solidlsp.settings import SolidLSPSettings
8 |
9 | pytest.skip("Scala must be compiled for these tests to run through, which is a huge hassle", allow_module_level=True)
10 |
11 | MAIN_FILE_PATH = os.path.join("src", "main", "scala", "com", "example", "Main.scala")
12 |
13 | pytestmark = pytest.mark.scala
14 |
15 |
16 | @pytest.fixture(scope="module")
17 | def scala_ls():
18 | repo_root = os.path.abspath("test/resources/repos/scala")
19 | config = LanguageServerConfig(code_language=Language.SCALA)
20 | solidlsp_settings = SolidLSPSettings()
21 | ls = ScalaLanguageServer(config, repo_root, solidlsp_settings)
22 |
23 | with ls.start_server():
24 | yield ls
25 |
26 |
27 | def test_scala_document_symbols(scala_ls):
28 | """Test document symbols for Main.scala"""
29 | symbols, _ = scala_ls.request_document_symbols(MAIN_FILE_PATH).get_all_symbols_and_roots()
30 | symbol_names = [s["name"] for s in symbols]
31 | assert symbol_names[0] == "com.example"
32 | assert symbol_names[1] == "Main"
33 | assert symbol_names[2] == "main"
34 | assert symbol_names[3] == "result"
35 | assert symbol_names[4] == "sum"
36 | assert symbol_names[5] == "add"
37 | assert symbol_names[6] == "someMethod"
38 | assert symbol_names[7] == "str"
39 | assert symbol_names[8] == "Config"
40 | assert symbol_names[9] == "field1" # confirm https://github.com/oraios/serena/issues/688
41 |
42 |
43 | def test_scala_references_within_same_file(scala_ls):
44 | """Test finding references within the same file."""
45 | definitions = scala_ls.request_definition(MAIN_FILE_PATH, 12, 23)
46 | first_def = definitions[0]
47 | assert first_def["uri"].endswith("Main.scala")
48 | assert first_def["range"]["start"]["line"] == 16
49 | assert first_def["range"]["start"]["character"] == 6
50 | assert first_def["range"]["end"]["line"] == 16
51 | assert first_def["range"]["end"]["character"] == 9
52 |
53 |
54 | def test_scala_find_definition_and_references_across_files(scala_ls):
55 | definitions = scala_ls.request_definition(MAIN_FILE_PATH, 8, 25)
56 | assert len(definitions) == 1
57 |
58 | first_def = definitions[0]
59 | assert first_def["uri"].endswith("Utils.scala")
60 | assert first_def["range"]["start"]["line"] == 7
61 | assert first_def["range"]["start"]["character"] == 6
62 | assert first_def["range"]["end"]["line"] == 7
63 | assert first_def["range"]["end"]["character"] == 14
64 |
```
--------------------------------------------------------------------------------
/test/resources/repos/ruby/test_repo/examples/user_management.rb:
--------------------------------------------------------------------------------
```ruby
1 | require '../services.rb'
2 | require '../models.rb'
3 |
4 | class UserStats
5 | attr_reader :user_count, :active_users, :last_updated
6 |
7 | def initialize
8 | @user_count = 0
9 | @active_users = 0
10 | @last_updated = Time.now
11 | end
12 |
13 | def update_stats(total, active)
14 | @user_count = total
15 | @active_users = active
16 | @last_updated = Time.now
17 | end
18 |
19 | def activity_ratio
20 | return 0.0 if @user_count == 0
21 | (@active_users.to_f / @user_count * 100).round(2)
22 | end
23 |
24 | def formatted_stats
25 | "Users: #{@user_count}, Active: #{@active_users} (#{activity_ratio}%)"
26 | end
27 | end
28 |
29 | class UserManager
30 | def initialize
31 | @service = Services::UserService.new
32 | @stats = UserStats.new
33 | end
34 |
35 | def create_user_with_tracking(id, name, email = nil)
36 | user = @service.create_user(id, name)
37 | user.email = email if email
38 |
39 | update_statistics
40 | notify_user_created(user)
41 |
42 | user
43 | end
44 |
45 | def get_user_details(id)
46 | user = @service.get_user(id)
47 | return nil unless user
48 |
49 | {
50 | user_info: user.full_info,
51 | created_at: Time.now,
52 | stats: @stats.formatted_stats
53 | }
54 | end
55 |
56 | def bulk_create_users(user_data_list)
57 | created_users = []
58 |
59 | user_data_list.each do |data|
60 | user = create_user_with_tracking(data[:id], data[:name], data[:email])
61 | created_users << user
62 | end
63 |
64 | created_users
65 | end
66 |
67 | private
68 |
69 | def update_statistics
70 | total_users = @service.users.length
71 | # For demo purposes, assume all users are active
72 | @stats.update_stats(total_users, total_users)
73 | end
74 |
75 | def notify_user_created(user)
76 | puts "User created: #{user.name} (ID: #{user.id})"
77 | end
78 | end
79 |
80 | def process_user_data(raw_data)
81 | processed = raw_data.map do |entry|
82 | {
83 | id: entry["id"] || entry[:id],
84 | name: entry["name"] || entry[:name],
85 | email: entry["email"] || entry[:email]
86 | }
87 | end
88 |
89 | processed.reject { |entry| entry[:name].nil? || entry[:name].empty? }
90 | end
91 |
92 | def main
93 | # Example usage
94 | manager = UserManager.new
95 |
96 | sample_data = [
97 | { id: 1, name: "Alice Johnson", email: "[email protected]" },
98 | { id: 2, name: "Bob Smith", email: "[email protected]" },
99 | { id: 3, name: "Charlie Brown" }
100 | ]
101 |
102 | users = manager.bulk_create_users(sample_data)
103 |
104 | users.each do |user|
105 | details = manager.get_user_details(user.id)
106 | puts details[:user_info]
107 | end
108 |
109 | puts "\nFinal statistics:"
110 | stats = UserStats.new
111 | stats.update_stats(users.length, users.length)
112 | puts stats.formatted_stats
113 | end
114 |
115 | # Execute if this file is run directly
116 | main if __FILE__ == $0
```
--------------------------------------------------------------------------------
/test/resources/repos/powershell/test_repo/main.ps1:
--------------------------------------------------------------------------------
```
1 | # Main script demonstrating various PowerShell features
2 |
3 | # Import utility functions
4 | . "$PSScriptRoot\utils.ps1"
5 |
6 | # Global variables
7 | $Script:ScriptName = "Main Script"
8 | $Script:Counter = 0
9 |
10 | <#
11 | .SYNOPSIS
12 | Greets a user with various greeting styles.
13 | .PARAMETER Username
14 | The name of the user to greet.
15 | .PARAMETER GreetingType
16 | The type of greeting (formal, casual, or default).
17 | #>
18 | function Greet-User {
19 | [CmdletBinding()]
20 | param(
21 | [Parameter(Mandatory = $true)]
22 | [string]$Username,
23 |
24 | [Parameter(Mandatory = $false)]
25 | [ValidateSet("formal", "casual", "default")]
26 | [string]$GreetingType = "default"
27 | )
28 |
29 | switch ($GreetingType) {
30 | "formal" {
31 | Write-Output "Good day, $Username!"
32 | }
33 | "casual" {
34 | Write-Output "Hey $Username!"
35 | }
36 | default {
37 | Write-Output "Hello, $Username!"
38 | }
39 | }
40 | }
41 |
42 | <#
43 | .SYNOPSIS
44 | Processes an array of items with the specified operation.
45 | .PARAMETER Items
46 | The array of items to process.
47 | .PARAMETER Operation
48 | The operation to perform (count, uppercase).
49 | #>
50 | function Process-Items {
51 | [CmdletBinding()]
52 | param(
53 | [Parameter(Mandatory = $true)]
54 | [string[]]$Items,
55 |
56 | [Parameter(Mandatory = $true)]
57 | [ValidateSet("count", "uppercase")]
58 | [string]$Operation
59 | )
60 |
61 | foreach ($item in $Items) {
62 | switch ($Operation) {
63 | "count" {
64 | $Script:Counter++
65 | Write-Output "Processing item $($Script:Counter): $item"
66 | }
67 | "uppercase" {
68 | Write-Output $item.ToUpper()
69 | }
70 | }
71 | }
72 | }
73 |
74 | <#
75 | .SYNOPSIS
76 | Main entry point for the script.
77 | #>
78 | function Main {
79 | [CmdletBinding()]
80 | param(
81 | [Parameter(Mandatory = $false)]
82 | [string]$User = "World",
83 |
84 | [Parameter(Mandatory = $false)]
85 | [string]$Greeting = "default"
86 | )
87 |
88 | Write-Output "Starting $Script:ScriptName"
89 |
90 | # Use the Greet-User function
91 | Greet-User -Username $User -GreetingType $Greeting
92 |
93 | # Process some items
94 | $items = @("item1", "item2", "item3")
95 | Write-Output "Processing items..."
96 | Process-Items -Items $items -Operation "count"
97 |
98 | # Use utility functions from utils.ps1
99 | $upperName = Convert-ToUpperCase -InputString $User
100 | Write-Output "Uppercase name: $upperName"
101 |
102 | $trimmed = Remove-Whitespace -InputString " Hello World "
103 | Write-Output "Trimmed: '$trimmed'"
104 |
105 | Write-Output "Script completed successfully"
106 | }
107 |
108 | # Run main function
109 | Main @args
110 |
```
--------------------------------------------------------------------------------
/test/resources/repos/vue/test_repo/src/composables/useTheme.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ref, computed, watch, inject, provide, type InjectionKey, type Ref } from 'vue'
2 |
3 | /**
4 | * Theme configuration type
5 | */
6 | export interface ThemeConfig {
7 | isDark: boolean
8 | primaryColor: string
9 | fontSize: number
10 | }
11 |
12 | /**
13 | * Injection key for theme - demonstrates provide/inject pattern
14 | */
15 | export const ThemeKey: InjectionKey<Ref<ThemeConfig>> = Symbol('theme')
16 |
17 | /**
18 | * Composable for theme management with watchers.
19 | * Demonstrates: watch, provide/inject, localStorage interaction
20 | */
21 | export function useThemeProvider() {
22 | // Initialize theme from localStorage or defaults
23 | const loadThemeFromStorage = (): ThemeConfig => {
24 | const stored = localStorage.getItem('app-theme')
25 | if (stored) {
26 | try {
27 | return JSON.parse(stored)
28 | } catch {
29 | // Fall through to defaults
30 | }
31 | }
32 | return {
33 | isDark: false,
34 | primaryColor: '#667eea',
35 | fontSize: 16
36 | }
37 | }
38 |
39 | const theme = ref<ThemeConfig>(loadThemeFromStorage())
40 |
41 | // Computed properties
42 | const isDarkMode = computed(() => theme.value.isDark)
43 | const themeClass = computed(() => theme.value.isDark ? 'dark-theme' : 'light-theme')
44 |
45 | // Watch for theme changes and persist to localStorage
46 | watch(
47 | theme,
48 | (newTheme) => {
49 | localStorage.setItem('app-theme', JSON.stringify(newTheme))
50 | document.documentElement.className = newTheme.isDark ? 'dark' : 'light'
51 | },
52 | { deep: true }
53 | )
54 |
55 | // Methods
56 | const toggleDarkMode = (): void => {
57 | theme.value.isDark = !theme.value.isDark
58 | }
59 |
60 | const setPrimaryColor = (color: string): void => {
61 | theme.value.primaryColor = color
62 | }
63 |
64 | const setFontSize = (size: number): void => {
65 | if (size >= 12 && size <= 24) {
66 | theme.value.fontSize = size
67 | }
68 | }
69 |
70 | const resetTheme = (): void => {
71 | theme.value = {
72 | isDark: false,
73 | primaryColor: '#667eea',
74 | fontSize: 16
75 | }
76 | }
77 |
78 | // Provide theme to child components
79 | provide(ThemeKey, theme)
80 |
81 | return {
82 | theme,
83 | isDarkMode,
84 | themeClass,
85 | toggleDarkMode,
86 | setPrimaryColor,
87 | setFontSize,
88 | resetTheme
89 | }
90 | }
91 |
92 | /**
93 | * Composable for consuming theme in child components.
94 | * Demonstrates: inject pattern
95 | */
96 | export function useTheme() {
97 | const theme = inject(ThemeKey)
98 |
99 | if (!theme) {
100 | throw new Error('useTheme must be used within a component that provides ThemeKey')
101 | }
102 |
103 | const isDark = computed(() => theme.value.isDark)
104 | const primaryColor = computed(() => theme.value.primaryColor)
105 | const fontSize = computed(() => theme.value.fontSize)
106 |
107 | return {
108 | theme,
109 | isDark,
110 | primaryColor,
111 | fontSize
112 | }
113 | }
114 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Base stage with common dependencies
2 | FROM python:3.11-slim AS base
3 | SHELL ["/bin/bash", "-c"]
4 |
5 | # Set environment variables to make Python print directly to the terminal and avoid .pyc files.
6 | ENV PYTHONUNBUFFERED=1
7 | ENV PYTHONDONTWRITEBYTECODE=1
8 |
9 | # Install system dependencies required for package manager and build tools.
10 | # sudo, wget, zip needed for some assistants, like junie
11 | RUN apt-get update && apt-get install -y --no-install-recommends \
12 | curl \
13 | build-essential \
14 | git \
15 | ssh \
16 | sudo \
17 | wget \
18 | zip \
19 | unzip \
20 | git \
21 | && rm -rf /var/lib/apt/lists/*
22 |
23 | # Install pipx.
24 | RUN python3 -m pip install --no-cache-dir pipx \
25 | && pipx ensurepath
26 |
27 | # Install nodejs
28 | ENV NVM_VERSION=0.40.3
29 | ENV NODE_VERSION=22.18.0
30 | RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash
31 | # standard location
32 | ENV NVM_DIR=/root/.nvm
33 | RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
34 | RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
35 | RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
36 | ENV PATH="${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH}"
37 |
38 | # Add local bin to the path
39 | ENV PATH="${PATH}:/root/.local/bin"
40 |
41 | # Install the latest version of uv
42 | RUN curl -LsSf https://astral.sh/uv/install.sh | sh
43 |
44 | # Install Rust and rustup for rust-analyzer support (minimal profile)
45 | ENV RUSTUP_HOME=/usr/local/rustup
46 | ENV CARGO_HOME=/usr/local/cargo
47 | ENV PATH="${CARGO_HOME}/bin:${PATH}"
48 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
49 | --default-toolchain stable \
50 | --profile minimal \
51 | && rustup component add rust-analyzer
52 |
53 | # Set the working directory
54 | WORKDIR /workspaces/serena
55 |
56 | # Copy all files for development
57 | COPY . /workspaces/serena/
58 |
59 | # Install sed
60 | RUN apt-get update && apt-get install -y sed
61 |
62 | # Create Serena configuration
63 | ENV SERENA_HOME=/workspaces/serena/config
64 | RUN mkdir -p $SERENA_HOME
65 | RUN cp src/serena/resources/serena_config.template.yml $SERENA_HOME/serena_config.yml
66 | RUN sed -i 's/^gui_log_window: .*/gui_log_window: False/' $SERENA_HOME/serena_config.yml
67 | RUN sed -i 's/^web_dashboard_listen_address: .*/web_dashboard_listen_address: 0.0.0.0/' $SERENA_HOME/serena_config.yml
68 | RUN sed -i 's/^web_dashboard_open_on_launch: .*/web_dashboard_open_on_launch: False/' $SERENA_HOME/serena_config.yml
69 |
70 | # Create virtual environment and install dependencies
71 | RUN uv venv
72 | RUN . .venv/bin/activate
73 | RUN uv pip install -r pyproject.toml -e .
74 | ENV PATH="/workspaces/serena/.venv/bin:${PATH}"
75 |
76 | # Entrypoint to ensure environment is activated
77 | ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"]
78 |
```
--------------------------------------------------------------------------------
/src/serena/resources/config/modes/editing.yml:
--------------------------------------------------------------------------------
```yaml
1 | description: All tools, with detailed instructions for code editing
2 | prompt: |
3 | You are operating in editing mode. You can edit files with the provided tools.
4 | You adhere to the project's code style and patterns.
5 |
6 | Use symbolic editing tools whenever possible for precise code modifications.
7 | If no explicit editing task has yet been provided, wait for the user to provide one. Do not be overly eager.
8 |
9 | When writing new code, think about where it belongs best. Don't generate new files if you don't plan on actually
10 | properly integrating them into the codebase.
11 |
12 | You have two main approaches for editing code: (a) editing at the symbol level and (b) file-based editing.
13 | The symbol-based approach is appropriate if you need to adjust an entire symbol, e.g. a method, a class, a function, etc.
14 | It is not appropriate if you need to adjust just a few lines of code within a larger symbol.
15 |
16 | **Symbolic editing**
17 | Use symbolic retrieval tools to identify the symbols you need to edit.
18 | If you need to replace the definition of a symbol, use the `replace_symbol_body` tool.
19 | If you want to add some new code at the end of the file, use the `insert_after_symbol` tool with the last top-level symbol in the file.
20 | Similarly, you can use `insert_before_symbol` with the first top-level symbol in the file to insert code at the beginning of a file.
21 | You can understand relationships between symbols by using the `find_referencing_symbols` tool. If not explicitly requested otherwise by the user,
22 | you make sure that when you edit a symbol, the change is either backward-compatible or you find and update all references as needed.
23 | The `find_referencing_symbols` tool will give you code snippets around the references as well as symbolic information.
24 | You can assume that all symbol editing tools are reliable, so you never need to verify the results if the tools return without error.
25 |
26 | {% if 'replace_content' in available_tools %}
27 | **File-based editing**
28 | The `replace_content` tool allows you to perform regex-based replacements within files (as well as simple string replacements).
29 | This is your primary tool for editing code whenever replacing or deleting a whole symbol would be a more expensive operation,
30 | e.g. if you need to adjust just a few lines of code within a method.
31 | You are extremely good at regex, so you never need to check whether the replacement produced the correct result.
32 | In particular, you know how to use wildcards effectively in order to avoid specifying the full original text to be replaced!
33 | {% endif %}
34 | excluded_tools:
35 | - replace_lines
36 | - insert_at_line
37 | - delete_lines
38 |
```
--------------------------------------------------------------------------------
/docs/03-special-guides/custom_agent.md:
--------------------------------------------------------------------------------
```markdown
1 | # Custom Agents with Serena
2 |
3 | As a reference implementation, we provide an integration with the [Agno](https://docs.agno.com/introduction/playground) agent framework.
4 | Agno is a model-agnostic agent framework that allows you to turn Serena into an agent
5 | (independent of the MCP technology) with a large number of underlying LLMs. While Agno has recently
6 | added support for MCP servers out of the box, our Agno integration predates this and is a good illustration of how
7 | easy it is to integrate Serena into an arbitrary agent framework.
8 |
9 | Here's how it works:
10 |
11 | 1. Download the agent-ui code with npx
12 | ```shell
13 | npx create-agent-ui@latest
14 | ```
15 | or, alternatively, clone it manually:
16 | ```shell
17 | git clone https://github.com/agno-agi/agent-ui.git
18 | cd agent-ui
19 | pnpm install
20 | pnpm dev
21 | ```
22 |
23 | 2. Install serena with the optional requirements:
24 | ```shell
25 | # You can also only select agno,google or agno,anthropic instead of all-extras
26 | uv pip install --all-extras -r pyproject.toml -e .
27 | ```
28 |
29 | 3. Copy `.env.example` to `.env` and fill in the API keys for the provider(s) you
30 | intend to use.
31 |
32 | 4. Start the agno agent app with
33 | ```shell
34 | uv run python scripts/agno_agent.py
35 | ```
36 | By default, the script uses Claude as the model, but you can choose any model
37 | supported by Agno (which is essentially any existing model).
38 |
39 | 5. In a new terminal, start the agno UI with
40 | ```shell
41 | cd agent-ui
42 | pnpm dev
43 | ```
44 | Connect the UI to the agent you started above and start chatting. You will have
45 | the same tools as in the MCP server version.
46 |
47 |
48 | Here is a short demo of Serena performing a small analysis task with the newest Gemini model:
49 |
50 | https://github.com/user-attachments/assets/ccfcb968-277d-4ca9-af7f-b84578858c62
51 |
52 |
53 | ⚠️ IMPORTANT: In contrast to the MCP server approach, tool execution in the Agno UI does
54 | not ask for the user's permission. The shell tool is particularly critical, as it can perform arbitrary code execution.
55 | While we have never encountered any issues with
56 | this in our testing with Claude, allowing this may not be entirely safe.
57 | You may choose to disable certain tools for your setup in your Serena project's
58 | configuration file (`.yml`).
59 |
60 |
61 | ## Other Agent Frameworks
62 |
63 | It should be straightforward to incorporate Serena into any
64 | agent framework (like [pydantic-ai](https://ai.pydantic.dev/), [langgraph](https://langchain-ai.github.io/langgraph/tutorials/introduction/) or others).
65 | Typically, you need only to write an adapter for Serena's tools to the tool representation in the framework of your choice,
66 | as was done by us for Agno with `SerenaAgnoToolkit` (see `/src/serena/agno.py`).
67 |
68 |
```
--------------------------------------------------------------------------------
/test/solidlsp/python/test_retrieval_with_ignored_dirs.py:
--------------------------------------------------------------------------------
```python
1 | from collections.abc import Generator
2 | from pathlib import Path
3 |
4 | import pytest
5 |
6 | from solidlsp import SolidLanguageServer
7 | from solidlsp.ls_config import Language
8 | from test.conftest import start_ls_context
9 |
10 | # This mark will be applied to all tests in this module
11 | pytestmark = pytest.mark.python
12 |
13 |
14 | @pytest.fixture(scope="module")
15 | def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]:
16 | """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored."""
17 | ignored_paths = ["scripts", "custom_test"]
18 | with start_ls_context(language=Language.PYTHON, ignored_paths=ignored_paths) as ls:
19 | yield ls
20 |
21 |
22 | @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True)
23 | def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
24 | """Tests that request_full_symbol_tree ignores the configured directory."""
25 | root = ls_with_ignored_dirs.request_full_symbol_tree()[0]
26 | root_children = root["children"]
27 | children_names = {child["name"] for child in root_children}
28 | assert children_names == {"test_repo", "examples"}
29 |
30 |
31 | @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True)
32 | def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
33 | """Tests that find_references ignores the configured directory."""
34 | # Location of Item, which is referenced in scripts
35 | definition_file = "test_repo/models.py"
36 | definition_line = 56
37 | definition_col = 6
38 |
39 | references = ls_with_ignored_dirs.request_references(definition_file, definition_line, definition_col)
40 |
41 | # assert that scripts does not appear in the references
42 | assert not any("scripts" in ref["relativePath"] for ref in references)
43 |
44 |
45 | @pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True)
46 | def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
47 | """Tests that refs and symbols with glob patterns are ignored."""
48 | ignored_paths = ["*ipts", "custom_t*"]
49 | with start_ls_context(language=Language.PYTHON, repo_path=str(repo_path), ignored_paths=ignored_paths) as ls:
50 | # same as in the above tests
51 | root = ls.request_full_symbol_tree()[0]
52 | root_children = root["children"]
53 | children_names = {child["name"] for child in root_children}
54 | assert children_names == {"test_repo", "examples"}
55 |
56 | # test that the refs and symbols with glob patterns are ignored
57 | definition_file = "test_repo/models.py"
58 | definition_line = 56
59 | definition_col = 6
60 |
61 | references = ls.request_references(definition_file, definition_line, definition_col)
62 | assert not any("scripts" in ref["relativePath"] for ref in references)
63 |
```
--------------------------------------------------------------------------------
/src/solidlsp/settings.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Defines settings for Solid-LSP
3 | """
4 |
5 | import logging
6 | import os
7 | import pathlib
8 | from dataclasses import dataclass, field
9 | from typing import TYPE_CHECKING, Any
10 |
11 | from sensai.util.string import ToStringMixin
12 |
13 | if TYPE_CHECKING:
14 | from solidlsp.ls_config import Language
15 |
16 | log = logging.getLogger(__name__)
17 |
18 |
19 | @dataclass
20 | class SolidLSPSettings:
21 | solidlsp_dir: str = str(pathlib.Path.home() / ".solidlsp")
22 | """
23 | Path to the directory in which to store global Solid-LSP data (which is not project-specific)
24 | """
25 | project_data_relative_path: str = ".solidlsp"
26 | """
27 | Relative path within each project directory where Solid-LSP can store project-specific data, e.g. cache files.
28 | For instance, if this is ".solidlsp" and the project is located at "/home/user/myproject",
29 | then Solid-LSP will store project-specific data in "/home/user/myproject/.solidlsp".
30 | """
31 | ls_specific_settings: dict["Language", dict[str, Any]] = field(default_factory=dict)
32 | """
33 | Advanced configuration option allowing to configure language server implementation specific options.
34 | Have a look at the docstring of the constructors of the corresponding LS implementations within solidlsp to see which options are available.
35 | No documentation on options means no options are available.
36 | """
37 |
38 | def __post_init__(self) -> None:
39 | os.makedirs(str(self.solidlsp_dir), exist_ok=True)
40 | os.makedirs(str(self.ls_resources_dir), exist_ok=True)
41 |
42 | @property
43 | def ls_resources_dir(self) -> str:
44 | return os.path.join(str(self.solidlsp_dir), "language_servers", "static")
45 |
46 | class CustomLSSettings(ToStringMixin):
47 | def __init__(self, settings: dict[str, Any] | None) -> None:
48 | self.settings = settings or {}
49 |
50 | def get(self, key: str, default_value: Any = None) -> Any:
51 | """
52 | Returns the custom setting for the given key or the default value if not set.
53 | If a custom value is set for the given key, the retrieval is logged.
54 |
55 | :param key: the key
56 | :param default_value: the default value to use if no custom value is set
57 | :return: the value
58 | """
59 | if key in self.settings:
60 | value = self.settings[key]
61 | log.info("Using custom LS setting %s for key '%s'", value, key)
62 | else:
63 | value = default_value
64 | return value
65 |
66 | def get_ls_specific_settings(self, language: "Language") -> CustomLSSettings:
67 | """
68 | Get the language server specific settings for the given language.
69 |
70 | :param language: The programming language.
71 | :return: A dictionary of settings for the language server.
72 | """
73 | return self.CustomLSSettings(self.ls_specific_settings.get(language))
74 |
```
--------------------------------------------------------------------------------
/docs/02-usage/025_jetbrains_plugin.md:
--------------------------------------------------------------------------------
```markdown
1 | # The Serena JetBrains Plugin
2 |
3 | The [JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) allows Serena to
4 | leverage the powerful code analysis and editing capabilities of your JetBrains IDE.
5 |
6 | ```{raw} html
7 | <p>
8 | <a href="https://plugins.jetbrains.com/plugin/28946-serena/">
9 | <img style="background-color:transparent;" src="../_static/images/jetbrains-marketplace-button.png">
10 | </a>
11 | </p>
12 | ```
13 |
14 | We recommend the JetBrains plugin as the preferred way of using Serena,
15 | especially for users of JetBrains IDEs.
16 |
17 | **Purchasing the JetBrains Plugin supports the Serena project.**
18 | The proceeds from plugin sales allow us to dedicate more resources to further developing and improving Serena.
19 |
20 | ## Configuring Serena
21 |
22 | After installing the plugin, you need to configure Serena to use it.
23 |
24 | **Central Configuration**.
25 |
26 | Edit the global Serena configuration file located at `~/.serena/serena_config.yml`
27 | (`%USERPROFILE%\.serena\serena_config.yml` on Windows).
28 | Change the `language_backend` setting as follows:
29 |
30 | ```yaml
31 | language_backend: JetBrains
32 | ```
33 |
34 | *Note*: you can also use the button `Edit Global Serena Config` in the Serena MCP dashboard to open the config file in your default editor.
35 |
36 | **Per-Instance Configuration**.
37 | The configuration setting in the global config file can be overridden on a
38 | per-instance basis by providing the arguments `--language-backend JetBrains` when
39 | launching the Serena MCP server.
40 |
41 | **Verifying the Setup**.
42 | You can verify that Serena is using the JetBrains plugin by either checking the dashboard, where
43 | you will see `Languages:
44 | Using JetBrains backend` in the configuration overview.
45 | You will also notice that your client will use the JetBrains-specific tools like `jet_brains_find_symbol` and others like it.
46 |
47 |
48 | ## Advantages of the JetBrains Plugin
49 |
50 | There are multiple features that are only available when using the JetBrains plugin:
51 |
52 | * **External library indexing**: Dependencies and libraries are fully indexed and accessible to Serena
53 | * **No additional setup**: No need to download or configure separate language servers
54 | * **Enhanced performance**: Faster tool execution thanks to optimized IDE integration
55 | * **Multi-language excellence**: First-class support for polyglot projects with multiple languages and frameworks
56 |
57 | We are also working on additional features like a `move_symbol` tool and debugging-related capabilities that
58 | will be available exclusively through the JetBrains plugin.
59 |
60 | ## Usage with Other Editors
61 |
62 | We realize that not everyone uses a JetBrains IDE as their main code editor.
63 | You can still take advantage of the JetBrains plugin by running a JetBrains IDE instance alongside your
64 | preferred editor. Most JetBrains IDEs have a free community edition that you can use for this purpose.
65 | You just need to make sure that the project you are working on is open and indexed in the JetBrains IDE,
66 | so that Serena can connect to it.
67 |
```
--------------------------------------------------------------------------------
/test/solidlsp/kotlin/test_kotlin_basic.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 |
3 | import pytest
4 |
5 | from solidlsp import SolidLanguageServer
6 | from solidlsp.ls_config import Language
7 | from solidlsp.ls_utils import SymbolUtils
8 |
9 |
10 | @pytest.mark.kotlin
11 | class TestKotlinLanguageServer:
12 | @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True)
13 | def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
14 | symbols = language_server.request_full_symbol_tree()
15 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree"
16 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree"
17 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model class not found in symbol tree"
18 |
19 | @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True)
20 | def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
21 | # Use correct Kotlin file paths
22 | file_path = os.path.join("src", "main", "kotlin", "test_repo", "Utils.kt")
23 | refs = language_server.request_references(file_path, 3, 12)
24 | assert any("Main.kt" in ref.get("relativePath", "") for ref in refs), "Main should reference Utils.printHello"
25 |
26 | # Dynamically determine the correct line/column for the 'Model' class name
27 | file_path = os.path.join("src", "main", "kotlin", "test_repo", "Model.kt")
28 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
29 | model_symbol = None
30 | for sym in symbols[0]:
31 | print(sym)
32 | print("\n")
33 | if sym.get("name") == "Model" and sym.get("kind") == 23: # 23 = Class
34 | model_symbol = sym
35 | break
36 | assert model_symbol is not None, "Could not find 'Model' class symbol in Model.kt"
37 | # Use selectionRange if present, otherwise fall back to range
38 | if "selectionRange" in model_symbol:
39 | sel_start = model_symbol["selectionRange"]["start"]
40 | else:
41 | sel_start = model_symbol["range"]["start"]
42 | refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
43 | assert any(
44 | "Main.kt" in ref.get("relativePath", "") for ref in refs
45 | ), "Main should reference Model (tried all positions in selectionRange)"
46 |
47 | @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True)
48 | def test_overview_methods(self, language_server: SolidLanguageServer) -> None:
49 | symbols = language_server.request_full_symbol_tree()
50 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview"
51 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview"
52 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview"
53 |
```
--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/variables.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Test module for variable declarations and usage.
3 |
4 | This module tests various types of variable declarations and usages including:
5 | - Module-level variables
6 | - Class-level variables
7 | - Instance variables
8 | - Variable reassignments
9 | """
10 |
11 | from dataclasses import dataclass, field
12 |
13 | # Module-level variables
14 | module_var = "Initial module value"
15 |
16 | reassignable_module_var = 10
17 | reassignable_module_var = 20 # Reassigned
18 |
19 | # Module-level variable with type annotation
20 | typed_module_var: int = 42
21 |
22 |
23 | # Regular class with class and instance variables
24 | class VariableContainer:
25 | """Class that contains various variables."""
26 |
27 | # Class-level variables
28 | class_var = "Initial class value"
29 |
30 | reassignable_class_var = True
31 | reassignable_class_var = False # Reassigned #noqa: PIE794
32 |
33 | # Class-level variable with type annotation
34 | typed_class_var: str = "typed value"
35 |
36 | def __init__(self):
37 | # Instance variables
38 | self.instance_var = "Initial instance value"
39 | self.reassignable_instance_var = 100
40 |
41 | # Instance variable with type annotation
42 | self.typed_instance_var: list[str] = ["item1", "item2"]
43 |
44 | def modify_instance_var(self):
45 | # Reassign instance variable
46 | self.instance_var = "Modified instance value"
47 | self.reassignable_instance_var = 200 # Reassigned
48 |
49 | def use_module_var(self):
50 | # Use module-level variables
51 | result = module_var + " used in method"
52 | other_result = reassignable_module_var + 5
53 | return result, other_result
54 |
55 | def use_class_var(self):
56 | # Use class-level variables
57 | result = VariableContainer.class_var + " used in method"
58 | other_result = VariableContainer.reassignable_class_var
59 | return result, other_result
60 |
61 |
62 | # Dataclass with variables
63 | @dataclass
64 | class VariableDataclass:
65 | """Dataclass that contains various fields."""
66 |
67 | # Field variables with type annotations
68 | id: int
69 | name: str
70 | items: list[str] = field(default_factory=list)
71 | metadata: dict[str, str] = field(default_factory=dict)
72 | optional_value: float | None = None
73 |
74 | # This will be reassigned in various places
75 | status: str = "pending"
76 |
77 |
78 | # Function that uses the module variables
79 | def use_module_variables():
80 | """Function that uses module-level variables."""
81 | result = module_var + " used in function"
82 | other_result = reassignable_module_var * 2
83 | return result, other_result
84 |
85 |
86 | # Create instances and use variables
87 | dataclass_instance = VariableDataclass(id=1, name="Test")
88 | dataclass_instance.status = "active" # Reassign dataclass field
89 |
90 | # Use variables at module level
91 | module_result = module_var + " used at module level"
92 | other_module_result = reassignable_module_var + 30
93 |
94 | # Create a second dataclass instance with different status
95 | second_dataclass = VariableDataclass(id=2, name="Another Test")
96 | second_dataclass.status = "completed" # Another reassignment of status
97 |
```
--------------------------------------------------------------------------------
/test/solidlsp/terraform/test_terraform_basic.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Basic integration tests for the Terraform 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 pytest
9 |
10 | from solidlsp import SolidLanguageServer
11 | from solidlsp.ls_config import Language
12 |
13 |
14 | @pytest.mark.terraform
15 | class TestLanguageServerBasics:
16 | """Test basic functionality of the Terraform language server."""
17 |
18 | @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
19 | def test_basic_definition(self, language_server: SolidLanguageServer) -> None:
20 | """Test basic definition lookup functionality."""
21 | # Simple test to verify the language server is working
22 | file_path = "main.tf"
23 | # Just try to get document symbols - this should work without hanging
24 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
25 | assert len(symbols) > 0, "Should find at least some symbols in main.tf"
26 |
27 | @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
28 | def test_request_references_aws_instance(self, language_server: SolidLanguageServer) -> None:
29 | """Test request_references on an aws_instance resource."""
30 | # Get references to an aws_instance resource in main.tf
31 | file_path = "main.tf"
32 | # Find aws_instance resources
33 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
34 | aws_instance_symbol = next((s for s in symbols[0] if s.get("name") == 'resource "aws_instance" "web_server"'), None)
35 | if not aws_instance_symbol or "selectionRange" not in aws_instance_symbol:
36 | raise AssertionError("aws_instance symbol or its selectionRange not found")
37 | sel_start = aws_instance_symbol["selectionRange"]["start"]
38 | references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
39 | assert len(references) >= 1, "aws_instance should be referenced at least once"
40 |
41 | @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
42 | def test_request_references_variable(self, language_server: SolidLanguageServer) -> None:
43 | """Test request_references on a variable."""
44 | # Get references to a variable in variables.tf
45 | file_path = "variables.tf"
46 | # Find variable definitions
47 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
48 | var_symbol = next((s for s in symbols[0] if s.get("name") == 'variable "instance_type"'), None)
49 | if not var_symbol or "selectionRange" not in var_symbol:
50 | raise AssertionError("variable symbol or its selectionRange not found")
51 | sel_start = var_symbol["selectionRange"]["start"]
52 | references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
53 | assert len(references) >= 1, "variable should be referenced at least once"
54 |
```
--------------------------------------------------------------------------------
/test/solidlsp/java/test_java_basic.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 |
3 | import pytest
4 |
5 | from solidlsp import SolidLanguageServer
6 | from solidlsp.ls_config import Language
7 | from solidlsp.ls_utils import SymbolUtils
8 | from test.conftest import language_tests_enabled
9 |
10 | pytestmark = [pytest.mark.java, pytest.mark.skipif(not language_tests_enabled(Language.JAVA), reason="Java tests disabled")]
11 |
12 |
13 | class TestJavaLanguageServer:
14 | @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True)
15 | def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
16 | symbols = language_server.request_full_symbol_tree()
17 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree"
18 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree"
19 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model class not found in symbol tree"
20 |
21 | @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True)
22 | def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
23 | # Use correct Maven/Java file paths
24 | file_path = os.path.join("src", "main", "java", "test_repo", "Utils.java")
25 | refs = language_server.request_references(file_path, 4, 20)
26 | assert any("Main.java" in ref.get("relativePath", "") for ref in refs), "Main should reference Utils.printHello"
27 |
28 | # Dynamically determine the correct line/column for the 'Model' class name
29 | file_path = os.path.join("src", "main", "java", "test_repo", "Model.java")
30 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
31 | model_symbol = None
32 | for sym in symbols[0]:
33 | if sym.get("name") == "Model" and sym.get("kind") == 5: # 5 = Class
34 | model_symbol = sym
35 | break
36 | assert model_symbol is not None, "Could not find 'Model' class symbol in Model.java"
37 | # Use selectionRange if present, otherwise fall back to range
38 | if "selectionRange" in model_symbol:
39 | sel_start = model_symbol["selectionRange"]["start"]
40 | else:
41 | sel_start = model_symbol["range"]["start"]
42 | refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
43 | assert any(
44 | "Main.java" in ref.get("relativePath", "") for ref in refs
45 | ), "Main should reference Model (tried all positions in selectionRange)"
46 |
47 | @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True)
48 | def test_overview_methods(self, language_server: SolidLanguageServer) -> None:
49 | symbols = language_server.request_full_symbol_tree()
50 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview"
51 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview"
52 | assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview"
53 |
```
--------------------------------------------------------------------------------
/test/solidlsp/elm/test_elm_basic.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 |
3 | import pytest
4 |
5 | from solidlsp import SolidLanguageServer
6 | from solidlsp.ls_config import Language
7 | from solidlsp.ls_utils import SymbolUtils
8 |
9 |
10 | @pytest.mark.elm
11 | class TestElmLanguageServer:
12 | @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
13 | def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
14 | symbols = language_server.request_full_symbol_tree()
15 | assert SymbolUtils.symbol_tree_contains_name(symbols, "greet"), "greet function not found in symbol tree"
16 | assert SymbolUtils.symbol_tree_contains_name(symbols, "calculateSum"), "calculateSum function not found in symbol tree"
17 | assert SymbolUtils.symbol_tree_contains_name(symbols, "formatMessage"), "formatMessage function not found in symbol tree"
18 | assert SymbolUtils.symbol_tree_contains_name(symbols, "addNumbers"), "addNumbers function not found in symbol tree"
19 |
20 | @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
21 | def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
22 | file_path = os.path.join("Main.elm")
23 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
24 | greet_symbol = None
25 | for sym in symbols[0]:
26 | if sym.get("name") == "greet":
27 | greet_symbol = sym
28 | break
29 | assert greet_symbol is not None, "Could not find 'greet' symbol in Main.elm"
30 | sel_start = greet_symbol["selectionRange"]["start"]
31 | refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
32 | assert any("Main.elm" in ref.get("relativePath", "") for ref in refs), "Main.elm should reference greet function"
33 |
34 | @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
35 | def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
36 | # Test formatMessage function which is defined in Utils.elm and used in Main.elm
37 | utils_path = os.path.join("Utils.elm")
38 | symbols = language_server.request_document_symbols(utils_path).get_all_symbols_and_roots()
39 | formatMessage_symbol = None
40 | for sym in symbols[0]:
41 | if sym.get("name") == "formatMessage":
42 | formatMessage_symbol = sym
43 | break
44 | assert formatMessage_symbol is not None, "Could not find 'formatMessage' symbol in Utils.elm"
45 |
46 | # Get references from the definition in Utils.elm
47 | sel_start = formatMessage_symbol["selectionRange"]["start"]
48 | refs = language_server.request_references(utils_path, sel_start["line"], sel_start["character"])
49 |
50 | # Verify that we found references
51 | assert refs, "Expected to find references for formatMessage"
52 |
53 | # Verify that at least one reference is in Main.elm (where formatMessage is used)
54 | assert any("Main.elm" in ref.get("relativePath", "") for ref in refs), "Expected to find usage of formatMessage in Main.elm"
55 |
```
--------------------------------------------------------------------------------
/docs/03-special-guides/scala_setup_guide_for_serena.md:
--------------------------------------------------------------------------------
```markdown
1 | # Scala Setup Guide for Serena
2 |
3 | This guide explains how to prepare a Scala project so that Serena can provide reliable code intelligence via Metals (Scala LSP) and how to run Scala tests manually.
4 |
5 | Serena automatically bootstraps the Metals language server using Coursier when needed. Your project, however, must be importable by a build server (BSP) — typically via Bloop or sbt’s built‑in BSP — so that Metals can compile and index your code.
6 |
7 | ---
8 | ## Prerequisites
9 |
10 | Install the following on your system and ensure they are available on `PATH`:
11 |
12 | - Java Development Kit (JDK). A modern LTS (e.g., 17 or 21) is recommended.
13 | - `sbt`
14 | - Coursier command (`cs`) or the legacy `coursier` launcher
15 | - Serena uses `cs` if available; if only `coursier` exists, it will attempt to install `cs`. If neither is present, install Coursier first.
16 |
17 | ---
18 | ## Quick Start (Recommended: VS Code + Metals auto‑import)
19 |
20 | 1. Open your Scala project in VS Code.
21 | 2. When prompted by Metals, accept “Import build”. Wait until the import and initial compile/indexing finish.
22 | 3. Run the “Connect to build server” command (id: `build.connect`).
23 | 4. Once the import completes, start Serena in your project root and use it.
24 |
25 | This flow ensures the `.bloop/` and (if applicable) `.metals/` directories are created and your build is known to the build server that Metals uses.
26 |
27 | ---
28 | ## Manual Setup (No VS Code)
29 |
30 | Follow these steps if you prefer a manual setup or you are not using VS Code:
31 |
32 | These instructions cover the setup for projects that use sbt as the build tool, with Bloop as the BSP server.
33 |
34 |
35 | 1. Add Bloop to `project/plugins.sbt` in your Scala project:
36 | ```scala
37 | // project/plugins.sbt
38 | addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "<version>")
39 | ```
40 | Replace `<version>` with an appropriate current version from the Metals documentation.
41 |
42 | 2. Export Bloop configuration with sources:
43 | ```bash
44 | sbt -Dbloop.export-jar-classifiers=sources bloopInstall
45 | ```
46 | This creates a `.bloop/` directory containing your project’s build metadata for the BSP server.
47 |
48 | 3. Compile from sbt to verify the build:
49 | ```bash
50 | sbt compile
51 | ```
52 |
53 | 4. Start Serena in your project root. Serena will bootstrap Metals (if not already present) and connect to the build server using the configuration exported above.
54 |
55 | ---
56 | ## Using Serena with Scala
57 |
58 | - Serena automatically detects Scala files (`*.scala`, `*.sbt`) and will start a Metals process per project when needed.
59 | - On first run, you may see messages like “Bootstrapping metals…” in the Serena logs — this is expected.
60 | - Optimal results require that your project compiles successfully via the build server (BSP). If compilation fails, fix build errors in `sbt` first.
61 |
62 |
63 | Notes:
64 | - Ensure you completed the manual or auto‑import steps so that the build is compiled and indexed; otherwise, code navigation and references may be incomplete until the first successful compile.
65 |
66 | ## Reference
67 | - Metals + sbt: [https://scalameta.org/metals/docs/build-tools/sbt](https://scalameta.org/metals/docs/build-tools/sbt)
```
--------------------------------------------------------------------------------
/test/solidlsp/rust/test_rust_basic.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 |
3 | import pytest
4 |
5 | from solidlsp import SolidLanguageServer
6 | from solidlsp.ls_config import Language
7 | from solidlsp.ls_utils import SymbolUtils
8 |
9 |
10 | @pytest.mark.rust
11 | class TestRustLanguageServer:
12 | @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True)
13 | def test_find_references_raw(self, language_server: SolidLanguageServer) -> None:
14 | # Directly test the request_references method for the add function
15 | file_path = os.path.join("src", "lib.rs")
16 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
17 | add_symbol = None
18 | for sym in symbols[0]:
19 | if sym.get("name") == "add":
20 | add_symbol = sym
21 | break
22 | assert add_symbol is not None, "Could not find 'add' function symbol in lib.rs"
23 | sel_start = add_symbol["selectionRange"]["start"]
24 | refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
25 | assert any(
26 | "main.rs" in ref.get("relativePath", "") for ref in refs
27 | ), "main.rs should reference add (raw, tried all positions in selectionRange)"
28 |
29 | @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True)
30 | def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
31 | symbols = language_server.request_full_symbol_tree()
32 | assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree"
33 | assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add function not found in symbol tree"
34 | # Add more as needed based on test_repo
35 |
36 | @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True)
37 | def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
38 | # Find references to 'add' defined in lib.rs, should be referenced from main.rs
39 | file_path = os.path.join("src", "lib.rs")
40 | symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
41 | add_symbol = None
42 | for sym in symbols[0]:
43 | if sym.get("name") == "add":
44 | add_symbol = sym
45 | break
46 | assert add_symbol is not None, "Could not find 'add' function symbol in lib.rs"
47 | sel_start = add_symbol["selectionRange"]["start"]
48 | refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
49 | assert any(
50 | "main.rs" in ref.get("relativePath", "") for ref in refs
51 | ), "main.rs should reference add (tried all positions in selectionRange)"
52 |
53 | @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True)
54 | def test_overview_methods(self, language_server: SolidLanguageServer) -> None:
55 | symbols = language_server.request_full_symbol_tree()
56 | assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main missing from overview"
57 | assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add missing from overview"
58 |
```
--------------------------------------------------------------------------------
/docs/03-special-guides/groovy_setup_guide_for_serena.md:
--------------------------------------------------------------------------------
```markdown
1 | # Groovy Setup Guide for Serena
2 |
3 | The Groovy support in Serena is incomplete and requires the user to provide a functioning,
4 | JVM-based Groovy language server as a jar. This intermediate state allows the contributors
5 | of Groovy support to use Serena internally and hopefully to accelerate their open-source
6 | release of a Groovy language server in the future.
7 |
8 | If you happen to have a Groovy language server JAR file, you can configure Serena to use it
9 | by following the instructions below.
10 |
11 | ---
12 | ## Prerequisites
13 |
14 | - Groovy Language Server JAR file
15 | - Can be any open-source Groovy language server or your custom implementation
16 | - The JAR must be compatible with standard LSP protocol
17 |
18 | ---
19 | ## Configuration
20 |
21 | Configure Groovy Language Server by adding settings to your `~/.serena/serena_config.yml`:
22 |
23 | ### Basic Configuration
24 |
25 | ```yaml
26 | ls_specific_settings:
27 | groovy:
28 | ls_jar_path: '/path/to/groovy-language-server.jar'
29 | ls_jar_options: '-Xmx2G -Xms512m'
30 | ```
31 |
32 | ### Custom Java Paths
33 |
34 | If you have specific Java installations:
35 |
36 | ```yaml
37 | ls_specific_settings:
38 | groovy:
39 | ls_jar_path: '/path/to/groovy-language-server.jar'
40 | ls_java_home_path: '/usr/lib/jvm/java-21-openjdk' # Custom JAVA_HOME directory
41 | ls_jar_options: '-Xmx2G -Xms512m' # Optional JVM options
42 | ```
43 |
44 | ### Configuration Options
45 |
46 | - `ls_jar_path`: Absolute path to your Groovy Language Server JAR file (required)
47 | - `ls_java_home_path`: Custom JAVA_HOME directory for Java installation (optional)
48 | - When specified, Serena will use this Java installation instead of downloading bundled Java
49 | - Java executable path is automatically determined based on platform:
50 | - Windows: `{ls_java_home_path}/bin/java.exe`
51 | - Linux/macOS: `{ls_java_home_path}/bin/java`
52 | - Validates that Java executable exists at the expected location
53 | - `ls_jar_options`: JVM options for language server (optional)
54 | - Common options:
55 | - `-Xmx<size>`: Maximum heap size (e.g., `-Xmx2G` for 2GB)
56 | - `-Xms<size>`: Initial heap size (e.g., `-Xms512m` for 512MB)
57 |
58 | ---
59 | ## Project Structure Requirements
60 |
61 | For optimal Groovy Language Server performance, ensure your project follows standard Groovy/Gradle structure:
62 |
63 | ```
64 | project-root/
65 | ├── src/
66 | │ ├── main/
67 | │ │ ├── groovy/
68 | │ │ └── resources/
69 | │ └── test/
70 | │ ├── groovy/
71 | │ └── resources/
72 | ├── build.gradle or build.gradle.kts
73 | ├── settings.gradle or settings.gradle.kts
74 | └── gradle/
75 | └── wrapper/
76 | ```
77 |
78 | ---
79 | ## Using Serena with Groovy
80 |
81 | - Serena automatically detects Groovy files (`*.groovy`, `*.gvy`) and will start a Groovy Language Server JAR process per project when needed.
82 | - Optimal results require that your project compiles successfully via Gradle or Maven. If compilation fails, fix build errors in your build tool first.
83 |
84 | ## Reference
85 |
86 | - **Groovy Documentation**: [https://groovy-lang.org/documentation.html](https://groovy-lang.org/documentation.html)
87 | - **Gradle Documentation**: [https://docs.gradle.org](https://docs.gradle.org)
88 | - **Serena Configuration**: [../02-usage/050_configuration.md](../02-usage/050_configuration.md)
```
--------------------------------------------------------------------------------
/docs/03-special-guides/serena_on_chatgpt.md:
--------------------------------------------------------------------------------
```markdown
1 |
2 | # Connecting Serena MCP Server to ChatGPT via MCPO & Cloudflare Tunnel
3 |
4 | This guide explains how to expose a **locally running Serena MCP server** (powered by MCPO) to the internet using **Cloudflare Tunnel**, and how to connect it to **ChatGPT as a Custom GPT with tool access**.
5 |
6 | Once configured, ChatGPT becomes a powerful **coding agent** with direct access to your codebase, shell, and file system — so **read the security notes carefully**.
7 |
8 | ---
9 | ## Prerequisites
10 |
11 | Make sure you have [uv](https://docs.astral.sh/uv/getting-started/installation/)
12 | and [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) installed.
13 |
14 | ## 1. Start the Serena MCP Server via MCPO
15 |
16 | Run the following command to launch Serena as http server (assuming port 8000):
17 |
18 | ```bash
19 | uvx mcpo --port 8000 --api-key <YOUR_SECRET_KEY> -- \
20 | uvx --from git+https://github.com/oraios/serena \
21 | serena start-mcp-server --context chatgpt --project $(pwd)
22 | ```
23 |
24 | - `--api-key` is required to secure the server.
25 | - `--project` should point to the root of your codebase.
26 |
27 | You can also use other options, and you don't have to pass `--project` if you want to work on multiple projects
28 | or want to activate it later. See
29 |
30 | ```shell
31 | uvx --from git+https://github.com/oraios/serena serena start-mcp-server --help
32 | ```
33 |
34 | ---
35 |
36 | ## 2. Expose the Server Using Cloudflare Tunnel
37 |
38 | Run:
39 |
40 | ```bash
41 | cloudflared tunnel --url http://localhost:8000
42 | ```
43 |
44 | This will give you a **public HTTPS URL** like:
45 |
46 | ```
47 | https://serena-agent-tunnel.trycloudflare.com
48 | ```
49 |
50 | Your server is now securely exposed to the internet.
51 |
52 | ---
53 |
54 | ## 3. Connect It to ChatGPT (Custom GPT)
55 |
56 | ### Steps:
57 |
58 | 1. Go to [ChatGPT → Explore GPTs → Create](https://chat.openai.com/gpts/editor)
59 | 2. During setup, click **“Add APIs”**
60 | 3. Set up **API Key authentication** with the auth type as **Bearer** and enter the api key you used to start the MCPO server.
61 | 4. In the **Schema** section, click on **import from URL** and paste `<cloudflared_url>/openapi.json` with the URL you got from the previous step.
62 | 5. Add the following line to the top of the imported JSON schema:
63 | ```
64 | "servers": ["url": "<cloudflared_url>"],
65 | ```
66 | **Important**: don't include a trailing slash at the end of the URL!
67 |
68 | ChatGPT will read the schema and create functions automatically.
69 |
70 | ---
71 |
72 | ## Security Warning — Read Carefully
73 |
74 | Depending on your configuration and enabled tools, Serena's MCP server may:
75 | - Execute **arbitrary shell commands**
76 | - Read, write, and modify **files in your codebase**
77 |
78 | This gives ChatGPT the same powers as a remote developer on your machine.
79 |
80 | ### ⚠️ Key Rules:
81 | - **NEVER expose your API key**
82 | - **Only expose this server when needed**, and monitor its use.
83 |
84 | In your project’s `.serena/project.yml` or global config, you can disable tools like:
85 |
86 | ```yaml
87 | excluded_tools:
88 | - execute_shell_command
89 | - ...
90 | read_only: true
91 | ```
92 |
93 | This is strongly recommended if you want a read-only or safer agent.
94 |
95 |
96 | ---
97 |
98 | ## Final Thoughts
99 |
100 | With this setup, ChatGPT becomes a coding assistant **running on your local code** — able to index, search, edit, and even run shell commands depending on your configuration.
101 |
102 | Use responsibly, and keep security in mind.
103 |
```
--------------------------------------------------------------------------------
/test/resources/repos/ruby/test_repo/variables.rb:
--------------------------------------------------------------------------------
```ruby
1 | require './models.rb'
2 |
3 | # Global variables for testing references
4 | $global_counter = 0
5 | $global_config = {
6 | debug: true,
7 | timeout: 30
8 | }
9 |
10 | class DataContainer
11 | attr_accessor :status, :data, :metadata
12 |
13 | def initialize
14 | @status = "pending"
15 | @data = {}
16 | @metadata = {
17 | created_at: Time.now,
18 | version: "1.0"
19 | }
20 | end
21 |
22 | def update_status(new_status)
23 | old_status = @status
24 | @status = new_status
25 | log_status_change(old_status, new_status)
26 | end
27 |
28 | def process_data(input_data)
29 | @data = input_data
30 | @status = "processing"
31 |
32 | # Process the data
33 | result = @data.transform_values { |v| v.to_s.upcase }
34 | @status = "completed"
35 |
36 | result
37 | end
38 |
39 | def get_metadata_info
40 | info = "Status: #{@status}, Version: #{@metadata[:version]}"
41 | info += ", Created: #{@metadata[:created_at]}"
42 | info
43 | end
44 |
45 | private
46 |
47 | def log_status_change(old_status, new_status)
48 | puts "Status changed from #{old_status} to #{new_status}"
49 | end
50 | end
51 |
52 | class StatusTracker
53 | def initialize
54 | @tracked_items = []
55 | end
56 |
57 | def add_item(item)
58 | @tracked_items << item
59 | item.status = "tracked" if item.respond_to?(:status=)
60 | end
61 |
62 | def find_by_status(target_status)
63 | @tracked_items.select { |item| item.status == target_status }
64 | end
65 |
66 | def update_all_status(new_status)
67 | @tracked_items.each do |item|
68 | item.status = new_status if item.respond_to?(:status=)
69 | end
70 | end
71 | end
72 |
73 | # Module level variables and functions
74 | module ProcessingHelper
75 | PROCESSING_MODES = ["sync", "async", "batch"].freeze
76 |
77 | @@instance_count = 0
78 |
79 | def self.create_processor(mode = "sync")
80 | @@instance_count += 1
81 | {
82 | id: @@instance_count,
83 | mode: mode,
84 | created_at: Time.now
85 | }
86 | end
87 |
88 | def self.get_instance_count
89 | @@instance_count
90 | end
91 | end
92 |
93 | # Test instances for reference testing
94 | dataclass_instance = DataContainer.new
95 | dataclass_instance.status = "initialized"
96 |
97 | second_dataclass = DataContainer.new
98 | second_dataclass.update_status("ready")
99 |
100 | tracker = StatusTracker.new
101 | tracker.add_item(dataclass_instance)
102 | tracker.add_item(second_dataclass)
103 |
104 | # Function that uses the variables
105 | def demonstrate_variable_usage
106 | puts "Global counter: #{$global_counter}"
107 |
108 | container = DataContainer.new
109 | container.status = "demo"
110 |
111 | processor = ProcessingHelper.create_processor("async")
112 | puts "Created processor #{processor[:id]} in #{processor[:mode]} mode"
113 |
114 | container
115 | end
116 |
117 | # More complex variable interactions
118 | class VariableInteractionTest
119 | def initialize
120 | @internal_status = "created"
121 | @data_containers = []
122 | end
123 |
124 | def add_container(container)
125 | @data_containers << container
126 | container.status = "added_to_collection"
127 | @internal_status = "modified"
128 | end
129 |
130 | def process_all_containers
131 | @data_containers.each do |container|
132 | container.status = "batch_processed"
133 | end
134 | @internal_status = "processing_complete"
135 | end
136 |
137 | def get_status_summary
138 | statuses = @data_containers.map(&:status)
139 | {
140 | internal: @internal_status,
141 | containers: statuses,
142 | count: @data_containers.length
143 | }
144 | end
145 | end
146 |
147 | # Create instances for testing
148 | interaction_test = VariableInteractionTest.new
149 | interaction_test.add_container(dataclass_instance)
150 | interaction_test.add_container(second_dataclass)
```
--------------------------------------------------------------------------------
/src/serena/tools/config_tools.py:
--------------------------------------------------------------------------------
```python
1 | from serena.config.context_mode import SerenaAgentMode
2 | from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
3 |
4 |
5 | class OpenDashboardTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActiveProject):
6 | """
7 | Opens the Serena web dashboard in the default web browser.
8 | The dashboard provides logs, session information, and tool usage statistics.
9 | """
10 |
11 | def apply(self) -> str:
12 | """
13 | Opens the Serena web dashboard in the default web browser.
14 | """
15 | if self.agent.open_dashboard():
16 | return f"Serena web dashboard has been opened in the user's default web browser: {self.agent.get_dashboard_url()}"
17 | else:
18 | return f"Serena web dashboard could not be opened automatically; tell the user to open it via {self.agent.get_dashboard_url()}"
19 |
20 |
21 | class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
22 | """
23 | Activates a project based on the project name or path.
24 | """
25 |
26 | def apply(self, project: str) -> str:
27 | """
28 | Activates the project with the given name or path.
29 |
30 | :param project: the name of a registered project to activate or a path to a project directory
31 | """
32 | active_project = self.agent.activate_project_from_path_or_name(project)
33 | result = active_project.get_activation_message()
34 | result += "\nIMPORTANT: If you have not yet read the 'Serena Instructions Manual', do it now before continuing!"
35 | return result
36 |
37 |
38 | class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional):
39 | """
40 | Removes a project from the Serena configuration.
41 | """
42 |
43 | def apply(self, project_name: str) -> str:
44 | """
45 | Removes a project from the Serena configuration.
46 |
47 | :param project_name: Name of the project to remove
48 | """
49 | self.agent.serena_config.remove_project(project_name)
50 | return f"Successfully removed project '{project_name}' from configuration."
51 |
52 |
53 | class SwitchModesTool(Tool, ToolMarkerOptional):
54 | """
55 | Activates modes by providing a list of their names
56 | """
57 |
58 | def apply(self, modes: list[str]) -> str:
59 | """
60 | Activates the desired modes, like ["editing", "interactive"] or ["planning", "one-shot"]
61 |
62 | :param modes: the names of the modes to activate
63 | """
64 | mode_instances = [SerenaAgentMode.load(mode) for mode in modes]
65 | self.agent.set_modes(mode_instances)
66 |
67 | # Inform the Agent about the activated modes and the currently active tools
68 | result_str = f"Successfully activated modes: {', '.join([mode.name for mode in mode_instances])}" + "\n"
69 | result_str += "\n".join([mode_instance.prompt for mode_instance in mode_instances]) + "\n"
70 | result_str += f"Currently active tools: {', '.join(self.agent.get_active_tool_names())}"
71 | return result_str
72 |
73 |
74 | class GetCurrentConfigTool(Tool):
75 | """
76 | Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
77 | """
78 |
79 | def apply(self) -> str:
80 | """
81 | Print the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
82 | """
83 | return self.agent.get_current_config_overview()
84 |
```
--------------------------------------------------------------------------------
/test/solidlsp/toml/test_toml_ignored_dirs.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Tests for TOML language server directory ignoring functionality.
3 |
4 | These tests validate that the Taplo language server correctly ignores
5 | TOML-specific directories like target, .cargo, and node_modules.
6 | """
7 |
8 | import pytest
9 |
10 | from solidlsp import SolidLanguageServer
11 | from solidlsp.ls_config import Language
12 |
13 | pytestmark = pytest.mark.toml
14 |
15 |
16 | @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
17 | class TestTomlIgnoredDirectories:
18 | """Test TOML-specific directory ignoring behavior."""
19 |
20 | def test_default_ignored_directories(self, language_server: SolidLanguageServer) -> None:
21 | """Test that default TOML directories are ignored."""
22 | # Test that TOML/Rust/Node-specific directories are ignored by default
23 | assert language_server.is_ignored_dirname("target"), "target should be ignored"
24 | assert language_server.is_ignored_dirname(".cargo"), ".cargo should be ignored"
25 | assert language_server.is_ignored_dirname("node_modules"), "node_modules should be ignored"
26 |
27 | # Directories starting with . are ignored by base class
28 | assert language_server.is_ignored_dirname(".git"), ".git should be ignored"
29 | assert language_server.is_ignored_dirname(".venv"), ".venv should be ignored"
30 |
31 | def test_important_directories_not_ignored(self, language_server: SolidLanguageServer) -> None:
32 | """Test that important directories are not ignored."""
33 | # Common project directories should not be ignored
34 | assert not language_server.is_ignored_dirname("src"), "src should not be ignored"
35 | assert not language_server.is_ignored_dirname("crates"), "crates should not be ignored"
36 | assert not language_server.is_ignored_dirname("lib"), "lib should not be ignored"
37 | assert not language_server.is_ignored_dirname("tests"), "tests should not be ignored"
38 | assert not language_server.is_ignored_dirname("config"), "config should not be ignored"
39 |
40 | def test_cargo_related_directories(self, language_server: SolidLanguageServer) -> None:
41 | """Test Cargo/Rust-related directory handling."""
42 | # Rust build directories should be ignored
43 | assert language_server.is_ignored_dirname("target"), "target (Rust build) should be ignored"
44 | assert language_server.is_ignored_dirname(".cargo"), ".cargo should be ignored"
45 |
46 | # But important Rust directories should not be ignored
47 | assert not language_server.is_ignored_dirname("benches"), "benches should not be ignored"
48 | assert not language_server.is_ignored_dirname("examples"), "examples should not be ignored"
49 |
50 | def test_various_cache_directories(self, language_server: SolidLanguageServer) -> None:
51 | """Test various cache and temporary directories are ignored."""
52 | # Directories starting with . are ignored by base class
53 | assert language_server.is_ignored_dirname(".cache"), ".cache should be ignored"
54 |
55 | # IDE directories (start with .)
56 | assert language_server.is_ignored_dirname(".idea"), ".idea should be ignored"
57 | assert language_server.is_ignored_dirname(".vscode"), ".vscode should be ignored"
58 |
59 | # Note: __pycache__ is NOT ignored by TOML server (only Python servers ignore it)
60 | assert not language_server.is_ignored_dirname("__pycache__"), "__pycache__ is not TOML-specific"
61 |
```
--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/utils.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Utility functions and classes demonstrating various Python features.
3 | """
4 |
5 | import logging
6 | from collections.abc import Callable
7 | from typing import Any, TypeVar
8 |
9 | # Type variables for generic functions
10 | T = TypeVar("T")
11 | U = TypeVar("U")
12 |
13 |
14 | def setup_logging(level: str = "INFO") -> logging.Logger:
15 | """Set up and return a configured logger"""
16 | levels = {
17 | "DEBUG": logging.DEBUG,
18 | "INFO": logging.INFO,
19 | "WARNING": logging.WARNING,
20 | "ERROR": logging.ERROR,
21 | "CRITICAL": logging.CRITICAL,
22 | }
23 |
24 | logger = logging.getLogger("test_repo")
25 | logger.setLevel(levels.get(level.upper(), logging.INFO))
26 |
27 | handler = logging.StreamHandler()
28 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
29 | handler.setFormatter(formatter)
30 | logger.addHandler(handler)
31 |
32 | return logger
33 |
34 |
35 | # Decorator example
36 | def log_execution(func: Callable) -> Callable:
37 | """Decorator to log function execution"""
38 |
39 | def wrapper(*args, **kwargs):
40 | logger = logging.getLogger("test_repo")
41 | logger.info(f"Executing function: {func.__name__}")
42 | result = func(*args, **kwargs)
43 | logger.info(f"Completed function: {func.__name__}")
44 | return result
45 |
46 | return wrapper
47 |
48 |
49 | # Higher-order function
50 | def map_list(items: list[T], mapper: Callable[[T], U]) -> list[U]:
51 | """Map a function over a list of items"""
52 | return [mapper(item) for item in items]
53 |
54 |
55 | # Class with various Python features
56 | class ConfigManager:
57 | """Manages configuration with various access patterns"""
58 |
59 | _instance = None
60 |
61 | # Singleton pattern
62 | def __new__(cls, *args, **kwargs):
63 | if not cls._instance:
64 | cls._instance = super().__new__(cls)
65 | return cls._instance
66 |
67 | def __init__(self, initial_config: dict[str, Any] | None = None):
68 | if not hasattr(self, "initialized"):
69 | self.config = initial_config or {}
70 | self.initialized = True
71 |
72 | def __getitem__(self, key: str) -> Any:
73 | """Allow dictionary-like access"""
74 | return self.config.get(key)
75 |
76 | def __setitem__(self, key: str, value: Any) -> None:
77 | """Allow dictionary-like setting"""
78 | self.config[key] = value
79 |
80 | @property
81 | def debug_mode(self) -> bool:
82 | """Property example"""
83 | return self.config.get("debug", False)
84 |
85 | @debug_mode.setter
86 | def debug_mode(self, value: bool) -> None:
87 | self.config["debug"] = value
88 |
89 |
90 | # Context manager example
91 | class Timer:
92 | """Context manager for timing code execution"""
93 |
94 | def __init__(self, name: str = "Timer"):
95 | self.name = name
96 | self.start_time = None
97 | self.end_time = None
98 |
99 | def __enter__(self):
100 | import time
101 |
102 | self.start_time = time.time()
103 | return self
104 |
105 | def __exit__(self, exc_type, exc_val, exc_tb):
106 | import time
107 |
108 | self.end_time = time.time()
109 | print(f"{self.name} took {self.end_time - self.start_time:.6f} seconds")
110 |
111 |
112 | # Functions with default arguments
113 | def retry(func: Callable, max_attempts: int = 3, delay: float = 1.0) -> Any:
114 | """Retry a function with backoff"""
115 | import time
116 |
117 | for attempt in range(max_attempts):
118 | try:
119 | return func()
120 | except Exception as e:
121 | if attempt == max_attempts - 1:
122 | raise e
123 | time.sleep(delay * (2**attempt))
124 |
```
--------------------------------------------------------------------------------
/test/resources/repos/vue/test_repo/src/composables/useFormatter.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ref, computed } from 'vue'
2 | import type { Ref, ComputedRef } from 'vue'
3 | import type { FormatOptions } from '@/types'
4 |
5 | /**
6 | * Composable for formatting numbers with various options.
7 | * Demonstrates: composable pattern, refs, computed, type imports
8 | */
9 | export function useFormatter(initialPrecision: number = 2) {
10 | // State
11 | const precision = ref<number>(initialPrecision)
12 | const useGrouping = ref<boolean>(true)
13 | const locale = ref<string>('en-US')
14 |
15 | // Computed properties
16 | const formatOptions = computed((): FormatOptions => ({
17 | maxDecimals: precision.value,
18 | useGrouping: useGrouping.value
19 | }))
20 |
21 | // Methods
22 | const formatNumber = (value: number): string => {
23 | return value.toLocaleString(locale.value, {
24 | minimumFractionDigits: precision.value,
25 | maximumFractionDigits: precision.value,
26 | useGrouping: useGrouping.value
27 | })
28 | }
29 |
30 | const formatCurrency = (value: number, currency: string = 'USD'): string => {
31 | return value.toLocaleString(locale.value, {
32 | style: 'currency',
33 | currency,
34 | minimumFractionDigits: precision.value,
35 | maximumFractionDigits: precision.value
36 | })
37 | }
38 |
39 | const formatPercentage = (value: number): string => {
40 | return `${(value * 100).toFixed(precision.value)}%`
41 | }
42 |
43 | const setPrecision = (newPrecision: number): void => {
44 | if (newPrecision >= 0 && newPrecision <= 10) {
45 | precision.value = newPrecision
46 | }
47 | }
48 |
49 | const toggleGrouping = (): void => {
50 | useGrouping.value = !useGrouping.value
51 | }
52 |
53 | const setLocale = (newLocale: string): void => {
54 | locale.value = newLocale
55 | }
56 |
57 | // Return composable API
58 | return {
59 | // State (readonly)
60 | precision: computed(() => precision.value),
61 | useGrouping: computed(() => useGrouping.value),
62 | locale: computed(() => locale.value),
63 | formatOptions,
64 |
65 | // Methods
66 | formatNumber,
67 | formatCurrency,
68 | formatPercentage,
69 | setPrecision,
70 | toggleGrouping,
71 | setLocale
72 | }
73 | }
74 |
75 | /**
76 | * Composable for time formatting.
77 | * Demonstrates: simpler composable, pure functions
78 | */
79 | export function useTimeFormatter() {
80 | const formatTime = (date: Date): string => {
81 | return date.toLocaleTimeString('en-US', {
82 | hour: '2-digit',
83 | minute: '2-digit',
84 | second: '2-digit'
85 | })
86 | }
87 |
88 | const formatDate = (date: Date): string => {
89 | return date.toLocaleDateString('en-US', {
90 | year: 'numeric',
91 | month: 'long',
92 | day: 'numeric'
93 | })
94 | }
95 |
96 | const formatDateTime = (date: Date): string => {
97 | return `${formatDate(date)} ${formatTime(date)}`
98 | }
99 |
100 | const getRelativeTime = (date: Date): string => {
101 | const now = new Date()
102 | const diffMs = now.getTime() - date.getTime()
103 | const diffSecs = Math.floor(diffMs / 1000)
104 | const diffMins = Math.floor(diffSecs / 60)
105 | const diffHours = Math.floor(diffMins / 60)
106 | const diffDays = Math.floor(diffHours / 24)
107 |
108 | if (diffSecs < 60) return 'just now'
109 | if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`
110 | if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`
111 | return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`
112 | }
113 |
114 | return {
115 | formatTime,
116 | formatDate,
117 | formatDateTime,
118 | getRelativeTime
119 | }
120 | }
121 |
122 | /**
123 | * Type definitions for return types
124 | */
125 | export type UseFormatterReturn = ReturnType<typeof useFormatter>
126 | export type UseTimeFormatterReturn = ReturnType<typeof useTimeFormatter>
127 |
```
--------------------------------------------------------------------------------
/test/solidlsp/markdown/test_markdown_basic.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Basic integration tests for the markdown language server functionality.
3 |
4 | These tests validate the functionality of the language server APIs
5 | like request_document_symbols using the markdown test repository.
6 | """
7 |
8 | import pytest
9 |
10 | from solidlsp import SolidLanguageServer
11 | from solidlsp.ls_config import Language
12 |
13 |
14 | @pytest.mark.markdown
15 | class TestMarkdownLanguageServerBasics:
16 | """Test basic functionality of the markdown language server."""
17 |
18 | @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
19 | def test_markdown_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
20 | """Test that markdown language server can be initialized successfully."""
21 | assert language_server is not None
22 | assert language_server.language == Language.MARKDOWN
23 |
24 | @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
25 | def test_markdown_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
26 | """Test request_document_symbols for markdown files."""
27 | # Test getting symbols from README.md
28 | all_symbols, _root_symbols = language_server.request_document_symbols("README.md").get_all_symbols_and_roots()
29 |
30 | # Extract heading symbols (LSP Symbol Kind 15 is String, but marksman uses kind 15 for headings)
31 | # Note: Different markdown LSPs may use different symbol kinds for headings
32 | # Marksman typically uses kind 15 (String) for markdown headings
33 | heading_names = [symbol["name"] for symbol in all_symbols]
34 |
35 | # Should detect headings from README.md
36 | assert "Test Repository" in heading_names or len(all_symbols) > 0, "Should find at least one heading"
37 |
38 | @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
39 | def test_markdown_request_symbols_from_guide(self, language_server: SolidLanguageServer) -> None:
40 | """Test symbol detection in guide.md file."""
41 | all_symbols, _root_symbols = language_server.request_document_symbols("guide.md").get_all_symbols_and_roots()
42 |
43 | # At least some headings should be found
44 | assert len(all_symbols) > 0, f"Should find headings in guide.md, found {len(all_symbols)}"
45 |
46 | @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
47 | def test_markdown_request_symbols_from_api(self, language_server: SolidLanguageServer) -> None:
48 | """Test symbol detection in api.md file."""
49 | all_symbols, _root_symbols = language_server.request_document_symbols("api.md").get_all_symbols_and_roots()
50 |
51 | # Should detect headings from api.md
52 | assert len(all_symbols) > 0, f"Should find headings in api.md, found {len(all_symbols)}"
53 |
54 | @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
55 | def test_markdown_request_document_symbols_with_body(self, language_server: SolidLanguageServer) -> None:
56 | """Test request_document_symbols with body extraction."""
57 | # Test with include_body=True
58 | all_symbols, _root_symbols = language_server.request_document_symbols("README.md").get_all_symbols_and_roots()
59 |
60 | # Should have found some symbols
61 | assert len(all_symbols) > 0, "Should find symbols in README.md"
62 |
63 | # Note: Not all markdown LSPs provide body information for symbols
64 | # This test is more lenient and just verifies the API works
65 | assert all_symbols is not None, "Should return symbols even if body extraction is limited"
66 |
```
--------------------------------------------------------------------------------
/src/serena/tools/memory_tools.py:
--------------------------------------------------------------------------------
```python
1 | from typing import Literal
2 |
3 | from serena.tools import ReplaceContentTool, Tool, ToolMarkerCanEdit
4 |
5 |
6 | class WriteMemoryTool(Tool, ToolMarkerCanEdit):
7 | """
8 | Writes a named memory (for future reference) to Serena's project-specific memory store.
9 | """
10 |
11 | def apply(self, memory_file_name: str, content: str, max_answer_chars: int = -1) -> str:
12 | """
13 | Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
14 | The memory name should be meaningful.
15 | """
16 | # NOTE: utf-8 encoding is configured in the MemoriesManager
17 | if max_answer_chars == -1:
18 | max_answer_chars = self.agent.serena_config.default_max_tool_answer_chars
19 | if len(content) > max_answer_chars:
20 | raise ValueError(
21 | f"Content for {memory_file_name} is too long. Max length is {max_answer_chars} characters. "
22 | + "Please make the content shorter."
23 | )
24 |
25 | return self.memories_manager.save_memory(memory_file_name, content)
26 |
27 |
28 | class ReadMemoryTool(Tool):
29 | """
30 | Reads the memory with the given name from Serena's project-specific memory store.
31 | """
32 |
33 | def apply(self, memory_file_name: str, max_answer_chars: int = -1) -> str:
34 | """
35 | Read the content of a memory file. This tool should only be used if the information
36 | is relevant to the current task. You can infer whether the information
37 | is relevant from the memory file name.
38 | You should not read the same memory file multiple times in the same conversation.
39 | """
40 | return self.memories_manager.load_memory(memory_file_name)
41 |
42 |
43 | class ListMemoriesTool(Tool):
44 | """
45 | Lists memories in Serena's project-specific memory store.
46 | """
47 |
48 | def apply(self) -> str:
49 | """
50 | List available memories. Any memory can be read using the `read_memory` tool.
51 | """
52 | return self._to_json(self.memories_manager.list_memories())
53 |
54 |
55 | class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
56 | """
57 | Deletes a memory from Serena's project-specific memory store.
58 | """
59 |
60 | def apply(self, memory_file_name: str) -> str:
61 | """
62 | Delete a memory file. Should only happen if a user asks for it explicitly,
63 | for example by saying that the information retrieved from a memory file is no longer correct
64 | or no longer relevant for the project.
65 | """
66 | return self.memories_manager.delete_memory(memory_file_name)
67 |
68 |
69 | class EditMemoryTool(Tool, ToolMarkerCanEdit):
70 | def apply(
71 | self,
72 | memory_file_name: str,
73 | needle: str,
74 | repl: str,
75 | mode: Literal["literal", "regex"],
76 | ) -> str:
77 | r"""
78 | Replaces content matching a regular expression in a memory.
79 |
80 | :param memory_file_name: the name of the memory
81 | :param needle: the string or regex pattern to search for.
82 | If `mode` is "literal", this string will be matched exactly.
83 | If `mode` is "regex", this string will be treated as a regular expression (syntax of Python's `re` module,
84 | with flags DOTALL and MULTILINE enabled).
85 | :param repl: the replacement string (verbatim).
86 | :param mode: either "literal" or "regex", specifying how the `needle` parameter is to be interpreted.
87 | """
88 | replace_content_tool = self.agent.get_tool(ReplaceContentTool)
89 | rel_path = self.memories_manager.get_memory_file_path(memory_file_name).relative_to(self.get_project_root())
90 | return replace_content_tool.replace_content(str(rel_path), needle, repl, mode=mode, require_not_ignored=False)
91 |
```
--------------------------------------------------------------------------------
/test/serena/test_task_executor.py:
--------------------------------------------------------------------------------
```python
1 | import time
2 |
3 | import pytest
4 |
5 | from serena.task_executor import TaskExecutor
6 |
7 |
8 | @pytest.fixture
9 | def executor():
10 | """
11 | Fixture for a basic SerenaAgent without a project
12 | """
13 | return TaskExecutor("TestExecutor")
14 |
15 |
16 | class Task:
17 | def __init__(self, delay: float, exception: bool = False):
18 | self.delay = delay
19 | self.exception = exception
20 | self.did_run = False
21 |
22 | def run(self):
23 | self.did_run = True
24 | time.sleep(self.delay)
25 | if self.exception:
26 | raise ValueError("Task failed")
27 | return True
28 |
29 |
30 | def test_task_executor_sequence(executor):
31 | """
32 | Tests that a sequence of tasks is executed correctly
33 | """
34 | future1 = executor.issue_task(Task(1).run, name="task1")
35 | future2 = executor.issue_task(Task(1).run, name="task2")
36 | assert future1.result() is True
37 | assert future2.result() is True
38 |
39 |
40 | def test_task_executor_exception(executor):
41 | """
42 | Tests that tasks that raise exceptions are handled correctly, i.e. that
43 | * the exception is propagated,
44 | * subsequent tasks are still executed.
45 | """
46 | future1 = executor.issue_task(Task(1, exception=True).run, name="task1")
47 | future2 = executor.issue_task(Task(1).run, name="task2")
48 | have_exception = False
49 | try:
50 | assert future1.result()
51 | except Exception as e:
52 | assert isinstance(e, ValueError)
53 | have_exception = True
54 | assert have_exception
55 | assert future2.result() is True
56 |
57 |
58 | def test_task_executor_cancel_current(executor):
59 | """
60 | Tests that tasks that are cancelled are handled correctly, i.e. that
61 | * subsequent tasks are executed as soon as cancellation ensues.
62 | * the cancelled task raises CancelledError when result() is called.
63 | """
64 | start_time = time.time()
65 | future1 = executor.issue_task(Task(10).run, name="task1")
66 | future2 = executor.issue_task(Task(1).run, name="task2")
67 | time.sleep(1)
68 | future1.cancel()
69 | assert future2.result() is True
70 | end_time = time.time()
71 | assert (end_time - start_time) < 9, "Cancelled task did not stop in time"
72 | have_cancelled_error = False
73 | try:
74 | future1.result()
75 | except Exception as e:
76 | assert e.__class__.__name__ == "CancelledError"
77 | have_cancelled_error = True
78 | assert have_cancelled_error
79 |
80 |
81 | def test_task_executor_cancel_future(executor):
82 | """
83 | Tests that when a future task is cancelled, it is never run at all
84 | """
85 | task1 = Task(10)
86 | task2 = Task(1)
87 | future1 = executor.issue_task(task1.run, name="task1")
88 | future2 = executor.issue_task(task2.run, name="task2")
89 | time.sleep(1)
90 | future2.cancel()
91 | future1.cancel()
92 | try:
93 | future2.result()
94 | except:
95 | pass
96 | assert task1.did_run
97 | assert not task2.did_run
98 |
99 |
100 | def test_task_executor_cancellation_via_task_info(executor):
101 | start_time = time.time()
102 | executor.issue_task(Task(10).run, "task1")
103 | executor.issue_task(Task(10).run, "task2")
104 | task_infos = executor.get_current_tasks()
105 | task_infos2 = executor.get_current_tasks()
106 |
107 | # test expected tasks
108 | assert len(task_infos) == 2
109 | assert "task1" in task_infos[0].name
110 | assert "task2" in task_infos[1].name
111 |
112 | # test task identifiers being stable
113 | assert task_infos2[0].task_id == task_infos[0].task_id
114 |
115 | # test cancellation
116 | task_infos[0].cancel()
117 | time.sleep(0.5)
118 | task_infos3 = executor.get_current_tasks()
119 | assert len(task_infos3) == 1 # Cancelled task is gone from the queue
120 | task_infos3[0].cancel()
121 | try:
122 | task_infos3[0].future.result()
123 | except:
124 | pass
125 | end_time = time.time()
126 | assert (end_time - start_time) < 9, "Cancelled task did not stop in time"
127 |
```
--------------------------------------------------------------------------------
/test/solidlsp/util/test_zip.py:
--------------------------------------------------------------------------------
```python
1 | import sys
2 | import zipfile
3 | from pathlib import Path
4 |
5 | import pytest
6 |
7 | from solidlsp.util.zip import SafeZipExtractor
8 |
9 |
10 | @pytest.fixture
11 | def temp_zip_file(tmp_path: Path) -> Path:
12 | """Create a temporary ZIP file for testing."""
13 | zip_path = tmp_path / "test.zip"
14 | with zipfile.ZipFile(zip_path, "w") as zipf:
15 | zipf.writestr("file1.txt", "Hello World 1")
16 | zipf.writestr("file2.txt", "Hello World 2")
17 | zipf.writestr("folder/file3.txt", "Hello World 3")
18 | return zip_path
19 |
20 |
21 | def test_extract_all_success(temp_zip_file: Path, tmp_path: Path) -> None:
22 | """All files should extract without error."""
23 | dest_dir = tmp_path / "extracted"
24 | extractor = SafeZipExtractor(temp_zip_file, dest_dir, verbose=False)
25 | extractor.extract_all()
26 |
27 | assert (dest_dir / "file1.txt").read_text() == "Hello World 1"
28 | assert (dest_dir / "file2.txt").read_text() == "Hello World 2"
29 | assert (dest_dir / "folder" / "file3.txt").read_text() == "Hello World 3"
30 |
31 |
32 | def test_include_patterns(temp_zip_file: Path, tmp_path: Path) -> None:
33 | """Only files matching include_patterns should be extracted."""
34 | dest_dir = tmp_path / "extracted"
35 | extractor = SafeZipExtractor(temp_zip_file, dest_dir, verbose=False, include_patterns=["*.txt"])
36 | extractor.extract_all()
37 |
38 | assert (dest_dir / "file1.txt").exists()
39 | assert (dest_dir / "file2.txt").exists()
40 | assert (dest_dir / "folder" / "file3.txt").exists()
41 |
42 |
43 | def test_exclude_patterns(temp_zip_file: Path, tmp_path: Path) -> None:
44 | """Files matching exclude_patterns should be skipped."""
45 | dest_dir = tmp_path / "extracted"
46 | extractor = SafeZipExtractor(temp_zip_file, dest_dir, verbose=False, exclude_patterns=["file2.txt"])
47 | extractor.extract_all()
48 |
49 | assert (dest_dir / "file1.txt").exists()
50 | assert not (dest_dir / "file2.txt").exists()
51 | assert (dest_dir / "folder" / "file3.txt").exists()
52 |
53 |
54 | def test_include_and_exclude_patterns(temp_zip_file: Path, tmp_path: Path) -> None:
55 | """Exclude should override include if both match."""
56 | dest_dir = tmp_path / "extracted"
57 | extractor = SafeZipExtractor(
58 | temp_zip_file,
59 | dest_dir,
60 | verbose=False,
61 | include_patterns=["*.txt"],
62 | exclude_patterns=["file1.txt"],
63 | )
64 | extractor.extract_all()
65 |
66 | assert not (dest_dir / "file1.txt").exists()
67 | assert (dest_dir / "file2.txt").exists()
68 | assert (dest_dir / "folder" / "file3.txt").exists()
69 |
70 |
71 | def test_skip_on_error(monkeypatch, temp_zip_file: Path, tmp_path: Path) -> None:
72 | """Should skip a file that raises an error and continue extracting others."""
73 | dest_dir = tmp_path / "extracted"
74 |
75 | original_open = zipfile.ZipFile.open
76 |
77 | def failing_open(self, member, *args, **kwargs):
78 | if member.filename == "file2.txt":
79 | raise OSError("Simulated failure")
80 | return original_open(self, member, *args, **kwargs)
81 |
82 | # Patch the method on the class, not on an instance
83 | monkeypatch.setattr(zipfile.ZipFile, "open", failing_open)
84 |
85 | extractor = SafeZipExtractor(temp_zip_file, dest_dir, verbose=False)
86 | extractor.extract_all()
87 |
88 | assert (dest_dir / "file1.txt").exists()
89 | assert not (dest_dir / "file2.txt").exists()
90 | assert (dest_dir / "folder" / "file3.txt").exists()
91 |
92 |
93 | @pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows-only test")
94 | def test_long_path_normalization(temp_zip_file: Path, tmp_path: Path) -> None:
95 | r"""Ensure _normalize_path adds \\?\\ prefix on Windows."""
96 | dest_dir = tmp_path / ("a" * 250) # Simulate long path
97 | extractor = SafeZipExtractor(temp_zip_file, dest_dir, verbose=False)
98 | norm_path = extractor._normalize_path(dest_dir / "file.txt")
99 | assert str(norm_path).startswith("\\\\?\\")
100 |
```
--------------------------------------------------------------------------------
/src/serena/util/logging.py:
--------------------------------------------------------------------------------
```python
1 | import queue
2 | import threading
3 | from collections.abc import Callable
4 | from typing import Optional
5 |
6 | from sensai.util import logging
7 |
8 | from serena.constants import SERENA_LOG_FORMAT
9 |
10 | lg = logging
11 |
12 |
13 | class MemoryLogHandler(logging.Handler):
14 | def __init__(self, level: int = logging.NOTSET) -> None:
15 | super().__init__(level=level)
16 | self.setFormatter(logging.Formatter(SERENA_LOG_FORMAT))
17 | self._log_buffer = LogBuffer()
18 | self._log_queue: queue.Queue[str] = queue.Queue()
19 | self._stop_event = threading.Event()
20 | self._emit_callbacks: list[Callable[[str], None]] = []
21 |
22 | # start background thread to process logs
23 | self.worker_thread = threading.Thread(target=self._process_queue, daemon=True)
24 | self.worker_thread.start()
25 |
26 | def add_emit_callback(self, callback: Callable[[str], None]) -> None:
27 | """
28 | Adds a callback that will be called with each log message.
29 | The callback should accept a single string argument (the log message).
30 | """
31 | self._emit_callbacks.append(callback)
32 |
33 | def emit(self, record: logging.LogRecord) -> None:
34 | msg = self.format(record)
35 | self._log_queue.put_nowait(msg)
36 |
37 | def _process_queue(self) -> None:
38 | while not self._stop_event.is_set():
39 | try:
40 | msg = self._log_queue.get(timeout=1)
41 | self._log_buffer.append(msg)
42 | for callback in self._emit_callbacks:
43 | try:
44 | callback(msg)
45 | except:
46 | pass
47 | self._log_queue.task_done()
48 | except queue.Empty:
49 | continue
50 |
51 | def get_log_messages(self) -> list[str]:
52 | return self._log_buffer.get_log_messages()
53 |
54 |
55 | class LogBuffer:
56 | """
57 | A thread-safe buffer for storing log messages.
58 | """
59 |
60 | def __init__(self) -> None:
61 | self._log_messages: list[str] = []
62 | self._lock = threading.Lock()
63 |
64 | def append(self, msg: str) -> None:
65 | with self._lock:
66 | self._log_messages.append(msg)
67 |
68 | def get_log_messages(self) -> list[str]:
69 | with self._lock:
70 | return self._log_messages.copy()
71 |
72 |
73 | class SuspendedLoggersContext:
74 | """A context manager that provides an isolated logging environment.
75 |
76 | Temporarily removes all root log handlers upon entry, providing a clean slate
77 | for defining new log handlers within the context. Upon exit, restores the original
78 | logging configuration. This is useful when you need to temporarily configure
79 | an isolated logging setup with well-defined log handlers.
80 |
81 | The context manager:
82 | - Removes all existing (root) log handlers on entry
83 | - Allows defining new temporary handlers within the context
84 | - Restores the original configuration (handlers and root log level) on exit
85 |
86 | Example:
87 | >>> with SuspendedLoggersContext():
88 | ... # No handlers are active here (configure your own and set desired log level)
89 | ... pass
90 | >>> # Original log handlers are restored here
91 |
92 | """
93 |
94 | def __init__(self) -> None:
95 | self.saved_root_handlers: list = []
96 | self.saved_root_level: Optional[int] = None
97 |
98 | def __enter__(self) -> "SuspendedLoggersContext":
99 | root_logger = lg.getLogger()
100 | self.saved_root_handlers = root_logger.handlers.copy()
101 | self.saved_root_level = root_logger.level
102 | root_logger.handlers.clear()
103 | return self
104 |
105 | def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore
106 | root_logger = lg.getLogger()
107 | root_logger.handlers = self.saved_root_handlers
108 | if self.saved_root_level is not None:
109 | root_logger.setLevel(self.saved_root_level)
110 |
```
--------------------------------------------------------------------------------
/roadmap.md:
--------------------------------------------------------------------------------
```markdown
1 | # Roadmap
2 |
3 | This document gives an overview of the ongoing and future development of Serena.
4 | If you have a proposal or want to discuss something, feel free to open a discussion
5 | on Github. For a summary of the past development, see the [changelog](/CHANGELOG.md).
6 |
7 | Want to see us reach our goals faster? You can help out with an issue, start a discussion, or
8 | inform us about funding opportunities so that we can devote more time to the project.
9 |
10 | ## Overall Goals
11 |
12 | Serena has the potential to be the go-to tool for most LLM coding tasks, since it is
13 | unique in its ability to be used as MCP Server in any kind of environment
14 | while still being a capable agent. We want to achieve the following goals in terms of functionality:
15 |
16 | 1. Top performance (comparable to API-based coding agents) when used through official (free) clients like Claude Desktop.
17 | 1. Lowering API costs and potentially improving performance of coding clients (Claude Code, Codex, Cline, Roo, Cursor/Windsurf/VSCode etc).
18 | 1. Transparency and simplicity of use. Achieved through the dashboard/logging GUI.
19 | 1. Integrations with major frameworks that don't accept MCP. Usable as a library.
20 |
21 | Apart from the functional goals, we have the goal of having great code design, so that Serena can be viewed
22 | as a reference for how to implement MCP Servers. Such projects are an emerging technology, and
23 | best practices are yet to be determined. We will share our experiences in [lessons learned](/lessons_learned.md).
24 |
25 |
26 | ## Immediate/Ongoing
27 |
28 | - Support for projects using multiple programming languages.
29 | - Evaluate whether `ReplaceLinesTool` can be removed in favor of a more reliable and performant editing approach.
30 | - Generally experiment with various approaches to editing tools
31 | - Manual evaluation on selected tasks from SWE-verified
32 | - Manual evaluation of cost-lowering and performance when used within popular non-MCP agents
33 | - Improvements in prompts, in particular giving examples and extending modes and contexts
34 |
35 | ## Upcoming
36 |
37 | - Publishing Serena as a package that can also be used as library
38 | - Use linting and type-hierarchy from the LSP in tools
39 | - Tools for refactoring (rename, move) - speculative, maybe won't do this.
40 | - Tracking edits and rolling them back with the dashboard
41 | - Improve configurability and safety of shell tool. Maybe autogeneration of tools from a list of commands and descriptions.
42 | - Transparent comparison with DesktopCommander and ...
43 | - Automatic evaluation using OpenHands, submission to SWE-Bench
44 | - Evaluation whether incorporating other MCPs increases performance or usability (memory bank is a candidate)
45 | - More documentation and best practices
46 |
47 | ## Stretch
48 |
49 | - Allow for sandboxing and parallel instances of Serena, maybe use openhands or codex for that
50 | - Incorporate a verifier model or generally a second model (maybe for applying edits) as a tool.
51 | - Building on the above, allow for the second model itself to be reachable through an MCP server, so it can be used for free
52 | - Tracking edits performed with shell tools
53 |
54 | ## Beyond Serena
55 |
56 | The technologies and approaches taken in Serena can be used for various research and service ideas. Some thought that we had are:
57 |
58 | - PR and issue assistant working with GitHub, similar to how [OpenHands](https://github.com/All-Hands-AI/OpenHands)
59 | and [qodo](https://github.com/qodo-ai/pr-agent) operate. Should be callable through @serena
60 | - Tuning a coding LLM with Serena's tools with RL on one-shot tasks. We would need compute-funding for that
61 | - Develop a web app to quantitatively compare the performance of various agents by scraping PRs and manually crafted metadata.
62 | The main metric for coding agents should be *developer experience*, and that is hard to grasp and is poorly correlated with
63 | performance on current benchmarks.
```