#
tokens: 43192/50000 4/291 files (page 11/14)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 11 of 14. Use http://codebase.md/oraios/serena?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .devcontainer
│   └── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── config.yml
│   │   ├── feature_request.md
│   │   └── issue--bug--performance-problem--question-.md
│   └── workflows
│       ├── codespell.yml
│       ├── docker.yml
│       ├── 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
│       │   ├── 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
│   │       ├── 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
│       ├── 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

--------------------------------------------------------------------------------
/test/solidlsp/ruby/test_ruby_symbol_retrieval.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the Ruby language server symbol-related functionality.
  3 | 
  4 | These tests focus on the following methods:
  5 | - request_containing_symbol
  6 | - request_referencing_symbols
  7 | - request_defining_symbol
  8 | - request_document_symbols integration
  9 | """
 10 | 
 11 | import os
 12 | 
 13 | import pytest
 14 | 
 15 | from solidlsp import SolidLanguageServer
 16 | from solidlsp.ls_config import Language
 17 | from solidlsp.ls_types import SymbolKind
 18 | 
 19 | pytestmark = pytest.mark.ruby
 20 | 
 21 | 
 22 | class TestRubyLanguageServerSymbols:
 23 |     """Test the Ruby language server's symbol-related functionality."""
 24 | 
 25 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
 26 |     def test_request_containing_symbol_method(self, language_server: SolidLanguageServer) -> None:
 27 |         """Test request_containing_symbol for a method."""
 28 |         # Test for a position inside the create_user method
 29 |         file_path = os.path.join("services.rb")
 30 |         # Look for a position inside the create_user method body
 31 |         containing_symbol = language_server.request_containing_symbol(file_path, 11, 10, include_body=True)
 32 | 
 33 |         # Verify that we found the containing symbol
 34 |         assert containing_symbol is not None, "Should find containing symbol for method position"
 35 |         assert containing_symbol["name"] == "create_user", f"Expected 'create_user', got '{containing_symbol['name']}'"
 36 |         assert (
 37 |             containing_symbol["kind"] == SymbolKind.Method.value
 38 |         ), f"Expected Method kind ({SymbolKind.Method.value}), got {containing_symbol['kind']}"
 39 | 
 40 |         # Verify location information
 41 |         assert "location" in containing_symbol, "Containing symbol should have location information"
 42 |         location = containing_symbol["location"]
 43 |         assert "range" in location, "Location should contain range information"
 44 |         assert "start" in location["range"], "Range should have start position"
 45 |         assert "end" in location["range"], "Range should have end position"
 46 | 
 47 |         # Verify container information
 48 |         if "containerName" in containing_symbol:
 49 |             assert containing_symbol["containerName"] in [
 50 |                 "Services::UserService",
 51 |                 "UserService",
 52 |             ], f"Expected UserService container, got '{containing_symbol['containerName']}'"
 53 | 
 54 |         # Verify body content if available
 55 |         if "body" in containing_symbol:
 56 |             body = containing_symbol["body"]
 57 |             assert "def create_user" in body, "Method body should contain method definition"
 58 |             assert len(body.strip()) > 0, "Method body should not be empty"
 59 | 
 60 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
 61 |     def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
 62 |         """Test request_containing_symbol for a class."""
 63 |         # Test for a position inside the UserService class but outside any method
 64 |         file_path = os.path.join("services.rb")
 65 |         # Line around the class definition
 66 |         containing_symbol = language_server.request_containing_symbol(file_path, 5, 5)
 67 | 
 68 |         # Verify that we found the containing symbol
 69 |         assert containing_symbol is not None, "Should find containing symbol for class position"
 70 |         assert containing_symbol["name"] == "UserService", f"Expected 'UserService', got '{containing_symbol['name']}'"
 71 |         assert (
 72 |             containing_symbol["kind"] == SymbolKind.Class.value
 73 |         ), f"Expected Class kind ({SymbolKind.Class.value}), got {containing_symbol['kind']}"
 74 | 
 75 |         # Verify location information exists
 76 |         assert "location" in containing_symbol, "Class symbol should have location information"
 77 |         location = containing_symbol["location"]
 78 |         assert "range" in location, "Location should contain range"
 79 |         assert "start" in location["range"] and "end" in location["range"], "Range should have start and end positions"
 80 | 
 81 |         # Verify the class is properly nested in the Services module
 82 |         if "containerName" in containing_symbol:
 83 |             assert (
 84 |                 containing_symbol["containerName"] == "Services"
 85 |             ), f"Expected 'Services' as container, got '{containing_symbol['containerName']}'"
 86 | 
 87 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
 88 |     def test_request_containing_symbol_module(self, language_server: SolidLanguageServer) -> None:
 89 |         """Test request_containing_symbol for a module context."""
 90 |         # Test that we can find the Services module in document symbols
 91 |         file_path = os.path.join("services.rb")
 92 |         symbols, _roots = language_server.request_document_symbols(file_path)
 93 | 
 94 |         # Verify Services module appears in document symbols
 95 |         services_module = None
 96 |         for symbol in symbols:
 97 |             if symbol.get("name") == "Services" and symbol.get("kind") == SymbolKind.Module:
 98 |                 services_module = symbol
 99 |                 break
