This is page 3 of 11. Use http://codebase.md/oraios/serena?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 │ ├── junie.yml │ ├── lint_and_docs.yaml │ ├── publish.yml │ └── pytest.yml ├── .gitignore ├── .serena │ ├── 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 │ ├── custom_agent.md │ └── serena_on_chatgpt.md ├── flake.lock ├── flake.nix ├── lessons_learned.md ├── LICENSE ├── llms-install.md ├── public │ └── .gitignore ├── pyproject.toml ├── README.md ├── resources │ ├── 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 ├── 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 │ │ ├── mcp.py │ │ ├── project.py │ │ ├── prompt_factory.py │ │ ├── resources │ │ │ ├── config │ │ │ │ ├── contexts │ │ │ │ │ ├── agent.yml │ │ │ │ │ ├── chatgpt.yml │ │ │ │ │ ├── codex.yml │ │ │ │ │ ├── context.template.yml │ │ │ │ │ ├── desktop-app.yml │ │ │ │ │ ├── ide-assistant.yml │ │ │ │ │ └── oaicompat-agent.yml │ │ │ │ ├── internal_modes │ │ │ │ │ └── jetbrains.yml │ │ │ │ ├── modes │ │ │ │ │ ├── editing.yml │ │ │ │ │ ├── interactive.yml │ │ │ │ │ ├── mode.template.yml │ │ │ │ │ ├── no-onboarding.yml │ │ │ │ │ ├── onboarding.yml │ │ │ │ │ ├── one-shot.yml │ │ │ │ │ └── planning.yml │ │ │ │ └── prompt_templates │ │ │ │ ├── simple_tool_outputs.yml │ │ │ │ └── system_prompt.yml │ │ │ ├── dashboard │ │ │ │ ├── dashboard.js │ │ │ │ ├── index.html │ │ │ │ ├── jquery.min.js │ │ │ │ ├── serena-icon-16.png │ │ │ │ ├── serena-icon-32.png │ │ │ │ ├── serena-icon-48.png │ │ │ │ ├── serena-logs-dark-mode.png │ │ │ │ └── serena-logs.png │ │ │ ├── project.template.yml │ │ │ └── serena_config.template.yml │ │ ├── symbol.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 │ │ ├── exception.py │ │ ├── file_system.py │ │ ├── general.py │ │ ├── git.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 │ │ ├── gopls.py │ │ ├── intelephense.py │ │ ├── jedi_server.py │ │ ├── kotlin_language_server.py │ │ ├── lua_ls.py │ │ ├── marksman.py │ │ ├── nixd_ls.py │ │ ├── omnisharp │ │ │ ├── initialize_params.json │ │ │ ├── runtime_dependencies.json │ │ │ └── workspace_did_change_configuration.json │ │ ├── omnisharp.py │ │ ├── perl_language_server.py │ │ ├── pyright_server.py │ │ ├── r_language_server.py │ │ ├── ruby_lsp.py │ │ ├── rust_analyzer.py │ │ ├── solargraph.py │ │ ├── sourcekit_lsp.py │ │ ├── terraform_ls.py │ │ ├── typescript_language_server.py │ │ ├── vts_language_server.py │ │ └── zls.py │ ├── ls_config.py │ ├── ls_exceptions.py │ ├── ls_handler.py │ ├── ls_logger.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 │ ├── subprocess_util.py │ └── zip.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 │ │ ├── go │ │ │ └── test_repo │ │ │ └── main.go │ │ ├── java │ │ │ └── test_repo │ │ │ ├── pom.xml │ │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── test_repo │ │ │ ├── Main.java │ │ │ ├── Model.java │ │ │ ├── ModelUser.java │ │ │ └── Utils.java │ │ ├── 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 │ │ ├── nix │ │ │ └── test_repo │ │ │ ├── .gitignore │ │ │ ├── default.nix │ │ │ ├── flake.nix │ │ │ ├── lib │ │ │ │ └── utils.nix │ │ │ ├── modules │ │ │ │ └── example.nix │ │ │ └── scripts │ │ │ └── hello.sh │ │ ├── perl │ │ │ └── test_repo │ │ │ ├── helper.pl │ │ │ └── main.pl │ │ ├── php │ │ │ └── test_repo │ │ │ ├── helper.php │ │ │ ├── index.php │ │ │ └── simple_var.php │ │ ├── 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 │ │ ├── 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 │ │ ├── swift │ │ │ └── test_repo │ │ │ ├── Package.swift │ │ │ └── src │ │ │ ├── main.swift │ │ │ └── utils.swift │ │ ├── terraform │ │ │ └── test_repo │ │ │ ├── data.tf │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ │ ├── typescript │ │ │ └── test_repo │ │ │ ├── .serena │ │ │ │ └── project.yml │ │ │ ├── index.ts │ │ │ ├── tsconfig.json │ │ │ └── use_helper.ts │ │ └── 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_edit_marker.py │ │ ├── test_mcp.py │ │ ├── test_serena_agent.py │ │ ├── test_symbol_editing.py │ │ ├── test_symbol.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 │ ├── go │ │ └── test_go_basic.py │ ├── java │ │ └── test_java_basic.py │ ├── kotlin │ │ └── test_kotlin_basic.py │ ├── lua │ │ └── test_lua_basic.py │ ├── markdown │ │ ├── __init__.py │ │ └── test_markdown_basic.py │ ├── nix │ │ └── test_nix_basic.py │ ├── perl │ │ └── test_perl_basic.py │ ├── php │ │ └── test_php_basic.py │ ├── python │ │ ├── test_python_basic.py │ │ ├── test_retrieval_with_ignored_dirs.py │ │ └── test_symbol_retrieval.py │ ├── r │ │ ├── __init__.py │ │ └── test_r_basic.py │ ├── ruby │ │ ├── test_ruby_basic.py │ │ └── test_ruby_symbol_retrieval.py │ ├── rust │ │ ├── test_rust_2024_edition.py │ │ └── test_rust_basic.py │ ├── swift │ │ └── test_swift_basic.py │ ├── terraform │ │ └── test_terraform_basic.py │ ├── typescript │ │ └── test_typescript_basic.py │ ├── util │ │ └── test_zip.py │ └── zig │ └── test_zig_basic.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.serena/memories/serena_core_concepts_and_architecture.md: -------------------------------------------------------------------------------- ```markdown # Serena Core Concepts and Architecture ## High-Level Architecture Serena is built around a dual-layer architecture: 1. **SerenaAgent** - The main orchestrator that manages projects, tools, and user interactions 2. **SolidLanguageServer** - A unified wrapper around Language Server Protocol (LSP) implementations ## Core Components ### 1. SerenaAgent (`src/serena/agent.py`) The central coordinator that: - Manages active projects and their configurations - Coordinates between different tools and contexts - Handles language server lifecycle - Manages memory persistence - Provides MCP (Model Context Protocol) server interface Key responsibilities: - **Project Management** - Activating, switching between projects - **Tool Registry** - Loading and managing available tools based on context/mode - **Language Server Integration** - Starting/stopping language servers per project - **Memory Management** - Persistent storage of project knowledge - **Task Execution** - Coordinating complex multi-step operations ### 2. SolidLanguageServer (`src/solidlsp/ls.py`) A unified abstraction over multiple language servers that provides: - **Language-agnostic interface** for symbol operations - **Caching layer** for performance optimization - **Error handling and recovery** for unreliable language servers - **Uniform API** regardless of underlying LSP implementation Core capabilities: - Symbol discovery and navigation - Code completion and hover information - Find references and definitions - Document and workspace symbol search - File watching and change notifications ### 3. Tool System (`src/serena/tools/`) Modular tool architecture with several categories: #### File Tools (`file_tools.py`) - File system operations (read, write, list directories) - Text search and pattern matching - Regex-based replacements #### Symbol Tools (`symbol_tools.py`) - Language-aware symbol finding and navigation - Symbol body replacement and insertion - Reference finding across codebase #### Memory Tools (`memory_tools.py`) - Project knowledge persistence - Memory retrieval and management - Onboarding information storage #### Configuration Tools (`config_tools.py`) - Project activation and switching - Mode and context management - Tool inclusion/exclusion ### 4. Configuration System (`src/serena/config/`) Multi-layered configuration supporting: - **Contexts** - Define available tools and their behavior - **Modes** - Specify operational patterns (interactive, editing, etc.) - **Projects** - Per-project settings and language server configs - **Tool Sets** - Grouped tool collections for different use cases ## Language Server Integration ### Language Support Model Each supported language has: 1. **Language Server Implementation** (`src/solidlsp/language_servers/`) 2. **Runtime Dependencies** - Managed downloads of language servers 3. **Test Repository** (`test/resources/repos/<language>/`) 4. **Test Suite** (`test/solidlsp/<language>/`) ### Language Server Lifecycle 1. **Discovery** - Find language servers or download them automatically 2. **Initialization** - Start server process and perform LSP handshake 3. **Project Setup** - Open workspace and configure language-specific settings 4. **Operation** - Handle requests/responses with caching and error recovery 5. **Shutdown** - Clean shutdown of server processes ### Supported Languages Current language support includes: - **C#** - Microsoft.CodeAnalysis.LanguageServer (.NET 9) - **Python** - Pyright or Jedi - **TypeScript/JavaScript** - TypeScript Language Server - **Rust** - rust-analyzer - **Go** - gopls - **Java** - Eclipse JDT Language Server - **Kotlin** - Kotlin Language Server - **PHP** - Intelephense - **Ruby** - Solargraph - **Clojure** - clojure-lsp - **Elixir** - ElixirLS - **Dart** - Dart Language Server - **C/C++** - clangd - **Terraform** - terraform-ls ## Memory and Knowledge Management ### Memory System - **Markdown-based storage** in `.serena/memories/` directory - **Contextual retrieval** - memories loaded based on relevance - **Project-specific** knowledge persistence - **Onboarding support** - guided setup for new projects ### Knowledge Categories - **Project Structure** - Directory layouts, build systems - **Architecture Patterns** - How the codebase is organized - **Development Workflows** - Testing, building, deployment - **Domain Knowledge** - Business logic and requirements ## MCP Server Interface Serena exposes its functionality through Model Context Protocol: - **Tool Discovery** - AI agents can enumerate available tools - **Context-Aware Operations** - Tools behave based on active project/mode - **Stateful Sessions** - Maintains project state across interactions - **Error Handling** - Graceful degradation when tools fail ## Error Handling and Resilience ### Language Server Reliability - **Timeout Management** - Configurable timeouts for LSP requests - **Process Recovery** - Automatic restart of crashed language servers - **Fallback Behavior** - Graceful degradation when LSP unavailable - **Caching Strategy** - Reduces impact of server failures ### Project Activation Safety - **Validation** - Verify project structure before activation - **Error Isolation** - Project failures don't affect other projects - **Recovery Mechanisms** - Automatic cleanup and retry logic ## Performance Considerations ### Caching Strategy - **Symbol Cache** - In-memory caching of expensive symbol operations - **File System Cache** - Reduced disk I/O for repeated operations - **Language Server Cache** - Persistent cache across sessions ### Resource Management - **Language Server Pooling** - Reuse servers across projects when possible - **Memory Management** - Automatic cleanup of unused resources - **Background Operations** - Async operations don't block user interactions ## Extension Points ### Adding New Languages 1. Implement language server class in `src/solidlsp/language_servers/` 2. Add runtime dependencies configuration 3. Create test repository and test suite 4. Update language enumeration and configuration ### Adding New Tools 1. Inherit from `Tool` base class in `tools_base.py` 2. Implement required methods and parameter validation 3. Register tool in appropriate tool registry 4. Add to context/mode configurations as needed ### Custom Contexts and Modes - Define new contexts in YAML configuration files - Specify tool sets and operational patterns - Configure for specific development workflows ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/dart_language_server.py: -------------------------------------------------------------------------------- ```python import logging import os import pathlib from solidlsp.ls import SolidLanguageServer from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection class DartLanguageServer(SolidLanguageServer): """ Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. """ def __init__(self, config, logger, repository_root_path, solidlsp_settings: SolidLSPSettings): """ Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ executable_path = self._setup_runtime_dependencies(logger, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), "dart", solidlsp_settings, ) @classmethod def _setup_runtime_dependencies(cls, logger: "LanguageServerLogger", solidlsp_settings: SolidLSPSettings) -> str: deps = RuntimeDependencyCollection( [ RuntimeDependency( id="DartLanguageServer", description="Dart Language Server for Linux (x64)", url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip", platform_id="linux-x64", archive_type="zip", binary_name="dart-sdk/bin/dart", ), RuntimeDependency( id="DartLanguageServer", description="Dart Language Server for Windows (x64)", url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip", platform_id="win-x64", archive_type="zip", binary_name="dart-sdk/bin/dart.exe", ), RuntimeDependency( id="DartLanguageServer", description="Dart Language Server for Windows (arm64)", url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-arm64-release.zip", platform_id="win-arm64", archive_type="zip", binary_name="dart-sdk/bin/dart.exe", ), RuntimeDependency( id="DartLanguageServer", description="Dart Language Server for macOS (x64)", url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip", platform_id="osx-x64", archive_type="zip", binary_name="dart-sdk/bin/dart", ), RuntimeDependency( id="DartLanguageServer", description="Dart Language Server for macOS (arm64)", url="https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip", platform_id="osx-arm64", archive_type="zip", binary_name="dart-sdk/bin/dart", ), ] ) dart_ls_dir = cls.ls_resources_dir(solidlsp_settings) dart_executable_path = deps.binary_path(dart_ls_dir) if not os.path.exists(dart_executable_path): deps.install(logger, dart_ls_dir) assert os.path.exists(dart_executable_path) os.chmod(dart_executable_path, 0o755) return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" @staticmethod def _get_initialize_params(repository_absolute_path: str): """ Returns the initialize params for the Dart Language Server. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params = { "capabilities": {}, "initializationOptions": { "onlyAnalyzeProjectsWithOpenFiles": False, "closingLabels": False, "outline": False, "flutterOutline": False, "allowOpenUri": False, }, "trace": "verbose", "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": pathlib.Path(repository_absolute_path).as_uri(), "workspaceFolders": [ { "uri": root_uri, "name": os.path.basename(repository_absolute_path), } ], } return initialize_params def _start_server(self): """ Start the language server and yield when the server is ready. """ def execute_client_command_handler(params): return [] def do_nothing(params): return def check_experimental_status(params): pass def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", do_nothing) self.server.on_notification("language/status", do_nothing) self.server.on_notification("window/logMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) self.logger.log("Starting dart-language-server server process", logging.INFO) self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) self.logger.log( "Sending initialize request to dart-language-server", logging.DEBUG, ) init_response = self.server.send_request("initialize", initialize_params) self.logger.log( f"Received initialize response from dart-language-server: {init_response}", logging.INFO, ) self.server.notify.initialized({}) ``` -------------------------------------------------------------------------------- /src/serena/tools/jetbrains_plugin_client.py: -------------------------------------------------------------------------------- ```python """ Client for the Serena JetBrains Plugin """ import json import logging from pathlib import Path from typing import Any, Optional, Self, TypeVar import requests from requests import Response from sensai.util.string import ToStringMixin from serena.project import Project T = TypeVar("T") log = logging.getLogger(__name__) class SerenaClientError(Exception): """Base exception for Serena client errors.""" class ConnectionError(SerenaClientError): """Raised when connection to the service fails.""" class APIError(SerenaClientError): """Raised when the API returns an error response.""" class ServerNotFoundError(Exception): """Raised when the plugin's service is not found.""" class JetBrainsPluginClient(ToStringMixin): """ Python client for the Serena Backend Service. Provides simple methods to interact with all available endpoints. """ BASE_PORT = 0x5EA2 last_port: int | None = None def __init__(self, port: int, timeout: int = 30): self.base_url = f"http://127.0.0.1:{port}" self.timeout = timeout self.session = requests.Session() self.session.headers.update({"Content-Type": "application/json", "Accept": "application/json"}) def _tostring_includes(self) -> list[str]: return ["base_url", "timeout"] @classmethod def from_project(cls, project: Project) -> Self: resolved_path = Path(project.project_root).resolve() if cls.last_port is not None: client = JetBrainsPluginClient(cls.last_port) if client.matches(resolved_path): return client for port in range(cls.BASE_PORT, cls.BASE_PORT + 20): client = JetBrainsPluginClient(port) if client.matches(resolved_path): log.info("Found JetBrains IDE service at port %d for project %s", port, resolved_path) cls.last_port = port return client raise ServerNotFoundError("Found no Serena service in a JetBrains IDE instance for the project at " + str(resolved_path)) def matches(self, resolved_path: Path) -> bool: try: return Path(self.project_root()).resolve() == resolved_path except ConnectionError: return False def _make_request(self, method: str, endpoint: str, data: Optional[dict] = None) -> dict[str, Any]: url = f"{self.base_url}{endpoint}" response: Response | None = None try: if method.upper() == "GET": response = self.session.get(url, timeout=self.timeout) elif method.upper() == "POST": json_data = json.dumps(data) if data else None response = self.session.post(url, data=json_data, timeout=self.timeout) else: raise ValueError(f"Unsupported HTTP method: {method}") response.raise_for_status() # Try to parse JSON response try: return self._pythonify_response(response.json()) except json.JSONDecodeError: # If response is not JSON, return raw text return {"response": response.text} except requests.exceptions.ConnectionError as e: raise ConnectionError(f"Failed to connect to Serena service at {url}: {e}") except requests.exceptions.Timeout as e: raise ConnectionError(f"Request to {url} timed out: {e}") except requests.exceptions.HTTPError as e: if response is not None: raise APIError(f"API request failed with status {response.status_code}: {response.text}") raise APIError(f"API request failed with HTTP error: {e}") except requests.exceptions.RequestException as e: raise SerenaClientError(f"Request failed: {e}") @staticmethod def _pythonify_response(response: T) -> T: """ Converts dictionary keys from camelCase to snake_case recursively. :response: the response in which to convert keys (dictionary or list) """ to_snake_case = lambda s: "".join(["_" + c.lower() if c.isupper() else c for c in s]) def convert(x): # type: ignore if isinstance(x, dict): return {to_snake_case(k): convert(v) for k, v in x.items()} elif isinstance(x, list): return [convert(item) for item in x] else: return x return convert(response) def project_root(self) -> str: response = self._make_request("GET", "/status") return response["project_root"] def find_symbol( self, name_path: str, relative_path: str | None = None, include_body: bool = False, depth: int = 0, include_location: bool = False ) -> dict[str, Any]: """ Find symbols by name. :param name_path: the name path to match :param relative_path: the relative path to which to restrict the search :param include_body: whether to include symbol body content :param depth: depth of children to include (0 = no children) :return: Dictionary containing 'symbols' list with matching symbols """ request_data = { "namePath": name_path, "relativePath": relative_path, "includeBody": include_body, "depth": depth, "includeLocation": include_location, } return self._make_request("POST", "/findSymbol", request_data) def find_references(self, name_path: str, relative_path: str) -> dict[str, Any]: """ Find references to a symbol. :param name_path: the name path of the symbol :param relative_path: the relative path :return: dictionary containing 'symbols' list with symbol references """ request_data = {"namePath": name_path, "relativePath": relative_path} return self._make_request("POST", "/findReferences", request_data) def get_symbols_overview(self, relative_path: str) -> dict[str, Any]: """ :param relative_path: the relative path to a source file """ request_data = {"relativePath": relative_path} return self._make_request("POST", "/getSymbolsOverview", request_data) def is_service_available(self) -> bool: try: self.project_root() return True except (ConnectionError, APIError): return False def close(self) -> None: self.session.close() def __enter__(self) -> Self: return self def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore self.close() ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml [build-system] build-backend = "hatchling.build" requires = ["hatchling"] [project] name = "serena-agent" version = "0.1.4" description = "" authors = [{ name = "Oraios AI", email = "[email protected]" }] readme = "README.md" requires-python = ">=3.11, <3.12" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.11", ] dependencies = [ "requests>=2.32.3,<3", "pyright>=1.1.396,<2", "overrides>=7.7.0,<8", "python-dotenv>=1.0.0, <2", "mcp==1.12.3", "flask>=3.0.0", "sensai-utils>=1.5.0", "pydantic>=2.10.6", "types-pyyaml>=6.0.12.20241230", "pyyaml>=6.0.2", "ruamel.yaml>=0.18.0", "jinja2>=3.1.6", "dotenv>=0.9.9", "pathspec>=0.12.1", "psutil>=7.0.0", "docstring_parser>=0.16", "joblib>=1.5.1", "tqdm>=4.67.1", "tiktoken>=0.9.0", "anthropic>=0.54.0", ] [[tool.uv.index]] name = "testpypi" url = "https://test.pypi.org/simple/" publish-url = "https://test.pypi.org/legacy/" explicit = true [project.scripts] serena = "serena.cli:top_level" serena-mcp-server = "serena.cli:start_mcp_server" index-project = "serena.cli:index_project" # deprecated [project.license] text = "MIT" [project.optional-dependencies] dev = [ "black[jupyter]>=23.7.0", "jinja2", # In version 1.0.4 we get a NoneType error related to some config conversion (yml_analytics is None and should be a list) "mypy>=1.16.1", "poethepoet>=0.20.0", "pytest>=8.0.2", "pytest-xdist>=3.5.0", "ruff>=0.0.285", "toml-sort>=0.24.2", "types-pyyaml>=6.0.12.20241230", "syrupy>=4.9.1", "types-requests>=2.32.4.20241230", ] agno = ["agno>=1.2.6", "sqlalchemy>=2.0.40"] google = ["google-genai>=1.8.0"] [project.urls] Homepage = "https://github.com/oraios/serena" [tool.hatch.build.targets.wheel] packages = ["src/serena", "src/interprompt", "src/solidlsp"] [tool.black] line-length = 140 target-version = ["py311"] exclude = ''' /( src/solidlsp/language_servers/.*/static|src/multilspy )/ ''' [tool.doc8] max-line-length = 1000 [tool.mypy] allow_redefinition = true check_untyped_defs = true disallow_incomplete_defs = true disallow_untyped_defs = true ignore_missing_imports = true no_implicit_optional = true pretty = true show_error_codes = true show_error_context = true show_traceback = true strict_equality = true strict_optional = true warn_no_return = true warn_redundant_casts = true warn_unreachable = true warn_unused_configs = true warn_unused_ignores = false exclude = "^build/|^docs/" [tool.poe.env] PYDEVD_DISABLE_FILE_VALIDATION = "1" [tool.poe.tasks] # Uses PYTEST_MARKERS env var for default markers # For custom markers, one can either adjust the env var or just use -m option in the command line, # as the second -m option will override the first one. test = "pytest test -vv -m \"${PYTEST_MARKERS:-not java and not rust and not erlang}\"" _black_check = "black --check src scripts test" _ruff_check = "ruff check src scripts test" _black_format = "black src scripts test" _ruff_format = "ruff check --fix src scripts test" lint = ["_black_check", "_ruff_check"] format = ["_ruff_format", "_black_format"] _mypy = "mypy src/serena" type-check = ["_mypy"] [tool.ruff] target-version = "py311" line-length = 140 exclude = ["src/solidlsp/language_servers/**/static", "src/multilspy"] [tool.ruff.format] quote-style = "double" indent-style = "space" line-ending = "auto" skip-magic-trailing-comma = false docstring-code-format = true [tool.ruff.lint] select = [ "ASYNC", "B", "C4", "C90", "COM", "D", "DTZ", "E", "F", "FLY", "G", "I", "ISC", "PIE", "PLC", "PLE", "PLW", "RET", "RUF", "RSE", "SIM", "TID", "UP", "W", "YTT", ] ignore = [ "PLC0415", "RUF002", "RUF005", "SIM118", "SIM108", "E501", "E741", "B008", "B011", "B028", "D100", "D101", "D102", "D103", "D104", "D105", "D107", "D200", "D203", "D213", "D401", "D402", "DTZ005", "E402", "E501", "E701", "E731", "C408", "E203", "G004", "RET505", "D106", "D205", "D212", "PLW2901", "B027", "D404", "D407", "D408", "D409", "D400", "D415", "COM812", "RET503", "RET504", "UP038", "F403", "F405", "C401", "C901", "ASYNC230", "ISC003", "B024", "B007", "SIM102", "W291", "W293", "B009", "SIM103", # forbids multiple returns "SIM110", # requires use of any(...) instead of for-loop "G001", # forbids str.format in log statements "E722", # forbids unspecific except clause "SIM105", # forbids empty/general except clause "SIM113", # wants to enforce use of enumerate "E712", # forbids equality comparison with True/False "UP007", # forbids some uses of Union "TID252", # forbids relative imports "B904", # forces use of raise from other_exception "RUF012", # forbids mutable attributes as ClassVar "SIM117", # forbids nested with statements "C400", # wants to unnecessarily force use of list comprehension "UP037", # can incorrectly (!) convert quoted type to unquoted type, causing an error "UP045", # imposes T | None instead of Optional[T] ] unfixable = ["F841", "F601", "F602", "B018"] extend-fixable = ["F401", "B905", "W291"] [tool.ruff.lint.mccabe] max-complexity = 20 [tool.ruff.lint.per-file-ignores] "tests/**" = ["D103"] "scripts/**" = ["D103"] [tool.pytest.ini_options] addopts = "--snapshot-patch-pycharm-diff" markers = [ "clojure: language server running for Clojure", "python: language server running for Python", "go: language server running for Go", "java: language server running for Java", "kotlin: language server running for kotlin", "rust: language server running for Rust", "typescript: language server running for TypeScript", "php: language server running for PHP", "perl: language server running for Perl", "csharp: language server running for C#", "elixir: language server running for Elixir", "elm: language server running for Elm", "terraform: language server running for Terraform", "swift: language server running for Swift", "bash: language server running for Bash", "r: language server running for R", "snapshot: snapshot tests for symbolic editing operations", "ruby: language server running for Ruby (uses ruby-lsp)", "zig: language server running for Zig", "lua: language server running for Lua", "nix: language server running for Nix", "dart: language server running for Dart", "erlang: language server running for Erlang", "al: language server running for AL (Microsoft Dynamics 365 Business Central)", "markdown: language server running for Markdown", ] [tool.codespell] # Ref: https://github.com/codespell-project/codespell#using-a-config-file skip = '.git*,*.svg,*.lock,*.min.*' check-hidden = true # ignore-regex = '' # ignore-words-list = '' ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/common.py: -------------------------------------------------------------------------------- ```python from __future__ import annotations import logging import os import platform import subprocess from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, replace from typing import Any, cast from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PlatformUtils from solidlsp.util.subprocess_util import subprocess_kwargs log = logging.getLogger(__name__) @dataclass(kw_only=True) class RuntimeDependency: """Represents a runtime dependency for a language server.""" id: str platform_id: str | None = None url: str | None = None archive_type: str | None = None binary_name: str | None = None command: str | list[str] | None = None package_name: str | None = None package_version: str | None = None extract_path: str | None = None description: str | None = None class RuntimeDependencyCollection: """Utility to handle installation of runtime dependencies.""" def __init__(self, dependencies: Sequence[RuntimeDependency], overrides: Iterable[Mapping[str, Any]] = ()) -> None: """Initialize the collection with a list of dependencies and optional overrides. :param dependencies: List of base RuntimeDependency instances. The combination of 'id' and 'platform_id' must be unique. :param overrides: List of dictionaries which represent overrides or additions to the base dependencies. Each entry must contain at least the 'id' key, and optionally 'platform_id' to uniquely identify the dependency to override. """ self._id_and_platform_id_to_dep: dict[tuple[str, str | None], RuntimeDependency] = {} for dep in dependencies: dep_key = (dep.id, dep.platform_id) if dep_key in self._id_and_platform_id_to_dep: raise ValueError(f"Duplicate runtime dependency with id '{dep.id}' and platform_id '{dep.platform_id}':\n{dep}") self._id_and_platform_id_to_dep[dep_key] = dep for dep_values_override in overrides: override_key = cast(tuple[str, str | None], (dep_values_override["id"], dep_values_override.get("platform_id"))) base_dep = self._id_and_platform_id_to_dep.get(override_key) if base_dep is None: new_runtime_dep = RuntimeDependency(**dep_values_override) self._id_and_platform_id_to_dep[override_key] = new_runtime_dep else: self._id_and_platform_id_to_dep[override_key] = replace(base_dep, **dep_values_override) def get_dependencies_for_platform(self, platform_id: str) -> list[RuntimeDependency]: return [d for d in self._id_and_platform_id_to_dep.values() if d.platform_id in (platform_id, "any", "platform-agnostic", None)] def get_dependencies_for_current_platform(self) -> list[RuntimeDependency]: return self.get_dependencies_for_platform(PlatformUtils.get_platform_id().value) def get_single_dep_for_current_platform(self, dependency_id: str | None = None) -> RuntimeDependency: deps = self.get_dependencies_for_current_platform() if dependency_id is not None: deps = [d for d in deps if d.id == dependency_id] if len(deps) != 1: raise RuntimeError( f"Expected exactly one runtime dependency for platform-{PlatformUtils.get_platform_id().value} and {dependency_id=}, found {len(deps)}" ) return deps[0] def binary_path(self, target_dir: str) -> str: dep = self.get_single_dep_for_current_platform() if not dep.binary_name: return target_dir return os.path.join(target_dir, dep.binary_name) def install(self, logger: LanguageServerLogger, target_dir: str) -> dict[str, str]: """Install all dependencies for the current platform into *target_dir*. Returns a mapping from dependency id to the resolved binary path. """ os.makedirs(target_dir, exist_ok=True) results: dict[str, str] = {} for dep in self.get_dependencies_for_current_platform(): if dep.url: self._install_from_url(dep, logger, target_dir) if dep.command: self._run_command(dep.command, target_dir) if dep.binary_name: results[dep.id] = os.path.join(target_dir, dep.binary_name) else: results[dep.id] = target_dir return results @staticmethod def _run_command(command: str | list[str], cwd: str) -> None: kwargs = subprocess_kwargs() if not PlatformUtils.get_platform_id().is_windows(): import pwd kwargs["user"] = pwd.getpwuid(os.getuid()).pw_name is_windows = platform.system() == "Windows" if not isinstance(command, str) and not is_windows: # Since we are using the shell, we need to convert the command list to a single string # on Linux/macOS command = " ".join(command) log.info("Running command %s in '%s'", f"'{command}'" if isinstance(command, str) else command, cwd) completed_process = subprocess.run( command, shell=True, check=True, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs, ) if completed_process.returncode != 0: log.warning("Command '%s' failed with return code %d", command, completed_process.returncode) log.warning("Command output:\n%s", completed_process.stdout) else: log.info( "Command completed successfully", ) @staticmethod def _install_from_url(dep: RuntimeDependency, logger: LanguageServerLogger, target_dir: str) -> None: assert dep.url is not None if dep.archive_type in ("gz", "binary") and dep.binary_name: dest = os.path.join(target_dir, dep.binary_name) FileUtils.download_and_extract_archive(logger, dep.url, dest, dep.archive_type) else: FileUtils.download_and_extract_archive(logger, dep.url, target_dir, dep.archive_type or "zip") def quote_windows_path(path: str) -> str: """ Quote a path for Windows command execution if needed. On Windows, paths need to be quoted for proper command execution. The function checks if the path is already quoted to avoid double-quoting. On other platforms, the path is returned unchanged. Args: path: The file path to potentially quote Returns: The quoted path on Windows (if not already quoted), unchanged path on other platforms """ if platform.system() == "Windows": # Check if already quoted to avoid double-quoting if path.startswith('"') and path.endswith('"'): return path return f'"{path}"' return path ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/r_language_server.py: -------------------------------------------------------------------------------- ```python import logging import os import pathlib import subprocess import threading from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings class RLanguageServer(SolidLanguageServer): """R Language Server implementation using the languageserver R package.""" @override def _get_wait_time_for_cross_file_referencing(self) -> float: return 5.0 # R language server needs extra time for workspace indexing in CI environments @override def is_ignored_dirname(self, dirname: str) -> bool: # For R projects, ignore common directories return super().is_ignored_dirname(dirname) or dirname in [ "renv", # R environment management "packrat", # Legacy R package management ".Rproj.user", # RStudio project files "vignettes", # Package vignettes (often large) ] @staticmethod def _check_r_installation(): """Check if R and languageserver are available.""" try: # Check R installation result = subprocess.run(["R", "--version"], capture_output=True, text=True, check=False) if result.returncode != 0: raise RuntimeError("R is not installed or not in PATH") # Check languageserver package result = subprocess.run( ["R", "--vanilla", "--quiet", "--slave", "-e", "if (!require('languageserver', quietly=TRUE)) quit(status=1)"], capture_output=True, text=True, check=False, ) if result.returncode != 0: raise RuntimeError( "R languageserver package is not installed.\nInstall it with: R -e \"install.packages('languageserver')\"" ) except FileNotFoundError: raise RuntimeError("R is not installed. Please install R from https://www.r-project.org/") def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings ): # Check R installation self._check_r_installation() # R command to start language server # Use --vanilla for minimal startup and --quiet to suppress all output except LSP # Set specific options to improve parsing stability r_cmd = 'R --vanilla --quiet --slave -e "options(languageserver.debug_mode = FALSE); languageserver::run()"' super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=r_cmd, cwd=repository_root_path), "r", solidlsp_settings, ) self.server_ready = threading.Event() @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """Initialize params for R Language Server.""" root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params = { "locale": "en", "capabilities": { "textDocument": { "synchronization": {"didSave": True, "dynamicRegistration": True}, "completion": { "dynamicRegistration": True, "completionItem": { "snippetSupport": True, "commitCharactersSupport": True, "documentationFormat": ["markdown", "plaintext"], "deprecatedSupport": True, "preselectSupport": True, }, }, "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, "definition": {"dynamicRegistration": True}, "references": {"dynamicRegistration": True}, "documentSymbol": { "dynamicRegistration": True, "hierarchicalDocumentSymbolSupport": True, "symbolKind": {"valueSet": list(range(1, 27))}, }, "formatting": {"dynamicRegistration": True}, "rangeFormatting": {"dynamicRegistration": True}, }, "workspace": { "workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}, "symbol": { "dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}, }, }, }, "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": root_uri, "workspaceFolders": [ { "uri": root_uri, "name": os.path.basename(repository_absolute_path), } ], } return initialize_params def _start_server(self): """Start R Language Server process.""" def window_log_message(msg): self.logger.log(f"R LSP: window/logMessage: {msg}", logging.INFO) def do_nothing(params): return def register_capability_handler(params): return # Register LSP message handlers self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.logger.log("Starting R Language Server process", logging.INFO) self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) self.logger.log( "Sending initialize request to R Language Server", logging.INFO, ) init_response = self.server.send.initialize(initialize_params) # Verify server capabilities capabilities = init_response.get("capabilities", {}) assert "textDocumentSync" in capabilities if "completionProvider" in capabilities: self.logger.log("R LSP completion provider available", logging.INFO) if "definitionProvider" in capabilities: self.logger.log("R LSP definition provider available", logging.INFO) self.server.notify.initialized({}) self.completions_available.set() # R Language Server is ready after initialization self.server_ready.set() ``` -------------------------------------------------------------------------------- /src/serena/tools/jetbrains_tools.py: -------------------------------------------------------------------------------- ```python import json from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicRead from serena.tools.jetbrains_plugin_client import JetBrainsPluginClient class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). """ def apply( self, name_path: str, depth: int = 0, relative_path: str | None = None, include_body: bool = False, max_answer_chars: int = -1, ) -> str: """ Retrieves information on all symbols/code entities (classes, methods, etc.) based on the given `name_path`, which represents a pattern for the symbol's path within the symbol tree of a single file. The returned symbol location can be used for edits or further queries. Specify `depth > 0` to retrieve children (e.g., methods of a class). The matching behavior is determined by the structure of `name_path`, which can either be a simple name (e.g. "method") or a name path like "class/method" (relative name path) or "/class/method" (absolute name path). Note that the name path is not a path in the file system but rather a path in the symbol tree **within a single file**. Thus, file or directory names should never be included in the `name_path`. For restricting the search to a single file or directory, pass the `relative_path` parameter. The retrieved symbols' `name_path` attribute will always be composed of symbol names, never file or directory names. Key aspects of the name path matching behavior: - The name of the retrieved symbols will match the last segment of `name_path`, while preceding segments will restrict the search to symbols that have a desired sequence of ancestors. - If there is no `/` in `name_path`, there is no restriction on the ancestor symbols. For example, passing `method` will match against all symbols with name paths like `method`, `class/method`, `class/nested_class/method`, etc. - If `name_path` contains at least one `/`, the matching is restricted to symbols with the respective ancestors. For example, passing `class/method` will match against `class/method` as well as `nested_class/class/method` but not `other_class/method`. - If `name_path` starts with a `/`, it will be treated as an absolute name path pattern, i.e. all ancestors are provided and must match. For example, passing `/class` will match only against top-level symbols named `class` but will not match `nested_class/class`. Passing `/class/method` will match `class/method` but not `outer_class/class/method`. :param name_path: The name path pattern to search for, see above for details. :param depth: Depth to retrieve descendants (e.g., 1 for class methods/attributes). :param relative_path: Optional. Restrict search to this file or directory. If None, searches entire codebase. If a directory is passed, the search will be restricted to the files in that directory. If a file is passed, the search will be restricted to that file. If you have some knowledge about the codebase, you should use this parameter, as it will significantly speed up the search as well as reduce the number of results. :param include_body: If True, include the symbol's source code. Use judiciously. :param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned. -1 means the default value from the config will be used. :return: JSON string: a list of symbols (with locations) matching the name. """ with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.find_symbol( name_path=name_path, relative_path=relative_path, depth=depth, include_body=include_body, ) result = json.dumps(response_dict) return self._limit_length(result, max_answer_chars) class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Finds symbols that reference the given symbol """ def apply( self, name_path: str, relative_path: str, max_answer_chars: int = -1, ) -> str: """ Finds symbols that reference the symbol at the given `name_path`. The result will contain metadata about the referencing symbols. :param name_path: name path of the symbol for which to find references; matching logic as described in find symbol tool. :param relative_path: the relative path to the file containing the symbol for which to find references. Note that here you can't pass a directory but must pass a file. :param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned. -1 means the default value from the config will be used. :return: a list of JSON objects with the symbols referencing the requested symbol """ with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.find_references( name_path=name_path, relative_path=relative_path, ) result = json.dumps(response_dict) return self._limit_length(result, max_answer_chars) class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Retrieves an overview of the top-level symbols within a specified file """ def apply( self, relative_path: str, max_answer_chars: int = -1, ) -> str: """ Gets an overview of the top-level symbols in the given file. Calling this is often a good idea before more targeted reading, searching or editing operations on the code symbols. Before requesting a symbol overview, it is usually a good idea to narrow down the scope of the overview by first understanding the basic directory structure of the repository that you can get from memories or by using the `list_dir` and `find_file` tools (or similar). :param relative_path: the relative path to the file to get the overview of :param max_answer_chars: max characters for the JSON result. If exceeded, no content is returned. -1 means the default value from the config will be used. :return: a JSON object containing the symbols """ with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.get_symbols_overview( relative_path=relative_path, ) result = json.dumps(response_dict) return self._limit_length(result, max_answer_chars) ``` -------------------------------------------------------------------------------- /test/solidlsp/elixir/test_elixir_ignored_dirs.py: -------------------------------------------------------------------------------- ```python from collections.abc import Generator from pathlib import Path import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.conftest import create_ls from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON # These marks will be applied to all tests in this module pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")] @pytest.fixture(scope="module") def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]: """Fixture to set up an LS for the elixir test repo with the 'scripts' directory ignored.""" ignored_paths = ["scripts", "ignored_dir"] ls = create_ls(ignored_paths=ignored_paths, language=Language.ELIXIR) ls.start() try: yield ls finally: ls.stop() @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ELIXIR], indirect=True) def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that request_full_symbol_tree ignores the configured directory.""" root = ls_with_ignored_dirs.request_full_symbol_tree()[0] root_children = root["children"] children_names = {child["name"] for child in root_children} # Should have lib and test directories, but not scripts or ignored_dir expected_dirs = {"lib", "test"} assert expected_dirs.issubset(children_names), f"Expected {expected_dirs} to be in {children_names}" assert "scripts" not in children_names, f"scripts should not be in {children_names}" assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names}" @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ELIXIR], indirect=True) def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that find_references ignores the configured directory.""" # Location of User struct, which is referenced in scripts and ignored_dir definition_file = "lib/models.ex" # Find the User struct definition symbols = ls_with_ignored_dirs.request_document_symbols(definition_file) user_symbol = None for symbol_group in symbols: user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None) if user_symbol: break if not user_symbol or "selectionRange" not in user_symbol: pytest.skip("User symbol not found for reference testing") sel_start = user_symbol["selectionRange"]["start"] references = ls_with_ignored_dirs.request_references(definition_file, sel_start["line"], sel_start["character"]) # Assert that scripts and ignored_dir do not appear in the references assert not any("scripts" in ref["relativePath"] for ref in references), "scripts should be ignored" assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored" @pytest.mark.parametrize("repo_path", [Language.ELIXIR], indirect=True) def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: """Tests that refs and symbols with glob patterns are ignored.""" ignored_paths = ["*cripts", "ignored_*"] # codespell:ignore cripts ls = create_ls(ignored_paths=ignored_paths, repo_path=str(repo_path), language=Language.ELIXIR) ls.start() try: # Same as in the above tests root = ls.request_full_symbol_tree()[0] root_children = root["children"] children_names = {child["name"] for child in root_children} # Should have lib and test directories, but not scripts or ignored_dir expected_dirs = {"lib", "test"} assert expected_dirs.issubset(children_names), f"Expected {expected_dirs} to be in {children_names}" assert "scripts" not in children_names, f"scripts should not be in {children_names} (glob pattern)" assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names} (glob pattern)" # Test that the refs and symbols with glob patterns are ignored definition_file = "lib/models.ex" # Find the User struct definition symbols = ls.request_document_symbols(definition_file) user_symbol = None for symbol_group in symbols: user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None) if user_symbol: break if user_symbol and "selectionRange" in user_symbol: sel_start = user_symbol["selectionRange"]["start"] references = ls.request_references(definition_file, sel_start["line"], sel_start["character"]) # Assert that scripts and ignored_dir do not appear in references assert not any("scripts" in ref["relativePath"] for ref in references), "scripts should be ignored (glob)" assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored (glob)" finally: ls.stop() @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) def test_default_ignored_directories(language_server: SolidLanguageServer): """Test that default Elixir directories are ignored.""" # Test that Elixir-specific directories are ignored by default assert language_server.is_ignored_dirname("_build"), "_build should be ignored" assert language_server.is_ignored_dirname("deps"), "deps should be ignored" assert language_server.is_ignored_dirname(".elixir_ls"), ".elixir_ls should be ignored" assert language_server.is_ignored_dirname("cover"), "cover should be ignored" assert language_server.is_ignored_dirname("node_modules"), "node_modules should be ignored" # Test that important directories are not ignored assert not language_server.is_ignored_dirname("lib"), "lib should not be ignored" assert not language_server.is_ignored_dirname("test"), "test should not be ignored" assert not language_server.is_ignored_dirname("config"), "config should not be ignored" assert not language_server.is_ignored_dirname("priv"), "priv should not be ignored" @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer): """Test that symbol tree excludes build and dependency directories.""" symbol_tree = language_server.request_full_symbol_tree() if symbol_tree: root = symbol_tree[0] children_names = {child["name"] for child in root.get("children", [])} # Build and dependency directories should not appear ignored_dirs = {"_build", "deps", ".elixir_ls", "cover", "node_modules"} found_ignored = ignored_dirs.intersection(children_names) assert len(found_ignored) == 0, f"Found ignored directories in symbol tree: {found_ignored}" # Important directories should appear important_dirs = {"lib", "test"} found_important = important_dirs.intersection(children_names) assert len(found_important) > 0, f"Expected to find important directories: {important_dirs}, got: {children_names}" ``` -------------------------------------------------------------------------------- /test/resources/repos/python/test_repo/test_repo/models.py: -------------------------------------------------------------------------------- ```python """ Models module that demonstrates various Python class patterns. """ from abc import ABC, abstractmethod from typing import Any, Generic, TypeVar T = TypeVar("T") class BaseModel(ABC): """ Abstract base class for all models. """ def __init__(self, id: str, name: str | None = None): self.id = id self.name = name or id @abstractmethod def to_dict(self) -> dict[str, Any]: """Convert model to dictionary representation""" @classmethod def from_dict(cls, data: dict[str, Any]) -> "BaseModel": """Create a model instance from dictionary data""" id = data.get("id", "") name = data.get("name") return cls(id=id, name=name) class User(BaseModel): """ User model representing a system user. """ def __init__(self, id: str, name: str | None = None, email: str = "", roles: list[str] | None = None): super().__init__(id, name) self.email = email self.roles = roles or [] def to_dict(self) -> dict[str, Any]: return {"id": self.id, "name": self.name, "email": self.email, "roles": self.roles} @classmethod def from_dict(cls, data: dict[str, Any]) -> "User": instance = super().from_dict(data) instance.email = data.get("email", "") instance.roles = data.get("roles", []) return instance def has_role(self, role: str) -> bool: """Check if user has a specific role""" return role in self.roles class Item(BaseModel): """ Item model representing a product or service. """ def __init__(self, id: str, name: str | None = None, price: float = 0.0, category: str = ""): super().__init__(id, name) self.price = price self.category = category def to_dict(self) -> dict[str, Any]: return {"id": self.id, "name": self.name, "price": self.price, "category": self.category} def get_display_price(self) -> str: """Format price for display""" return f"${self.price:.2f}" # Generic type example class Collection(Generic[T]): def __init__(self, items: list[T] | None = None): self.items = items or [] def add(self, item: T) -> None: self.items.append(item) def get_all(self) -> list[T]: return self.items # Factory function def create_user_object(id: str, name: str, email: str, roles: list[str] | None = None) -> User: """Factory function to create a user""" return User(id=id, name=name, email=email, roles=roles) # Multiple inheritance examples class Loggable: """ Mixin class that provides logging functionality. Example of a common mixin pattern used with multiple inheritance. """ def __init__(self, **kwargs): super().__init__(**kwargs) self.log_entries: list[str] = [] def log(self, message: str) -> None: """Add a log entry""" self.log_entries.append(message) def get_logs(self) -> list[str]: """Get all log entries""" return self.log_entries class Serializable: """ Mixin class that provides JSON serialization capabilities. Another example of a mixin for multiple inheritance. """ def __init__(self, **kwargs): super().__init__(**kwargs) def to_json(self) -> dict[str, Any]: """Convert to JSON-serializable dictionary""" return self.to_dict() if hasattr(self, "to_dict") else {} @classmethod def from_json(cls, data: dict[str, Any]) -> Any: """Create instance from JSON data""" return cls.from_dict(data) if hasattr(cls, "from_dict") else cls(**data) class Auditable: """ Mixin for tracking creation and modification timestamps. """ def __init__(self, **kwargs): super().__init__(**kwargs) self.created_at: str = kwargs.get("created_at", "") self.updated_at: str = kwargs.get("updated_at", "") def update_timestamp(self, timestamp: str) -> None: """Update the last modified timestamp""" self.updated_at = timestamp # Diamond inheritance pattern class BaseService(ABC): """ Base class for service objects - demonstrates diamond inheritance pattern. """ def __init__(self, name: str = "base"): self.service_name = name @abstractmethod def get_service_info(self) -> dict[str, str]: """Get service information""" class DataService(BaseService): """ Data handling service. """ def __init__(self, **kwargs): name = kwargs.pop("name", "data") super().__init__(name=name) self.data_source = kwargs.get("data_source", "default") def get_service_info(self) -> dict[str, str]: return {"service_type": "data", "service_name": self.service_name, "data_source": self.data_source} class NetworkService(BaseService): """ Network connectivity service. """ def __init__(self, **kwargs): name = kwargs.pop("name", "network") super().__init__(name=name) self.endpoint = kwargs.get("endpoint", "localhost") def get_service_info(self) -> dict[str, str]: return {"service_type": "network", "service_name": self.service_name, "endpoint": self.endpoint} class DataSyncService(DataService, NetworkService): """ Service that syncs data over network - example of diamond inheritance. Inherits from both DataService and NetworkService, which both inherit from BaseService. """ def __init__(self, **kwargs): super().__init__(**kwargs) self.sync_interval = kwargs.get("sync_interval", 60) def get_service_info(self) -> dict[str, str]: info = super().get_service_info() info.update({"service_type": "data_sync", "sync_interval": str(self.sync_interval)}) return info # Multiple inheritance with mixins class LoggableUser(User, Loggable): """ User class with logging capabilities. Example of extending a concrete class with a mixin. """ def __init__(self, id: str, name: str | None = None, email: str = "", roles: list[str] | None = None): super().__init__(id=id, name=name, email=email, roles=roles) def add_role(self, role: str) -> None: """Add a role to the user and log the action""" if role not in self.roles: self.roles.append(role) self.log(f"Added role '{role}' to user {self.id}") class TrackedItem(Item, Serializable, Auditable): """ Item with serialization and auditing capabilities. Example of a class inheriting from a concrete class and multiple mixins. """ def __init__( self, id: str, name: str | None = None, price: float = 0.0, category: str = "", created_at: str = "", updated_at: str = "" ): super().__init__(id=id, name=name, price=price, category=category, created_at=created_at, updated_at=updated_at) self.stock_level = 0 def update_stock(self, quantity: int) -> None: """Update stock level and timestamp""" self.stock_level = quantity self.update_timestamp(f"stock_update_{quantity}") def to_dict(self) -> dict[str, Any]: result = super().to_dict() result.update({"stock_level": self.stock_level, "created_at": self.created_at, "updated_at": self.updated_at}) return result ``` -------------------------------------------------------------------------------- /.serena/memories/adding_new_language_support_guide.md: -------------------------------------------------------------------------------- ```markdown # Adding New Language Support to Serena This guide explains how to add support for a new programming language to Serena. ## Overview Adding a new language involves: 1. **Language Server Implementation** - Creating a language-specific server class 2. **Language Registration** - Adding the language to enums and configurations 3. **Test Repository** - Creating a minimal test project 4. **Test Suite** - Writing comprehensive tests 5. **Runtime Dependencies** - Configuring automatic language server downloads ## Step 1: Language Server Implementation ### 1.1 Create Language Server Class Create a new file in `src/solidlsp/language_servers/` (e.g., `new_language_server.py`). Have a look at `intelephense.py` for a reference implementation of a language server which downloads all its dependencies, at `gopls.py` for an LS that needs some preinstalled dependencies, and on `pyright_server.py` that does not need any additional dependencies because the language server can be installed directly as python package. ```python from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo class NewLanguageServer(SolidLanguageServer): """ Language server implementation for NewLanguage. """ def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): # Determine language server command cmd = self._get_language_server_command() super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "new_language", # Language ID for LSP ) def _get_language_server_command(self) -> list[str]: """Get the command to start the language server.""" # Example: return ["new-language-server", "--stdio"] pass @override def is_ignored_dirname(self, dirname: str) -> bool: """Define language-specific directories to ignore.""" return super().is_ignored_dirname(dirname) or dirname in ["build", "dist", "target"] ``` ### 1.2 Language Server Discovery and Installation For languages requiring automatic installation, implement download logic similar to C#: ```python @classmethod def _ensure_server_installed(cls, logger: LanguageServerLogger) -> str: """Ensure language server is installed and return path.""" # Check system installation first system_server = shutil.which("new-language-server") if system_server: return system_server # Download and install if needed server_path = cls._download_and_install_server(logger) return server_path def _download_and_install_server(cls, logger: LanguageServerLogger) -> str: """Download and install the language server.""" # Implementation specific to your language server pass ``` ### 1.3 LSP Initialization Override initialization methods if needed: ```python def _get_initialize_params(self) -> InitializeParams: """Return language-specific initialization parameters.""" return { "processId": os.getpid(), "rootUri": PathUtils.path_to_uri(self.repository_root_path), "capabilities": { # Language-specific capabilities } } def _start_server(self): """Start the language server with custom handlers.""" # Set up notification handlers self.server.on_notification("window/logMessage", self._handle_log_message) # Start server and initialize self.server.start() init_response = self.server.send.initialize(self._get_initialize_params()) self.server.notify.initialized({}) ``` ## Step 2: Language Registration ### 2.1 Add to Language Enum In `src/solidlsp/ls_config.py`, add your language to the `Language` enum: ```python class Language(str, Enum): # Existing languages... NEW_LANGUAGE = "new_language" def get_source_fn_matcher(self) -> FilenameMatcher: match self: # Existing cases... case self.NEW_LANGUAGE: return FilenameMatcher("*.newlang", "*.nl") # File extensions ``` ### 2.2 Update Language Server Factory In `src/solidlsp/ls.py`, add your language to the `create` method: ```python @classmethod def create(cls, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str) -> "SolidLanguageServer": match config.code_language: # Existing cases... case Language.NEW_LANGUAGE: from solidlsp.language_servers.new_language_server import NewLanguageServer return NewLanguageServer(config, logger, repository_root_path) ``` ## Step 3: Test Repository ### 3.1 Create Test Project Create a minimal project in `test/resources/repos/new_language/test_repo/`: ``` test/resources/repos/new_language/test_repo/ ├── main.newlang # Main source file ├── lib/ │ └── helper.newlang # Additional source for testing ├── project.toml # Project configuration (if applicable) └── .gitignore # Ignore build artifacts ``` ### 3.2 Example Source Files Create meaningful source files that demonstrate: - **Classes/Types** - For symbol testing - **Functions/Methods** - For reference finding - **Imports/Dependencies** - For cross-file operations - **Nested Structures** - For hierarchical symbol testing Example `main.newlang`: ``` import lib.helper class Calculator { func add(a: Int, b: Int) -> Int { return a + b } func subtract(a: Int, b: Int) -> Int { return helper.subtract(a, b) // Reference to imported function } } class Program { func main() { let calc = Calculator() let result = calc.add(5, 3) // Reference to add method print(result) } } ``` ## Step 4: Test Suite Testing the language server implementation is of crucial importance, and the tests will form the main part of the review process. Make sure that the tests are up to the standard of Serena to make the review go smoother. General rules for tests: 1. Tests for symbols and references should always check that the expected symbol names and references were actually found. Just testing that a list came back or that the result is not None is insufficient. 2. Tests should never be skipped, the only exception is skipping based on some package being available or on an unsupported OS. 3. Tests should run in CI, check if there is a suitable GitHub action for installing the dependencies. ### 4.1 Basic Tests Create `test/solidlsp/new_language/test_new_language_basic.py`. Have a look at the structure of existing tests, for example, in `test/solidlsp/php/test_php_basic.py` You should at least test: 1. Finding symbols 2. Finding within-file references 3. Finding cross-file references Have a look at `test/solidlsp/php/test_php_basic.py` as an example for what should be tested. Don't forget to add a new language marker to `pytest.ini`. ### 4.2 Integration Tests Consider adding new cases to the parametrized tests in `test_serena_agent.py` for the new language. ### 5 Documentation Update: - **README.md** - Add language to supported languages list - **CHANGELOG.md** - Document the new language support - **Language-specific docs** - Installation requirements, known issues ``` -------------------------------------------------------------------------------- /test/solidlsp/php/test_php_basic.py: -------------------------------------------------------------------------------- ```python from pathlib import Path import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language @pytest.mark.php class TestPhpLanguageServer: @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None: """Test that the language server starts and stops successfully.""" # The fixture already handles start and stop assert language_server.is_running() assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve() @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None: # In index.php: # Line 9 (1-indexed): $greeting = greet($userName); # Line 11 (1-indexed): echo $greeting; # We want to find the definition of $greeting (defined on line 9) # from its usage in echo $greeting; on line 11. # LSP is 0-indexed: definition on line 8, usage on line 10. # $greeting in echo $greeting; is at char 5 on line 11 (0-indexed: line 10, char 5) # e c h o $ g r e e t i n g # ^ char 5 definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 10, 6) # cursor on 'g' in $greeting assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}" assert len(definition_location_list) == 1 definition_location = definition_location_list[0] assert definition_location["uri"].endswith("index.php") # Definition of $greeting is on line 10 (1-indexed) / line 9 (0-indexed), char 0 assert definition_location["range"]["start"]["line"] == 9 assert definition_location["range"]["start"]["character"] == 0 @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 12, 5) # helperFunction assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}" assert len(definition_location_list) == 1 definition_location = definition_location_list[0] assert definition_location["uri"].endswith("helper.php") assert definition_location["range"]["start"]["line"] == 2 assert definition_location["range"]["start"]["character"] == 0 @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) def test_find_definition_simple_variable(self, language_server: SolidLanguageServer, repo_path: Path) -> None: file_path = str(repo_path / "simple_var.php") # In simple_var.php: # Line 2 (1-indexed): $localVar = "test"; # Line 3 (1-indexed): echo $localVar; # LSP is 0-indexed: definition on line 1, usage on line 2 # Find definition of $localVar (char 5 on line 3 / 0-indexed: line 2, char 5) # $localVar in echo $localVar; (e c h o $ l o c a l V a r) # ^ char 5 definition_location_list = language_server.request_definition(file_path, 2, 6) # cursor on 'l' in $localVar assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}" assert len(definition_location_list) == 1 definition_location = definition_location_list[0] assert definition_location["uri"].endswith("simple_var.php") assert definition_location["range"]["start"]["line"] == 1 # Definition of $localVar (0-indexed) assert definition_location["range"]["start"]["character"] == 0 # $localVar (0-indexed) @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None: index_php_path = str(repo_path / "index.php") # In index.php (0-indexed lines): # Line 9: $greeting = greet($userName); // Definition of $greeting # Line 11: echo $greeting; // Usage of $greeting # Find references for $greeting from its usage in "echo $greeting;" (line 11, char 6 for 'g') references = language_server.request_references(index_php_path, 11, 6) assert references # Intelephense, when asked for references from usage, seems to only return the usage itself. assert len(references) == 1, "Expected to find 1 reference for $greeting (the usage itself)" expected_locations = [{"uri_suffix": "index.php", "line": 11, "character": 5}] # Usage: echo $greeting (points to $) # Convert actual references to a comparable format and sort actual_locations = sorted( [ { "uri_suffix": loc["uri"].split("/")[-1], "line": loc["range"]["start"]["line"], "character": loc["range"]["start"]["character"], } for loc in references ], key=lambda x: (x["uri_suffix"], x["line"], x["character"]), ) expected_locations = sorted(expected_locations, key=lambda x: (x["uri_suffix"], x["line"], x["character"])) assert actual_locations == expected_locations @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: helper_php_path = str(repo_path / "helper.php") # In index.php (0-indexed lines): # Line 13: helperFunction(); // Usage of helperFunction # Find references for helperFunction from its definition references = language_server.request_references(helper_php_path, 2, len("function ")) assert references, f"Expected non-empty references for helperFunction but got {references=}" # Intelephense might return 1 (usage) or 2 (usage + definition) references. # Let's check for at least the usage in index.php # Definition is in helper.php, line 2, char 0 (based on previous findings) # Usage is in index.php, line 13, char 0 actual_locations_comparable = [] for loc in references: actual_locations_comparable.append( { "uri_suffix": loc["uri"].split("/")[-1], "line": loc["range"]["start"]["line"], "character": loc["range"]["start"]["character"], } ) usage_in_index_php = {"uri_suffix": "index.php", "line": 13, "character": 0} assert usage_in_index_php in actual_locations_comparable, "Usage of helperFunction in index.php not found" ``` -------------------------------------------------------------------------------- /src/serena/resources/config/modes/editing.yml: -------------------------------------------------------------------------------- ```yaml description: All tools, with detailed instructions for code editing prompt: | You are operating in editing mode. You can edit files with the provided tools to implement the requested changes to the code base while adhering to the project's code style and patterns. Use symbolic editing tools whenever possible for precise code modifications. If no editing task has yet been provided, wait for the user to provide one. When writing new code, think about where it belongs best. Don't generate new files if you don't plan on actually integrating them into the codebase, instead use the editing tools to insert the code directly into the existing files in that case. You have two main approaches for editing code - editing by regex and editing by symbol. The symbol-based approach is appropriate if you need to adjust an entire symbol, e.g. a method, a class, a function, etc. But it is not appropriate if you need to adjust just a few lines of code within a symbol, for that you should use the regex-based approach that is described below. Let us first discuss the symbol-based approach. Symbols are identified by their name path and relative file path, see the description of the `find_symbol` tool for more details on how the `name_path` matches symbols. You can get information about available symbols by using the `get_symbols_overview` tool for finding top-level symbols in a file, or by using `find_symbol` if you already know the symbol's name path. You generally try to read as little code as possible while still solving your task, meaning you only read the bodies when you need to, and after you have found the symbol you want to edit. Before calling symbolic reading tools, you should have a basic understanding of the repository structure that you can get from memories or by using the `list_dir` and `find_file` tools (or similar). For example, if you are working with python code and already know that you need to read the body of the constructor of the class Foo, you can directly use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit, you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding to read the desired methods with `include_body=True`. In particular, keep in mind the description of the `replace_symbol_body` tool. If you want to add some new code at the end of the file, you should use the `insert_after_symbol` tool with the last top-level symbol in the file. If you want to add an import, often a good strategy is to use `insert_before_symbol` with the first top-level symbol in the file. You can understand relationships between symbols by using the `find_referencing_symbols` tool. If not explicitly requested otherwise by a user, you make sure that when you edit a symbol, it is either done in a backward-compatible way, or you find and adjust the references as needed. The `find_referencing_symbols` tool will give you code snippets around the references, as well as symbolic information. You will generally be able to use the info from the snippets and the regex-based approach to adjust the references as well. You can assume that all symbol editing tools are reliable, so you don't need to verify the results if the tool returns without error. {% if 'replace_regex' in available_tools %} Let us discuss the regex-based approach. The regex-based approach is your primary tool for editing code whenever replacing or deleting a whole symbol would be a more expensive operation. This is the case if you need to adjust just a few lines of code within a method, or a chunk that is much smaller than a whole symbol. You use other tools to find the relevant content and then use your knowledge of the codebase to write the regex, if you haven't collected enough information of this content yet. You are extremely good at regex, so you never need to check whether the replacement produced the correct result. In particular, you know what to escape and what not to escape, and you know how to use wildcards. Also, the regex tool never adds any indentation (contrary to the symbolic editing tools), so you have to take care to add the correct indentation when using it to insert code. Moreover, the replacement tool will fail if it can't perform the desired replacement, and this is all the feedback you need. Your overall goal for replacement operations is to use relatively short regexes, since I want you to minimize the number of output tokens. For replacements of larger chunks of code, this means you intelligently make use of wildcards for the middle part and of characteristic snippets for the before/after parts that uniquely identify the chunk. For small replacements, up to a single line, you follow the following rules: 1. If the snippet to be replaced is likely to be unique within the file, you perform the replacement by directly using the escaped version of the original. 2. If the snippet is probably not unique, and you want to replace all occurrences, you use the `allow_multiple_occurrences` flag. 3. If the snippet is not unique, and you want to replace a specific occurrence, you make use of the code surrounding the snippet to extend the regex with content before/after such that the regex will have exactly one match. 4. You generally assume that a snippet is unique, knowing that the tool will return an error on multiple matches. You only read more file content (for crafvarting a more specific regex) if such a failure unexpectedly occurs. Examples: 1 Small replacement You have read code like ```python ... x = linear(x) x = relu(x) return x ... ``` and you want to replace `x = relu(x)` with `x = gelu(x)`. You first try `replace_regex()` with the regex `x = relu\(x\)` and the replacement `x = gelu(x)`. If this fails due to multiple matches, you will try `(linear\(x\)\s*)x = relu\(x\)(\s*return)` with the replacement `\1x = gelu(x)\2`. 2 Larger replacement You have read code like ```python def my_func(): ... # a comment before the snippet x = add_fifteen(x) # beginning of long section within my_func .... # end of long section call_subroutine(z) call_second_subroutine(z) ``` and you want to replace the code starting with `x = add_fifteen(x)` until (including) `call_subroutine(z)`, but not `call_second_subroutine(z)`. Initially, you assume that the the beginning and end of the chunk uniquely determine it within the file. Therefore, you perform the replacement by using the regex `x = add_fifteen\(x\)\s*.*?call_subroutine\(z\)` and the replacement being the new code you want to insert. If this fails due to multiple matches, you will try to extend the regex with the content before/after the snippet and match groups. The matching regex becomes: `(before the snippet\s*)x = add_fifteen\(x\)\s*.*?call_subroutine\(z\)` and the replacement includes the group as (schematically): `\1<new_code>` Generally, I remind you that you rely on the regex tool with providing you the correct feedback, no need for more verification! IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! {% endif %} excluded_tools: - replace_lines - insert_at_line - delete_lines ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/marksman.py: -------------------------------------------------------------------------------- ```python """ Provides Markdown specific instantiation of the LanguageServer class using marksman. Contains various configurations and settings specific to Markdown. """ import logging import os import pathlib import threading from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection class Marksman(SolidLanguageServer): """ Provides Markdown specific instantiation of the LanguageServer class using marksman. """ marksman_releases = "https://github.com/artempyanykh/marksman/releases/download/2024-12-18" runtime_dependencies = RuntimeDependencyCollection( [ RuntimeDependency( id="marksman", url=f"{marksman_releases}/marksman-linux-x64", platform_id="linux-x64", archive_type="binary", binary_name="marksman", ), RuntimeDependency( id="marksman", url=f"{marksman_releases}/marksman-linux-arm64", platform_id="linux-arm64", archive_type="binary", binary_name="marksman", ), RuntimeDependency( id="marksman", url=f"{marksman_releases}/marksman-macos", platform_id="osx-x64", archive_type="binary", binary_name="marksman", ), RuntimeDependency( id="marksman", url=f"{marksman_releases}/marksman-macos", platform_id="osx-arm64", archive_type="binary", binary_name="marksman", ), RuntimeDependency( id="marksman", url=f"{marksman_releases}/marksman.exe", platform_id="win-x64", archive_type="binary", binary_name="marksman.exe", ), ] ) @classmethod def _setup_runtime_dependencies( cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings ) -> str: """Setup runtime dependencies for marksman and return the command to start the server.""" deps = cls.runtime_dependencies dependency = deps.get_single_dep_for_current_platform() marksman_ls_dir = cls.ls_resources_dir(solidlsp_settings) marksman_executable_path = deps.binary_path(marksman_ls_dir) if not os.path.exists(marksman_executable_path): logger.log( f"Downloading marksman from {dependency.url} to {marksman_ls_dir}", logging.INFO, ) deps.install(logger, marksman_ls_dir) if not os.path.exists(marksman_executable_path): raise FileNotFoundError(f"Download failed? Could not find marksman executable at {marksman_executable_path}") os.chmod(marksman_executable_path, 0o755) return marksman_executable_path def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings ): """ Creates a Marksman instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ marksman_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=f"{marksman_executable_path} server", cwd=repository_root_path), "markdown", solidlsp_settings, ) self.server_ready = threading.Event() @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["node_modules", ".obsidian", ".vitepress", ".vuepress"] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Marksman Language Server. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params: InitializeParams = { # type: ignore "processId": os.getpid(), "locale": "en", "rootPath": repository_absolute_path, "rootUri": root_uri, "capabilities": { "textDocument": { "synchronization": {"didSave": True, "dynamicRegistration": True}, "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}}, "definition": {"dynamicRegistration": True}, "references": {"dynamicRegistration": True}, "documentSymbol": { "dynamicRegistration": True, "hierarchicalDocumentSymbolSupport": True, "symbolKind": {"valueSet": list(range(1, 27))}, }, "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, "codeAction": {"dynamicRegistration": True}, }, "workspace": { "workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}, "symbol": {"dynamicRegistration": True}, }, }, "workspaceFolders": [ { "uri": root_uri, "name": os.path.basename(repository_absolute_path), } ], } return initialize_params def _start_server(self): """ Starts the Marksman Language Server and waits for it to be ready. """ def register_capability_handler(_params): return def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) def do_nothing(_params): return self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.logger.log("Starting marksman server process", logging.INFO) self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) self.logger.log( "Sending initialize request from LSP client to marksman server and awaiting response", logging.INFO, ) init_response = self.server.send.initialize(initialize_params) self.logger.log(f"Received initialize response from marksman server: {init_response}", logging.DEBUG) # Verify server capabilities assert "textDocumentSync" in init_response["capabilities"] assert "completionProvider" in init_response["capabilities"] assert "definitionProvider" in init_response["capabilities"] self.server.notify.initialized({}) # marksman is typically ready immediately after initialization self.logger.log("Marksman server initialization complete", logging.INFO) self.server_ready.set() self.completions_available.set() ``` -------------------------------------------------------------------------------- /test/solidlsp/elixir/test_elixir_integration.py: -------------------------------------------------------------------------------- ```python """ Integration tests for Elixir language server with test repository. These tests verify that the language server works correctly with a real Elixir project and can perform advanced operations like cross-file symbol resolution. """ import os from pathlib import Path import pytest from serena.project import Project from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON # These marks will be applied to all tests in this module pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")] class TestElixirIntegration: """Integration tests for Elixir language server with test repository.""" @pytest.fixture def elixir_test_repo_path(self): """Get the path to the Elixir test repository.""" test_dir = Path(__file__).parent.parent.parent return str(test_dir / "resources" / "repos" / "elixir" / "test_repo") def test_elixir_repo_structure(self, elixir_test_repo_path): """Test that the Elixir test repository has the expected structure.""" repo_path = Path(elixir_test_repo_path) # Check that key files exist assert (repo_path / "mix.exs").exists(), "mix.exs should exist" assert (repo_path / "lib" / "test_repo.ex").exists(), "main module should exist" assert (repo_path / "lib" / "utils.ex").exists(), "utils module should exist" assert (repo_path / "lib" / "models.ex").exists(), "models module should exist" assert (repo_path / "lib" / "services.ex").exists(), "services module should exist" assert (repo_path / "lib" / "examples.ex").exists(), "examples module should exist" assert (repo_path / "test" / "test_repo_test.exs").exists(), "test file should exist" assert (repo_path / "test" / "models_test.exs").exists(), "models test should exist" @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) def test_cross_file_symbol_resolution(self, language_server: SolidLanguageServer): """Test that symbols can be resolved across different files.""" # Test that User struct from models.ex can be found when referenced in services.ex services_file = os.path.join("lib", "services.ex") # Find where User is referenced in services.ex content = language_server.retrieve_full_file_content(services_file) lines = content.split("\n") user_reference_line = None for i, line in enumerate(lines): if "alias TestRepo.Models.{User" in line: user_reference_line = i break if user_reference_line is None: pytest.skip("Could not find User reference in services.ex") # Try to find the definition defining_symbol = language_server.request_defining_symbol(services_file, user_reference_line, 30) if defining_symbol and "location" in defining_symbol: # Should point to models.ex assert "models.ex" in defining_symbol["location"]["uri"] @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) def test_module_hierarchy_understanding(self, language_server: SolidLanguageServer): """Test that the language server understands Elixir module hierarchy.""" models_file = os.path.join("lib", "models.ex") symbols = language_server.request_document_symbols(models_file) if symbols: # Flatten symbol structure all_symbols = [] for symbol_group in symbols: if isinstance(symbol_group, list): all_symbols.extend(symbol_group) else: all_symbols.append(symbol_group) symbol_names = [s.get("name", "") for s in all_symbols] # Should understand nested module structure expected_modules = ["TestRepo.Models", "User", "Item", "Order"] found_modules = [name for name in expected_modules if any(name in symbol_name for symbol_name in symbol_names)] assert len(found_modules) > 0, f"Expected modules {expected_modules}, found symbols {symbol_names}" def test_file_extension_matching(self): """Test that the Elixir language recognizes the correct file extensions.""" language = Language.ELIXIR matcher = language.get_source_fn_matcher() # Test Elixir file extensions assert matcher.is_relevant_filename("lib/test_repo.ex") assert matcher.is_relevant_filename("test/test_repo_test.exs") assert matcher.is_relevant_filename("config/config.exs") assert matcher.is_relevant_filename("mix.exs") assert matcher.is_relevant_filename("lib/models.ex") assert matcher.is_relevant_filename("lib/services.ex") # Test non-Elixir files assert not matcher.is_relevant_filename("README.md") assert not matcher.is_relevant_filename("lib/test_repo.py") assert not matcher.is_relevant_filename("package.json") assert not matcher.is_relevant_filename("Cargo.toml") class TestElixirProject: @pytest.mark.parametrize("project", [Language.ELIXIR], indirect=True) def test_comprehensive_symbol_search(self, project: Project): """Test comprehensive symbol search across the entire project.""" # Search for all function definitions function_pattern = r"def\s+\w+\s*[\(\s]" function_matches = project.search_source_files_for_pattern(function_pattern) # Should find functions across multiple files if function_matches: files_with_functions = set() for match in function_matches: if match.source_file_path: files_with_functions.add(os.path.basename(match.source_file_path)) # Should find functions in multiple files expected_files = {"models.ex", "services.ex", "examples.ex", "utils.ex", "test_repo.ex"} found_files = expected_files.intersection(files_with_functions) assert len(found_files) > 0, f"Expected functions in {expected_files}, found in {files_with_functions}" # Search for struct definitions struct_pattern = r"defstruct\s+\[" struct_matches = project.search_source_files_for_pattern(struct_pattern) if struct_matches: # Should find structs primarily in models.ex models_structs = [m for m in struct_matches if m.source_file_path and "models.ex" in m.source_file_path] assert len(models_structs) > 0, "Should find struct definitions in models.ex" @pytest.mark.parametrize("project", [Language.ELIXIR], indirect=True) def test_protocol_and_implementation_understanding(self, project: Project): """Test that the language server understands Elixir protocols and implementations.""" # Search for protocol definitions protocol_pattern = r"defprotocol\s+\w+" protocol_matches = project.search_source_files_for_pattern(protocol_pattern, paths_include_glob="**/models.ex") if protocol_matches: # Should find the Serializable protocol serializable_matches = [m for m in protocol_matches if "Serializable" in str(m)] assert len(serializable_matches) > 0, "Should find Serializable protocol definition" # Search for protocol implementations impl_pattern = r"defimpl\s+\w+" impl_matches = project.search_source_files_for_pattern(impl_pattern, paths_include_glob="**/models.ex") if impl_matches: # Should find multiple implementations assert len(impl_matches) >= 3, f"Should find at least 3 protocol implementations, found {len(impl_matches)}" ``` -------------------------------------------------------------------------------- /test/solidlsp/al/test_al_basic.py: -------------------------------------------------------------------------------- ```python import os import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils @pytest.mark.al class TestALLanguageServer: @pytest.mark.parametrize("language_server", [Language.AL], indirect=True) def test_find_symbol(self, language_server: SolidLanguageServer) -> None: """Test that AL Language Server can find symbols in the test repository.""" symbols = language_server.request_full_symbol_tree() # Check for table symbols - AL returns full object names like 'Table 50000 "TEST Customer"' assert SymbolUtils.symbol_tree_contains_name(symbols, 'Table 50000 "TEST Customer"'), "TEST Customer table not found in symbol tree" # Check for page symbols assert SymbolUtils.symbol_tree_contains_name( symbols, 'Page 50001 "TEST Customer Card"' ), "TEST Customer Card page not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name( symbols, 'Page 50002 "TEST Customer List"' ), "TEST Customer List page not found in symbol tree" # Check for codeunit symbols assert SymbolUtils.symbol_tree_contains_name(symbols, "Codeunit 50000 CustomerMgt"), "CustomerMgt codeunit not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name( symbols, "Codeunit 50001 PaymentProcessorImpl" ), "PaymentProcessorImpl codeunit not found in symbol tree" # Check for enum symbol assert SymbolUtils.symbol_tree_contains_name(symbols, "Enum 50000 CustomerType"), "CustomerType enum not found in symbol tree" # Check for interface symbol assert SymbolUtils.symbol_tree_contains_name( symbols, "Interface IPaymentProcessor" ), "IPaymentProcessor interface not found in symbol tree" @pytest.mark.parametrize("language_server", [Language.AL], indirect=True) def test_find_table_fields(self, language_server: SolidLanguageServer) -> None: """Test that AL Language Server can find fields within a table.""" file_path = os.path.join("src", "Tables", "Customer.Table.al") symbols = language_server.request_document_symbols(file_path) # AL tables should have their fields as child symbols customer_table = None _all_symbols, root_symbols = symbols for sym in root_symbols: if "TEST Customer" in sym.get("name", ""): customer_table = sym break assert customer_table is not None, "Could not find TEST Customer table symbol" # Check for field symbols (AL nests fields under a "fields" group) if "children" in customer_table: # Find the fields group fields_group = None for child in customer_table.get("children", []): if child.get("name") == "fields": fields_group = child break assert fields_group is not None, "Fields group not found in Customer table" # Check actual field names if "children" in fields_group: field_names = [child.get("name", "") for child in fields_group.get("children", [])] assert any("Name" in name for name in field_names), f"Name field not found. Fields: {field_names}" assert any("Balance" in name for name in field_names), f"Balance field not found. Fields: {field_names}" @pytest.mark.parametrize("language_server", [Language.AL], indirect=True) def test_find_procedures(self, language_server: SolidLanguageServer) -> None: """Test that AL Language Server can find procedures in codeunits.""" file_path = os.path.join("src", "Codeunits", "CustomerMgt.Codeunit.al") symbols = language_server.request_document_symbols(file_path) # Find the codeunit symbol - AL returns 'Codeunit 50000 CustomerMgt' codeunit_symbol = None _all_symbols, root_symbols = symbols for sym in root_symbols: if "CustomerMgt" in sym.get("name", ""): codeunit_symbol = sym break assert codeunit_symbol is not None, "Could not find CustomerMgt codeunit symbol" # Check for procedure symbols (if hierarchical) if "children" in codeunit_symbol: procedure_names = [child.get("name", "") for child in codeunit_symbol.get("children", [])] assert any("CreateCustomer" in name for name in procedure_names), "CreateCustomer procedure not found" # Note: UpdateCustomerBalance doesn't exist in our test repo, check for actual procedures assert any("TestNoSeries" in name for name in procedure_names), "TestNoSeries procedure not found" @pytest.mark.parametrize("language_server", [Language.AL], indirect=True) def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: """Test that AL Language Server can find references to symbols.""" # Find references to the Customer table from the CustomerMgt codeunit table_file = os.path.join("src", "Tables", "Customer.Table.al") symbols = language_server.request_document_symbols(table_file) # Find the Customer table symbol customer_symbol = None _all_symbols, root_symbols = symbols for sym in root_symbols: if "TEST Customer" in sym.get("name", ""): customer_symbol = sym break if customer_symbol and "selectionRange" in customer_symbol: sel_start = customer_symbol["selectionRange"]["start"] refs = language_server.request_references(table_file, sel_start["line"], sel_start["character"]) # The Customer table should be referenced in CustomerMgt.Codeunit.al assert any( "CustomerMgt.Codeunit.al" in ref.get("relativePath", "") for ref in refs ), "Customer table should be referenced in CustomerMgt.Codeunit.al" # It should also be referenced in CustomerCard.Page.al assert any( "CustomerCard.Page.al" in ref.get("relativePath", "") for ref in refs ), "Customer table should be referenced in CustomerCard.Page.al" @pytest.mark.parametrize("language_server", [Language.AL], indirect=True) def test_cross_file_symbols(self, language_server: SolidLanguageServer) -> None: """Test that AL Language Server can handle cross-file symbol relationships.""" # Get all symbols to verify cross-file visibility symbols = language_server.request_full_symbol_tree() # Count how many AL-specific symbols we found al_symbols = [] def collect_symbols(syms): for sym in syms: if isinstance(sym, dict): name = sym.get("name", "") # Look for AL object names (Table, Page, Codeunit, etc.) if any(keyword in name for keyword in ["Table", "Page", "Codeunit", "Enum", "Interface"]): al_symbols.append(name) if "children" in sym: collect_symbols(sym["children"]) collect_symbols(symbols) # We should find symbols from multiple files assert len(al_symbols) >= 5, f"Expected at least 5 AL object symbols, found {len(al_symbols)}: {al_symbols}" # Verify we have symbols from different AL object types has_table = any("Table" in s for s in al_symbols) has_page = any("Page" in s for s in al_symbols) has_codeunit = any("Codeunit" in s for s in al_symbols) assert has_table, f"No Table symbols found in: {al_symbols}" assert has_page, f"No Page symbols found in: {al_symbols}" assert has_codeunit, f"No Codeunit symbols found in: {al_symbols}" ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/elm_language_server.py: -------------------------------------------------------------------------------- ```python """ Provides Elm specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Elm. """ import logging import os import pathlib import shutil import threading from overrides import override from sensai.util.logging import LogTime from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection class ElmLanguageServer(SolidLanguageServer): """ Provides Elm specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Elm. """ def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings ): """ Creates an ElmLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ elm_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=elm_lsp_executable_path, cwd=repository_root_path), "elm", solidlsp_settings, ) self.server_ready = threading.Event() @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in [ "elm-stuff", "node_modules", "dist", "build", ] @classmethod def _setup_runtime_dependencies( cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings ) -> list[str]: """ Setup runtime dependencies for Elm Language Server and return the command to start the server. """ # Check if elm-language-server is already installed globally system_elm_ls = shutil.which("elm-language-server") if system_elm_ls: logger.log(f"Found system-installed elm-language-server at {system_elm_ls}", logging.INFO) return [system_elm_ls, "--stdio"] # Verify node and npm are installed is_node_installed = shutil.which("node") is not None assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." deps = RuntimeDependencyCollection( [ RuntimeDependency( id="elm-language-server", description="@elm-tooling/elm-language-server package", command=["npm", "install", "--prefix", "./", "@elm-tooling/[email protected]"], platform_id="any", ), ] ) # Install elm-language-server if not already installed elm_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "elm-lsp") elm_ls_executable_path = os.path.join(elm_ls_dir, "node_modules", ".bin", "elm-language-server") if not os.path.exists(elm_ls_executable_path): logger.log(f"Elm Language Server executable not found at {elm_ls_executable_path}. Installing...", logging.INFO) with LogTime("Installation of Elm language server dependencies", logger=logger.logger): deps.install(logger, elm_ls_dir) if not os.path.exists(elm_ls_executable_path): raise FileNotFoundError( f"elm-language-server executable not found at {elm_ls_executable_path}, something went wrong with the installation." ) return [elm_ls_executable_path, "--stdio"] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Elm Language Server. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params = { "locale": "en", "capabilities": { "textDocument": { "synchronization": {"didSave": True, "dynamicRegistration": True}, "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}}, "definition": {"dynamicRegistration": True}, "references": {"dynamicRegistration": True}, "documentSymbol": { "dynamicRegistration": True, "hierarchicalDocumentSymbolSupport": True, "symbolKind": {"valueSet": list(range(1, 27))}, }, "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, "codeAction": {"dynamicRegistration": True}, "rename": {"dynamicRegistration": True}, }, "workspace": { "workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}, "symbol": {"dynamicRegistration": True}, }, }, "initializationOptions": { "elmPath": "elm", "elmFormatPath": "elm-format", "elmTestPath": "elm-test", "skipInstallPackageConfirmation": True, "onlyUpdateDiagnosticsOnSave": False, }, "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": root_uri, "workspaceFolders": [ { "uri": root_uri, "name": os.path.basename(repository_absolute_path), } ], } return initialize_params def _start_server(self): """ Starts the Elm Language Server, waits for the server to be ready and yields the LanguageServer instance. """ def do_nothing(params): return def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_notification("window/logMessage", window_log_message) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.logger.log("Starting Elm server process", logging.INFO) self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) self.logger.log( "Sending initialize request from LSP client to LSP server and awaiting response", logging.INFO, ) init_response = self.server.send.initialize(initialize_params) # Elm-specific capability checks assert "textDocumentSync" in init_response["capabilities"] assert "completionProvider" in init_response["capabilities"] assert "definitionProvider" in init_response["capabilities"] assert "referencesProvider" in init_response["capabilities"] assert "documentSymbolProvider" in init_response["capabilities"] self.server.notify.initialized({}) self.logger.log("Elm server initialized successfully, waiting for workspace scan...", logging.INFO) self.server_ready.set() self.completions_available.set() self.logger.log("Elm server ready", logging.INFO) @override def _get_wait_time_for_cross_file_referencing(self) -> float: return 1.0 ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/intelephense.py: -------------------------------------------------------------------------------- ```python """ Provides PHP specific instantiation of the LanguageServer class using Intelephense. """ import logging import os import pathlib import shutil from time import sleep from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection class Intelephense(SolidLanguageServer): """ Provides PHP specific instantiation of the LanguageServer class using Intelephense. You can pass the following entries in ls_specific_settings["php"]: - maxMemory - maxFileSize """ @override def is_ignored_dirname(self, dirname: str) -> bool: # For PHP projects, we should ignore: # - vendor: third-party dependencies managed by Composer # - node_modules: if the project has JavaScript components # - cache: commonly used for caching return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] @classmethod def _setup_runtime_dependencies( cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings ) -> str: """ Setup runtime dependencies for Intelephense and return the command to start the server. """ platform_id = PlatformUtils.get_platform_id() valid_platforms = [ PlatformId.LINUX_x64, PlatformId.LINUX_arm64, PlatformId.OSX, PlatformId.OSX_x64, PlatformId.OSX_arm64, PlatformId.WIN_x64, PlatformId.WIN_arm64, ] assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy PHP at the moment" # Verify both node and npm are installed is_node_installed = shutil.which("node") is not None assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." is_npm_installed = shutil.which("npm") is not None assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." # Install intelephense if not already installed intelephense_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "php-lsp") os.makedirs(intelephense_ls_dir, exist_ok=True) intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") if not os.path.exists(intelephense_executable_path): deps = RuntimeDependencyCollection( [ RuntimeDependency( id="intelephense", command="npm install --prefix ./ [email protected]", platform_id="any", ) ] ) deps.install(logger, intelephense_ls_dir) assert os.path.exists( intelephense_executable_path ), f"intelephense executable not found at {intelephense_executable_path}, something went wrong." return f"{intelephense_executable_path} --stdio" def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings ): # Setup runtime dependencies before initializing intelephense_cmd = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), "php", solidlsp_settings, ) self.request_id = 0 def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialization params for the Intelephense Language Server. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params = { "locale": "en", "capabilities": { "textDocument": { "synchronization": {"didSave": True, "dynamicRegistration": True}, "definition": {"dynamicRegistration": True}, }, "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}}, }, "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": root_uri, "workspaceFolders": [ { "uri": root_uri, "name": os.path.basename(repository_absolute_path), } ], } initialization_options = {} # Add license key if provided via environment variable license_key = os.environ.get("INTELEPHENSE_LICENSE_KEY") if license_key: initialization_options["licenceKey"] = license_key custom_intelephense_settings = self._solidlsp_settings.ls_specific_settings.get(self.get_language_enum_instance(), {}) max_memory = custom_intelephense_settings.get("maxMemory") max_file_size = custom_intelephense_settings.get("maxFileSize") if max_memory is not None: initialization_options["intelephense.maxMemory"] = max_memory if max_file_size is not None: initialization_options["intelephense.files.maxSize"] = max_file_size initialize_params["initializationOptions"] = initialization_options return initialize_params def _start_server(self): """Start Intelephense server process""" def register_capability_handler(params): return def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) def do_nothing(params): return self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.logger.log("Starting Intelephense server process", logging.INFO) self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) self.logger.log( "Sending initialize request from LSP client to LSP server and awaiting response", logging.INFO, ) init_response = self.server.send.initialize(initialize_params) self.logger.log( "After sent initialize params", logging.INFO, ) # Verify server capabilities assert "textDocumentSync" in init_response["capabilities"] assert "completionProvider" in init_response["capabilities"] assert "definitionProvider" in init_response["capabilities"] self.server.notify.initialized({}) self.completions_available.set() # Intelephense server is typically ready immediately after initialization # TODO: This is probably incorrect; the server does send an initialized notification, which we could wait for! @override # For some reason, the LS may need longer to process this, so we just retry def _send_references_request(self, relative_file_path: str, line: int, column: int): # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is # despite the LS having processed requests already. I don't know what causes this, but sleeping # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. # May be related to the time it takes to read the files or something like that. # The sleeping doesn't seem to be needed on all systems sleep(1) return super()._send_references_request(relative_file_path, line, column) @override def _send_definition_request(self, definition_params: DefinitionParams): # TODO: same as above, also only a problem if the definition is in another file sleep(1) return super()._send_definition_request(definition_params) ``` -------------------------------------------------------------------------------- /src/serena/config/context_mode.py: -------------------------------------------------------------------------------- ```python """ Context and Mode configuration loader """ import os from dataclasses import dataclass, field from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Self import yaml from sensai.util import logging from sensai.util.string import ToStringMixin from serena.config.serena_config import ToolInclusionDefinition from serena.constants import ( DEFAULT_CONTEXT, DEFAULT_MODES, INTERNAL_MODE_YAMLS_DIR, SERENAS_OWN_CONTEXT_YAMLS_DIR, SERENAS_OWN_MODE_YAMLS_DIR, USER_CONTEXT_YAMLS_DIR, USER_MODE_YAMLS_DIR, ) if TYPE_CHECKING: pass log = logging.getLogger(__name__) @dataclass(kw_only=True) class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin): """Represents a mode of operation for the agent, typically read off a YAML file. An agent can be in multiple modes simultaneously as long as they are not mutually exclusive. The modes can be adjusted after the agent is running, for example for switching from planning to editing. """ name: str prompt: str """ a Jinja2 template for the generation of the system prompt. It is formatted by the agent (see SerenaAgent._format_prompt()). """ description: str = "" def _tostring_includes(self) -> list[str]: return ["name"] def print_overview(self) -> None: """Print an overview of the mode.""" print(f"{self.name}:\n {self.description}") if self.excluded_tools: print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools))) @classmethod def from_yaml(cls, yaml_path: str | Path) -> Self: """Load a mode from a YAML file.""" with open(yaml_path, encoding="utf-8") as f: data = yaml.safe_load(f) name = data.pop("name", Path(yaml_path).stem) return cls(name=name, **data) @classmethod def get_path(cls, name: str) -> str: """Get the path to the YAML file for a mode.""" fname = f"{name}.yml" custom_mode_path = os.path.join(USER_MODE_YAMLS_DIR, fname) if os.path.exists(custom_mode_path): return custom_mode_path own_yaml_path = os.path.join(SERENAS_OWN_MODE_YAMLS_DIR, fname) if not os.path.exists(own_yaml_path): raise FileNotFoundError( f"Mode {name} not found in {USER_MODE_YAMLS_DIR} or in {SERENAS_OWN_MODE_YAMLS_DIR}." f"Available modes:\n{cls.list_registered_mode_names()}" ) return own_yaml_path @classmethod def from_name(cls, name: str) -> Self: """Load a registered Serena mode.""" mode_path = cls.get_path(name) return cls.from_yaml(mode_path) @classmethod def from_name_internal(cls, name: str) -> Self: """Loads an internal Serena mode""" yaml_path = os.path.join(INTERNAL_MODE_YAMLS_DIR, f"{name}.yml") if not os.path.exists(yaml_path): raise FileNotFoundError(f"Internal mode '{name}' not found in {INTERNAL_MODE_YAMLS_DIR}") return cls.from_yaml(yaml_path) @classmethod def list_registered_mode_names(cls, include_user_modes: bool = True) -> list[str]: """Names of all registered modes (from the corresponding YAML files in the serena repo).""" modes = [f.stem for f in Path(SERENAS_OWN_MODE_YAMLS_DIR).glob("*.yml") if f.name != "mode.template.yml"] if include_user_modes: modes += cls.list_custom_mode_names() return sorted(set(modes)) @classmethod def list_custom_mode_names(cls) -> list[str]: """Names of all custom modes defined by the user.""" return [f.stem for f in Path(USER_MODE_YAMLS_DIR).glob("*.yml")] @classmethod def load_default_modes(cls) -> list[Self]: """Load the default modes (interactive and editing).""" return [cls.from_name(mode) for mode in DEFAULT_MODES] @classmethod def load(cls, name_or_path: str | Path) -> Self: if str(name_or_path).endswith(".yml"): return cls.from_yaml(name_or_path) return cls.from_name(str(name_or_path)) @dataclass(kw_only=True) class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin): """Represents a context where the agent is operating (an IDE, a chat, etc.), typically read off a YAML file. An agent can only be in a single context at a time. The contexts cannot be changed after the agent is running. """ name: str prompt: str """ a Jinja2 template for the generation of the system prompt. It is formatted by the agent (see SerenaAgent._format_prompt()). """ description: str = "" tool_description_overrides: dict[str, str] = field(default_factory=dict) """Maps tool names to custom descriptions, default descriptions are extracted from the tool docstrings.""" def _tostring_includes(self) -> list[str]: return ["name"] @classmethod def from_yaml(cls, yaml_path: str | Path) -> Self: """Load a context from a YAML file.""" with open(yaml_path, encoding="utf-8") as f: data = yaml.safe_load(f) name = data.pop("name", Path(yaml_path).stem) # Ensure backwards compatibility for tool_description_overrides if "tool_description_overrides" not in data: data["tool_description_overrides"] = {} return cls(name=name, **data) @classmethod def get_path(cls, name: str) -> str: """Get the path to the YAML file for a context.""" fname = f"{name}.yml" custom_context_path = os.path.join(USER_CONTEXT_YAMLS_DIR, fname) if os.path.exists(custom_context_path): return custom_context_path own_yaml_path = os.path.join(SERENAS_OWN_CONTEXT_YAMLS_DIR, fname) if not os.path.exists(own_yaml_path): raise FileNotFoundError( f"Context {name} not found in {USER_CONTEXT_YAMLS_DIR} or in {SERENAS_OWN_CONTEXT_YAMLS_DIR}." f"Available contexts:\n{cls.list_registered_context_names()}" ) return own_yaml_path @classmethod def from_name(cls, name: str) -> Self: """Load a registered Serena context.""" context_path = cls.get_path(name) return cls.from_yaml(context_path) @classmethod def load(cls, name_or_path: str | Path) -> Self: if str(name_or_path).endswith(".yml"): return cls.from_yaml(name_or_path) return cls.from_name(str(name_or_path)) @classmethod def list_registered_context_names(cls, include_user_contexts: bool = True) -> list[str]: """Names of all registered contexts (from the corresponding YAML files in the serena repo).""" contexts = [f.stem for f in Path(SERENAS_OWN_CONTEXT_YAMLS_DIR).glob("*.yml")] if include_user_contexts: contexts += cls.list_custom_context_names() return sorted(set(contexts)) @classmethod def list_custom_context_names(cls) -> list[str]: """Names of all custom contexts defined by the user.""" return [f.stem for f in Path(USER_CONTEXT_YAMLS_DIR).glob("*.yml")] @classmethod def load_default(cls) -> Self: """Load the default context.""" return cls.from_name(DEFAULT_CONTEXT) def print_overview(self) -> None: """Print an overview of the mode.""" print(f"{self.name}:\n {self.description}") if self.excluded_tools: print(" excluded tools:\n " + ", ".join(sorted(self.excluded_tools))) class RegisteredContext(Enum): """A registered context.""" IDE_ASSISTANT = "ide-assistant" """For Serena running within an assistant that already has basic tools, like Claude Code, Cline, Cursor, etc.""" DESKTOP_APP = "desktop-app" """For Serena running within Claude Desktop or a similar app which does not have built-in tools for code editing.""" AGENT = "agent" """For Serena running as a standalone agent, e.g. through agno.""" def load(self) -> SerenaAgentContext: """Load the context.""" return SerenaAgentContext.from_name(self.value) class RegisteredMode(Enum): """A registered mode.""" INTERACTIVE = "interactive" """Interactive mode, for multi-turn interactions.""" EDITING = "editing" """Editing tools are activated.""" PLANNING = "planning" """Editing tools are deactivated.""" ONE_SHOT = "one-shot" """Non-interactive mode, where the goal is to finish a task autonomously.""" def load(self) -> SerenaAgentMode: """Load the mode.""" return SerenaAgentMode.from_name(self.value) ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/perl_language_server.py: -------------------------------------------------------------------------------- ```python """ Provides Perl specific instantiation of the LanguageServer class using Perl::LanguageServer. Note: Windows is not supported as Nix itself doesn't support Windows natively. """ import logging import os import pathlib import subprocess import time from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings class PerlLanguageServer(SolidLanguageServer): """ Provides Perl specific instantiation of the LanguageServer class using Perl::LanguageServer. """ @staticmethod def _get_perl_version(): """Get the installed Perl version or None if not found.""" try: result = subprocess.run(["perl", "-v"], capture_output=True, text=True, check=False) if result.returncode == 0: return result.stdout.strip() except FileNotFoundError: return None return None @staticmethod def _get_perl_language_server_version(): """Get the installed Perl::LanguageServer version or None if not found.""" try: result = subprocess.run( ["perl", "-MPerl::LanguageServer", "-e", "print $Perl::LanguageServer::VERSION"], capture_output=True, text=True, check=False, ) if result.returncode == 0: return result.stdout.strip() except FileNotFoundError: return None return None @override def is_ignored_dirname(self, dirname: str) -> bool: # For Perl projects, we should ignore: # - blib: build library directory # - local: local Perl module installation # - .carton: Carton dependency manager cache # - vendor: vendored dependencies # - _build: Module::Build output return super().is_ignored_dirname(dirname) or dirname in ["blib", "local", ".carton", "vendor", "_build", "cover_db"] @classmethod def _setup_runtime_dependencies(cls) -> str: """ Check if required Perl runtime dependencies are available. Raises RuntimeError with helpful message if dependencies are missing. """ platform_id = PlatformUtils.get_platform_id() valid_platforms = [ PlatformId.LINUX_x64, PlatformId.LINUX_arm64, PlatformId.OSX, PlatformId.OSX_x64, PlatformId.OSX_arm64, ] if platform_id not in valid_platforms: raise RuntimeError(f"Platform {platform_id} is not supported for Perl at the moment") perl_version = cls._get_perl_version() if not perl_version: raise RuntimeError( "Perl is not installed. Please install Perl from https://www.perl.org/get.html and make sure it is added to your PATH." ) perl_ls_version = cls._get_perl_language_server_version() if not perl_ls_version: raise RuntimeError( "Found a Perl version but Perl::LanguageServer is not installed.\n" "Please install Perl::LanguageServer: cpanm Perl::LanguageServer\n" "See: https://metacpan.org/pod/Perl::LanguageServer" ) return "perl -MPerl::LanguageServer -e 'Perl::LanguageServer::run'" def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings ): # Setup runtime dependencies before initializing perl_ls_cmd = self._setup_runtime_dependencies() super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=perl_ls_cmd, cwd=repository_root_path), "perl", solidlsp_settings, ) self.request_id = 0 @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for Perl::LanguageServer. Based on the expected structure from Perl::LanguageServer::Methods::_rpcreq_initialize. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params = { "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": root_uri, "capabilities": { "textDocument": { "synchronization": {"didSave": True, "dynamicRegistration": True}, "definition": {"dynamicRegistration": True}, "references": {"dynamicRegistration": True}, "documentSymbol": {"dynamicRegistration": True}, "hover": {"dynamicRegistration": True}, }, "workspace": { "workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}, "symbol": {"dynamicRegistration": True}, }, }, "initializationOptions": {}, "workspaceFolders": [ { "uri": root_uri, "name": os.path.basename(repository_absolute_path), } ], } return initialize_params def _start_server(self): """Start Perl::LanguageServer process""" def register_capability_handler(params): return def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) def do_nothing(params): return def workspace_configuration_handler(params): """Handle workspace/configuration request from Perl::LanguageServer.""" self.logger.log(f"Received workspace/configuration request: {params}", logging.INFO) perl_config = { "perlInc": [self.repository_root_path, "."], "fileFilter": [".pm", ".pl"], "ignoreDirs": [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"], } return [perl_config] self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_request("workspace/configuration", workspace_configuration_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.logger.log("Starting Perl::LanguageServer process", logging.INFO) self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) self.logger.log( "Sending initialize request from LSP client to LSP server and awaiting response", logging.INFO, ) init_response = self.server.send.initialize(initialize_params) self.logger.log( "After sent initialize params", logging.INFO, ) # Verify server capabilities assert "textDocumentSync" in init_response["capabilities"] assert "definitionProvider" in init_response["capabilities"] assert "referencesProvider" in init_response["capabilities"] self.server.notify.initialized({}) # Send workspace configuration to Perl::LanguageServer # Perl::LanguageServer requires didChangeConfiguration to set perlInc, fileFilter, and ignoreDirs # See: Perl::LanguageServer::Methods::workspace::_rpcnot_didChangeConfiguration perl_config = { "settings": { "perl": { "perlInc": [self.repository_root_path, "."], "fileFilter": [".pm", ".pl"], "ignoreDirs": [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"], } } } self.logger.log(f"Sending workspace/didChangeConfiguration notification with config: {perl_config}", logging.INFO) self.server.notify.workspace_did_change_configuration(perl_config) self.completions_available.set() # Perl::LanguageServer needs time to index files and resolve cross-file references # Without this delay, requests for definitions/references may return empty results settling_time = 0.5 self.logger.log(f"Allowing {settling_time} seconds for Perl::LanguageServer to index files...", logging.INFO) time.sleep(settling_time) self.logger.log("Perl::LanguageServer settling period complete", logging.INFO) ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/pyright_server.py: -------------------------------------------------------------------------------- ```python """ Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ import logging import os import pathlib import re import threading from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings class PyrightServer(SolidLanguageServer): """ Provides Python specific instantiation of the LanguageServer class using Pyright. Contains various configurations and settings specific to Python. """ def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings ): """ Creates a PyrightServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ super().__init__( config, logger, repository_root_path, # Note 1: we can also use `pyright-langserver --stdio` but it requires pyright to be installed with npm # Note 2: we can also use `bpyright-langserver --stdio` if we ever are unhappy with pyright ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path), "python", solidlsp_settings, ) # Event to signal when initial workspace analysis is complete self.analysis_complete = threading.Event() self.found_source_files = False @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Pyright Language Server. """ # Create basic initialization parameters initialize_params: InitializeParams = { # type: ignore "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": pathlib.Path(repository_absolute_path).as_uri(), "initializationOptions": { "exclude": [ "**/__pycache__", "**/.venv", "**/.env", "**/build", "**/dist", "**/.pixi", ], "reportMissingImports": "error", }, "capabilities": { "workspace": { "workspaceEdit": {"documentChanges": True}, "didChangeConfiguration": {"dynamicRegistration": True}, "didChangeWatchedFiles": {"dynamicRegistration": True}, "symbol": { "dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}, }, "executeCommand": {"dynamicRegistration": True}, }, "textDocument": { "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True}, "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, "signatureHelp": { "dynamicRegistration": True, "signatureInformation": { "documentationFormat": ["markdown", "plaintext"], "parameterInformation": {"labelOffsetSupport": True}, }, }, "definition": {"dynamicRegistration": True}, "references": {"dynamicRegistration": True}, "documentSymbol": { "dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}, "hierarchicalDocumentSymbolSupport": True, }, "publishDiagnostics": {"relatedInformation": True}, }, }, "workspaceFolders": [ {"uri": pathlib.Path(repository_absolute_path).as_uri(), "name": os.path.basename(repository_absolute_path)} ], } return initialize_params def _start_server(self): """ Starts the Pyright Language Server and waits for initial workspace analysis to complete. This prevents zombie processes by ensuring Pyright has finished its initial background tasks before we consider the server ready. Usage: ``` async with lsp.start_server(): # LanguageServer has been initialized and workspace analysis is complete await lsp.request_definition(...) await lsp.request_references(...) # Shutdown the LanguageServer on exit from scope # LanguageServer has been shutdown cleanly ``` """ def execute_client_command_handler(params): return [] def do_nothing(params): return def window_log_message(msg): """ Monitor Pyright's log messages to detect when initial analysis is complete. Pyright logs "Found X source files" when it finishes scanning the workspace. """ message_text = msg.get("message", "") self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO) # Look for "Found X source files" which indicates workspace scanning is complete # Unfortunately, pyright is unreliable and there seems to be no better way if re.search(r"Found \d+ source files?", message_text): self.logger.log("Pyright workspace scanning complete", logging.INFO) self.found_source_files = True self.analysis_complete.set() self.completions_available.set() def check_experimental_status(params): """ Also listen for experimental/serverStatus as a backup signal """ if params.get("quiescent") == True: self.logger.log("Received experimental/serverStatus with quiescent=true", logging.INFO) if not self.found_source_files: self.analysis_complete.set() self.completions_available.set() # Set up notification handlers self.server.on_request("client/registerCapability", do_nothing) self.server.on_notification("language/status", do_nothing) self.server.on_notification("window/logMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) self.logger.log("Starting pyright-langserver server process", logging.INFO) self.server.start() # Send proper initialization parameters initialize_params = self._get_initialize_params(self.repository_root_path) self.logger.log( "Sending initialize request from LSP client to pyright server and awaiting response", logging.INFO, ) init_response = self.server.send.initialize(initialize_params) self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) # Verify that the server supports our required features assert "textDocumentSync" in init_response["capabilities"] assert "completionProvider" in init_response["capabilities"] assert "definitionProvider" in init_response["capabilities"] # Complete the initialization handshake self.server.notify.initialized({}) # Wait for Pyright to complete its initial workspace analysis # This prevents zombie processes by ensuring background tasks finish self.logger.log("Waiting for Pyright to complete initial workspace analysis...", logging.INFO) if self.analysis_complete.wait(timeout=5.0): self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) else: self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) # Fallback: assume analysis is complete after timeout self.analysis_complete.set() self.completions_available.set() ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/clangd_language_server.py: -------------------------------------------------------------------------------- ```python """ Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. """ import logging import os import pathlib import threading from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection class ClangdLanguageServer(SolidLanguageServer): """ Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly. Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example. """ def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings ): """ Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ clangd_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), "cpp", solidlsp_settings, ) self.server_ready = threading.Event() self.service_ready_event = threading.Event() self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() @classmethod def _setup_runtime_dependencies( cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings ) -> str: """ Setup runtime dependencies for ClangdLanguageServer and return the command to start the server. """ deps = RuntimeDependencyCollection( [ RuntimeDependency( id="Clangd", description="Clangd for Linux (x64)", url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip", platform_id="linux-x64", archive_type="zip", binary_name="clangd_19.1.2/bin/clangd", ), RuntimeDependency( id="Clangd", description="Clangd for Windows (x64)", url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip", platform_id="win-x64", archive_type="zip", binary_name="clangd_19.1.2/bin/clangd.exe", ), RuntimeDependency( id="Clangd", description="Clangd for macOS (x64)", url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", platform_id="osx-x64", archive_type="zip", binary_name="clangd_19.1.2/bin/clangd", ), RuntimeDependency( id="Clangd", description="Clangd for macOS (Arm64)", url="https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", platform_id="osx-arm64", archive_type="zip", binary_name="clangd_19.1.2/bin/clangd", ), ] ) clangd_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "clangd") dep = deps.get_single_dep_for_current_platform() clangd_executable_path = deps.binary_path(clangd_ls_dir) if not os.path.exists(clangd_executable_path): logger.log( f"Clangd executable not found at {clangd_executable_path}. Downloading from {dep.url}", logging.INFO, ) deps.install(logger, clangd_ls_dir) if not os.path.exists(clangd_executable_path): raise FileNotFoundError( f"Clangd executable not found at {clangd_executable_path}.\n" "Make sure you have installed clangd. See https://clangd.llvm.org/installation" ) os.chmod(clangd_executable_path, 0o755) return clangd_executable_path @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the clangd Language Server. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params = { "locale": "en", "capabilities": { "textDocument": { "synchronization": {"didSave": True, "dynamicRegistration": True}, "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}}, "definition": {"dynamicRegistration": True}, }, "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}}, }, "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": root_uri, "workspaceFolders": [ { "uri": root_uri, "name": "$name", } ], } return initialize_params def _start_server(self): """ Starts the Clangd Language Server, waits for the server to be ready and yields the LanguageServer instance. Usage: ``` async with lsp.start_server(): # LanguageServer has been initialized and ready to serve requests await lsp.request_definition(...) await lsp.request_references(...) # Shutdown the LanguageServer on exit from scope # LanguageServer has been shutdown """ def register_capability_handler(params): assert "registrations" in params for registration in params["registrations"]: if registration["method"] == "workspace/executeCommand": self.initialize_searcher_command_available.set() self.resolve_main_method_available.set() return def lang_status_handler(params): # TODO: Should we wait for # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} # Before proceeding? if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": self.service_ready_event.set() def execute_client_command_handler(params): return [] def do_nothing(params): return def check_experimental_status(params): if params["quiescent"] == True: self.server_ready.set() def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("language/status", lang_status_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) self.logger.log("Starting Clangd server process", logging.INFO) self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) self.logger.log( "Sending initialize request from LSP client to LSP server and awaiting response", logging.INFO, ) init_response = self.server.send.initialize(initialize_params) assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 assert "completionProvider" in init_response["capabilities"] assert init_response["capabilities"]["completionProvider"] == { "triggerCharacters": [".", "<", ">", ":", '"', "/", "*"], "resolveProvider": False, } self.server.notify.initialized({}) self.completions_available.set() # set ready flag self.server_ready.set() self.server_ready.wait() ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/jedi_server.py: -------------------------------------------------------------------------------- ```python """ Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ import logging import os import pathlib from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings class JediServer(SolidLanguageServer): """ Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings ): """ Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path), "python", solidlsp_settings, ) @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Jedi Language Server. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params = { "processId": os.getpid(), "clientInfo": {"name": "Serena", "version": "0.1.0"}, "locale": "en", "rootPath": repository_absolute_path, "rootUri": root_uri, # Note: this is not necessarily the minimal set of capabilities... "capabilities": { "workspace": { "applyEdit": True, "workspaceEdit": { "documentChanges": True, "resourceOperations": ["create", "rename", "delete"], "failureHandling": "textOnlyTransactional", "normalizesLineEndings": True, "changeAnnotationSupport": {"groupsOnLabel": True}, }, "configuration": True, "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True}, "symbol": { "dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}, "tagSupport": {"valueSet": [1]}, "resolveSupport": {"properties": ["location.range"]}, }, "workspaceFolders": True, "fileOperations": { "dynamicRegistration": True, "didCreate": True, "didRename": True, "didDelete": True, "willCreate": True, "willRename": True, "willDelete": True, }, "inlineValue": {"refreshSupport": True}, "inlayHint": {"refreshSupport": True}, "diagnostics": {"refreshSupport": True}, }, "textDocument": { "publishDiagnostics": { "relatedInformation": True, "versionSupport": False, "tagSupport": {"valueSet": [1, 2]}, "codeDescriptionSupport": True, "dataSupport": True, }, "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True}, "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, "signatureHelp": { "dynamicRegistration": True, "signatureInformation": { "documentationFormat": ["markdown", "plaintext"], "parameterInformation": {"labelOffsetSupport": True}, "activeParameterSupport": True, }, "contextSupport": True, }, "definition": {"dynamicRegistration": True, "linkSupport": True}, "references": {"dynamicRegistration": True}, "documentHighlight": {"dynamicRegistration": True}, "documentSymbol": { "dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}, "hierarchicalDocumentSymbolSupport": True, "tagSupport": {"valueSet": [1]}, "labelSupport": True, }, "documentLink": {"dynamicRegistration": True, "tooltipSupport": True}, "typeDefinition": {"dynamicRegistration": True, "linkSupport": True}, "implementation": {"dynamicRegistration": True, "linkSupport": True}, "declaration": {"dynamicRegistration": True, "linkSupport": True}, "selectionRange": {"dynamicRegistration": True}, "callHierarchy": {"dynamicRegistration": True}, "linkedEditingRange": {"dynamicRegistration": True}, "typeHierarchy": {"dynamicRegistration": True}, "inlineValue": {"dynamicRegistration": True}, "inlayHint": { "dynamicRegistration": True, "resolveSupport": {"properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"]}, }, "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False}, }, "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}}, "experimental": { "serverStatusNotification": True, "openServerLogs": True, }, }, # See https://github.com/pappasam/jedi-language-server?tab=readme-ov-file # We use the default options except for maxSymbols, where 0 means no limit "initializationOptions": { "workspace": { "symbols": {"ignoreFolders": [".nox", ".tox", ".venv", "__pycache__", "venv"], "maxSymbols": 0}, }, }, "trace": "verbose", "workspaceFolders": [ { "uri": root_uri, "name": os.path.basename(repository_absolute_path), } ], } return initialize_params def _start_server(self): """ Starts the JEDI Language Server """ def execute_client_command_handler(params): return [] def do_nothing(params): return def check_experimental_status(params): if params["quiescent"] == True: self.completions_available.set() def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) self.server.on_request("client/registerCapability", do_nothing) self.server.on_notification("language/status", do_nothing) self.server.on_notification("window/logMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.server.on_notification("language/actionableNotification", do_nothing) self.server.on_notification("experimental/serverStatus", check_experimental_status) self.logger.log("Starting jedi-language-server server process", logging.INFO) self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) self.logger.log( "Sending initialize request from LSP client to LSP server and awaiting response", logging.INFO, ) init_response = self.server.send.initialize(initialize_params) assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 assert "completionProvider" in init_response["capabilities"] assert init_response["capabilities"]["completionProvider"] == { "triggerCharacters": [".", "'", '"'], "resolveProvider": True, } self.server.notify.initialized({}) ``` -------------------------------------------------------------------------------- /src/serena/resources/dashboard/index.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Serena Dashboard</title> <link rel="icon" type="image/png" sizes="16x16" href="serena-icon-16.png"> <link rel="icon" type="image/png" sizes="32x32" href="serena-icon-32.png"> <link rel="icon" type="image/png" sizes="48x48" href="serena-icon-48.png"> <script src="jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script> <script src="dashboard.js"></script> <style> :root { /* Light theme variables */ --bg-primary: #f5f5f5; --bg-secondary: #ffffff; --text-primary: #000000; --text-secondary: #333333; --text-muted: #666666; --border-color: #ddd; --btn-primary: #eaa45d; --btn-hover: #dca662; --btn-disabled: #6c757d; --shadow: 0 2px 4px rgba(0, 0, 0, 0.1); --tool-highlight: #ffff00; --tool-highlight-text: #000000; --log-debug: #808080; --log-info: #000000; --log-warning: #FF8C00; --log-error: #FF0000; --stats-header: #f8f9fa; } [data-theme="dark"] { /* Dark theme variables */ --bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --text-primary: #ffffff; --text-secondary: #e0e0e0; --text-muted: #b0b0b0; --border-color: #444; --btn-primary: #eaa45d; --btn-hover: #dca662; --btn-disabled: #6c757d; --shadow: 0 2px 4px rgba(0, 0, 0, 0.3); --tool-highlight: #ffd700; --tool-highlight-text: #000000; --log-debug: #808080; --log-info: #ffffff; --log-warning: #FF8C00; --log-error: #FF0000; --stats-header: #3a3a3a; } body { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; margin: 0; padding: 20px; background-color: var(--bg-primary); color: var(--text-primary); transition: background-color 0.3s ease, color 0.3s ease; } .header { text-align: center; margin-bottom: 20px; } .log-container { background-color: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 5px; height: 600px; overflow-y: auto; overflow-x: auto; padding: 10px; white-space: pre-wrap; font-size: 12px; line-height: 1.4; color: var(--text-primary); transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; } .controls { margin-bottom: 10px; text-align: center; display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; } .logo { margin-bottom: 10px; text-align: center; } .btn { background-color: var(--btn-primary); color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.3s ease; } .btn:hover { background-color: var(--btn-hover); } .btn:disabled { background-color: var(--btn-disabled); cursor: not-allowed; } .theme-toggle { display: flex; align-items: center; gap: 5px; background-color: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 4px; padding: 6px 12px; cursor: pointer; transition: background-color 0.3s ease, border-color 0.3s ease; } .theme-toggle:hover { background-color: var(--border-color); } .theme-toggle .icon { font-size: 16px; } .log-debug { color: var(--log-debug); } .log-info { color: var(--log-info); } .log-warning { color: var(--log-warning); } .log-error { color: var(--log-error); } .log-default { color: var(--log-info); } /* Tool name highlighting */ .tool-name { background-color: var(--tool-highlight); color: var(--tool-highlight-text); font-weight: bold; } .loading { text-align: center; color: var(--text-muted); font-style: italic; } .error-message { color: var(--log-error); text-align: center; margin: 10px 0; } .charts-container { display: flex; flex-wrap: wrap; gap: 15px; justify-content: space-between; max-width: 1400px; margin: 0 auto; } .chart-group { flex: 1; min-width: 280px; max-width: 320px; text-align: center; } .chart-wide { flex: 0 0 100%; min-width: 100%; margin-top: 10px; } .chart-group h3 { margin: 0 0 10px 0; color: var(--text-secondary); } .stats-summary { margin: 0 auto; border-collapse: collapse; background: var(--bg-secondary); border-radius: 5px; overflow: hidden; box-shadow: var(--shadow); transition: background-color 0.3s ease, box-shadow 0.3s ease; } .stats-summary th, .stats-summary td { padding: 10px 20px; text-align: left; border-bottom: 1px solid var(--border-color); color: var(--text-primary); transition: border-color 0.3s ease, color 0.3s ease; } .stats-summary th { background-color: var(--stats-header); font-weight: bold; transition: background-color 0.3s ease; } .stats-summary tr:last-child td { border-bottom: none; } @media (max-width: 768px) { .charts-container { flex-direction: column; } .chart-group, .chart-wide { min-width: auto; max-width: none; } .controls { flex-direction: column; gap: 5px; } } </style> </head> <body> <div class="header"> <img id="serena-logo" src="serena-logs.png" alt="Serena" style="max-width: 400px; height: auto;"> </div> <div class="controls"> <button id="load-logs" class="btn">Reload Log</button> <button id="shutdown" class="btn">Shutdown Server</button> <button id="toggle-stats" class="btn">Show Stats</button> <div id="theme-toggle" class="theme-toggle" title="Toggle theme"> <span class="icon" id="theme-icon">🌙</span> <span id="theme-text">Dark</span> </div> </div> <div id="error-container"></div> <div id="log-container" class="log-container"></div> <div id="stats-section" style="display:none; margin-top:20px;"> <div style="text-align:center; margin-bottom:20px;"> <button id="refresh-stats" class="btn">Refresh Stats</button> <button id="clear-stats" class="btn">Clear Stats</button> </div> <div id="stats-summary" style="margin-bottom:20px; text-align:center;"></div> <div id="estimator-name" style="text-align:center; margin-bottom:10px;"></div> <div id="no-stats-message" style="text-align:center; color:var(--text-muted); font-style:italic; display:none;"> No tool stats collected. Have you enabled tool stats collection in the configuration? </div> <div class="charts-container"> <div class="chart-group"> <h3>Tool Calls</h3> <canvas id="count-chart" height="200"></canvas> </div> <div class="chart-group"> <h3>Input Tokens</h3> <canvas id="input-chart" height="200"></canvas> </div> <div class="chart-group"> <h3>Output Tokens</h3> <canvas id="output-chart" height="200"></canvas> </div> <div class="chart-group chart-wide"> <h3>Input vs Output Tokens</h3> <canvas id="tokens-chart" height="120"></canvas> </div> </div> </div> <script> $(document).ready(function () { const dashboard = new Dashboard(); }); </script> </body> </html> ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/terraform_ls.py: -------------------------------------------------------------------------------- ```python import logging import os import shutil import threading from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PathUtils, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings from .common import RuntimeDependency, RuntimeDependencyCollection class TerraformLS(SolidLanguageServer): """ Provides Terraform specific instantiation of the LanguageServer class using terraform-ls. """ @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in [".terraform", "terraform.tfstate.d"] @staticmethod def _ensure_tf_command_available(logger: LanguageServerLogger): logger.log("Starting terraform version detection...", logging.DEBUG) # 1. Try to find terraform using shutil.which terraform_cmd = shutil.which("terraform") if terraform_cmd is not None: logger.log(f"Found terraform via shutil.which: {terraform_cmd}", logging.DEBUG) return # TODO: is this needed? # 2. Fallback to TERRAFORM_CLI_PATH (set by hashicorp/setup-terraform action) if not terraform_cmd: terraform_cli_path = os.environ.get("TERRAFORM_CLI_PATH") if terraform_cli_path: logger.log(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}", logging.DEBUG) # TODO: use binary name from runtime dependencies if we keep this code if os.name == "nt": terraform_binary = os.path.join(terraform_cli_path, "terraform.exe") else: terraform_binary = os.path.join(terraform_cli_path, "terraform") if os.path.exists(terraform_binary): terraform_cmd = terraform_binary logger.log(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}", logging.DEBUG) return raise RuntimeError( "Terraform executable not found, please ensure Terraform is installed." "See https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli for instructions." ) @classmethod def _setup_runtime_dependencies(cls, logger: LanguageServerLogger, solidlsp_settings: SolidLSPSettings) -> str: """ Setup runtime dependencies for terraform-ls. Downloads and installs terraform-ls if not already present. """ cls._ensure_tf_command_available(logger) platform_id = PlatformUtils.get_platform_id() deps = RuntimeDependencyCollection( [ RuntimeDependency( id="TerraformLS", description="terraform-ls for macOS (ARM64)", url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip", platform_id="osx-arm64", archive_type="zip", binary_name="terraform-ls", ), RuntimeDependency( id="TerraformLS", description="terraform-ls for macOS (x64)", url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip", platform_id="osx-x64", archive_type="zip", binary_name="terraform-ls", ), RuntimeDependency( id="TerraformLS", description="terraform-ls for Linux (ARM64)", url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_arm64.zip", platform_id="linux-arm64", archive_type="zip", binary_name="terraform-ls", ), RuntimeDependency( id="TerraformLS", description="terraform-ls for Linux (x64)", url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip", platform_id="linux-x64", archive_type="zip", binary_name="terraform-ls", ), RuntimeDependency( id="TerraformLS", description="terraform-ls for Windows (x64)", url="https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip", platform_id="win-x64", archive_type="zip", binary_name="terraform-ls.exe", ), ] ) dependency = deps.get_single_dep_for_current_platform() terraform_ls_executable_path = deps.binary_path(cls.ls_resources_dir(solidlsp_settings)) if not os.path.exists(terraform_ls_executable_path): logger.log(f"Downloading terraform-ls from {dependency.url}", logging.INFO) deps.install(logger, cls.ls_resources_dir(solidlsp_settings)) assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}" # Make the executable file executable on Unix-like systems if platform_id.value != "win-x64": os.chmod(terraform_ls_executable_path, 0o755) return terraform_ls_executable_path def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings ): """ Creates a TerraformLS instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ terraform_ls_executable_path = self._setup_runtime_dependencies(logger, solidlsp_settings) super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path), "terraform", solidlsp_settings, ) self.server_ready = threading.Event() self.request_id = 0 @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Terraform Language Server. """ root_uri = PathUtils.path_to_uri(repository_absolute_path) return { "processId": os.getpid(), "locale": "en", "rootPath": repository_absolute_path, "rootUri": root_uri, "capabilities": { "textDocument": { "synchronization": {"didSave": True, "dynamicRegistration": True}, "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}}, "definition": {"dynamicRegistration": True}, "documentSymbol": { "dynamicRegistration": True, "hierarchicalDocumentSymbolSupport": True, "symbolKind": {"valueSet": list(range(1, 27))}, }, }, "workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}}, }, "workspaceFolders": [ { "name": os.path.basename(repository_absolute_path), "uri": root_uri, } ], } def _start_server(self): """Start terraform-ls server process""" def register_capability_handler(params): return def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) def do_nothing(params): return self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.logger.log("Starting terraform-ls server process", logging.INFO) self.server.start() initialize_params = self._get_initialize_params(self.repository_root_path) self.logger.log( "Sending initialize request from LSP client to LSP server and awaiting response", logging.INFO, ) init_response = self.server.send.initialize(initialize_params) # Verify server capabilities assert "textDocumentSync" in init_response["capabilities"] assert "completionProvider" in init_response["capabilities"] assert "definitionProvider" in init_response["capabilities"] self.server.notify.initialized({}) self.completions_available.set() # terraform-ls server is typically ready immediately after initialization self.server_ready.set() self.server_ready.wait() ``` -------------------------------------------------------------------------------- /src/solidlsp/language_servers/erlang_language_server.py: -------------------------------------------------------------------------------- ```python """Erlang Language Server implementation using Erlang LS.""" import logging import os import shutil import subprocess import threading import time from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_logger import LanguageServerLogger from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings class ErlangLanguageServer(SolidLanguageServer): """Language server for Erlang using Erlang LS.""" def __init__( self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings, ): """ Creates an ErlangLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ self.erlang_ls_path = shutil.which("erlang_ls") if not self.erlang_ls_path: raise RuntimeError("Erlang LS not found. Install from: https://github.com/erlang-ls/erlang_ls") if not self._check_erlang_installation(): raise RuntimeError("Erlang/OTP not found. Install from: https://www.erlang.org/downloads") super().__init__( config, logger, repository_root_path, ProcessLaunchInfo(cmd=[self.erlang_ls_path, "--transport", "stdio"], cwd=repository_root_path), "erlang", solidlsp_settings, ) # Add server readiness tracking like Elixir self.server_ready = threading.Event() # Set generous timeout for Erlang LS initialization self.set_request_timeout(120.0) def _check_erlang_installation(self) -> bool: """Check if Erlang/OTP is available.""" try: result = subprocess.run(["erl", "-version"], check=False, capture_output=True, text=True, timeout=10) return result.returncode == 0 except (subprocess.SubprocessError, FileNotFoundError): return False @classmethod def _get_erlang_version(cls): """Get the installed Erlang/OTP version or None if not found.""" try: result = subprocess.run(["erl", "-version"], check=False, capture_output=True, text=True, timeout=10) if result.returncode == 0: return result.stderr.strip() # erl -version outputs to stderr except (subprocess.SubprocessError, FileNotFoundError): return None return None @classmethod def _check_rebar3_available(cls) -> bool: """Check if rebar3 build tool is available.""" try: result = subprocess.run(["rebar3", "version"], check=False, capture_output=True, text=True, timeout=10) return result.returncode == 0 except (subprocess.SubprocessError, FileNotFoundError): return False def _start_server(self): """Start Erlang LS server process with proper initialization waiting.""" def register_capability_handler(params): return def window_log_message(msg): """Handle window/logMessage notifications from Erlang LS""" message_text = msg.get("message", "") self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO) # Look for Erlang LS readiness signals # Common patterns: "Started Erlang LS", "initialized", "ready" readiness_signals = [ "Started Erlang LS", "server started", "initialized", "ready to serve requests", "compilation finished", "indexing complete", ] message_lower = message_text.lower() for signal in readiness_signals: if signal.lower() in message_lower: self.logger.log(f"Erlang LS readiness signal detected: {message_text}", logging.INFO) self.server_ready.set() break def do_nothing(params): return def check_server_ready(params): """Handle $/progress notifications from Erlang LS as fallback.""" value = params.get("value", {}) # Check for initialization completion progress if value.get("kind") == "end": message = value.get("message", "") if any(word in message.lower() for word in ["initialized", "ready", "complete"]): self.logger.log("Erlang LS initialization progress completed", logging.INFO) # Set as fallback if no window/logMessage was received if not self.server_ready.is_set(): self.server_ready.set() # Set up notification handlers self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_notification("$/progress", check_server_ready) self.server.on_notification("window/workDoneProgress/create", do_nothing) self.server.on_notification("$/workDoneProgress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) self.logger.log("Starting Erlang LS server process", logging.INFO) self.server.start() # Send initialize request initialize_params = { "processId": None, "rootPath": self.repository_root_path, "rootUri": f"file://{self.repository_root_path}", "capabilities": { "textDocument": { "synchronization": {"didSave": True}, "completion": {"dynamicRegistration": True}, "definition": {"dynamicRegistration": True}, "references": {"dynamicRegistration": True}, "documentSymbol": {"dynamicRegistration": True}, "hover": {"dynamicRegistration": True}, } }, } self.logger.log("Sending initialize request to Erlang LS", logging.INFO) init_response = self.server.send.initialize(initialize_params) # Verify server capabilities if "capabilities" in init_response: self.logger.log(f"Erlang LS capabilities: {list(init_response['capabilities'].keys())}", logging.INFO) self.server.notify.initialized({}) self.completions_available.set() # Wait for Erlang LS to be ready - adjust timeout based on environment is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" is_macos = os.uname().sysname == "Darwin" if hasattr(os, "uname") else False # macOS in CI can be particularly slow for language server startup if is_ci and is_macos: ready_timeout = 240.0 # 4 minutes for macOS CI env_desc = "macOS CI" elif is_ci: ready_timeout = 180.0 # 3 minutes for other CI env_desc = "CI" else: ready_timeout = 60.0 # 1 minute for local env_desc = "local" self.logger.log(f"Waiting up to {ready_timeout} seconds for Erlang LS readiness ({env_desc} environment)...", logging.INFO) if self.server_ready.wait(timeout=ready_timeout): self.logger.log("Erlang LS is ready and available for requests", logging.INFO) # Add settling period for indexing - adjust based on environment settling_time = 15.0 if is_ci else 5.0 self.logger.log(f"Allowing {settling_time} seconds for Erlang LS indexing to complete...", logging.INFO) time.sleep(settling_time) self.logger.log("Erlang LS settling period complete", logging.INFO) else: # Set ready anyway and continue - Erlang LS might not send explicit ready messages self.logger.log( f"Erlang LS readiness timeout reached after {ready_timeout}s, proceeding anyway (common in CI)", logging.WARNING ) self.server_ready.set() # Still give some time for basic initialization even without explicit readiness signal basic_settling_time = 20.0 if is_ci else 10.0 self.logger.log(f"Allowing {basic_settling_time} seconds for basic Erlang LS initialization...", logging.INFO) time.sleep(basic_settling_time) self.logger.log("Basic Erlang LS initialization period complete", logging.INFO) @override def is_ignored_dirname(self, dirname: str) -> bool: # For Erlang projects, we should ignore: # - _build: rebar3 build artifacts # - deps: dependencies # - ebin: compiled beam files # - .rebar3: rebar3 cache # - logs: log files # - node_modules: if the project has JavaScript components return super().is_ignored_dirname(dirname) or dirname in [ "_build", "deps", "ebin", ".rebar3", "logs", "node_modules", "_checkouts", "cover", ] def is_ignored_filename(self, filename: str) -> bool: """Check if a filename should be ignored.""" # Ignore compiled BEAM files if filename.endswith(".beam"): return True # Don't ignore Erlang source files, header files, or configuration files return False ```