100 | 
101 |         assert services_module is not None, "Services module not found in document symbols"
102 | 
103 |         # Test that UserService class has Services as container
104 |         # Position inside UserService class
105 |         containing_symbol = language_server.request_containing_symbol(file_path, 4, 8)
106 |         assert containing_symbol is not None
107 |         assert containing_symbol["name"] == "UserService"
108 |         assert containing_symbol["kind"] == SymbolKind.Class
109 |         # Verify the module context is preserved in containerName (if supported by the language server)
110 |         # ruby-lsp doesn't provide containerName, but Solargraph does
111 |         if "containerName" in containing_symbol:
112 |             assert containing_symbol.get("containerName") == "Services"
113 | 
114 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
115 |     def test_request_containing_symbol_nested_class(self, language_server: SolidLanguageServer) -> None:
116 |         """Test request_containing_symbol with nested classes."""
117 |         # Test for a position inside a nested class method
118 |         file_path = os.path.join("nested.rb")
119 |         # Position inside NestedClass.find_me method
120 |         containing_symbol = language_server.request_containing_symbol(file_path, 20, 10)
121 | 
122 |         # Verify that we found the innermost containing symbol
123 |         assert containing_symbol is not None
124 |         assert containing_symbol["name"] == "find_me"
125 |         assert containing_symbol["kind"] == SymbolKind.Method
126 | 
127 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
128 |     def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None:
129 |         """Test request_containing_symbol for a position with no containing symbol."""
130 |         # Test for a position outside any class/method (e.g., in requires)
131 |         file_path = os.path.join("services.rb")
132 |         # Line 1 is a require statement, not inside any class or method
133 |         containing_symbol = language_server.request_containing_symbol(file_path, 1, 5)
134 | 
135 |         # Should return None or an empty dictionary
136 |         assert containing_symbol is None or containing_symbol == {}
137 | 
138 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
139 |     def test_request_referencing_symbols_method(self, language_server: SolidLanguageServer) -> None:
140 |         """Test request_referencing_symbols for a method."""
141 |         # Test referencing symbols for create_user method
142 |         file_path = os.path.join("services.rb")
143 |         # Line containing the create_user method definition
144 |         symbols, _roots = language_server.request_document_symbols(file_path)
145 |         create_user_symbol = None
146 | 
147 |         # Find create_user method in the document symbols (Ruby returns flat list)
148 |         for symbol in symbols:
149 |             if symbol.get("name") == "create_user":
150 |                 create_user_symbol = symbol
151 |                 break
152 | 
153 |         if not create_user_symbol or "selectionRange" not in create_user_symbol:
154 |             pytest.skip("create_user symbol or its selectionRange not found")
155 | 
156 |         sel_start = create_user_symbol["selectionRange"]["start"]
157 |         ref_symbols = [
158 |             ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
159 |         ]
160 | 
161 |         # We might not have references in our simple test setup, so just verify structure
162 |         for symbol in ref_symbols:
163 |             assert "name" in symbol
164 |             assert "kind" in symbol
165 | 
166 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
167 |     def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None:
168 |         """Test request_referencing_symbols for a class."""
169 |         # Test referencing symbols for User class
170 |         file_path = os.path.join("models.rb")
171 |         # Find User class in document symbols
172 |         symbols, _roots = language_server.request_document_symbols(file_path)
173 |         user_symbol = None
174 | 
175 |         for symbol in symbols:
176 |             if symbol.get("name") == "User":
177 |                 user_symbol = symbol
178 |                 break
179 | 
180 |         if not user_symbol or "selectionRange" not in user_symbol:
181 |             pytest.skip("User symbol or its selectionRange not found")
182 | 
183 |         sel_start = user_symbol["selectionRange"]["start"]
184 |         ref_symbols = [
185 |             ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
186 |         ]
187 | 
188 |         # Verify structure of referencing symbols
189 |         for symbol in ref_symbols:
190 |             assert "name" in symbol
191 |             assert "kind" in symbol
192 |             if "location" in symbol and "range" in symbol["location"]:
193 |                 assert "start" in symbol["location"]["range"]
194 |                 assert "end" in symbol["location"]["range"]
195 | 
196 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
197 |     def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
198 |         """Test request_defining_symbol for a variable usage."""
199 |         # Test finding the definition of a variable in a method
200 |         file_path = os.path.join("services.rb")
201 |         # Look for @users variable usage
202 |         defining_symbol = language_server.request_defining_symbol(file_path, 12, 10)
203 | 
204 |         # This test might fail if the language server doesn't support it well
205 |         if defining_symbol is not None:
206 |             assert "name" in defining_symbol
207 |             assert "kind" in defining_symbol
208 | 
209 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
210 |     def test_request_defining_symbol_class(self, language_server: SolidLanguageServer) -> None:
211 |         """Test request_defining_symbol for a class reference."""
212 |         # Test finding the definition of the User class used in services
213 |         file_path = os.path.join("services.rb")
214 |         # Line that references User class
215 |         defining_symbol = language_server.request_defining_symbol(file_path, 11, 15)
216 | 
217 |         # This might not work perfectly in all Ruby language servers
218 |         if defining_symbol is not None:
219 |             assert "name" in defining_symbol
220 |             # The name might be "User" or the method that contains it
221 |             assert defining_symbol.get("name") is not None
222 | 
223 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
224 |     def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
225 |         """Test request_defining_symbol for a position with no symbol."""
226 |         # Test for a position with no symbol (e.g., whitespace or comment)
227 |         file_path = os.path.join("services.rb")
228 |         # Line 3 is likely a blank line or comment
229 |         defining_symbol = language_server.request_defining_symbol(file_path, 3, 0)
230 | 
231 |         # Should return None for positions with no symbol
232 |         assert defining_symbol is None or defining_symbol == {}
233 | 
234 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
235 |     def test_request_defining_symbol_nested_class(self, language_server: SolidLanguageServer) -> None:
236 |         """Test request_defining_symbol for nested class access."""
237 |         # Test finding definition of NestedClass
238 |         file_path = os.path.join("nested.rb")
239 |         # Position where NestedClass is referenced
240 |         defining_symbol = language_server.request_defining_symbol(file_path, 44, 25)
241 | 
242 |         # This is challenging for many language servers
243 |         if defining_symbol is not None:
244 |             assert "name" in defining_symbol
245 |             assert defining_symbol.get("name") in ["NestedClass", "OuterClass"]
246 | 
247 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
248 |     def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
249 |         """Test the integration between different symbol-related methods."""
250 |         file_path = os.path.join("models.rb")
251 | 
252 |         # Step 1: Find a method we know exists
253 |         containing_symbol = language_server.request_containing_symbol(file_path, 8, 5)  # inside initialize method
254 |         if containing_symbol is not None:
255 |             assert containing_symbol["name"] == "initialize"
256 | 
257 |             # Step 2: Get the defining symbol for the same position
258 |             defining_symbol = language_server.request_defining_symbol(file_path, 8, 5)
259 |             if defining_symbol is not None:
260 |                 assert defining_symbol["name"] == "initialize"
261 | 
262 |                 # Step 3: Verify that they refer to the same symbol type
263 |                 assert defining_symbol["kind"] == containing_symbol["kind"]
264 | 
265 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
266 |     def test_symbol_tree_structure_basic(self, language_server: SolidLanguageServer) -> None:
267 |         """Test that the symbol tree structure includes Ruby symbols."""
268 |         # Get all symbols in the test repository
269 |         repo_structure = language_server.request_full_symbol_tree()
270 |         assert len(repo_structure) >= 1
271 | 
272 |         # Look for our Ruby files in the structure
273 |         found_ruby_files = False
274 |         for root in repo_structure:
275 |             if "children" in root:
276 |                 for child in root["children"]:
277 |                     if child.get("name") in ["models", "services", "nested"]:
278 |                         found_ruby_files = True
279 |                         break
280 | 
281 |         # We should find at least some Ruby files in the symbol tree
282 |         assert found_ruby_files, "Ruby files not found in symbol tree"
283 | 
284 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
285 |     def test_document_symbols_detailed(self, language_server: SolidLanguageServer) -> None:
286 |         """Test document symbols for detailed Ruby file structure."""
287 |         file_path = os.path.join("models.rb")
288 |         symbols, roots = language_server.request_document_symbols(file_path)
289 | 
290 |         # Verify we have symbols
291 |         assert len(symbols) > 0 or len(roots) > 0
292 | 
293 |         # Look for expected class names
294 |         symbol_names = set()
295 |         all_symbols = symbols if symbols else roots
296 | 
297 |         for symbol in all_symbols:
298 |             symbol_names.add(symbol.get("name"))
299 |             # Add children names too
300 |             if "children" in symbol:
301 |                 for child in symbol["children"]:
302 |                     symbol_names.add(child.get("name"))
303 | 
304 |         # We should find at least some of our defined classes/methods
305 |         expected_symbols = {"User", "Item", "Order", "ItemHelpers"}
306 |         found_symbols = symbol_names.intersection(expected_symbols)
307 |         assert len(found_symbols) > 0, f"Expected symbols not found. Found: {symbol_names}"
308 | 
309 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
310 |     def test_module_and_class_hierarchy(self, language_server: SolidLanguageServer) -> None:
311 |         """Test symbol detection for modules and nested class hierarchies."""
312 |         file_path = os.path.join("nested.rb")
313 |         symbols, roots = language_server.request_document_symbols(file_path)
314 | 
315 |         # Verify we can detect the nested structure
316 |         assert len(symbols) > 0 or len(roots) > 0
317 | 
318 |         # Look for OuterClass and its nested elements
319 |         symbol_names = set()
320 |         all_symbols = symbols if symbols else roots
321 | 
322 |         for symbol in all_symbols:
323 |             symbol_names.add(symbol.get("name"))
324 |             if "children" in symbol:
325 |                 for child in symbol["children"]:
326 |                     symbol_names.add(child.get("name"))
327 |                     # Check deeply nested too
328 |                     if "children" in child:
329 |                         for grandchild in child["children"]:
330 |                             symbol_names.add(grandchild.get("name"))
331 | 
332 |         # Should find the outer class at minimum
333 |         assert "OuterClass" in symbol_names, f"OuterClass not found in symbols: {symbol_names}"
334 | 
335 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
336 |     def test_references_to_variables(self, language_server: SolidLanguageServer) -> None:
337 |         """Test request_referencing_symbols for a variable with detailed verification."""
338 |         file_path = os.path.join("variables.rb")
339 |         # Test references to @status variable in DataContainer class (around line 9)
340 |         ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 8, 4)]
341 | 
342 |         if len(ref_symbols) > 0:
343 |             # Verify we have references
344 |             assert len(ref_symbols) > 0, "Should find references to @status variable"
345 | 
346 |             # Check that we have location information
347 |             ref_with_locations = [ref for ref in ref_symbols if "location" in ref and "range" in ref["location"]]
348 |             assert len(ref_with_locations) > 0, "References should include location information"
349 | 
350 |             # Verify line numbers are reasonable (should be within the file)
351 |             ref_lines = [ref["location"]["range"]["start"]["line"] for ref in ref_with_locations]
352 |             assert all(line >= 0 for line in ref_lines), "Reference lines should be valid"
353 | 
354 |             # Check for specific reference locations we expect
355 |             # Lines where @status is modified/accessed
356 |             expected_line_ranges = [(20, 40), (45, 70)]  # Approximate ranges
357 |             found_in_expected_range = any(any(start <= line <= end for start, end in expected_line_ranges) for line in ref_lines)
358 |             assert found_in_expected_range, f"Expected references in ranges {expected_line_ranges}, found lines: {ref_lines}"
359 | 
360 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
361 |     def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None:
362 |         """Test request_referencing_symbols for a method parameter."""
363 |         # Test referencing symbols for a method parameter in get_user method
364 |         file_path = os.path.join("services.rb")
365 |         # Find get_user method and test parameter references
366 |         symbols, _roots = language_server.request_document_symbols(file_path)
367 |         get_user_symbol = None
368 | 
369 |         for symbol in symbols:
370 |             if symbol.get("name") == "get_user":
371 |                 get_user_symbol = symbol
372 |                 break
373 | 
374 |         if not get_user_symbol or "selectionRange" not in get_user_symbol:
375 |             pytest.skip("get_user symbol or its selectionRange not found")
376 | 
377 |         # Test parameter reference within method body
378 |         method_start_line = get_user_symbol["selectionRange"]["start"]["line"]
379 |         ref_symbols = [
380 |             ref.symbol
381 |             for ref in language_server.request_referencing_symbols(file_path, method_start_line + 1, 10)  # Position within method body
382 |         ]
383 | 
384 |         # Verify structure of referencing symbols
385 |         for symbol in ref_symbols:
386 |             assert "name" in symbol, "Symbol should have name"
387 |             assert "kind" in symbol, "Symbol should have kind"
388 |             if "location" in symbol and "range" in symbol["location"]:
389 |                 range_info = symbol["location"]["range"]
390 |                 assert "start" in range_info, "Range should have start"
391 |                 assert "end" in range_info, "Range should have end"
392 |                 # Verify line number is valid (references can be before method definition too)
393 |                 assert range_info["start"]["line"] >= 0, "Reference line should be valid"
394 | 
395 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
396 |     def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None:
397 |         """Test request_referencing_symbols for a position with no symbol."""
398 |         # Test for a position with no symbol (comment or blank line)
399 |         file_path = os.path.join("services.rb")
400 | 
401 |         # Try multiple positions that should have no symbols
402 |         test_positions = [(1, 0), (2, 0)]  # Comment/require lines
403 | 
404 |         for line, char in test_positions:
405 |             try:
406 |                 ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, line, char)]
407 |                 # If we get here, make sure we got an empty result or minimal results
408 |                 if ref_symbols:
409 |                     # Some language servers might return minimal info, verify it's reasonable
410 |                     assert len(ref_symbols) <= 3, f"Expected few/no references at line {line}, got {len(ref_symbols)}"
411 | 
412 |             except Exception as e:
413 |                 # Some language servers throw exceptions for invalid positions, which is acceptable
414 |                 assert (
415 |                     "symbol" in str(e).lower() or "position" in str(e).lower() or "reference" in str(e).lower()
416 |                 ), f"Exception should be related to symbol/position/reference issues, got: {e}"
417 | 
418 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
419 |     def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
420 |         """Test that request_dir_overview returns correct symbol information for files in a directory."""
421 |         # Get overview of the test repo directory
422 |         overview = language_server.request_dir_overview(".")
423 | 
424 |         # Verify that we have entries for our main files
425 |         expected_files = ["services.rb", "models.rb", "variables.rb", "nested.rb"]
426 |         found_files = []
427 | 
428 |         for file_path in overview.keys():
429 |             for expected in expected_files:
430 |                 if expected in file_path:
431 |                     found_files.append(expected)
432 |                     break
433 | 
434 |         assert len(found_files) >= 2, f"Should find at least 2 expected files, found: {found_files}"
435 | 
436 |         # Test specific symbols from services.rb if it exists
437 |         services_file_key = None
438 |         for file_path in overview.keys():
439 |             if "services.rb" in file_path:
440 |                 services_file_key = file_path
441 |                 break
442 | 
443 |         if services_file_key:
444 |             services_symbols = overview[services_file_key]
445 |             assert len(services_symbols) > 0, "services.rb should have symbols"
446 | 
447 |             # Check for expected symbols with detailed verification
448 |             symbol_names = [s[0] for s in services_symbols if isinstance(s, tuple) and len(s) > 0]
449 |             if not symbol_names:  # If not tuples, try different format
450 |                 symbol_names = [s.get("name") for s in services_symbols if hasattr(s, "get")]
451 | 
452 |             expected_symbols = ["Services", "UserService", "ItemService"]
453 |             found_expected = [name for name in expected_symbols if name in symbol_names]
454 |             assert len(found_expected) >= 1, f"Should find at least one expected symbol, found: {found_expected} in {symbol_names}"
455 | 
456 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
457 |     def test_request_document_overview(self, language_server: SolidLanguageServer) -> None:
458 |         """Test that request_document_overview returns correct symbol information for a file."""
459 |         # Get overview of the user_management.rb file
460 |         file_path = os.path.join("examples", "user_management.rb")
461 |         overview = language_server.request_document_overview(file_path)
462 | 
463 |         # Verify that we have symbol information
464 |         assert len(overview) > 0, "Document overview should contain symbols"
465 | 
466 |         # Look for expected symbols from the file
467 |         symbol_names = set()
468 |         for s_info in overview:
469 |             if isinstance(s_info, tuple) and len(s_info) > 0:
470 |                 symbol_names.add(s_info[0])
471 |             elif hasattr(s_info, "get"):
472 |                 symbol_names.add(s_info.get("name"))
473 |             elif isinstance(s_info, str):
474 |                 symbol_names.add(s_info)
475 | 
476 |         # We should find some of our defined classes/methods
477 |         expected_symbols = {"UserStats", "UserManager", "process_user_data", "main"}
478 |         found_symbols = symbol_names.intersection(expected_symbols)
479 |         assert len(found_symbols) > 0, f"Expected to find some symbols from {expected_symbols}, found: {symbol_names}"
480 | 
481 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
482 |     def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None:
483 |         """Test request_containing_symbol where the target is a variable."""
484 |         # Test for a position inside a variable definition or usage
485 |         file_path = os.path.join("variables.rb")
486 |         # Position around a variable assignment (e.g., @status = "pending")
487 |         containing_symbol = language_server.request_containing_symbol(file_path, 10, 5)
488 | 
489 |         # Verify that we found a containing symbol (likely the method or class)
490 |         if containing_symbol is not None:
491 |             assert "name" in containing_symbol, "Containing symbol should have a name"
492 |             assert "kind" in containing_symbol, "Containing symbol should have a kind"
493 |             # The containing symbol should be a method, class, or similar construct
494 |             expected_kinds = [SymbolKind.Method, SymbolKind.Class, SymbolKind.Function, SymbolKind.Constructor]
495 |             assert containing_symbol["kind"] in [
496 |                 k.value for k in expected_kinds
497 |             ], f"Expected containing symbol to be method/class/function, got kind: {containing_symbol['kind']}"
498 | 
499 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
500 |     def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
501 |         """Test request_containing_symbol for a function (not method)."""
502 |         # Test for a position inside a standalone function
503 |         file_path = os.path.join("variables.rb")
504 |         # Position inside the demonstrate_variable_usage function
505 |         containing_symbol = language_server.request_containing_symbol(file_path, 100, 10)
506 | 
507 |         if containing_symbol is not None:
508 |             assert containing_symbol["name"] in [
509 |                 "demonstrate_variable_usage",
510 |                 "main",
511 |             ], f"Expected function name, got: {containing_symbol['name']}"
512 |             assert containing_symbol["kind"] in [
513 |                 SymbolKind.Function.value,
514 |                 SymbolKind.Method.value,
515 |             ], f"Expected function or method kind, got: {containing_symbol['kind']}"
516 | 
517 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
518 |     def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
519 |         """Test request_containing_symbol with nested scopes."""
520 |         # Test for a position inside a method which is inside a class
521 |         file_path = os.path.join("services.rb")
522 |         # Position inside create_user method within UserService class
523 |         containing_symbol = language_server.request_containing_symbol(file_path, 12, 15)
524 | 
525 |         # Verify that we found the innermost containing symbol (the method)
526 |         assert containing_symbol is not None
527 |         assert containing_symbol["name"] == "create_user"
528 |         assert containing_symbol["kind"] == SymbolKind.Method
529 | 
530 |         # Verify the container context is preserved
531 |         if "containerName" in containing_symbol:
532 |             assert "UserService" in containing_symbol["containerName"]
533 | 
534 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
535 |     def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None:
536 |         """Test that the symbol tree structure correctly handles subdirectories."""
537 |         # Get symbols within the examples subdirectory
538 |         examples_structure = language_server.request_full_symbol_tree(within_relative_path="examples")
539 | 
540 |         if len(examples_structure) > 0:
541 |             # Should find the examples directory structure
542 |             assert len(examples_structure) >= 1, "Should find examples directory structure"
543 | 
544 |             # Look for the user_management file in the structure
545 |             found_user_management = False
546 |             for root in examples_structure:
547 |                 if "children" in root:
548 |                     for child in root["children"]:
549 |                         if "user_management" in child.get("name", ""):
550 |                             found_user_management = True
551 |                             # Verify the structure includes symbol information
552 |                             if "children" in child:
553 |                                 child_names = [c.get("name") for c in child["children"]]
554 |                                 expected_names = ["UserStats", "UserManager", "process_user_data"]
555 |                                 found_expected = [name for name in expected_names if name in child_names]
556 |                                 assert (
557 |                                     len(found_expected) > 0
558 |                                 ), f"Should find symbols in user_management, expected {expected_names}, found {child_names}"
559 |                             break
560 | 
561 |             if not found_user_management:
562 |                 pytest.skip("user_management file not found in examples subdirectory structure")
563 | 
564 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
565 |     def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
566 |         """Test request_defining_symbol for an imported/required class."""
567 |         # Test finding the definition of a class used from another file
568 |         file_path = os.path.join("examples", "user_management.rb")
569 |         # Position where Services::UserService is referenced
570 |         defining_symbol = language_server.request_defining_symbol(file_path, 25, 20)
571 | 
572 |         # This might not work perfectly in all Ruby language servers due to require complexity
573 |         if defining_symbol is not None:
574 |             assert "name" in defining_symbol
575 |             # The defining symbol should relate to UserService or Services
576 |             # The defining symbol should relate to UserService, Services, or the containing class
577 |             # Different language servers may resolve this differently
578 |             expected_names = ["UserService", "Services", "new", "UserManager"]
579 |             assert defining_symbol.get("name") in expected_names, f"Expected one of {expected_names}, got: {defining_symbol.get('name')}"
580 | 
581 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
582 |     def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None:
583 |         """Test request_defining_symbol for a method call."""
584 |         # Test finding the definition of a method being called
585 |         file_path = os.path.join("examples", "user_management.rb")
586 |         # Position at a method call like create_user
587 |         defining_symbol = language_server.request_defining_symbol(file_path, 30, 15)
588 | 
589 |         # Verify that we can find method definitions
590 |         if defining_symbol is not None:
591 |             assert "name" in defining_symbol
592 |             assert "kind" in defining_symbol
593 |             # Should be a method or constructor
594 |             assert defining_symbol.get("kind") in [SymbolKind.Method.value, SymbolKind.Constructor.value, SymbolKind.Function.value]
595 | 
596 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
597 |     def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None:
598 |         """Test request_defining_symbol for a nested function or block."""
599 |         # Test finding definition within nested contexts
600 |         file_path = os.path.join("nested.rb")
601 |         # Position inside or referencing nested functionality
602 |         defining_symbol = language_server.request_defining_symbol(file_path, 15, 10)
603 | 
604 |         # This is challenging for many language servers
605 |         if defining_symbol is not None:
606 |             assert "name" in defining_symbol
607 |             assert "kind" in defining_symbol
608 |             # Could be method, function, or variable depending on implementation
609 |             valid_kinds = [SymbolKind.Method.value, SymbolKind.Function.value, SymbolKind.Variable.value, SymbolKind.Class.value]
610 |             assert defining_symbol.get("kind") in valid_kinds
611 | 
612 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
613 |     def test_containing_symbol_of_var_is_file(self, language_server: SolidLanguageServer) -> None:
614 |         """Test that the containing symbol of a file-level variable is handled appropriately."""
615 |         # Test behavior with file-level variables or constants
616 |         file_path = os.path.join("variables.rb")
617 |         # Position at file-level variable/constant
618 |         containing_symbol = language_server.request_containing_symbol(file_path, 5, 5)
619 | 
620 |         # Different language servers handle file-level symbols differently
621 |         # Some return None, others return file-level containers
622 |         if containing_symbol is not None:
623 |             # If we get a symbol, verify its structure
624 |             assert "name" in containing_symbol
625 |             assert "kind" in containing_symbol
626 | 
```

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

```python
  1 | """
  2 | Provides Java specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Java.
  3 | """
  4 | 
  5 | import dataclasses
  6 | import logging
  7 | import os
  8 | import pathlib
  9 | import shutil
 10 | import threading
 11 | import uuid
 12 | from pathlib import PurePath
 13 | 
 14 | from overrides import override
 15 | 
 16 | from solidlsp.ls import SolidLanguageServer
 17 | from solidlsp.ls_config import LanguageServerConfig
 18 | from solidlsp.ls_logger import LanguageServerLogger
 19 | from solidlsp.ls_utils import FileUtils, PlatformUtils
 20 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 21 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 22 | from solidlsp.settings import SolidLSPSettings
 23 | 
 24 | 
 25 | @dataclasses.dataclass
 26 | class RuntimeDependencyPaths:
 27 |     """
 28 |     Stores the paths to the runtime dependencies of EclipseJDTLS
 29 |     """
 30 | 
 31 |     gradle_path: str
 32 |     lombok_jar_path: str
 33 |     jre_path: str
 34 |     jre_home_path: str
 35 |     jdtls_launcher_jar_path: str
 36 |     jdtls_readonly_config_path: str
 37 |     intellicode_jar_path: str
 38 |     intellisense_members_path: str
 39 | 
 40 | 
 41 | class EclipseJDTLS(SolidLanguageServer):
 42 |     """
 43 |     The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class
 44 |     """
 45 | 
 46 |     def __init__(
 47 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 48 |     ):
 49 |         """
 50 |         Creates a new EclipseJDTLS instance initializing the language server settings appropriately.
 51 |         This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 52 |         """
 53 |         runtime_dependency_paths = self._setupRuntimeDependencies(logger, config, solidlsp_settings)
 54 |         self.runtime_dependency_paths = runtime_dependency_paths
 55 | 
 56 |         # ws_dir is the workspace directory for the EclipseJDTLS server
 57 |         ws_dir = str(
 58 |             PurePath(
 59 |                 solidlsp_settings.ls_resources_dir,
 60 |                 "EclipseJDTLS",
 61 |                 "workspaces",
 62 |                 uuid.uuid4().hex,
 63 |             )
 64 |         )
 65 | 
 66 |         # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces
 67 |         shared_cache_location = str(PurePath(solidlsp_settings.ls_resources_dir, "lsp", "EclipseJDTLS", "sharedIndex"))
 68 |         os.makedirs(shared_cache_location, exist_ok=True)
 69 |         os.makedirs(ws_dir, exist_ok=True)
 70 | 
 71 |         jre_path = self.runtime_dependency_paths.jre_path
 72 |         lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path
 73 | 
 74 |         jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path
 75 | 
 76 |         data_dir = str(PurePath(ws_dir, "data_dir"))
 77 |         jdtls_config_path = str(PurePath(ws_dir, "config_path"))
 78 | 
 79 |         jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path
 80 | 
 81 |         if not os.path.exists(jdtls_config_path):
 82 |             shutil.copytree(jdtls_readonly_config_path, jdtls_config_path)
 83 | 
 84 |         for static_path in [
 85 |             jre_path,
 86 |             lombok_jar_path,
 87 |             jdtls_launcher_jar,
 88 |             jdtls_config_path,
 89 |             jdtls_readonly_config_path,
 90 |         ]:
 91 |             assert os.path.exists(static_path), static_path
 92 | 
 93 |         # TODO: Add "self.runtime_dependency_paths.jre_home_path"/bin to $PATH as well
 94 |         proc_env = {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path}
 95 |         proc_cwd = repository_root_path
 96 |         cmd = " ".join(
 97 |             [
 98 |                 jre_path,
 99 |                 "--add-modules=ALL-SYSTEM",
100 |                 "--add-opens",
101 |                 "java.base/java.util=ALL-UNNAMED",
102 |                 "--add-opens",
103 |                 "java.base/java.lang=ALL-UNNAMED",
104 |                 "--add-opens",
105 |                 "java.base/sun.nio.fs=ALL-UNNAMED",
106 |                 "-Declipse.application=org.eclipse.jdt.ls.core.id1",
107 |                 "-Dosgi.bundles.defaultStartLevel=4",
108 |                 "-Declipse.product=org.eclipse.jdt.ls.core.product",
109 |                 "-Djava.import.generatesMetadataFilesAtProjectRoot=false",
110 |                 "-Dfile.encoding=utf8",
111 |                 "-noverify",
112 |                 "-XX:+UseParallelGC",
113 |                 "-XX:GCTimeRatio=4",
114 |                 "-XX:AdaptiveSizePolicyWeight=90",
115 |                 "-Dsun.zip.disableMemoryMapping=true",
116 |                 "-Djava.lsp.joinOnCompletion=true",
117 |                 "-Xmx3G",
118 |                 "-Xms100m",
119 |                 "-Xlog:disable",
120 |                 "-Dlog.level=ALL",
121 |                 f'"-javaagent:{lombok_jar_path}"',
122 |                 f'"-Djdt.core.sharedIndexLocation={shared_cache_location}"',
123 |                 "-jar",
124 |                 f'"{jdtls_launcher_jar}"',
125 |                 "-configuration",
126 |                 f'"{jdtls_config_path}"',
127 |                 "-data",
128 |                 f'"{data_dir}"',
129 |             ]
130 |         )
131 | 
132 |         self.service_ready_event = threading.Event()
133 |         self.intellicode_enable_command_available = threading.Event()
134 |         self.initialize_searcher_command_available = threading.Event()
135 | 
136 |         super().__init__(
137 |             config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java", solidlsp_settings=solidlsp_settings
138 |         )
139 | 
140 |     @override
141 |     def is_ignored_dirname(self, dirname: str) -> bool:
142 |         # Ignore common Java build directories from different build tools:
143 |         # - Maven: target
144 |         # - Gradle: build, .gradle
145 |         # - Eclipse: bin, .settings
146 |         # - IntelliJ IDEA: out, .idea
147 |         # - General: classes, dist, lib
148 |         return super().is_ignored_dirname(dirname) or dirname in [
149 |             "target",  # Maven
150 |             "build",  # Gradle
151 |             "bin",  # Eclipse
152 |             "out",  # IntelliJ IDEA
153 |             "classes",  # General
154 |             "dist",  # General
155 |             "lib",  # General
156 |         ]
157 | 
158 |     @classmethod
159 |     def _setupRuntimeDependencies(
160 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
161 |     ) -> RuntimeDependencyPaths:
162 |         """
163 |         Setup runtime dependencies for EclipseJDTLS and return the paths.
164 |         """
165 |         platformId = PlatformUtils.get_platform_id()
166 | 
167 |         runtime_dependencies = {
168 |             "gradle": {
169 |                 "platform-agnostic": {
170 |                     "url": "https://services.gradle.org/distributions/gradle-8.14.2-bin.zip",
171 |                     "archiveType": "zip",
172 |                     "relative_extraction_path": ".",
173 |                 }
174 |             },
175 |             "vscode-java": {
176 |                 "darwin-arm64": {
177 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
178 |                     "archiveType": "zip",
179 |                     "relative_extraction_path": "vscode-java",
180 |                 },
181 |                 "osx-arm64": {
182 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
183 |                     "archiveType": "zip",
184 |                     "relative_extraction_path": "vscode-java",
185 |                     "jre_home_path": "extension/jre/21.0.7-macosx-aarch64",
186 |                     "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java",
187 |                     "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
188 |                     "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
189 |                     "jdtls_readonly_config_path": "extension/server/config_mac_arm",
190 |                 },
191 |                 "osx-x64": {
192 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix",
193 |                     "archiveType": "zip",
194 |                     "relative_extraction_path": "vscode-java",
195 |                     "jre_home_path": "extension/jre/21.0.7-macosx-x86_64",
196 |                     "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java",
197 |                     "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
198 |                     "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
199 |                     "jdtls_readonly_config_path": "extension/server/config_mac",
200 |                 },
201 |                 "linux-arm64": {
202 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix",
203 |                     "archiveType": "zip",
204 |                     "relative_extraction_path": "vscode-java",
205 |                 },
206 |                 "linux-x64": {
207 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix",
208 |                     "archiveType": "zip",
209 |                     "relative_extraction_path": "vscode-java",
210 |                     "jre_home_path": "extension/jre/21.0.7-linux-x86_64",
211 |                     "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java",
212 |                     "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
213 |                     "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
214 |                     "jdtls_readonly_config_path": "extension/server/config_linux",
215 |                 },
216 |                 "win-x64": {
217 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix",
218 |                     "archiveType": "zip",
219 |                     "relative_extraction_path": "vscode-java",
220 |                     "jre_home_path": "extension/jre/21.0.7-win32-x86_64",
221 |                     "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe",
222 |                     "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar",
223 |                     "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
224 |                     "jdtls_readonly_config_path": "extension/server/config_win",
225 |                 },
226 |             },
227 |             "intellicode": {
228 |                 "platform-agnostic": {
229 |                     "url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage",
230 |                     "alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage",
231 |                     "archiveType": "zip",
232 |                     "relative_extraction_path": "intellicode",
233 |                     "intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar",
234 |                     "intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members",
235 |                 }
236 |             },
237 |         }
238 | 
239 |         # assert platformId.value in [
240 |         #     "linux-x64",
241 |         #     "win-x64",
242 |         # ], "Only linux-x64 platform is supported for in multilspy at the moment"
243 | 
244 |         gradle_path = str(
245 |             PurePath(
246 |                 cls.ls_resources_dir(solidlsp_settings),
247 |                 "gradle-8.14.2",
248 |             )
249 |         )
250 | 
251 |         if not os.path.exists(gradle_path):
252 |             FileUtils.download_and_extract_archive(
253 |                 logger,
254 |                 runtime_dependencies["gradle"]["platform-agnostic"]["url"],
255 |                 str(PurePath(gradle_path).parent),
256 |                 runtime_dependencies["gradle"]["platform-agnostic"]["archiveType"],
257 |             )
258 | 
259 |         assert os.path.exists(gradle_path)
260 | 
261 |         dependency = runtime_dependencies["vscode-java"][platformId.value]
262 |         vscode_java_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"]))
263 |         os.makedirs(vscode_java_path, exist_ok=True)
264 |         jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"]))
265 |         jre_path = str(PurePath(vscode_java_path, dependency["jre_path"]))
266 |         lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"]))
267 |         jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"]))
268 |         jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"]))
269 |         if not all(
270 |             [
271 |                 os.path.exists(vscode_java_path),
272 |                 os.path.exists(jre_home_path),
273 |                 os.path.exists(jre_path),
274 |                 os.path.exists(lombok_jar_path),
275 |                 os.path.exists(jdtls_launcher_jar_path),
276 |                 os.path.exists(jdtls_readonly_config_path),
277 |             ]
278 |         ):
279 |             FileUtils.download_and_extract_archive(logger, dependency["url"], vscode_java_path, dependency["archiveType"])
280 | 
281 |         os.chmod(jre_path, 0o755)
282 | 
283 |         assert os.path.exists(vscode_java_path)
284 |         assert os.path.exists(jre_home_path)
285 |         assert os.path.exists(jre_path)
286 |         assert os.path.exists(lombok_jar_path)
287 |         assert os.path.exists(jdtls_launcher_jar_path)
288 |         assert os.path.exists(jdtls_readonly_config_path)
289 | 
290 |         dependency = runtime_dependencies["intellicode"]["platform-agnostic"]
291 |         intellicode_directory_path = str(PurePath(cls.ls_resources_dir(solidlsp_settings), dependency["relative_extraction_path"]))
292 |         os.makedirs(intellicode_directory_path, exist_ok=True)
293 |         intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"]))
294 |         intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"]))
295 |         if not all(
296 |             [
297 |                 os.path.exists(intellicode_directory_path),
298 |                 os.path.exists(intellicode_jar_path),
299 |                 os.path.exists(intellisense_members_path),
300 |             ]
301 |         ):
302 |             FileUtils.download_and_extract_archive(logger, dependency["url"], intellicode_directory_path, dependency["archiveType"])
303 | 
304 |         assert os.path.exists(intellicode_directory_path)
305 |         assert os.path.exists(intellicode_jar_path)
306 |         assert os.path.exists(intellisense_members_path)
307 | 
308 |         return RuntimeDependencyPaths(
309 |             gradle_path=gradle_path,
310 |             lombok_jar_path=lombok_jar_path,
311 |             jre_path=jre_path,
312 |             jre_home_path=jre_home_path,
313 |             jdtls_launcher_jar_path=jdtls_launcher_jar_path,
314 |             jdtls_readonly_config_path=jdtls_readonly_config_path,
315 |             intellicode_jar_path=intellicode_jar_path,
316 |             intellisense_members_path=intellisense_members_path,
317 |         )
318 | 
319 |     def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
320 |         """
321 |         Returns the initialize parameters for the EclipseJDTLS server.
322 |         """
323 |         # Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available
324 | 
325 |         if not os.path.isabs(repository_absolute_path):
326 |             repository_absolute_path = os.path.abspath(repository_absolute_path)
327 |         repo_uri = pathlib.Path(repository_absolute_path).as_uri()
328 | 
329 |         initialize_params = {
330 |             "locale": "en",
331 |             "rootPath": repository_absolute_path,
332 |             "rootUri": pathlib.Path(repository_absolute_path).as_uri(),
333 |             "capabilities": {
334 |                 "workspace": {
335 |                     "applyEdit": True,
336 |                     "workspaceEdit": {
337 |                         "documentChanges": True,
338 |                         "resourceOperations": ["create", "rename", "delete"],
339 |                         "failureHandling": "textOnlyTransactional",
340 |                         "normalizesLineEndings": True,
341 |                         "changeAnnotationSupport": {"groupsOnLabel": True},
342 |                     },
343 |                     "didChangeConfiguration": {"dynamicRegistration": True},
344 |                     "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True},
345 |                     "symbol": {
346 |                         "dynamicRegistration": True,
347 |                         "symbolKind": {"valueSet": list(range(1, 27))},
348 |                         "tagSupport": {"valueSet": [1]},
349 |                         "resolveSupport": {"properties": ["location.range"]},
350 |                     },
351 |                     "codeLens": {"refreshSupport": True},
352 |                     "executeCommand": {"dynamicRegistration": True},
353 |                     "configuration": True,
354 |                     "workspaceFolders": True,
355 |                     "semanticTokens": {"refreshSupport": True},
356 |                     "fileOperations": {
357 |                         "dynamicRegistration": True,
358 |                         "didCreate": True,
359 |                         "didRename": True,
360 |                         "didDelete": True,
361 |                         "willCreate": True,
362 |                         "willRename": True,
363 |                         "willDelete": True,
364 |                     },
365 |                     "inlineValue": {"refreshSupport": True},
366 |                     "inlayHint": {"refreshSupport": True},
367 |                     "diagnostics": {"refreshSupport": True},
368 |                 },
369 |                 "textDocument": {
370 |                     "publishDiagnostics": {
371 |                         "relatedInformation": True,
372 |                         "versionSupport": False,
373 |                         "tagSupport": {"valueSet": [1, 2]},
374 |                         "codeDescriptionSupport": True,
375 |                         "dataSupport": True,
376 |                     },
377 |                     "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
378 |                     # TODO: we have an assert that completion provider is not included in the capabilities at server startup
379 |                     #   Removing this will cause the assert to fail. Investigate why this is the case, simplify config
380 |                     "completion": {
381 |                         "dynamicRegistration": True,
382 |                         "contextSupport": True,
383 |                         "completionItem": {
384 |                             "snippetSupport": False,
385 |                             "commitCharactersSupport": True,
386 |                             "documentationFormat": ["markdown", "plaintext"],
387 |                             "deprecatedSupport": True,
388 |                             "preselectSupport": True,
389 |                             "tagSupport": {"valueSet": [1]},
390 |                             "insertReplaceSupport": False,
391 |                             "resolveSupport": {"properties": ["documentation", "detail", "additionalTextEdits"]},
392 |                             "insertTextModeSupport": {"valueSet": [1, 2]},
393 |                             "labelDetailsSupport": True,
394 |                         },
395 |                         "insertTextMode": 2,
396 |                         "completionItemKind": {
397 |                             "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
398 |                         },
399 |                         "completionList": {"itemDefaults": ["commitCharacters", "editRange", "insertTextFormat", "insertTextMode"]},
400 |                     },
401 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
402 |                     "signatureHelp": {
403 |                         "dynamicRegistration": True,
404 |                         "signatureInformation": {
405 |                             "documentationFormat": ["markdown", "plaintext"],
406 |                             "parameterInformation": {"labelOffsetSupport": True},
407 |                             "activeParameterSupport": True,
408 |                         },
409 |                     },
410 |                     "definition": {"dynamicRegistration": True, "linkSupport": True},
411 |                     "references": {"dynamicRegistration": True},
412 |                     "documentSymbol": {
413 |                         "dynamicRegistration": True,
414 |                         "symbolKind": {"valueSet": list(range(1, 27))},
415 |                         "hierarchicalDocumentSymbolSupport": True,
416 |                         "tagSupport": {"valueSet": [1]},
417 |                         "labelSupport": True,
418 |                     },
419 |                     "rename": {
420 |                         "dynamicRegistration": True,
421 |                         "prepareSupport": True,
422 |                         "prepareSupportDefaultBehavior": 1,
423 |                         "honorsChangeAnnotations": True,
424 |                     },
425 |                     "documentLink": {"dynamicRegistration": True, "tooltipSupport": True},
426 |                     "typeDefinition": {"dynamicRegistration": True, "linkSupport": True},
427 |                     "implementation": {"dynamicRegistration": True, "linkSupport": True},
428 |                     "colorProvider": {"dynamicRegistration": True},
429 |                     "declaration": {"dynamicRegistration": True, "linkSupport": True},
430 |                     "selectionRange": {"dynamicRegistration": True},
431 |                     "callHierarchy": {"dynamicRegistration": True},
432 |                     "semanticTokens": {
433 |                         "dynamicRegistration": True,
434 |                         "tokenTypes": [
435 |                             "namespace",
436 |                             "type",
437 |                             "class",
438 |                             "enum",
439 |                             "interface",
440 |                             "struct",
441 |                             "typeParameter",
442 |                             "parameter",
443 |                             "variable",
444 |                             "property",
445 |                             "enumMember",
446 |                             "event",
447 |                             "function",
448 |                             "method",
449 |                             "macro",
450 |                             "keyword",
451 |                             "modifier",
452 |                             "comment",
453 |                             "string",
454 |                             "number",
455 |                             "regexp",
456 |                             "operator",
457 |                             "decorator",
458 |                         ],
459 |                         "tokenModifiers": [
460 |                             "declaration",
461 |                             "definition",
462 |                             "readonly",
463 |                             "static",
464 |                             "deprecated",
465 |                             "abstract",
466 |                             "async",
467 |                             "modification",
468 |                             "documentation",
469 |                             "defaultLibrary",
470 |                         ],
471 |                         "formats": ["relative"],
472 |                         "requests": {"range": True, "full": {"delta": True}},
473 |                         "multilineTokenSupport": False,
474 |                         "overlappingTokenSupport": False,
475 |                         "serverCancelSupport": True,
476 |                         "augmentsSyntaxTokens": True,
477 |                     },
478 |                     "typeHierarchy": {"dynamicRegistration": True},
479 |                     "inlineValue": {"dynamicRegistration": True},
480 |                     "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False},
481 |                 },
482 |                 "general": {
483 |                     "staleRequestSupport": {
484 |                         "cancel": True,
485 |                         "retryOnContentModified": [
486 |                             "textDocument/semanticTokens/full",
487 |                             "textDocument/semanticTokens/range",
488 |                             "textDocument/semanticTokens/full/delta",
489 |                         ],
490 |                     },
491 |                     "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"},
492 |                     "positionEncodings": ["utf-16"],
493 |                 },
494 |                 "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}},
495 |             },
496 |             "initializationOptions": {
497 |                 "bundles": ["intellicode-core.jar"],
498 |                 "settings": {
499 |                     "java": {
500 |                         "home": None,
501 |                         "jdt": {
502 |                             "ls": {
503 |                                 "java": {"home": None},
504 |                                 "vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -Xlog:disable",
505 |                                 "lombokSupport": {"enabled": True},
506 |                                 "protobufSupport": {"enabled": True},
507 |                                 "androidSupport": {"enabled": True},
508 |                             }
509 |                         },
510 |                         "errors": {"incompleteClasspath": {"severity": "error"}},
511 |                         "configuration": {
512 |                             "checkProjectSettingsExclusions": False,
513 |                             "updateBuildConfiguration": "interactive",
514 |                             "maven": {
515 |                                 "userSettings": None,
516 |                                 "globalSettings": None,
517 |                                 "notCoveredPluginExecutionSeverity": "warning",
518 |                                 "defaultMojoExecutionAction": "ignore",
519 |                             },
520 |                             "workspaceCacheLimit": 90,
521 |                             "runtimes": [
522 |                                 {"name": "JavaSE-21", "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": True}
523 |                             ],
524 |                         },
525 |                         "trace": {"server": "verbose"},
526 |                         "import": {
527 |                             "maven": {
528 |                                 "enabled": True,
529 |                                 "offline": {"enabled": False},
530 |                                 "disableTestClasspathFlag": False,
531 |                             },
532 |                             "gradle": {
533 |                                 "enabled": True,
534 |                                 "wrapper": {"enabled": False},
535 |                                 "version": None,
536 |                                 "home": "abs(static/gradle-7.3.3)",
537 |                                 "java": {"home": "abs(static/launch_jres/21.0.7-linux-x86_64)"},
538 |                                 "offline": {"enabled": False},
539 |                                 "arguments": None,
540 |                                 "jvmArguments": None,
541 |                                 "user": {"home": None},
542 |                                 "annotationProcessing": {"enabled": True},
543 |                             },
544 |                             "exclusions": [
545 |                                 "**/node_modules/**",
546 |                                 "**/.metadata/**",
547 |                                 "**/archetype-resources/**",
548 |                                 "**/META-INF/maven/**",
549 |                             ],
550 |                             "generatesMetadataFilesAtProjectRoot": False,
551 |                         },
552 |                         "maven": {"downloadSources": True, "updateSnapshots": True},
553 |                         "eclipse": {"downloadSources": True},
554 |                         "signatureHelp": {"enabled": True, "description": {"enabled": True}},
555 |                         "implementationsCodeLens": {"enabled": True},
556 |                         "format": {
557 |                             "enabled": True,
558 |                             "settings": {"url": None, "profile": None},
559 |                             "comments": {"enabled": True},
560 |                             "onType": {"enabled": True},
561 |                             "insertSpaces": True,
562 |                             "tabSize": 4,
563 |                         },
564 |                         "saveActions": {"organizeImports": False},
565 |                         "project": {
566 |                             "referencedLibraries": ["lib/**/*.jar"],
567 |                             "importOnFirstTimeStartup": "automatic",
568 |                             "importHint": True,
569 |                             "resourceFilters": ["node_modules", "\\.git"],
570 |                             "encoding": "ignore",
571 |                             "exportJar": {"targetPath": "${workspaceFolder}/${workspaceFolderBasename}.jar"},
572 |                         },
573 |                         "contentProvider": {"preferred": None},
574 |                         "autobuild": {"enabled": True},
575 |                         "maxConcurrentBuilds": 1,
576 |                         "selectionRange": {"enabled": True},
577 |                         "showBuildStatusOnStart": {"enabled": "notification"},
578 |                         "server": {"launchMode": "Standard"},
579 |                         "sources": {"organizeImports": {"starThreshold": 99, "staticStarThreshold": 99}},
580 |                         "imports": {"gradle": {"wrapper": {"checksums": []}}},
581 |                         "templates": {"fileHeader": [], "typeComment": []},
582 |                         "references": {"includeAccessors": True, "includeDecompiledSources": True},
583 |                         "typeHierarchy": {"lazyLoad": False},
584 |                         "settings": {"url": None},
585 |                         "symbols": {"includeSourceMethodDeclarations": False},
586 |                         "inlayHints": {"parameterNames": {"enabled": "literals", "exclusions": []}},
587 |                         "codeAction": {"sortMembers": {"avoidVolatileChanges": True}},
588 |                         "compile": {
589 |                             "nullAnalysis": {
590 |                                 "nonnull": [
591 |                                     "javax.annotation.Nonnull",
592 |                                     "org.eclipse.jdt.annotation.NonNull",
593 |                                     "org.springframework.lang.NonNull",
594 |                                 ],
595 |                                 "nullable": [
596 |                                     "javax.annotation.Nullable",
597 |                                     "org.eclipse.jdt.annotation.Nullable",
598 |                                     "org.springframework.lang.Nullable",
599 |                                 ],
600 |                                 "mode": "automatic",
601 |                             }
602 |                         },
603 |                         "sharedIndexes": {"enabled": "auto", "location": ""},
604 |                         "silentNotification": False,
605 |                         "dependency": {
606 |                             "showMembers": False,
607 |                             "syncWithFolderExplorer": True,
608 |                             "autoRefresh": True,
609 |                             "refreshDelay": 2000,
610 |                             "packagePresentation": "flat",
611 |                         },
612 |                         "help": {"firstView": "auto", "showReleaseNotes": True, "collectErrorLog": False},
613 |                         "test": {"defaultConfig": "", "config": {}},
614 |                     }
615 |                 },
616 |             },
617 |             "trace": "verbose",
618 |             "processId": os.getpid(),
619 |             "workspaceFolders": [
620 |                 {
621 |                     "uri": repo_uri,
622 |                     "name": os.path.basename(repository_absolute_path),
623 |                 }
624 |             ],
625 |         }
626 | 
627 |         initialize_params["initializationOptions"]["workspaceFolders"] = [repo_uri]
628 |         bundles = [self.runtime_dependency_paths.intellicode_jar_path]
629 |         initialize_params["initializationOptions"]["bundles"] = bundles
630 |         initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [
631 |             {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True}
632 |         ]
633 | 
634 |         for runtime in initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]:
635 |             assert "name" in runtime
636 |             assert "path" in runtime
637 |             assert os.path.exists(runtime["path"]), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist"
638 | 
639 |         gradle_settings = initialize_params["initializationOptions"]["settings"]["java"]["import"]["gradle"]
640 |         gradle_settings["home"] = self.runtime_dependency_paths.gradle_path
641 |         gradle_settings["java"]["home"] = self.runtime_dependency_paths.jre_path
642 |         return initialize_params
643 | 
644 |     def _start_server(self):
645 |         """
646 |         Starts the Eclipse JDTLS Language Server
647 |         """
648 | 
649 |         def register_capability_handler(params):
650 |             assert "registrations" in params
651 |             for registration in params["registrations"]:
652 |                 if registration["method"] == "textDocument/completion":
653 |                     assert registration["registerOptions"]["resolveProvider"] == True
654 |                     assert registration["registerOptions"]["triggerCharacters"] == [
655 |                         ".",
656 |                         "@",
657 |                         "#",
658 |                         "*",
659 |                         " ",
660 |                     ]
661 |                     self.completions_available.set()
662 |                 if registration["method"] == "workspace/executeCommand":
663 |                     if "java.intellicode.enable" in registration["registerOptions"]["commands"]:
664 |                         self.intellicode_enable_command_available.set()
665 |             return
666 | 
667 |         def lang_status_handler(params):
668 |             # TODO: Should we wait for
669 |             # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
670 |             # Before proceeding?
671 |             if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
672 |                 self.service_ready_event.set()
673 | 
674 |         def execute_client_command_handler(params):
675 |             assert params["command"] == "_java.reloadBundles.command"
676 |             assert params["arguments"] == []
677 |             return []
678 | 
679 |         def window_log_message(msg):
680 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
681 | 
682 |         def do_nothing(params):
683 |             return
684 | 
685 |         self.server.on_request("client/registerCapability", register_capability_handler)
686 |         self.server.on_notification("language/status", lang_status_handler)
687 |         self.server.on_notification("window/logMessage", window_log_message)
688 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
689 |         self.server.on_notification("$/progress", do_nothing)
690 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
691 |         self.server.on_notification("language/actionableNotification", do_nothing)
692 | 
693 |         self.logger.log("Starting EclipseJDTLS server process", logging.INFO)
694 |         self.server.start()
695 |         initialize_params = self._get_initialize_params(self.repository_root_path)
696 | 
697 |         self.logger.log(
698 |             "Sending initialize request from LSP client to LSP server and awaiting response",
699 |             logging.INFO,
700 |         )
701 |         init_response = self.server.send.initialize(initialize_params)
702 |         assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
703 |         assert "completionProvider" not in init_response["capabilities"]
704 |         assert "executeCommandProvider" not in init_response["capabilities"]
705 | 
706 |         self.server.notify.initialized({})
707 | 
708 |         self.server.notify.workspace_did_change_configuration({"settings": initialize_params["initializationOptions"]["settings"]})
709 | 
710 |         self.intellicode_enable_command_available.wait()
711 | 
712 |         java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path
713 |         assert os.path.exists(java_intellisense_members_path)
714 |         intellicode_enable_result = self.server.send.execute_command(
715 |             {
716 |                 "command": "java.intellicode.enable",
717 |                 "arguments": [True, java_intellisense_members_path],
718 |             }
719 |         )
720 |         assert intellicode_enable_result
721 | 
722 |         # TODO: Add comments about why we wait here, and how this can be optimized
723 |         self.service_ready_event.wait()
724 | 
```

--------------------------------------------------------------------------------
/src/serena/cli.py:
--------------------------------------------------------------------------------

```python
  1 | import glob
  2 | import json
  3 | import os
  4 | import shutil
  5 | import subprocess
  6 | import sys
  7 | from logging import Logger
  8 | from pathlib import Path
  9 | from typing import Any, Literal
 10 | 
 11 | import click
 12 | from sensai.util import logging
 13 | from sensai.util.logging import FileLoggerContext, datetime_tag
 14 | from tqdm import tqdm
 15 | 
 16 | from serena.agent import SerenaAgent
 17 | from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
 18 | from serena.config.serena_config import ProjectConfig, SerenaConfig, SerenaPaths
 19 | from serena.constants import (
 20 |     DEFAULT_CONTEXT,
 21 |     DEFAULT_MODES,
 22 |     PROMPT_TEMPLATES_DIR_IN_USER_HOME,
 23 |     PROMPT_TEMPLATES_DIR_INTERNAL,
 24 |     SERENA_LOG_FORMAT,
 25 |     SERENA_MANAGED_DIR_IN_HOME,
 26 |     SERENAS_OWN_CONTEXT_YAMLS_DIR,
 27 |     SERENAS_OWN_MODE_YAMLS_DIR,
 28 |     USER_CONTEXT_YAMLS_DIR,
 29 |     USER_MODE_YAMLS_DIR,
 30 | )
 31 | from serena.mcp import SerenaMCPFactory, SerenaMCPFactorySingleProcess
 32 | from serena.project import Project
 33 | from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsOverviewTool, SearchForPatternTool, ToolRegistry
 34 | from serena.util.logging import MemoryLogHandler
 35 | from solidlsp.ls_config import Language
 36 | from solidlsp.util.subprocess_util import subprocess_kwargs
 37 | 
 38 | log = logging.getLogger(__name__)
 39 | 
 40 | # --------------------- Utilities -------------------------------------
 41 | 
 42 | 
 43 | def _open_in_editor(path: str) -> None:
 44 |     """Open the given file in the system's default editor or viewer."""
 45 |     editor = os.environ.get("EDITOR")
 46 |     run_kwargs = subprocess_kwargs()
 47 |     try:
 48 |         if editor:
 49 |             subprocess.run([editor, path], check=False, **run_kwargs)
 50 |         elif sys.platform.startswith("win"):
 51 |             try:
 52 |                 os.startfile(path)
 53 |             except OSError:
 54 |                 subprocess.run(["notepad.exe", path], check=False, **run_kwargs)
 55 |         elif sys.platform == "darwin":
 56 |             subprocess.run(["open", path], check=False, **run_kwargs)
 57 |         else:
 58 |             subprocess.run(["xdg-open", path], check=False, **run_kwargs)
 59 |     except Exception as e:
 60 |         print(f"Failed to open {path}: {e}")
 61 | 
 62 | 
 63 | class ProjectType(click.ParamType):
 64 |     """ParamType allowing either a project name or a path to a project directory."""
 65 | 
 66 |     name = "[PROJECT_NAME|PROJECT_PATH]"
 67 | 
 68 |     def convert(self, value: str, param: Any, ctx: Any) -> str:
 69 |         path = Path(value).resolve()
 70 |         if path.exists() and path.is_dir():
 71 |             return str(path)
 72 |         return value
 73 | 
 74 | 
 75 | PROJECT_TYPE = ProjectType()
 76 | 
 77 | 
 78 | class AutoRegisteringGroup(click.Group):
 79 |     """
 80 |     A click.Group subclass that automatically registers any click.Command
 81 |     attributes defined on the class into the group.
 82 | 
 83 |     After initialization, it inspects its own class for attributes that are
 84 |     instances of click.Command (typically created via @click.command) and
 85 |     calls self.add_command(cmd) on each. This lets you define your commands
 86 |     as static methods on the subclass for IDE-friendly organization without
 87 |     manual registration.
 88 |     """
 89 | 
 90 |     def __init__(self, name: str, help: str):
 91 |         super().__init__(name=name, help=help)
 92 |         # Scan class attributes for click.Command instances and register them.
 93 |         for attr in dir(self.__class__):
 94 |             cmd = getattr(self.__class__, attr)
 95 |             if isinstance(cmd, click.Command):
 96 |                 self.add_command(cmd)
 97 | 
 98 | 
 99 | class TopLevelCommands(AutoRegisteringGroup):
100 |     """Root CLI group containing the core Serena commands."""
101 | 
102 |     def __init__(self) -> None:
103 |         super().__init__(name="serena", help="Serena CLI commands. You can run `<command> --help` for more info on each command.")
104 | 
105 |     @staticmethod
106 |     @click.command("start-mcp-server", help="Starts the Serena MCP server.")
107 |     @click.option("--project", "project", type=PROJECT_TYPE, default=None, help="Path or name of project to activate at startup.")
108 |     @click.option("--project-file", "project", type=PROJECT_TYPE, default=None, help="[DEPRECATED] Use --project instead.")
109 |     @click.argument("project_file_arg", type=PROJECT_TYPE, required=False, default=None, metavar="")
110 |     @click.option(
111 |         "--context", type=str, default=DEFAULT_CONTEXT, show_default=True, help="Built-in context name or path to custom context YAML."
112 |     )
113 |     @click.option(
114 |         "--mode",
115 |         "modes",
116 |         type=str,
117 |         multiple=True,
118 |         default=DEFAULT_MODES,
119 |         show_default=True,
120 |         help="Built-in mode names or paths to custom mode YAMLs.",
121 |     )
122 |     @click.option(
123 |         "--transport",
124 |         type=click.Choice(["stdio", "sse", "streamable-http"]),
125 |         default="stdio",
126 |         show_default=True,
127 |         help="Transport protocol.",
128 |     )
129 |     @click.option("--host", type=str, default="0.0.0.0", show_default=True)
130 |     @click.option("--port", type=int, default=8000, show_default=True)
131 |     @click.option("--enable-web-dashboard", type=bool, is_flag=False, default=None, help="Override dashboard setting in config.")
132 |     @click.option("--enable-gui-log-window", type=bool, is_flag=False, default=None, help="Override GUI log window setting in config.")
133 |     @click.option(
134 |         "--log-level",
135 |         type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
136 |         default=None,
137 |         help="Override log level in config.",
138 |     )
139 |     @click.option("--trace-lsp-communication", type=bool, is_flag=False, default=None, help="Whether to trace LSP communication.")
140 |     @click.option("--tool-timeout", type=float, default=None, help="Override tool execution timeout in config.")
141 |     def start_mcp_server(
142 |         project: str | None,
143 |         project_file_arg: str | None,
144 |         context: str,
145 |         modes: tuple[str, ...],
146 |         transport: Literal["stdio", "sse", "streamable-http"],
147 |         host: str,
148 |         port: int,
149 |         enable_web_dashboard: bool | None,
150 |         enable_gui_log_window: bool | None,
151 |         log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None,
152 |         trace_lsp_communication: bool | None,
153 |         tool_timeout: float | None,
154 |     ) -> None:
155 |         # initialize logging, using INFO level initially (will later be adjusted by SerenaAgent according to the config)
156 |         #   * memory log handler (for use by GUI/Dashboard)
157 |         #   * stream handler for stderr (for direct console output, which will also be captured by clients like Claude Desktop)
158 |         #   * file handler
159 |         # (Note that stdout must never be used for logging, as it is used by the MCP server to communicate with the client.)
160 |         Logger.root.setLevel(logging.INFO)
161 |         formatter = logging.Formatter(SERENA_LOG_FORMAT)
162 |         memory_log_handler = MemoryLogHandler()
163 |         Logger.root.addHandler(memory_log_handler)
164 |         stderr_handler = logging.StreamHandler(stream=sys.stderr)
165 |         stderr_handler.formatter = formatter
166 |         Logger.root.addHandler(stderr_handler)
167 |         log_path = SerenaPaths().get_next_log_file_path("mcp")
168 |         file_handler = logging.FileHandler(log_path, mode="w")
169 |         file_handler.formatter = formatter
170 |         Logger.root.addHandler(file_handler)
171 | 
172 |         log.info("Initializing Serena MCP server")
173 |         log.info("Storing logs in %s", log_path)
174 |         project_file = project_file_arg or project
175 |         factory = SerenaMCPFactorySingleProcess(context=context, project=project_file, memory_log_handler=memory_log_handler)
176 |         server = factory.create_mcp_server(
177 |             host=host,
178 |             port=port,
179 |             modes=modes,
180 |             enable_web_dashboard=enable_web_dashboard,
181 |             enable_gui_log_window=enable_gui_log_window,
182 |             log_level=log_level,
183 |             trace_lsp_communication=trace_lsp_communication,
184 |             tool_timeout=tool_timeout,
185 |         )
186 |         if project_file_arg:
187 |             log.warning(
188 |                 "Positional project arg is deprecated; use --project instead. Used: %s",
189 |                 project_file,
190 |             )
191 |         log.info("Starting MCP server …")
192 |         server.run(transport=transport)
193 | 
194 |     @staticmethod
195 |     @click.command("print-system-prompt", help="Print the system prompt for a project.")
196 |     @click.argument("project", type=click.Path(exists=True), default=os.getcwd(), required=False)
197 |     @click.option(
198 |         "--log-level",
199 |         type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
200 |         default="WARNING",
201 |         help="Log level for prompt generation.",
202 |     )
203 |     @click.option("--only-instructions", is_flag=True, help="Print only the initial instructions, without prefix/postfix.")
204 |     @click.option(
205 |         "--context", type=str, default=DEFAULT_CONTEXT, show_default=True, help="Built-in context name or path to custom context YAML."
206 |     )
207 |     @click.option(
208 |         "--mode",
209 |         "modes",
210 |         type=str,
211 |         multiple=True,
212 |         default=DEFAULT_MODES,
213 |         show_default=True,
214 |         help="Built-in mode names or paths to custom mode YAMLs.",
215 |     )
216 |     def print_system_prompt(project: str, log_level: str, only_instructions: bool, context: str, modes: tuple[str, ...]) -> None:
217 |         prefix = "You will receive access to Serena's symbolic tools. Below are instructions for using them, take them into account."
218 |         postfix = "You begin by acknowledging that you understood the above instructions and are ready to receive tasks."
219 |         from serena.tools.workflow_tools import InitialInstructionsTool
220 | 
221 |         lvl = logging.getLevelNamesMapping()[log_level.upper()]
222 |         logging.configure(level=lvl)
223 |         context_instance = SerenaAgentContext.load(context)
224 |         mode_instances = [SerenaAgentMode.load(mode) for mode in modes]
225 |         agent = SerenaAgent(
226 |             project=os.path.abspath(project),
227 |             serena_config=SerenaConfig(web_dashboard=False, log_level=lvl),
228 |             context=context_instance,
229 |             modes=mode_instances,
230 |         )
231 |         tool = agent.get_tool(InitialInstructionsTool)
232 |         instr = tool.apply()
233 |         if only_instructions:
234 |             print(instr)
235 |         else:
236 |             print(f"{prefix}\n{instr}\n{postfix}")
237 | 
238 | 
239 | class ModeCommands(AutoRegisteringGroup):
240 |     """Group for 'mode' subcommands."""
241 | 
242 |     def __init__(self) -> None:
243 |         super().__init__(name="mode", help="Manage Serena modes. You can run `mode <command> --help` for more info on each command.")
244 | 
245 |     @staticmethod
246 |     @click.command("list", help="List available modes.")
247 |     def list() -> None:
248 |         mode_names = SerenaAgentMode.list_registered_mode_names()
249 |         max_len_name = max(len(name) for name in mode_names) if mode_names else 20
250 |         for name in mode_names:
251 |             mode_yml_path = SerenaAgentMode.get_path(name)
252 |             is_internal = Path(mode_yml_path).is_relative_to(SERENAS_OWN_MODE_YAMLS_DIR)
253 |             descriptor = "(internal)" if is_internal else f"(at {mode_yml_path})"
254 |             name_descr_string = f"{name:<{max_len_name + 4}}{descriptor}"
255 |             click.echo(name_descr_string)
256 | 
257 |     @staticmethod
258 |     @click.command("create", help="Create a new mode or copy an internal one.")
259 |     @click.option(
260 |         "--name",
261 |         "-n",
262 |         type=str,
263 |         default=None,
264 |         help="Name for the new mode. If --from-internal is passed may be left empty to create a mode of the same name, which will then override the internal mode.",
265 |     )
266 |     @click.option("--from-internal", "from_internal", type=str, default=None, help="Copy from an internal mode.")
267 |     def create(name: str, from_internal: str) -> None:
268 |         if not (name or from_internal):
269 |             raise click.UsageError("Provide at least one of --name or --from-internal.")
270 |         mode_name = name or from_internal
271 |         dest = os.path.join(USER_MODE_YAMLS_DIR, f"{mode_name}.yml")
272 |         src = (
273 |             os.path.join(SERENAS_OWN_MODE_YAMLS_DIR, f"{from_internal}.yml")
274 |             if from_internal
275 |             else os.path.join(SERENAS_OWN_MODE_YAMLS_DIR, "mode.template.yml")
276 |         )
277 |         if not os.path.exists(src):
278 |             raise FileNotFoundError(
279 |                 f"Internal mode '{from_internal}' not found in {SERENAS_OWN_MODE_YAMLS_DIR}. Available modes: {SerenaAgentMode.list_registered_mode_names()}"
280 |             )
281 |         os.makedirs(os.path.dirname(dest), exist_ok=True)
282 |         shutil.copyfile(src, dest)
283 |         click.echo(f"Created mode '{mode_name}' at {dest}")
284 |         _open_in_editor(dest)
285 | 
286 |     @staticmethod
287 |     @click.command("edit", help="Edit a custom mode YAML file.")
288 |     @click.argument("mode_name")
289 |     def edit(mode_name: str) -> None:
290 |         path = os.path.join(USER_MODE_YAMLS_DIR, f"{mode_name}.yml")
291 |         if not os.path.exists(path):
292 |             if mode_name in SerenaAgentMode.list_registered_mode_names(include_user_modes=False):
293 |                 click.echo(
294 |                     f"Mode '{mode_name}' is an internal mode and cannot be edited directly. "
295 |                     f"Use 'mode create --from-internal {mode_name}' to create a custom mode that overrides it before editing."
296 |                 )
297 |             else:
298 |                 click.echo(f"Custom mode '{mode_name}' not found. Create it with: mode create --name {mode_name}.")
299 |             return
300 |         _open_in_editor(path)
301 | 
302 |     @staticmethod
303 |     @click.command("delete", help="Delete a custom mode file.")
304 |     @click.argument("mode_name")
305 |     def delete(mode_name: str) -> None:
306 |         path = os.path.join(USER_MODE_YAMLS_DIR, f"{mode_name}.yml")
307 |         if not os.path.exists(path):
308 |             click.echo(f"Custom mode '{mode_name}' not found.")
309 |             return
310 |         os.remove(path)
311 |         click.echo(f"Deleted custom mode '{mode_name}'.")
312 | 
313 | 
314 | class ContextCommands(AutoRegisteringGroup):
315 |     """Group for 'context' subcommands."""
316 | 
317 |     def __init__(self) -> None:
318 |         super().__init__(
319 |             name="context", help="Manage Serena contexts. You can run `context <command> --help` for more info on each command."
320 |         )
321 | 
322 |     @staticmethod
323 |     @click.command("list", help="List available contexts.")
324 |     def list() -> None:
325 |         context_names = SerenaAgentContext.list_registered_context_names()
326 |         max_len_name = max(len(name) for name in context_names) if context_names else 20
327 |         for name in context_names:
328 |             context_yml_path = SerenaAgentContext.get_path(name)
329 |             is_internal = Path(context_yml_path).is_relative_to(SERENAS_OWN_CONTEXT_YAMLS_DIR)
330 |             descriptor = "(internal)" if is_internal else f"(at {context_yml_path})"
331 |             name_descr_string = f"{name:<{max_len_name + 4}}{descriptor}"
332 |             click.echo(name_descr_string)
333 | 
334 |     @staticmethod
335 |     @click.command("create", help="Create a new context or copy an internal one.")
336 |     @click.option(
337 |         "--name",
338 |         "-n",
339 |         type=str,
340 |         default=None,
341 |         help="Name for the new context. If --from-internal is passed may be left empty to create a context of the same name, which will then override the internal context",
342 |     )
343 |     @click.option("--from-internal", "from_internal", type=str, default=None, help="Copy from an internal context.")
344 |     def create(name: str, from_internal: str) -> None:
345 |         if not (name or from_internal):
346 |             raise click.UsageError("Provide at least one of --name or --from-internal.")
347 |         ctx_name = name or from_internal
348 |         dest = os.path.join(USER_CONTEXT_YAMLS_DIR, f"{ctx_name}.yml")
349 |         src = (
350 |             os.path.join(SERENAS_OWN_CONTEXT_YAMLS_DIR, f"{from_internal}.yml")
351 |             if from_internal
352 |             else os.path.join(SERENAS_OWN_CONTEXT_YAMLS_DIR, "context.template.yml")
353 |         )
354 |         if not os.path.exists(src):
355 |             raise FileNotFoundError(
356 |                 f"Internal context '{from_internal}' not found in {SERENAS_OWN_CONTEXT_YAMLS_DIR}. Available contexts: {SerenaAgentContext.list_registered_context_names()}"
357 |             )
358 |         os.makedirs(os.path.dirname(dest), exist_ok=True)
359 |         shutil.copyfile(src, dest)
360 |         click.echo(f"Created context '{ctx_name}' at {dest}")
361 |         _open_in_editor(dest)
362 | 
363 |     @staticmethod
364 |     @click.command("edit", help="Edit a custom context YAML file.")
365 |     @click.argument("context_name")
366 |     def edit(context_name: str) -> None:
367 |         path = os.path.join(USER_CONTEXT_YAMLS_DIR, f"{context_name}.yml")
368 |         if not os.path.exists(path):
369 |             if context_name in SerenaAgentContext.list_registered_context_names(include_user_contexts=False):
370 |                 click.echo(
371 |                     f"Context '{context_name}' is an internal context and cannot be edited directly. "
372 |                     f"Use 'context create --from-internal {context_name}' to create a custom context that overrides it before editing."
373 |                 )
374 |             else:
375 |                 click.echo(f"Custom context '{context_name}' not found. Create it with: context create --name {context_name}.")
376 |             return
377 |         _open_in_editor(path)
378 | 
379 |     @staticmethod
380 |     @click.command("delete", help="Delete a custom context file.")
381 |     @click.argument("context_name")
382 |     def delete(context_name: str) -> None:
383 |         path = os.path.join(USER_CONTEXT_YAMLS_DIR, f"{context_name}.yml")
384 |         if not os.path.exists(path):
385 |             click.echo(f"Custom context '{context_name}' not found.")
386 |             return
387 |         os.remove(path)
388 |         click.echo(f"Deleted custom context '{context_name}'.")
389 | 
390 | 
391 | class SerenaConfigCommands(AutoRegisteringGroup):
392 |     """Group for 'config' subcommands."""
393 | 
394 |     def __init__(self) -> None:
395 |         super().__init__(name="config", help="Manage Serena configuration.")
396 | 
397 |     @staticmethod
398 |     @click.command(
399 |         "edit", help="Edit serena_config.yml in your default editor. Will create a config file from the template if no config is found."
400 |     )
401 |     def edit() -> None:
402 |         config_path = os.path.join(SERENA_MANAGED_DIR_IN_HOME, "serena_config.yml")
403 |         if not os.path.exists(config_path):
404 |             SerenaConfig.generate_config_file(config_path)
405 |         _open_in_editor(config_path)
406 | 
407 | 
408 | class ProjectCommands(AutoRegisteringGroup):
409 |     """Group for 'project' subcommands."""
410 | 
411 |     def __init__(self) -> None:
412 |         super().__init__(
413 |             name="project", help="Manage Serena projects. You can run `project <command> --help` for more info on each command."
414 |         )
415 | 
416 |     @staticmethod
417 |     @click.command("generate-yml", help="Generate a project.yml file.")
418 |     @click.argument("project_path", type=click.Path(exists=True, file_okay=False), default=os.getcwd())
419 |     @click.option("--language", type=str, default=None, help="Programming language; inferred if not specified.")
420 |     def generate_yml(project_path: str, language: str | None = None) -> None:
421 |         yml_path = os.path.join(project_path, ProjectConfig.rel_path_to_project_yml())
422 |         if os.path.exists(yml_path):
423 |             raise FileExistsError(f"Project file {yml_path} already exists.")
424 |         lang_inst = None
425 |         if language:
426 |             try:
427 |                 lang_inst = Language[language.upper()]
428 |             except KeyError:
429 |                 all_langs = [l.name.lower() for l in Language.iter_all(include_experimental=True)]
430 |                 raise ValueError(f"Unknown language '{language}'. Supported: {all_langs}")
431 |         generated_conf = ProjectConfig.autogenerate(project_root=project_path, project_language=lang_inst)
432 |         print(f"Generated project.yml with language {generated_conf.language.value} at {yml_path}.")
433 | 
434 |     @staticmethod
435 |     @click.command("index", help="Index a project by saving symbols to the LSP cache.")
436 |     @click.argument("project", type=click.Path(exists=True), default=os.getcwd(), required=False)
437 |     @click.option(
438 |         "--log-level",
439 |         type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
440 |         default="WARNING",
441 |         help="Log level for indexing.",
442 |     )
443 |     @click.option("--timeout", type=float, default=10, help="Timeout for indexing a single file.")
444 |     def index(project: str, log_level: str, timeout: float) -> None:
445 |         ProjectCommands._index_project(project, log_level, timeout=timeout)
446 | 
447 |     @staticmethod
448 |     @click.command("index-deprecated", help="Deprecated alias for 'serena project index'.")
449 |     @click.argument("project", type=click.Path(exists=True), default=os.getcwd(), required=False)
450 |     @click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="WARNING")
451 |     @click.option("--timeout", type=float, default=10, help="Timeout for indexing a single file.")
452 |     def index_deprecated(project: str, log_level: str, timeout: float) -> None:
453 |         click.echo("Deprecated! Use `serena project index` instead.")
454 |         ProjectCommands._index_project(project, log_level, timeout=timeout)
455 | 
456 |     @staticmethod
457 |     def _index_project(project: str, log_level: str, timeout: float) -> None:
458 |         lvl = logging.getLevelNamesMapping()[log_level.upper()]
459 |         logging.configure(level=lvl)
460 |         serena_config = SerenaConfig.from_config_file()
461 |         proj = Project.load(os.path.abspath(project))
462 |         click.echo(f"Indexing symbols in project {project}…")
463 |         ls = proj.create_language_server(log_level=lvl, ls_timeout=timeout, ls_specific_settings=serena_config.ls_specific_settings)
464 |         log_file = os.path.join(project, ".serena", "logs", "indexing.txt")
465 | 
466 |         collected_exceptions: list[Exception] = []
467 |         files_failed = []
468 |         with ls.start_server():
469 |             files = proj.gather_source_files()
470 |             for i, f in enumerate(tqdm(files, desc="Indexing")):
471 |                 try:
472 |                     ls.request_document_symbols(f, include_body=False)
473 |                     ls.request_document_symbols(f, include_body=True)
474 |                 except Exception as e:
475 |                     log.error(f"Failed to index {f}, continuing.")
476 |                     collected_exceptions.append(e)
477 |                     files_failed.append(f)
478 |                 if (i + 1) % 10 == 0:
479 |                     ls.save_cache()
480 |             ls.save_cache()
481 |         click.echo(f"Symbols saved to {ls.cache_path}")
482 |         if len(files_failed) > 0:
483 |             os.makedirs(os.path.dirname(log_file), exist_ok=True)
484 |             with open(log_file, "w") as f:
485 |                 for file, exception in zip(files_failed, collected_exceptions, strict=True):
486 |                     f.write(f"{file}\n")
487 |                     f.write(f"{exception}\n")
488 |             click.echo(f"Failed to index {len(files_failed)} files, see:\n{log_file}")
489 | 
490 |     @staticmethod
491 |     @click.command("is_ignored_path", help="Check if a path is ignored by the project configuration.")
492 |     @click.argument("path", type=click.Path(exists=False, file_okay=True, dir_okay=True))
493 |     @click.argument("project", type=click.Path(exists=True, file_okay=False, dir_okay=True), default=os.getcwd())
494 |     def is_ignored_path(path: str, project: str) -> None:
495 |         """
496 |         Check if a given path is ignored by the project configuration.
497 | 
498 |         :param path: The path to check.
499 |         :param project: The path to the project directory, defaults to the current working directory.
500 |         """
501 |         proj = Project.load(os.path.abspath(project))
502 |         if os.path.isabs(path):
503 |             path = os.path.relpath(path, start=proj.project_root)
504 |         is_ignored = proj.is_ignored_path(path)
505 |         click.echo(f"Path '{path}' IS {'ignored' if is_ignored else 'IS NOT ignored'} by the project configuration.")
506 | 
507 |     @staticmethod
508 |     @click.command("index-file", help="Index a single file by saving its symbols to the LSP cache.")
509 |     @click.argument("file", type=click.Path(exists=True, file_okay=True, dir_okay=False))
510 |     @click.argument("project", type=click.Path(exists=True, file_okay=False, dir_okay=True), default=os.getcwd())
511 |     @click.option("--verbose", "-v", is_flag=True, help="Print detailed information about the indexed symbols.")
512 |     def index_file(file: str, project: str, verbose: bool) -> None:
513 |         """
514 |         Index a single file by saving its symbols to the LSP cache, useful for debugging.
515 |         :param file: path to the file to index, must be inside the project directory.
516 |         :param project: path to the project directory, defaults to the current working directory.
517 |         :param verbose: if set, prints detailed information about the indexed symbols.
518 |         """
519 |         proj = Project.load(os.path.abspath(project))
520 |         if os.path.isabs(file):
521 |             file = os.path.relpath(file, start=proj.project_root)
522 |         if proj.is_ignored_path(file, ignore_non_source_files=True):
523 |             click.echo(f"'{file}' is ignored or declared as non-code file by the project configuration, won't index.")
524 |             exit(1)
525 |         ls = proj.create_language_server()
526 |         with ls.start_server():
527 |             symbols, _ = ls.request_document_symbols(file, include_body=False)
528 |             ls.request_document_symbols(file, include_body=True)
529 |             if verbose:
530 |                 click.echo(f"Symbols in file '{file}':")
531 |                 for symbol in symbols:
532 |                     click.echo(f"  - {symbol['name']} at line {symbol['selectionRange']['start']['line']} of kind {symbol['kind']}")
533 |             ls.save_cache()
534 |             click.echo(f"Successfully indexed file '{file}', {len(symbols)} symbols saved to {ls.cache_path}.")
535 | 
536 |     @staticmethod
537 |     @click.command("health-check", help="Perform a comprehensive health check of the project's tools and language server.")
538 |     @click.argument("project", type=click.Path(exists=True, file_okay=False, dir_okay=True), default=os.getcwd())
539 |     def health_check(project: str) -> None:
540 |         """
541 |         Perform a comprehensive health check of the project's tools and language server.
542 | 
543 |         :param project: path to the project directory, defaults to the current working directory.
544 |         """
545 |         # NOTE: completely written by Claude Code, only functionality was reviewed, not implementation
546 |         logging.configure(level=logging.INFO)
547 |         project_path = os.path.abspath(project)
548 |         proj = Project.load(project_path)
549 | 
550 |         # Create log file with timestamp
551 |         timestamp = datetime_tag()
552 |         log_dir = os.path.join(project_path, ".serena", "logs", "health-checks")
553 |         os.makedirs(log_dir, exist_ok=True)
554 |         log_file = os.path.join(log_dir, f"health_check_{timestamp}.log")
555 | 
556 |         with FileLoggerContext(log_file, append=False, enabled=True):
557 |             log.info("Starting health check for project: %s", project_path)
558 | 
559 |             try:
560 |                 # Create SerenaAgent with dashboard disabled
561 |                 log.info("Creating SerenaAgent with disabled dashboard...")
562 |                 config = SerenaConfig(gui_log_window_enabled=False, web_dashboard=False)
563 |                 agent = SerenaAgent(project=project_path, serena_config=config)
564 |                 log.info("SerenaAgent created successfully")
565 | 
566 |                 # Find first non-empty file that can be analyzed
567 |                 log.info("Searching for analyzable files...")
568 |                 files = proj.gather_source_files()
569 |                 target_file = None
570 | 
571 |                 for file_path in files:
572 |                     try:
573 |                         full_path = os.path.join(project_path, file_path)
574 |                         if os.path.getsize(full_path) > 0:
575 |                             target_file = file_path
576 |                             log.info("Found analyzable file: %s", target_file)
577 |                             break
578 |                     except (OSError, FileNotFoundError):
579 |                         continue
580 | 
581 |                 if not target_file:
582 |                     log.error("No analyzable files found in project")
583 |                     click.echo("❌ Health check failed: No analyzable files found")
584 |                     click.echo(f"Log saved to: {log_file}")
585 |                     return
586 | 
587 |                 # Get tools from agent
588 |                 overview_tool = agent.get_tool(GetSymbolsOverviewTool)
589 |                 find_symbol_tool = agent.get_tool(FindSymbolTool)
590 |                 find_refs_tool = agent.get_tool(FindReferencingSymbolsTool)
591 |                 search_pattern_tool = agent.get_tool(SearchForPatternTool)
592 | 
593 |                 # Test 1: Get symbols overview
594 |                 log.info("Testing GetSymbolsOverviewTool on file: %s", target_file)
595 |                 overview_result = agent.execute_task(lambda: overview_tool.apply(target_file))
596 |                 overview_data = json.loads(overview_result)
597 |                 log.info("GetSymbolsOverviewTool returned %d symbols", len(overview_data))
598 | 
599 |                 if not overview_data:
600 |                     log.error("No symbols found in file %s", target_file)
601 |                     click.echo("❌ Health check failed: No symbols found in target file")
602 |                     click.echo(f"Log saved to: {log_file}")
603 |                     return
604 | 
605 |                 # Extract suitable symbol (prefer class or function over variables)
606 |                 # LSP symbol kinds: 5=class, 12=function, 6=method, 9=constructor
607 |                 preferred_kinds = [5, 12, 6, 9]  # class, function, method, constructor
608 | 
609 |                 selected_symbol = None
610 |                 for symbol in overview_data:
611 |                     if symbol.get("kind") in preferred_kinds:
612 |                         selected_symbol = symbol
613 |                         break
614 | 
615 |                 # If no preferred symbol found, use first available
616 |                 if not selected_symbol:
617 |                     selected_symbol = overview_data[0]
618 |                     log.info("No class or function found, using first available symbol")
619 | 
620 |                 symbol_name = selected_symbol.get("name_path", "unknown")
621 |                 symbol_kind = selected_symbol.get("kind", "unknown")
622 |                 log.info("Using symbol for testing: %s (kind: %d)", symbol_name, symbol_kind)
623 | 
624 |                 # Test 2: FindSymbolTool
625 |                 log.info("Testing FindSymbolTool for symbol: %s", symbol_name)
626 |                 find_symbol_result = agent.execute_task(
627 |                     lambda: find_symbol_tool.apply(symbol_name, relative_path=target_file, include_body=True)
628 |                 )
629 |                 find_symbol_data = json.loads(find_symbol_result)
630 |                 log.info("FindSymbolTool found %d matches for symbol %s", len(find_symbol_data), symbol_name)
631 | 
632 |                 # Test 3: FindReferencingSymbolsTool
633 |                 log.info("Testing FindReferencingSymbolsTool for symbol: %s", symbol_name)
634 |                 try:
635 |                     find_refs_result = agent.execute_task(lambda: find_refs_tool.apply(symbol_name, relative_path=target_file))
636 |                     find_refs_data = json.loads(find_refs_result)
637 |                     log.info("FindReferencingSymbolsTool found %d references for symbol %s", len(find_refs_data), symbol_name)
638 |                 except Exception as e:
639 |                     log.warning("FindReferencingSymbolsTool failed for symbol %s: %s", symbol_name, str(e))
640 |                     find_refs_data = []
641 | 
642 |                 # Test 4: SearchForPatternTool to verify references
643 |                 log.info("Testing SearchForPatternTool for pattern: %s", symbol_name)
644 |                 try:
645 |                     search_result = agent.execute_task(
646 |                         lambda: search_pattern_tool.apply(substring_pattern=symbol_name, restrict_search_to_code_files=True)
647 |                     )
648 |                     search_data = json.loads(search_result)
649 |                     pattern_matches = sum(len(matches) for matches in search_data.values())
650 |                     log.info("SearchForPatternTool found %d pattern matches for %s", pattern_matches, symbol_name)
651 |                 except Exception as e:
652 |                     log.warning("SearchForPatternTool failed for pattern %s: %s", symbol_name, str(e))
653 |                     pattern_matches = 0
654 | 
655 |                 # Verify tools worked as expected
656 |                 tools_working = True
657 |                 if not find_symbol_data:
658 |                     log.error("FindSymbolTool returned no results")
659 |                     tools_working = False
660 | 
661 |                 if len(find_refs_data) == 0 and pattern_matches == 0:
662 |                     log.warning("Both FindReferencingSymbolsTool and SearchForPatternTool found no matches - this might indicate an issue")
663 | 
664 |                 log.info("Health check completed successfully")
665 | 
666 |                 if tools_working:
667 |                     click.echo("✅ Health check passed - All tools working correctly")
668 |                 else:
669 |                     click.echo("⚠️  Health check completed with warnings - Check log for details")
670 | 
671 |             except Exception as e:
672 |                 log.exception("Health check failed with exception: %s", str(e))
673 |                 click.echo(f"❌ Health check failed: {e!s}")
674 | 
675 |             finally:
676 |                 click.echo(f"Log saved to: {log_file}")
677 | 
678 | 
679 | class ToolCommands(AutoRegisteringGroup):
680 |     """Group for 'tool' subcommands."""
681 | 
682 |     def __init__(self) -> None:
683 |         super().__init__(
684 |             name="tools",
685 |             help="Commands related to Serena's tools. You can run `serena tools <command> --help` for more info on each command.",
686 |         )
687 | 
688 |     @staticmethod
689 |     @click.command(
690 |         "list",
691 |         help="Prints an overview of the tools that are active by default (not just the active ones for your project). For viewing all tools, pass `--all / -a`",
692 |     )
693 |     @click.option("--quiet", "-q", is_flag=True)
694 |     @click.option("--all", "-a", "include_optional", is_flag=True, help="List all tools, including those not enabled by default.")
695 |     @click.option("--only-optional", is_flag=True, help="List only optional tools (those not enabled by default).")
696 |     def list(quiet: bool = False, include_optional: bool = False, only_optional: bool = False) -> None:
697 |         tool_registry = ToolRegistry()
698 |         if quiet:
699 |             if only_optional:
700 |                 tool_names = tool_registry.get_tool_names_optional()
701 |             elif include_optional:
702 |                 tool_names = tool_registry.get_tool_names()
703 |             else:
704 |                 tool_names = tool_registry.get_tool_names_default_enabled()
705 |             for tool_name in tool_names:
706 |                 click.echo(tool_name)
707 |         else:
708 |             ToolRegistry().print_tool_overview(include_optional=include_optional, only_optional=only_optional)
709 | 
710 |     @staticmethod
711 |     @click.command(
712 |         "description",
713 |         help="Print the description of a tool, optionally with a specific context (the latter may modify the default description).",
714 |     )
715 |     @click.argument("tool_name", type=str)
716 |     @click.option("--context", type=str, default=None, help="Context name or path to context file.")
717 |     def description(tool_name: str, context: str | None = None) -> None:
718 |         # Load the context
719 |         serena_context = None
720 |         if context:
721 |             serena_context = SerenaAgentContext.load(context)
722 | 
723 |         agent = SerenaAgent(
724 |             project=None,
725 |             serena_config=SerenaConfig(web_dashboard=False, log_level=logging.INFO),
726 |             context=serena_context,
727 |         )
728 |         tool = agent.get_tool_by_name(tool_name)
729 |         mcp_tool = SerenaMCPFactory.make_mcp_tool(tool)
730 |         click.echo(mcp_tool.description)
731 | 
732 | 
733 | class PromptCommands(AutoRegisteringGroup):
734 |     def __init__(self) -> None:
735 |         super().__init__(name="prompts", help="Commands related to Serena's prompts that are outside of contexts and modes.")
736 | 
737 |     @staticmethod
738 |     def _get_user_prompt_yaml_path(prompt_yaml_name: str) -> str:
739 |         os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
740 |         return os.path.join(PROMPT_TEMPLATES_DIR_IN_USER_HOME, prompt_yaml_name)
741 | 
742 |     @staticmethod
743 |     @click.command("list", help="Lists yamls that are used for defining prompts.")
744 |     def list() -> None:
745 |         serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")]
746 |         for prompt_yaml_name in serena_prompt_yaml_names:
747 |             user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name)
748 |             if os.path.exists(user_prompt_yaml_path):
749 |                 click.echo(f"{user_prompt_yaml_path} merged with default prompts in {prompt_yaml_name}")
750 |             else:
751 |                 click.echo(prompt_yaml_name)
752 | 
753 |     @staticmethod
754 |     @click.command("create-override", help="Create an override of an internal prompts yaml for customizing Serena's prompts")
755 |     @click.argument("prompt_yaml_name")
756 |     def create_override(prompt_yaml_name: str) -> None:
757 |         """
758 |         :param prompt_yaml_name: The yaml name of the prompt you want to override. Call the `list` command for discovering valid prompt yaml names.
759 |         :return:
760 |         """
761 |         # for convenience, we can pass names without .yml
762 |         if not prompt_yaml_name.endswith(".yml"):
763 |             prompt_yaml_name = prompt_yaml_name + ".yml"
764 |         user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name)
765 |         if os.path.exists(user_prompt_yaml_path):
766 |             raise FileExistsError(f"{user_prompt_yaml_path} already exists.")
767 |         serena_prompt_yaml_path = os.path.join(PROMPT_TEMPLATES_DIR_INTERNAL, prompt_yaml_name)
768 |         shutil.copyfile(serena_prompt_yaml_path, user_prompt_yaml_path)
769 |         _open_in_editor(user_prompt_yaml_path)
770 | 
771 |     @staticmethod
772 |     @click.command("edit-override", help="Edit an existing prompt override file")
773 |     @click.argument("prompt_yaml_name")
774 |     def edit_override(prompt_yaml_name: str) -> None:
775 |         """
776 |         :param prompt_yaml_name: The yaml name of the prompt override to edit.
777 |         :return:
778 |         """
779 |         # for convenience, we can pass names without .yml
780 |         if not prompt_yaml_name.endswith(".yml"):
781 |             prompt_yaml_name = prompt_yaml_name + ".yml"
782 |         user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name)
783 |         if not os.path.exists(user_prompt_yaml_path):
784 |             click.echo(f"Override file '{prompt_yaml_name}' not found. Create it with: prompts create-override {prompt_yaml_name}")
785 |             return
786 |         _open_in_editor(user_prompt_yaml_path)
787 | 
788 |     @staticmethod
789 |     @click.command("list-overrides", help="List existing prompt override files")
790 |     def list_overrides() -> None:
791 |         os.makedirs(PROMPT_TEMPLATES_DIR_IN_USER_HOME, exist_ok=True)
792 |         serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")]
793 |         override_files = glob.glob(os.path.join(PROMPT_TEMPLATES_DIR_IN_USER_HOME, "*.yml"))
794 |         for file_path in override_files:
795 |             if os.path.basename(file_path) in serena_prompt_yaml_names:
796 |                 click.echo(file_path)
797 | 
798 |     @staticmethod
799 |     @click.command("delete-override", help="Delete a prompt override file")
800 |     @click.argument("prompt_yaml_name")
801 |     def delete_override(prompt_yaml_name: str) -> None:
802 |         """
803 | 
804 |         :param prompt_yaml_name:  The yaml name of the prompt override to delete."
805 |         :return:
806 |         """
807 |         # for convenience, we can pass names without .yml
808 |         if not prompt_yaml_name.endswith(".yml"):
809 |             prompt_yaml_name = prompt_yaml_name + ".yml"
810 |         user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name)
811 |         if not os.path.exists(user_prompt_yaml_path):
812 |             click.echo(f"Override file '{prompt_yaml_name}' not found.")
813 |             return
814 |         os.remove(user_prompt_yaml_path)
815 |         click.echo(f"Deleted override file '{prompt_yaml_name}'.")
816 | 
817 | 
818 | # Expose groups so we can reference them in pyproject.toml
819 | mode = ModeCommands()
820 | context = ContextCommands()
821 | project = ProjectCommands()
822 | config = SerenaConfigCommands()
823 | tools = ToolCommands()
824 | prompts = PromptCommands()
825 | 
826 | # Expose toplevel commands for the same reason
827 | top_level = TopLevelCommands()
828 | start_mcp_server = top_level.start_mcp_server
829 | index_project = project.index_deprecated
830 | 
831 | # needed for the help script to work - register all subcommands to the top-level group
832 | for subgroup in (mode, context, project, config, tools, prompts):
833 |     top_level.add_command(subgroup)
834 | 
835 | 
836 | def get_help() -> str:
837 |     """Retrieve the help text for the top-level Serena CLI."""
838 |     return top_level.get_help(click.Context(top_level, info_name="serena"))
839 | 
```

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

```python
  1 | """AL Language Server implementation for Microsoft Dynamics 365 Business Central."""
  2 | 
  3 | import logging
  4 | import os
  5 | import pathlib
  6 | import platform
  7 | import stat
  8 | import time
  9 | import zipfile
 10 | from pathlib import Path
 11 | 
 12 | import requests
 13 | from overrides import override
 14 | 
 15 | from solidlsp.language_servers.common import quote_windows_path
 16 | from solidlsp.ls import SolidLanguageServer
 17 | from solidlsp.ls_config import LanguageServerConfig
 18 | from solidlsp.ls_logger import LanguageServerLogger
 19 | from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink
 20 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 21 | from solidlsp.settings import SolidLSPSettings
 22 | 
 23 | 
 24 | class ALLanguageServer(SolidLanguageServer):
 25 |     """
 26 |     Language server implementation for AL (Microsoft Dynamics 365 Business Central).
 27 | 
 28 |     This implementation uses the AL Language Server from the VS Code AL extension
 29 |     (ms-dynamics-smb.al). The extension must be installed or available locally.
 30 | 
 31 |     Key Features:
 32 |     - Automatic download of AL extension from VS Code marketplace if not present
 33 |     - Platform-specific executable detection (Windows/Linux/macOS)
 34 |     - Special initialization sequence required by AL Language Server
 35 |     - Custom AL-specific LSP commands (al/gotodefinition, al/setActiveWorkspace)
 36 |     - File opening requirement before symbol retrieval
 37 |     """
 38 | 
 39 |     def __init__(
 40 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 41 |     ):
 42 |         """
 43 |         Initialize the AL Language Server.
 44 | 
 45 |         Args:
 46 |             config: Language server configuration
 47 |             logger: Logger instance for debugging
 48 |             repository_root_path: Root path of the AL project (must contain app.json)
 49 |             solidlsp_settings: Solid LSP settings
 50 | 
 51 |         Note:
 52 |             The initialization process will automatically:
 53 |             1. Check for AL extension in the resources directory
 54 |             2. Download it from VS Code marketplace if not found
 55 |             3. Extract and configure the platform-specific executable
 56 | 
 57 |         """
 58 |         # Setup runtime dependencies and get the language server command
 59 |         # This will download the AL extension if needed
 60 |         cmd = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 61 | 
 62 |         self._project_load_check_supported: bool = True
 63 |         """Whether the AL server supports the project load status check request.
 64 |         
 65 |         Some AL server versions don't support the 'al/hasProjectClosureLoadedRequest'
 66 |         custom LSP request. This flag starts as True and is set to False if the
 67 |         request fails, preventing repeated unsuccessful attempts.
 68 |         """
 69 | 
 70 |         super().__init__(
 71 |             config,
 72 |             logger,
 73 |             repository_root_path,
 74 |             ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path),
 75 |             "al",  # Language ID for LSP
 76 |             solidlsp_settings,
 77 |         )
 78 | 
 79 |     @classmethod
 80 |     def _download_al_extension(cls, logger: LanguageServerLogger, url: str, target_dir: str) -> bool:
 81 |         """
 82 |         Download and extract the AL extension from VS Code marketplace.
 83 | 
 84 |         The VS Code marketplace packages extensions as .vsix files (which are ZIP archives).
 85 |         This method downloads the VSIX file and extracts it to get the language server binaries.
 86 | 
 87 |         Args:
 88 |             logger: Logger for tracking download progress
 89 |             url: VS Code marketplace URL for the AL extension
 90 |             target_dir: Directory where the extension will be extracted
 91 | 
 92 |         Returns:
 93 |             True if successful, False otherwise
 94 | 
 95 |         Note:
 96 |             The download includes progress tracking and proper user-agent headers
 97 |             to ensure compatibility with the VS Code marketplace.
 98 | 
 99 |         """
100 |         try:
101 |             logger.log(f"Downloading AL extension from {url}", logging.INFO)
102 | 
103 |             # Create target directory for the extension
104 |             os.makedirs(target_dir, exist_ok=True)
105 | 
106 |             # Download with proper headers to mimic VS Code marketplace client
107 |             # These headers are required for the marketplace to serve the VSIX file
108 |             headers = {
109 |                 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
110 |                 "Accept": "application/octet-stream, application/vsix, */*",
111 |             }
112 | 
113 |             response = requests.get(url, headers=headers, stream=True, timeout=300)
114 |             response.raise_for_status()
115 | 
116 |             # Save to temporary VSIX file (will be deleted after extraction)
117 |             temp_file = os.path.join(target_dir, "al_extension_temp.vsix")
118 |             total_size = int(response.headers.get("content-length", 0))
119 | 
120 |             logger.log(f"Downloading {total_size / 1024 / 1024:.1f} MB...", logging.INFO)
121 | 
122 |             with open(temp_file, "wb") as f:
123 |                 downloaded = 0
124 |                 for chunk in response.iter_content(chunk_size=8192):
125 |                     if chunk:
126 |                         f.write(chunk)
127 |                         downloaded += len(chunk)
128 |                         if total_size > 0 and downloaded % (10 * 1024 * 1024) == 0:  # Log progress every 10MB
129 |                             progress = (downloaded / total_size) * 100
130 |                             logger.log(f"Download progress: {progress:.1f}%", logging.INFO)
131 | 
132 |             logger.log("Download complete, extracting...", logging.INFO)
133 | 
134 |             # Extract VSIX file (VSIX files are just ZIP archives with a different extension)
135 |             # This will extract the extension folder containing the language server binaries
136 |             with zipfile.ZipFile(temp_file, "r") as zip_ref:
137 |                 zip_ref.extractall(target_dir)
138 | 
139 |             # Clean up temp file
140 |             os.remove(temp_file)
141 | 
142 |             logger.log("AL extension extracted successfully", logging.INFO)
143 |             return True
144 | 
145 |         except Exception as e:
146 |             logger.log(f"Error downloading/extracting AL extension: {e}", logging.ERROR)
147 |             return False
148 | 
149 |     @classmethod
150 |     def _setup_runtime_dependencies(
151 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
152 |     ) -> str:
153 |         """
154 |         Setup runtime dependencies for AL Language Server and return the command to start the server.
155 | 
156 |         This method handles the complete setup process:
157 |         1. Checks for existing AL extension installations
158 |         2. Downloads from VS Code marketplace if not found
159 |         3. Configures executable permissions on Unix systems
160 |         4. Returns the properly formatted command string
161 | 
162 |         The AL Language Server executable is located in different paths based on the platform:
163 |         - Windows: bin/win32/Microsoft.Dynamics.Nav.EditorServices.Host.exe
164 |         - Linux: bin/linux/Microsoft.Dynamics.Nav.EditorServices.Host
165 |         - macOS: bin/darwin/Microsoft.Dynamics.Nav.EditorServices.Host
166 |         """
167 |         system = platform.system()
168 | 
169 |         # Find existing extension or download if needed
170 |         extension_path = cls._find_al_extension(logger, solidlsp_settings)
171 |         if extension_path is None:
172 |             logger.log("AL extension not found on disk, attempting to download...", logging.INFO)
173 |             extension_path = cls._download_and_install_al_extension(logger, solidlsp_settings)
174 | 
175 |         if extension_path is None:
176 |             raise RuntimeError(
177 |                 "Failed to locate or download AL Language Server. Please either:\n"
178 |                 "1. Set AL_EXTENSION_PATH environment variable to the AL extension directory\n"
179 |                 "2. Install the AL extension in VS Code (ms-dynamics-smb.al)\n"
180 |                 "3. Ensure internet connection for automatic download"
181 |             )
182 | 
183 |         # Build executable path based on platform
184 |         executable_path = cls._get_executable_path(extension_path, system)
185 | 
186 |         if not os.path.exists(executable_path):
187 |             raise RuntimeError(f"AL Language Server executable not found at: {executable_path}")
188 | 
189 |         # Prepare and return the executable command
190 |         return cls._prepare_executable(executable_path, system, logger)
191 | 
192 |     @classmethod
193 |     def _find_al_extension(cls, logger: LanguageServerLogger, solidlsp_settings: SolidLSPSettings) -> str | None:
194 |         """
195 |         Find AL extension in various locations.
196 | 
197 |         Search order:
198 |         1. Environment variable (AL_EXTENSION_PATH)
199 |         2. Default download location (~/.serena/ls_resources/al-extension)
200 |         3. VS Code installed extensions
201 | 
202 |         Returns:
203 |             Path to AL extension directory or None if not found
204 | 
205 |         """
206 |         # Check environment variable
207 |         env_path = os.environ.get("AL_EXTENSION_PATH")
208 |         if env_path and os.path.exists(env_path):
209 |             logger.log(f"Found AL extension via AL_EXTENSION_PATH: {env_path}", logging.DEBUG)
210 |             return env_path
211 |         elif env_path:
212 |             logger.log(f"AL_EXTENSION_PATH set but directory not found: {env_path}", logging.WARNING)
213 | 
214 |         # Check default download location
215 |         default_path = os.path.join(cls.ls_resources_dir(solidlsp_settings), "al-extension", "extension")
216 |         if os.path.exists(default_path):
217 |             logger.log(f"Found AL extension in default location: {default_path}", logging.DEBUG)
218 |             return default_path
219 | 
220 |         # Search VS Code extensions
221 |         vscode_path = cls._find_al_extension_in_vscode(logger)
222 |         if vscode_path:
223 |             logger.log(f"Found AL extension in VS Code: {vscode_path}", logging.DEBUG)
224 |             return vscode_path
225 | 
226 |         logger.log("AL extension not found in any known location", logging.DEBUG)
227 |         return None
228 | 
229 |     @classmethod
230 |     def _download_and_install_al_extension(cls, logger: LanguageServerLogger, solidlsp_settings: SolidLSPSettings) -> str | None:
231 |         """
232 |         Download and install AL extension from VS Code marketplace.
233 | 
234 |         Returns:
235 |             Path to installed extension or None if download failed
236 | 
237 |         """
238 |         al_extension_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "al-extension")
239 | 
240 |         # AL extension version - using latest stable version
241 |         AL_VERSION = "latest"
242 |         url = f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-dynamics-smb/vsextensions/al/{AL_VERSION}/vspackage"
243 | 
244 |         logger.log(f"Downloading AL extension from: {url}", logging.INFO)
245 | 
246 |         if cls._download_al_extension(logger, url, al_extension_dir):
247 |             extension_path = os.path.join(al_extension_dir, "extension")
248 |             if os.path.exists(extension_path):
249 |                 logger.log("AL extension downloaded and installed successfully", logging.INFO)
250 |                 return extension_path
251 |             else:
252 |                 logger.log(f"Download completed but extension not found at: {extension_path}", logging.ERROR)
253 |         else:
254 |             logger.log("Failed to download AL extension from marketplace", logging.ERROR)
255 | 
256 |         return None
257 | 
258 |     @classmethod
259 |     def _get_executable_path(cls, extension_path: str, system: str) -> str:
260 |         """
261 |         Build platform-specific executable path.
262 | 
263 |         Args:
264 |             extension_path: Path to AL extension directory
265 |             system: Operating system name
266 | 
267 |         Returns:
268 |             Full path to executable
269 | 
270 |         """
271 |         if system == "Windows":
272 |             return os.path.join(extension_path, "bin", "win32", "Microsoft.Dynamics.Nav.EditorServices.Host.exe")
273 |         elif system == "Linux":
274 |             return os.path.join(extension_path, "bin", "linux", "Microsoft.Dynamics.Nav.EditorServices.Host")
275 |         elif system == "Darwin":
276 |             return os.path.join(extension_path, "bin", "darwin", "Microsoft.Dynamics.Nav.EditorServices.Host")
277 |         else:
278 |             raise RuntimeError(f"Unsupported platform: {system}")
279 | 
280 |     @classmethod
281 |     def _prepare_executable(cls, executable_path: str, system: str, logger: LanguageServerLogger) -> str:
282 |         """
283 |         Prepare the executable by setting permissions and handling path quoting.
284 | 
285 |         Args:
286 |             executable_path: Path to the executable
287 |             system: Operating system name
288 |             logger: Logger instance
289 | 
290 |         Returns:
291 |             Properly formatted command string
292 | 
293 |         """
294 |         # Make sure executable has proper permissions on Unix-like systems
295 |         if system in ["Linux", "Darwin"]:
296 |             st = os.stat(executable_path)
297 |             os.chmod(executable_path, st.st_mode | stat.S_IEXEC)
298 |             logger.log(f"Set execute permission on: {executable_path}", logging.DEBUG)
299 | 
300 |         logger.log(f"Using AL Language Server executable: {executable_path}", logging.INFO)
301 | 
302 |         # The AL Language Server uses stdio for LSP communication by default
303 |         # Use the utility function to handle Windows path quoting
304 |         return quote_windows_path(executable_path)
305 | 
306 |     @classmethod
307 |     def _get_language_server_command_fallback(cls, logger: LanguageServerLogger) -> str:
308 |         """
309 |         Get the command to start the AL language server.
310 | 
311 |         Returns:
312 |             Command string to launch the AL language server
313 | 
314 |         Raises:
315 |             RuntimeError: If AL extension cannot be found
316 | 
317 |         """
318 |         # Check if AL extension path is configured via environment variable
319 |         al_extension_path = os.environ.get("AL_EXTENSION_PATH")
320 | 
321 |         if not al_extension_path:
322 |             # Try to find the extension in the current working directory
323 |             # (for development/testing when extension is in the serena repo)
324 |             cwd_path = Path.cwd()
325 |             potential_extension = None
326 | 
327 |             # Look for ms-dynamics-smb.al-* directories
328 |             for item in cwd_path.iterdir():
329 |                 if item.is_dir() and item.name.startswith("ms-dynamics-smb.al-"):
330 |                     potential_extension = item
331 |                     break
332 | 
333 |             if potential_extension:
334 |                 al_extension_path = str(potential_extension)
335 |                 logger.log(f"Found AL extension in current directory: {al_extension_path}", logging.DEBUG)
336 |             else:
337 |                 # Try to find in common VS Code extension locations
338 |                 al_extension_path = cls._find_al_extension_in_vscode(logger)
339 | 
340 |         if not al_extension_path:
341 |             raise RuntimeError(
342 |                 "AL Language Server not found. Please either:\n"
343 |                 "1. Set AL_EXTENSION_PATH environment variable to the VS Code AL extension directory\n"
344 |                 "2. Install the AL extension in VS Code (ms-dynamics-smb.al)\n"
345 |                 "3. Place the extension directory in the current working directory"
346 |             )
347 | 
348 |         # Determine platform-specific executable
349 |         system = platform.system()
350 |         if system == "Windows":
351 |             executable = os.path.join(al_extension_path, "bin", "win32", "Microsoft.Dynamics.Nav.EditorServices.Host.exe")
352 |         elif system == "Linux":
353 |             executable = os.path.join(al_extension_path, "bin", "linux", "Microsoft.Dynamics.Nav.EditorServices.Host")
354 |         elif system == "Darwin":
355 |             executable = os.path.join(al_extension_path, "bin", "darwin", "Microsoft.Dynamics.Nav.EditorServices.Host")
356 |         else:
357 |             raise RuntimeError(f"Unsupported platform: {system}")
358 | 
359 |         # Verify executable exists
360 |         if not os.path.exists(executable):
361 |             raise RuntimeError(
362 |                 f"AL Language Server executable not found at: {executable}\nPlease ensure the AL extension is properly installed."
363 |             )
364 | 
365 |         # Make sure executable has proper permissions on Unix-like systems
366 |         if system in ["Linux", "Darwin"]:
367 |             st = os.stat(executable)
368 |             os.chmod(executable, st.st_mode | stat.S_IEXEC)
369 | 
370 |         logger.log(f"Using AL Language Server executable: {executable}", logging.INFO)
371 | 
372 |         # The AL Language Server uses stdio for LSP communication (no --stdio flag needed)
373 |         # Use the utility function to handle Windows path quoting
374 |         return quote_windows_path(executable)
375 | 
376 |     @classmethod
377 |     def _find_al_extension_in_vscode(cls, logger: LanguageServerLogger) -> str | None:
378 |         """
379 |         Try to find AL extension in common VS Code extension locations.
380 | 
381 |         Returns:
382 |             Path to AL extension directory or None if not found
383 | 
384 |         """
385 |         home = Path.home()
386 |         possible_paths = []
387 | 
388 |         # Common VS Code extension paths
389 |         if platform.system() == "Windows":
390 |             possible_paths.extend(
391 |                 [
392 |                     home / ".vscode" / "extensions",
393 |                     home / ".vscode-insiders" / "extensions",
394 |                     Path(os.environ.get("APPDATA", "")) / "Code" / "User" / "extensions",
395 |                     Path(os.environ.get("APPDATA", "")) / "Code - Insiders" / "User" / "extensions",
396 |                 ]
397 |             )
398 |         else:
399 |             possible_paths.extend(
400 |                 [
401 |                     home / ".vscode" / "extensions",
402 |                     home / ".vscode-server" / "extensions",
403 |                     home / ".vscode-insiders" / "extensions",
404 |                 ]
405 |             )
406 | 
407 |         for base_path in possible_paths:
408 |             if base_path.exists():
409 |                 logger.log(f"Searching for AL extension in: {base_path}", logging.DEBUG)
410 |                 # Look for AL extension directories
411 |                 for item in base_path.iterdir():
412 |                     if item.is_dir() and item.name.startswith("ms-dynamics-smb.al-"):
413 |                         logger.log(f"Found AL extension at: {item}", logging.DEBUG)
414 |                         return str(item)
415 | 
416 |         return None
417 | 
418 |     @staticmethod
419 |     def _get_initialize_params(repository_absolute_path: str) -> dict:
420 |         """
421 |         Returns the initialize params for the AL Language Server.
422 |         """
423 |         # Ensure we have an absolute path for URI generation
424 |         repository_path = pathlib.Path(repository_absolute_path).resolve()
425 |         root_uri = repository_path.as_uri()
426 | 
427 |         # AL requires extensive capabilities based on VS Code trace
428 |         initialize_params = {
429 |             "processId": os.getpid(),
430 |             "rootPath": str(repository_path),
431 |             "rootUri": root_uri,
432 |             "capabilities": {
433 |                 "workspace": {
434 |                     "applyEdit": True,
435 |                     "workspaceEdit": {
436 |                         "documentChanges": True,
437 |                         "resourceOperations": ["create", "rename", "delete"],
438 |                         "failureHandling": "textOnlyTransactional",
439 |                         "normalizesLineEndings": True,
440 |                     },
441 |                     "configuration": True,
442 |                     "didChangeWatchedFiles": {"dynamicRegistration": True},
443 |                     "symbol": {"dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}},
444 |                     "executeCommand": {"dynamicRegistration": True},
445 |                     "didChangeConfiguration": {"dynamicRegistration": True},
446 |                     "workspaceFolders": True,
447 |                 },
448 |                 "textDocument": {
449 |                     "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
450 |                     "completion": {
451 |                         "dynamicRegistration": True,
452 |                         "contextSupport": True,
453 |                         "completionItem": {
454 |                             "snippetSupport": True,
455 |                             "commitCharactersSupport": True,
456 |                             "documentationFormat": ["markdown", "plaintext"],
457 |                             "deprecatedSupport": True,
458 |                             "preselectSupport": True,
459 |                         },
460 |                     },
461 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
462 |                     "definition": {"dynamicRegistration": True, "linkSupport": True},
463 |                     "references": {"dynamicRegistration": True},
464 |                     "documentHighlight": {"dynamicRegistration": True},
465 |                     "documentSymbol": {
466 |                         "dynamicRegistration": True,
467 |                         "symbolKind": {"valueSet": list(range(1, 27))},
468 |                         "hierarchicalDocumentSymbolSupport": True,
469 |                     },
470 |                     "codeAction": {"dynamicRegistration": True},
471 |                     "formatting": {"dynamicRegistration": True},
472 |                     "rangeFormatting": {"dynamicRegistration": True},
473 |                     "rename": {"dynamicRegistration": True, "prepareSupport": True},
474 |                 },
475 |                 "window": {
476 |                     "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}},
477 |                     "showDocument": {"support": True},
478 |                     "workDoneProgress": True,
479 |                 },
480 |             },
481 |             "trace": "verbose",
482 |             "workspaceFolders": [{"uri": root_uri, "name": repository_path.name}],
483 |         }
484 | 
485 |         return initialize_params
486 | 
487 |     @override
488 |     def _start_server(self):
489 |         """
490 |         Starts the AL Language Server process and initializes it.
491 | 
492 |         This method sets up custom notification handlers for AL-specific messages
493 |         before starting the server. The AL server sends various notifications
494 |         during initialization and project loading that need to be handled.
495 |         """
496 | 
497 |         # Set up event handlers
498 |         def do_nothing(params):
499 |             return
500 | 
501 |         def window_log_message(msg):
502 |             self.logger.log(f"AL LSP: window/logMessage: {msg}", logging.INFO)
503 | 
504 |         def publish_diagnostics(params):
505 |             # AL server publishes diagnostics during initialization
506 |             uri = params.get("uri", "")
507 |             diagnostics = params.get("diagnostics", [])
508 |             self.logger.log(f"AL LSP: Diagnostics for {uri}: {len(diagnostics)} issues", logging.DEBUG)
509 | 
510 |         def handle_al_notifications(params):
511 |             # AL server sends custom notifications during project loading
512 |             self.logger.log("AL LSP: Notification received", logging.DEBUG)
513 | 
514 |         # Register handlers for AL-specific notifications
515 |         # These notifications are sent by the AL server during initialization and operation
516 |         self.server.on_notification("window/logMessage", window_log_message)  # Server log messages
517 |         self.server.on_notification("textDocument/publishDiagnostics", publish_diagnostics)  # Compilation diagnostics
518 |         self.server.on_notification("$/progress", do_nothing)  # Progress notifications during loading
519 |         self.server.on_notification("al/refreshExplorerObjects", handle_al_notifications)  # AL-specific object updates
520 | 
521 |         # Start the server process
522 |         self.logger.log("Starting AL Language Server process", logging.INFO)
523 |         self.server.start()
524 | 
525 |         # Send initialize request
526 |         initialize_params = self._get_initialize_params(self.repository_root_path)
527 | 
528 |         self.logger.log(
529 |             "Sending initialize request from LSP client to AL LSP server and awaiting response",
530 |             logging.INFO,
531 |         )
532 | 
533 |         # Send initialize and wait for response
534 |         resp = self.server.send_request("initialize", initialize_params)
535 |         if resp is None:
536 |             raise RuntimeError("AL Language Server initialization failed - no response")
537 | 
538 |         self.logger.log("AL Language Server initialized successfully", logging.INFO)
539 | 
540 |         # Send initialized notification
541 |         self.server.send_notification("initialized", {})
542 |         self.logger.log("Sent initialized notification", logging.INFO)
543 | 
544 |     @override
545 |     def start(self) -> "ALLanguageServer":
546 |         """
547 |         Start the AL Language Server with special initialization.
548 |         """
549 |         # Call parent start method
550 |         super().start()
551 | 
552 |         # AL-specific post-initialization
553 |         self._post_initialize_al_workspace()
554 | 
555 |         # Note: set_active_workspace() can be called manually if needed for multi-workspace scenarios
556 |         # We don't call it automatically to avoid issues during single-workspace initialization
557 | 
558 |         return self
559 | 
560 |     def _post_initialize_al_workspace(self) -> None:
561 |         """
562 |         Post-initialization setup for AL Language Server.
563 | 
564 |         The AL server requires additional setup after initialization:
565 |         1. Send workspace configuration - provides AL settings and paths
566 |         2. Open app.json to trigger project loading - AL uses app.json to identify project structure
567 |         3. Optionally wait for project to be loaded if supported
568 | 
569 |         This special initialization sequence is unique to AL and necessary for proper
570 |         symbol resolution and navigation features.
571 |         """
572 |         # No sleep needed - server is already initialized
573 | 
574 |         # Send workspace configuration first
575 |         # This tells AL about assembly paths, package caches, and code analysis settings
576 |         try:
577 |             self.server.send_notification(
578 |                 "workspace/didChangeConfiguration",
579 |                 {
580 |                     "settings": {
581 |                         "workspacePath": self.repository_root_path,
582 |                         "alResourceConfigurationSettings": {
583 |                             "assemblyProbingPaths": ["./.netpackages"],
584 |                             "codeAnalyzers": [],
585 |                             "enableCodeAnalysis": False,
586 |                             "backgroundCodeAnalysis": "Project",
587 |                             "packageCachePaths": ["./.alpackages"],
588 |                             "ruleSetPath": None,
589 |                             "enableCodeActions": True,
590 |                             "incrementalBuild": False,
591 |                             "outputAnalyzerStatistics": True,
592 |                             "enableExternalRulesets": True,
593 |                         },
594 |                         "setActiveWorkspace": True,
595 |                         "expectedProjectReferenceDefinitions": [],
596 |                         "activeWorkspaceClosure": [self.repository_root_path],
597 |                     }
598 |                 },
599 |             )
600 |             self.logger.log("Sent workspace configuration", logging.DEBUG)
601 |         except Exception as e:
602 |             self.logger.log(f"Failed to send workspace config: {e}", logging.WARNING)
603 | 
604 |         # Check if app.json exists and open it
605 |         # app.json is the AL project manifest file (similar to package.json for Node.js)
606 |         # Opening it triggers AL to load the project and index all AL files
607 |         app_json_path = Path(self.repository_root_path) / "app.json"
608 |         if app_json_path.exists():
609 |             try:
610 |                 with open(app_json_path, encoding="utf-8") as f:
611 |                     app_json_content = f.read()
612 | 
613 |                 # Use forward slashes for URI
614 |                 app_json_uri = app_json_path.as_uri()
615 | 
616 |                 # Send textDocument/didOpen for app.json
617 |                 self.server.send_notification(
618 |                     "textDocument/didOpen",
619 |                     {"textDocument": {"uri": app_json_uri, "languageId": "json", "version": 1, "text": app_json_content}},
620 |                 )
621 | 
622 |                 self.logger.log(f"Opened app.json: {app_json_uri}", logging.DEBUG)
623 |             except Exception as e:
624 |                 self.logger.log(f"Failed to open app.json: {e}", logging.WARNING)
625 | 
626 |         # Try to set active workspace (AL-specific custom LSP request)
627 |         # This is optional and may not be supported by all AL server versions
628 |         workspace_uri = Path(self.repository_root_path).resolve().as_uri()
629 |         try:
630 |             result = self.server.send_request(
631 |                 "al/setActiveWorkspace",
632 |                 {
633 |                     "currentWorkspaceFolderPath": {"uri": workspace_uri, "name": Path(self.repository_root_path).name, "index": 0},
634 |                     "settings": {
635 |                         "workspacePath": self.repository_root_path,
636 |                         "setActiveWorkspace": True,
637 |                     },
638 |                 },
639 |                 timeout=2,  # Quick timeout since this is optional
640 |             )
641 |             self.logger.log(f"Set active workspace result: {result}", logging.DEBUG)
642 |         except Exception as e:
643 |             # This is a custom AL request, not critical if it fails
644 |             self.logger.log(f"Failed to set active workspace (non-critical): {e}", logging.DEBUG)
645 | 
646 |         # Check if project supports load status check (optional)
647 |         # Many AL server versions don't support this, so we use a short timeout
648 |         # and continue regardless of the result
649 |         self._wait_for_project_load(timeout=3)
650 | 
651 |     @override
652 |     def is_ignored_dirname(self, dirname: str) -> bool:
653 |         """
654 |         Define AL-specific directories to ignore during file scanning.
655 | 
656 |         These directories contain generated files, dependencies, or cache data
657 |         that should not be analyzed for symbols.
658 | 
659 |         Args:
660 |             dirname: Directory name to check
661 | 
662 |         Returns:
663 |             True if directory should be ignored
664 | 
665 |         """
666 |         al_ignore_dirs = {
667 |             ".alpackages",  # AL package cache - downloaded dependencies
668 |             ".alcache",  # AL compiler cache - intermediate compilation files
669 |             ".altemplates",  # AL templates - code generation templates
670 |             ".snapshots",  # Test snapshots - test result snapshots
671 |             "out",  # Compiled output - generated .app files
672 |             ".vscode",  # VS Code settings - editor configuration
673 |             "Reference",  # Reference assemblies - .NET dependencies
674 |             ".netpackages",  # .NET packages - NuGet packages for AL
675 |             "bin",  # Binary output - compiled binaries
676 |             "obj",  # Object files - intermediate build artifacts
677 |         }
678 | 
679 |         # Check parent class ignore list first, then AL-specific
680 |         return super().is_ignored_dirname(dirname) or dirname in al_ignore_dirs
681 | 
682 |     @override
683 |     def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> list[dict]:
684 |         """
685 |         Override to handle AL's requirement of opening files before requesting symbols.
686 | 
687 |         The AL Language Server requires files to be explicitly opened via textDocument/didOpen
688 |         before it can provide meaningful symbols. Without this, it only returns directory symbols.
689 |         This is different from most language servers which can provide symbols for unopened files.
690 | 
691 |         This method:
692 |         1. Scans the repository for all AL files (.al and .dal extensions)
693 |         2. Opens each file with the AL server
694 |         3. Requests symbols for each file
695 |         4. Combines all symbols into a hierarchical tree structure
696 |         5. Closes the files to free resources
697 | 
698 |         Args:
699 |             within_relative_path: Restrict search to this file or directory path
700 |             include_body: Whether to include symbol body content
701 | 
702 |         Returns:
703 |             Full symbol tree with all AL symbols from opened files organized by directory
704 | 
705 |         """
706 |         self.logger.log("AL: Starting request_full_symbol_tree with file opening", logging.DEBUG)
707 | 
708 |         # Determine the root path for scanning
709 |         if within_relative_path is not None:
710 |             within_abs_path = os.path.join(self.repository_root_path, within_relative_path)
711 |             if not os.path.exists(within_abs_path):
712 |                 raise FileNotFoundError(f"File or directory not found: {within_abs_path}")
713 | 
714 |             if os.path.isfile(within_abs_path):
715 |                 # Single file case - use parent class implementation
716 |                 _, root_nodes = self.request_document_symbols(within_relative_path, include_body=include_body)
717 |                 return root_nodes
718 | 
719 |             # Directory case - scan within this directory
720 |             scan_root = Path(within_abs_path)
721 |         else:
722 |             # Scan entire repository
723 |             scan_root = Path(self.repository_root_path)
724 | 
725 |         # For AL, we always need to open files to get symbols
726 |         al_files = []
727 | 
728 |         # Walk through the repository to find all AL files
729 |         for root, dirs, files in os.walk(scan_root):
730 |             # Skip ignored directories
731 |             dirs[:] = [d for d in dirs if not self.is_ignored_dirname(d)]
732 | 
733 |             # Find AL files
734 |             for file in files:
735 |                 if file.endswith((".al", ".dal")):
736 |                     file_path = Path(root) / file
737 |                     # Use forward slashes for consistent paths
738 |                     try:
739 |                         relative_path = str(file_path.relative_to(self.repository_root_path)).replace("\\", "/")
740 |                         al_files.append((file_path, relative_path))
741 |                     except ValueError:
742 |                         # File is outside repository root, skip it
743 |                         continue
744 | 
745 |         self.logger.log(f"AL: Found {len(al_files)} AL files", logging.DEBUG)
746 | 
747 |         if not al_files:
748 |             self.logger.log("AL: No AL files found in repository", logging.WARNING)
749 |             return []
750 | 
751 |         # Collect all symbols from all files
752 |         all_file_symbols = []
753 | 
754 |         for file_path, relative_path in al_files:
755 |             try:
756 |                 # Use our overridden request_document_symbols which handles opening
757 |                 self.logger.log(f"AL: Getting symbols for {relative_path}", logging.DEBUG)
758 |                 all_syms, root_syms = self.request_document_symbols(relative_path, include_body=include_body)
759 | 
760 |                 if root_syms:
761 |                     # Create a file-level symbol containing the document symbols
762 |                     file_symbol = {
763 |                         "name": file_path.stem,  # Just the filename without extension
764 |                         "kind": 1,  # File
765 |                         "children": root_syms,
766 |                         "location": {
767 |                             "uri": file_path.as_uri(),
768 |                             "relativePath": relative_path,
769 |                             "absolutePath": str(file_path),
770 |                             "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}},
771 |                         },
772 |                     }
773 |                     all_file_symbols.append(file_symbol)
774 |                     self.logger.log(f"AL: Added {len(root_syms)} symbols from {relative_path}", logging.DEBUG)
775 |                 elif all_syms:
776 |                     # If we only got all_syms but not root, use all_syms
777 |                     file_symbol = {
778 |                         "name": file_path.stem,
779 |                         "kind": 1,  # File
780 |                         "children": all_syms,
781 |                         "location": {
782 |                             "uri": file_path.as_uri(),
783 |                             "relativePath": relative_path,
784 |                             "absolutePath": str(file_path),
785 |                             "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}},
786 |                         },
787 |                     }
788 |                     all_file_symbols.append(file_symbol)
789 |                     self.logger.log(f"AL: Added {len(all_syms)} symbols from {relative_path}", logging.DEBUG)
790 | 
791 |             except Exception as e:
792 |                 self.logger.log(f"AL: Failed to get symbols for {relative_path}: {e}", logging.WARNING)
793 | 
794 |         if all_file_symbols:
795 |             self.logger.log(f"AL: Returning symbols from {len(all_file_symbols)} files", logging.DEBUG)
796 | 
797 |             # Group files by directory
798 |             directory_structure = {}
799 | 
800 |             for file_symbol in all_file_symbols:
801 |                 rel_path = file_symbol["location"]["relativePath"]
802 |                 path_parts = rel_path.split("/")
803 | 
804 |                 if len(path_parts) > 1:
805 |                     # File is in a subdirectory
806 |                     dir_path = "/".join(path_parts[:-1])
807 |                     if dir_path not in directory_structure:
808 |                         directory_structure[dir_path] = []
809 |                     directory_structure[dir_path].append(file_symbol)
810 |                 else:
811 |                     # File is in root
812 |                     if "." not in directory_structure:
813 |                         directory_structure["."] = []
814 |                     directory_structure["."].append(file_symbol)
815 | 
816 |             # Build hierarchical structure
817 |             result = []
818 |             repo_path = Path(self.repository_root_path)
819 |             for dir_path, file_symbols in directory_structure.items():
820 |                 if dir_path == ".":
821 |                     # Root level files
822 |                     result.extend(file_symbols)
823 |                 else:
824 |                     # Create directory symbol
825 |                     dir_symbol = {
826 |                         "name": Path(dir_path).name,
827 |                         "kind": 4,  # Package/Directory
828 |                         "children": file_symbols,
829 |                         "location": {
830 |                             "relativePath": dir_path,
831 |                             "absolutePath": str(repo_path / dir_path),
832 |                             "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}},
833 |                         },
834 |                     }
835 |                     result.append(dir_symbol)
836 | 
837 |             return result
838 |         else:
839 |             self.logger.log("AL: No symbols found in any files", logging.WARNING)
840 |             return []
841 | 
842 |     # ===== Phase 1: Custom AL Command Implementations =====
843 | 
844 |     @override
845 |     def _send_definition_request(self, definition_params: DefinitionParams) -> Definition | list[LocationLink] | None:
846 |         """
847 |         Override to use AL's custom gotodefinition command.
848 | 
849 |         AL Language Server uses 'al/gotodefinition' instead of the standard
850 |         'textDocument/definition' request. This custom command provides better
851 |         navigation for AL-specific constructs like table extensions, page extensions,
852 |         and codeunit references.
853 | 
854 |         If the custom command fails, we fall back to the standard LSP method.
855 |         """
856 |         # Convert standard params to AL format (same structure, different method)
857 |         al_params = {"textDocument": definition_params["textDocument"], "position": definition_params["position"]}
858 | 
859 |         try:
860 |             # Use custom AL command instead of standard LSP
861 |             response = self.server.send_request("al/gotodefinition", al_params)
862 |             self.logger.log(f"AL gotodefinition response: {response}", logging.DEBUG)
863 |             return response
864 |         except Exception as e:
865 |             self.logger.log(f"Failed to use al/gotodefinition, falling back to standard: {e}", logging.WARNING)
866 |             # Fallback to standard LSP method if custom command fails
867 |             return super()._send_definition_request(definition_params)
868 | 
869 |     def check_project_loaded(self) -> bool:
870 |         """
871 |         Check if AL project closure is fully loaded.
872 | 
873 |         Uses AL's custom 'al/hasProjectClosureLoadedRequest' to determine if
874 |         the project and all its dependencies have been fully loaded and indexed.
875 |         This is important because AL operations may fail or return incomplete
876 |         results if the project is still loading.
877 | 
878 |         Returns:
879 |             bool: True if project is loaded, False otherwise
880 | 
881 |         """
882 |         if not hasattr(self, "server") or not self.server_started:
883 |             self.logger.log("Cannot check project load - server not started", logging.DEBUG)
884 |             return False
885 | 
886 |         # Check if we've already determined this request isn't supported
887 |         if not self._project_load_check_supported:
888 |             return True  # Assume loaded if check isn't supported
889 | 
890 |         try:
891 |             # Use a very short timeout since this is just a status check
892 |             response = self.server.send_request("al/hasProjectClosureLoadedRequest", {}, timeout=1)
893 |             # Response can be boolean directly, dict with 'loaded' field, or None
894 |             if isinstance(response, bool):
895 |                 return response
896 |             elif isinstance(response, dict):
897 |                 return response.get("loaded", False)
898 |             elif response is None:
899 |                 # None typically means the project is still loading
900 |                 self.logger.log("Project load check returned None", logging.DEBUG)
901 |                 return False
902 |             else:
903 |                 self.logger.log(f"Unexpected response type for project load check: {type(response)}", logging.DEBUG)
904 |                 return False
905 |         except Exception as e:
906 |             # Mark as unsupported to avoid repeated failed attempts
907 |             self._project_load_check_supported = False
908 |             self.logger.log(f"Project load check not supported by this AL server version: {e}", logging.DEBUG)
909 |             # Assume loaded if we can't check
910 |             return True
911 | 
912 |     def _wait_for_project_load(self, timeout: int = 3) -> bool:
913 |         """
914 |         Wait for project to be fully loaded.
915 | 
916 |         Polls the AL server to check if the project is loaded.
917 |         This is optional as not all AL server versions support this check.
918 |         We use a short timeout and continue regardless of the result.
919 | 
920 |         Args:
921 |             timeout: Maximum time to wait in seconds (default 3s)
922 | 
923 |         Returns:
924 |             bool: True if project loaded within timeout, False otherwise
925 | 
926 |         """
927 |         start_time = time.time()
928 |         self.logger.log(f"Checking AL project load status (timeout: {timeout}s)...", logging.DEBUG)
929 | 
930 |         while time.time() - start_time < timeout:
931 |             if self.check_project_loaded():
932 |                 elapsed = time.time() - start_time
933 |                 self.logger.log(f"AL project fully loaded after {elapsed:.1f}s", logging.INFO)
934 |                 return True
935 |             time.sleep(0.5)
936 | 
937 |         self.logger.log(f"Project load check timed out after {timeout}s (non-critical)", logging.DEBUG)
938 |         return False
939 | 
940 |     def set_active_workspace(self, workspace_uri: str | None = None) -> None:
941 |         """
942 |         Set the active AL workspace.
943 | 
944 |         This is important when multiple workspaces exist to ensure operations
945 |         target the correct workspace. The AL server can handle multiple projects
946 |         simultaneously, but only one can be "active" at a time for operations
947 |         like symbol search and navigation.
948 | 
949 |         This uses the custom 'al/setActiveWorkspace' LSP command.
950 | 
951 |         Args:
952 |             workspace_uri: URI of workspace to set as active, or None to use repository root
953 | 
954 |         """
955 |         if not hasattr(self, "server") or not self.server_started:
956 |             self.logger.log("Cannot set active workspace - server not started", logging.DEBUG)
957 |             return
958 | 
959 |         if workspace_uri is None:
960 |             workspace_uri = Path(self.repository_root_path).resolve().as_uri()
961 | 
962 |         params = {"workspaceUri": workspace_uri}
963 | 
964 |         try:
965 |             self.server.send_request("al/setActiveWorkspace", params)
966 |             self.logger.log(f"Set active workspace to: {workspace_uri}", logging.INFO)
967 |         except Exception as e:
968 |             self.logger.log(f"Failed to set active workspace: {e}", logging.WARNING)
969 |             # Non-critical error, continue operation
970 | 
```
Page 11/14FirstPrevNextLast